165 lines
5.9 KiB
Python
165 lines
5.9 KiB
Python
"""大周天抽取候选审核服务(记忆候选 + 世界实体候选)。
|
||
|
||
记忆候选 approve 派生为 ``memories``(source=big_cycle);世界实体候选 approve
|
||
→ confirmed 进入世界模型。审核仅 pending 可操作(见 [大周天记忆/世界抽取落地设计]
|
||
../docs/domain/core-domains.md §5)。
|
||
"""
|
||
|
||
from app.core.errors import AppError
|
||
from app.core.time import utc_now
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.models.memory import Memory
|
||
from app.repositories import candidates as candidates_repo
|
||
from app.repositories import world as world_repo
|
||
from app.schemas.candidates import MemoryCandidateResponse
|
||
from app.schemas.common import Page
|
||
from app.schemas.world import WorldEntityResponse
|
||
from app.services.audit import record_audit_event
|
||
from app.services.serializers import (
|
||
character_world_entity_to_response,
|
||
memory_candidate_to_response,
|
||
)
|
||
|
||
|
||
async def list_memory_proposals(
|
||
*,
|
||
character_id: str,
|
||
source: str | None = None,
|
||
status: str | None = None,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Page[MemoryCandidateResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
all_items = await candidates_repo.list_memory_candidates(
|
||
session, character_id, source=source, status=status
|
||
)
|
||
start = max(page - 1, 0) * page_size
|
||
items = all_items[start : start + page_size]
|
||
return Page(
|
||
items=[memory_candidate_to_response(i) for i in items],
|
||
page=page,
|
||
page_size=page_size,
|
||
total=len(all_items),
|
||
)
|
||
|
||
|
||
async def approve_memory_proposal(
|
||
candidate_id: str,
|
||
) -> MemoryCandidateResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
candidate = await candidates_repo.get_memory_candidate(session, candidate_id)
|
||
if candidate is None:
|
||
raise AppError("not_found", "记忆候选不存在。")
|
||
if candidate.status != "pending":
|
||
raise AppError("conflict", f"候选当前状态为 {candidate.status},不可审核。")
|
||
session.add(
|
||
Memory(
|
||
character_id=candidate.character_id,
|
||
type=candidate.type,
|
||
title=candidate.title,
|
||
content=candidate.content,
|
||
source="big_cycle",
|
||
confidence=candidate.confidence,
|
||
importance=candidate.importance,
|
||
sensitivity=candidate.sensitivity,
|
||
evidence_ids_json=candidate.evidence_ids_json,
|
||
status="active",
|
||
)
|
||
)
|
||
candidate.status = "applied"
|
||
candidate.reviewed_at = utc_now()
|
||
await record_audit_event(
|
||
session,
|
||
action="extraction.memory.applied",
|
||
actor="user",
|
||
character_id=candidate.character_id,
|
||
input_summary=candidate.title[:200],
|
||
resource_type="memory_candidate",
|
||
resource_id=candidate.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(candidate)
|
||
return memory_candidate_to_response(candidate)
|
||
|
||
|
||
async def reject_memory_proposal(
|
||
candidate_id: str,
|
||
) -> MemoryCandidateResponse:
|
||
return await _reject_memory(candidate_id)
|
||
|
||
|
||
async def list_world_entities_pending(
|
||
*,
|
||
character_id: str,
|
||
status: str | None = None,
|
||
page: int = 1,
|
||
page_size: int = 20,
|
||
) -> Page[WorldEntityResponse]:
|
||
async with AsyncSessionLocal() as session:
|
||
items, total = await world_repo.list_entities(
|
||
session, character_id, status=status, page=page, page_size=page_size
|
||
)
|
||
return Page(
|
||
items=[character_world_entity_to_response(i) for i in items],
|
||
page=page,
|
||
page_size=page_size,
|
||
total=total,
|
||
)
|
||
|
||
|
||
async def approve_world_entity(entity_id: str) -> WorldEntityResponse:
|
||
return await _review_world_entity(entity_id, "confirmed", decision="auto")
|
||
|
||
|
||
async def reject_world_entity(entity_id: str) -> WorldEntityResponse:
|
||
return await _review_world_entity(entity_id, "rejected", decision="denied")
|
||
|
||
|
||
async def _reject_memory(candidate_id: str) -> MemoryCandidateResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
candidate = await candidates_repo.get_memory_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()
|
||
await record_audit_event(
|
||
session,
|
||
action="extraction.memory.rejected",
|
||
actor="user",
|
||
decision="denied",
|
||
character_id=candidate.character_id,
|
||
input_summary=candidate.title[:200],
|
||
resource_type="memory_candidate",
|
||
resource_id=candidate.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(candidate)
|
||
return memory_candidate_to_response(candidate)
|
||
|
||
|
||
async def _review_world_entity(
|
||
entity_id: str, target_status: str, *, decision: str
|
||
) -> WorldEntityResponse:
|
||
async with AsyncSessionLocal() as session:
|
||
entity = await world_repo.get_entity(session, entity_id)
|
||
if entity is None:
|
||
raise AppError("not_found", "世界实体不存在。")
|
||
if entity.status != "pending":
|
||
raise AppError("conflict", f"实体当前状态为 {entity.status},不可审核。")
|
||
entity.status = target_status
|
||
await record_audit_event(
|
||
session,
|
||
action=f"extraction.world.{target_status}",
|
||
actor="user",
|
||
decision=decision,
|
||
character_id=entity.character_id,
|
||
input_summary=entity.name[:200],
|
||
resource_type="character_world_entity",
|
||
resource_id=entity.id,
|
||
)
|
||
await session.commit()
|
||
await session.refresh(entity)
|
||
return character_world_entity_to_response(entity)
|