Files
2026-08-21 16:00:13 +08:00

206 lines
7.3 KiB
Python

"""演化候选派生与审核服务。
applier 注册表按 ``type`` 派生到下游(本期 personality→人格版本、procedural→
程序记忆,其余返回 None 降级)。审核闭环 approve/reject 复用派生能力,自管
session 并记审计(见 [大周天演化候选落地设计]
../docs/domain/core-domains.md §6-§7)。
"""
from collections.abc import Awaitable, Callable
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.errors import AppError
from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
from app.models.character import Character
from app.models.evolution import EvolutionCandidate
from app.models.memory import Memory
from app.repositories import evolution as evolution_repo
from app.repositories import personality as personality_repo
from app.repositories.json_utils import loads_dict, loads_list
from app.schemas.common import Page
from app.schemas.evolution import EvolutionCandidateResponse
from app.services.audit import record_audit_event
from app.services.personality import derive_personality_version
Applier = Callable[..., Awaitable[str]]
async def apply_evolution_candidate(
session: AsyncSession, candidate: EvolutionCandidate, *, actor: str = "system"
) -> str | None:
"""应用演化候选到下游;返回派生资源 ID,无 applier 时返回 None(降级)。"""
applier = _APPLIERS.get(candidate.type)
if applier is None:
return None
return await applier(session, candidate, actor=actor)
async def _apply_personality(
session: AsyncSession, candidate: EvolutionCandidate, *, actor: str
) -> str:
base = await personality_repo.get_latest_personality(session, candidate.character_id)
if base is None:
raise AppError("invalid_request", "人物尚无人格版本,无法应用演化候选。")
overrides = loads_dict(candidate.delta_json)
new_version = derive_personality_version(
base, overrides, reason=f"evolution:{candidate.id}", created_by="big_cycle"
)
session.add(new_version)
await session.flush()
character = await session.get(Character, candidate.character_id)
if character is None:
raise AppError("not_found", "人物不存在。")
character.current_personality_version_id = new_version.id
await _record_apply(
session, candidate, f"type=personality version={new_version.id}", actor=actor
)
return new_version.id
async def _apply_procedural(
session: AsyncSession, candidate: EvolutionCandidate, *, actor: str
) -> str:
delta = loads_dict(candidate.delta_json)
memory = Memory(
character_id=candidate.character_id,
type="procedural",
title=str(delta.get("title") or candidate.title),
content=str(delta.get("content") or candidate.proposal),
source="big_cycle",
importance=candidate.confidence,
)
session.add(memory)
await session.flush()
await _record_apply(session, candidate, f"type=procedural memory={memory.id}", actor=actor)
return memory.id
async def list_evolution_candidates(
*,
character_id: str,
status: str | None = None,
candidate_type: str | None = None,
page: int = 1,
page_size: int = 20,
) -> Page[EvolutionCandidateResponse]:
async with AsyncSessionLocal() as session:
items, total = await evolution_repo.list_candidates(
session,
character_id,
status=status,
candidate_type=candidate_type,
page=page,
page_size=page_size,
)
return Page(
items=[_to_response(item) for item in items],
page=page,
page_size=page_size,
total=total,
)
async def approve_evolution_candidate(
candidate_id: str,
) -> EvolutionCandidateResponse:
async with AsyncSessionLocal() as session:
candidate = await evolution_repo.get_candidate(session, candidate_id)
if candidate is None:
raise AppError("not_found", "演化候选不存在。")
if candidate.status != "pending":
raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。")
target_id = await apply_evolution_candidate(session, candidate, actor="user")
if target_id is None:
raise AppError("not_implemented", "该类型派生待后续阶段。")
candidate.status = "applied"
candidate.resulting_target_id = target_id
candidate.reviewed_at = utc_now()
candidate.reviewed_by = "user"
await session.commit()
await session.refresh(candidate)
return _to_response(candidate)
async def reject_evolution_candidate(
candidate_id: str,
) -> EvolutionCandidateResponse:
async with AsyncSessionLocal() as session:
candidate = await evolution_repo.get_candidate(session, candidate_id)
if candidate is None:
raise AppError("not_found", "演化候选不存在。")
if candidate.status != "pending":
raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。")
candidate.status = "rejected"
candidate.reviewed_at = utc_now()
candidate.reviewed_by = "user"
await record_audit_event(
session,
action="evolution.candidate.rejected",
actor="user",
risk_level=candidate.level,
decision="denied",
character_id=candidate.character_id,
input_summary=candidate.proposal[:200],
resource_type="evolution_candidate",
resource_id=candidate.id,
)
await session.commit()
await session.refresh(candidate)
return _to_response(candidate)
def _to_response(candidate: EvolutionCandidate) -> EvolutionCandidateResponse:
return EvolutionCandidateResponse(
id=candidate.id,
character_id=candidate.character_id,
cycle_id=candidate.cycle_id,
type=candidate.type,
source=candidate.source,
level=candidate.level,
scope=candidate.scope,
title=candidate.title,
proposal=candidate.proposal,
delta=loads_dict(candidate.delta_json),
evidence_ids=loads_list(candidate.evidence_ids_json),
confidence=candidate.confidence,
benefit_score=candidate.benefit_score,
risk_score=candidate.risk_score,
drift_score=candidate.drift_score,
auto_apply_allowed=candidate.auto_apply_allowed,
status=candidate.status,
resulting_target_id=candidate.resulting_target_id,
error_message=candidate.error_message,
reviewed_at=candidate.reviewed_at,
reviewed_by=candidate.reviewed_by,
created_at=candidate.created_at,
)
async def _record_apply(
session: AsyncSession,
candidate: EvolutionCandidate,
output_summary: str,
*,
actor: str,
) -> None:
await record_audit_event(
session,
action="evolution.candidate.applied",
actor=actor,
risk_level=candidate.level,
decision="auto",
character_id=candidate.character_id,
input_summary=candidate.proposal[:200],
output_summary=output_summary,
resource_type="evolution_candidate",
resource_id=candidate.id,
)
_APPLIERS: dict[str, Applier] = {
"personality": _apply_personality,
"procedural": _apply_procedural,
}