初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,205 @@
|
||||
"""演化候选派生与审核服务。
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in new issue
Block a user