"""ORM 到 API schema 的转换。""" from datetime import UTC, timedelta from typing import Any from app.core.time import to_iso, utc_now from app.models.candidates import MemoryCandidate, ProfileCandidate from app.models.character import Character from app.models.chat import ChatMessage, ChatSession from app.models.governance import ( AgentRosterEntry, GenerationPreferences, Goal, PerceptionChannel, Plan, ToolPolicy, WorldEntity, ) from app.models.imports import CharacterImportJob, ImportItem from app.models.memory import Memory from app.models.personality import PersonalityProposal, PersonalityVersion from app.models.runtime import Event, Run from app.models.safety import AuditEvent from app.models.world import CharacterWorldEntity, WorldRelation from app.repositories.json_utils import loads_dict, loads_list from app.schemas.admin import ( AgentRosterResponse, AuditEntry, GoalNode, PerceptionChannelResponse, PlanSummaryResponse, WorldEntityResponse, ) from app.schemas.candidates import MemoryCandidateResponse, ProfileCandidateResponse from app.schemas.character import CharacterResponse, CharacterSummaryResponse from app.schemas.chat import ChatMessageResponse, ChatSessionResponse from app.schemas.imports import ImportItemResponse, ImportJobResponse from app.schemas.memory import MemoryResponse from app.schemas.personality import PersonalityProposalResponse, PersonalityResponse from app.schemas.preferences import GenerationPreferencesResponse from app.schemas.runtime import EventResponse, RunResponse from app.schemas.safety import SafetyPolicyItem, ToolPolicyResponse from app.schemas.world import ( WorldEntityResponse as CharacterWorldEntityResponse, ) from app.schemas.world import ( WorldRelationResponse, ) def _check_avatar(character_id: str) -> str | None: """检查角色是否有头像文件,有则返回 URL。""" from app.services.avatar import _find_avatar_file if _find_avatar_file(character_id): return f"/api/v1/characters/{character_id}/avatar" return None def character_to_response(character: Character) -> CharacterResponse: return CharacterResponse( id=character.id, name=character.name, display_name=character.display_name, status=character.status, language=character.language, profile=loads_dict(character.profile_json), model_policy=loads_dict(character.model_policy_json), current_personality_version_id=character.current_personality_version_id, privacy_level=character.privacy_level, owner_scope=character.owner_scope, is_system_template=character.is_system_template, source_template_id=character.source_template_id, avatar_url=_check_avatar(character.id), created_at=to_iso(character.created_at) or "", updated_at=to_iso(character.updated_at) or "", ) def character_to_summary( character: Character, *, memory_count: int, personality_version: int ) -> CharacterSummaryResponse: """人物列表项(api-spec §6);memory_count / personality_version 由调用方查得。""" return CharacterSummaryResponse( id=character.id, name=character.name, display_name=character.display_name, status=character.status, memory_count=memory_count, personality_version=personality_version, activated_at=to_iso(character.activated_at), ) def personality_to_response(personality: PersonalityVersion) -> PersonalityResponse: return PersonalityResponse( id=personality.id, character_id=personality.character_id, version=personality.version, identity_profile=personality.identity_profile, role_destiny=personality.role_destiny, personality_seed=personality.personality_seed, traits=loads_dict(personality.traits), expression_style=personality.expression_style, values=loads_dict(personality.values), relationship_pattern=personality.relationship_pattern, emotion_baseline=loads_dict(personality.emotion_baseline), reason=personality.reason, evidence_ids=loads_list(personality.evidence_ids_json), created_by=personality.created_by, created_at=to_iso(personality.created_at) or "", ) def proposal_to_response(proposal: PersonalityProposal) -> PersonalityProposalResponse: return PersonalityProposalResponse( proposal_id=proposal.id, character_id=proposal.character_id, status=proposal.status, reason=proposal.reason, fields=loads_dict(proposal.fields_json), resulting_version_id=proposal.resulting_version_id, reviewed_at=to_iso(proposal.reviewed_at), created_at=to_iso(proposal.created_at) or "", ) def chat_session_to_response( chat_session: ChatSession, *, message_count: int = 0, last_message_preview: str | None = None, ) -> ChatSessionResponse: return ChatSessionResponse( session_id=chat_session.id, character_id=chat_session.character_id, title=chat_session.title, status=chat_session.status, created_at=to_iso(chat_session.created_at) or "", updated_at=to_iso(chat_session.updated_at) or "", message_count=message_count, last_message_preview=last_message_preview, ) def chat_message_to_response(message: ChatMessage) -> ChatMessageResponse: return ChatMessageResponse( message_id=message.id, session_id=message.session_id, character_id=message.character_id, role=message.role, content=message.content, modality=message.modality, run_id=message.run_id, metadata=loads_dict(message.metadata_json), sensitivity=message.sensitivity, versions=loads_list(message.versions_json), active_version_idx=message.active_version_idx, created_at=to_iso(message.created_at) or "", deleted_at=to_iso(message.deleted_at), ) def memory_to_response(memory: Memory) -> MemoryResponse: return MemoryResponse( id=memory.id, character_id=memory.character_id, type=memory.type, title=memory.title, content=memory.content, source=memory.source, confidence=memory.confidence, importance=memory.importance, sensitivity=memory.sensitivity, evidence_ids=loads_list(memory.evidence_ids_json), status=memory.status, created_at=to_iso(memory.created_at) or "", updated_at=to_iso(memory.updated_at) or "", deleted_at=to_iso(memory.deleted_at), expires_at=to_iso(memory.expires_at), ) def run_to_response(run: Run) -> RunResponse: return RunResponse( id=run.id, character_id=run.character_id, session_id=run.session_id, status=run.status, goal=run.goal, origin=run.origin, model_hint=run.model_hint, started_at=to_iso(run.started_at), completed_at=to_iso(run.completed_at), error_code=run.error_code, error_message=run.error_message, created_at=to_iso(run.created_at) or "", updated_at=to_iso(run.updated_at) or "", ) def event_to_response(event: Event) -> EventResponse: payload: dict[str, Any] = loads_dict(event.payload_json) return EventResponse( event_id=event.id, run_id=event.run_id, character_id=event.character_id, type=event.type, sequence=event.sequence, visibility=event.visibility, payload=payload, created_at=to_iso(event.created_at) or "", trace_id=event.trace_id, ) # —— 司命台治理对象 —— def goal_to_response(goal: Goal) -> GoalNode: return GoalNode( id=goal.id, character_id=goal.character_id, title=goal.title, description=goal.description, level=goal.level, type=goal.type, status=goal.status, priority=goal.priority, progress=goal.progress, constraints=loads_list(goal.constraints_json), parent_id=goal.parent_id, success_criteria=loads_list(goal.success_criteria_json), updated_at=to_iso(goal.updated_at), ) def perception_channel_to_response(channel: PerceptionChannel) -> PerceptionChannelResponse: return PerceptionChannelResponse( channel=channel.channel, name=channel.name, description=channel.description, enabled=channel.enabled, consent_status=channel.consent_status, privacy_level=channel.privacy_level, risk_level=channel.risk_level, last_active_at=to_iso(channel.last_active_at), ) def world_entity_to_response(entity: WorldEntity) -> WorldEntityResponse: return WorldEntityResponse( id=entity.id, type=entity.type, name=entity.name, attributes=loads_dict(entity.attributes_json), state_type=entity.state_type, confidence=entity.confidence, sources=loads_list(entity.sources_json), updated_at=to_iso(entity.updated_at), ) def character_world_entity_to_response( entity: CharacterWorldEntity, ) -> CharacterWorldEntityResponse: """人物运行时世界实体(api-spec §12);与治理版 WorldEntity 按 character_id 隔离。""" return CharacterWorldEntityResponse( id=entity.id, character_id=entity.character_id, type=entity.type, name=entity.name, attributes=loads_dict(entity.attributes_json), state_type=entity.state_type, status=entity.status, confidence=entity.confidence, evidence_ids=loads_list(entity.evidence_ids_json), sources=loads_list(entity.sources_json), created_at=to_iso(entity.created_at) or "", updated_at=to_iso(entity.updated_at) or "", ) def world_relation_to_response(relation: WorldRelation) -> WorldRelationResponse: return WorldRelationResponse( id=relation.id, character_id=relation.character_id, subject_id=relation.subject_id, predicate=relation.predicate, object_id=relation.object_id, confidence=relation.confidence, evidence_ids=loads_list(relation.evidence_ids_json), created_at=to_iso(relation.created_at) or "", updated_at=to_iso(relation.updated_at) or "", ) def plan_to_response(plan: Plan) -> PlanSummaryResponse: return PlanSummaryResponse( id=plan.id, goal_id=plan.goal_id or "", title=plan.title, status=plan.status, assumptions=loads_list(plan.assumptions_json), risks=loads_list(plan.risks_json), required_tools=loads_list(plan.required_tools_json), success_criteria=loads_list(plan.success_criteria_json), step_count=plan.step_count, completed_steps=plan.completed_steps, updated_at=to_iso(plan.updated_at), ) _ACTIVE_WINDOW = timedelta(hours=1) def agent_to_response(agent: AgentRosterEntry) -> AgentRosterResponse: """presence 为派生呈现态:启用且最近一小时内活跃 → active,否则 idle。""" presence = "idle" if agent.enabled and agent.last_active_at: last = agent.last_active_at if last.tzinfo is None: last = last.replace(tzinfo=UTC) if utc_now() - last <= _ACTIVE_WINDOW: presence = "active" return AgentRosterResponse( role=agent.role, name=agent.name, responsibility=agent.responsibility, enabled=agent.enabled, presence=presence, last_active_at=to_iso(agent.last_active_at), ) def generation_preferences_to_response( prefs: GenerationPreferences, ) -> GenerationPreferencesResponse: return GenerationPreferencesResponse( default_model_hint=prefs.default_model_hint, temperature=prefs.temperature, top_p=prefs.top_p, max_output_tokens=prefs.max_output_tokens, streaming=prefs.streaming, ) def audit_to_response(event: AuditEvent) -> AuditEntry: summary = event.reason or event.input_summary or event.output_summary or event.action return AuditEntry( audit_id=event.id, pivot=event.pivot or "memory", actor=event.actor, action=event.action, subject=event.resource_id, decision=event.decision, risk_level=event.risk_level, summary=summary, trace_id=event.trace_id, created_at=to_iso(event.created_at), ) def tool_policy_to_response(policy: ToolPolicy, *, executable: bool = False) -> ToolPolicyResponse: """工具策略完整元数据;executable 由调用方按 builtin/registered specs 派生。""" return ToolPolicyResponse( name=policy.name, description=policy.description, risk_level=policy.risk_level, permission_policy=policy.permission_policy, side_effect=policy.side_effect, audit_required=policy.audit_required, source=policy.source, enabled=policy.enabled, executable=executable, created_at=to_iso(policy.created_at), updated_at=to_iso(policy.updated_at), ) def tool_policy_to_safety_item(policy: ToolPolicy) -> SafetyPolicyItem: """策略子集(§16 GET /safety/policies 元素)。""" return SafetyPolicyItem( tool=policy.name, risk_level=policy.risk_level, permission_policy=policy.permission_policy, side_effect=policy.side_effect, audit_required=policy.audit_required, ) def import_job_to_response(job: CharacterImportJob) -> ImportJobResponse: return ImportJobResponse( id=job.id, character_id=job.character_id, material_type=job.material_type, source_type=job.source_type, title=job.title, status=job.status, format=job.format, media_type=job.media_type, mime_type=job.mime_type, item_count=job.item_count, consent_status=job.consent_status, usage_scope=job.usage_scope, error_code=job.error_code, error_message=job.error_message, created_at=to_iso(job.created_at) or "", updated_at=to_iso(job.updated_at) or "", ) def import_item_to_response(item: ImportItem) -> ImportItemResponse: return ImportItemResponse( id=item.id, import_job_id=item.import_job_id, item_type=item.item_type, role=item.role, sequence=item.sequence, content=item.content, metadata=loads_dict(item.metadata_json), created_at=to_iso(item.created_at) or "", ) def profile_candidate_to_response(candidate: ProfileCandidate) -> ProfileCandidateResponse: return ProfileCandidateResponse( id=candidate.id, character_id=candidate.character_id, import_job_id=candidate.import_job_id, field=candidate.field, value=candidate.value, confidence=candidate.confidence, evidence_ids=loads_list(candidate.evidence_ids_json), status=candidate.status, reason=candidate.reason, reviewed_at=to_iso(candidate.reviewed_at), created_at=to_iso(candidate.created_at) or "", ) def memory_candidate_to_response(candidate: MemoryCandidate) -> MemoryCandidateResponse: return MemoryCandidateResponse( id=candidate.id, character_id=candidate.character_id, import_job_id=candidate.import_job_id, cycle_id=candidate.cycle_id, type=candidate.type, title=candidate.title, content=candidate.content, confidence=candidate.confidence, importance=candidate.importance, sensitivity=candidate.sensitivity, evidence_ids=loads_list(candidate.evidence_ids_json), status=candidate.status, reason=candidate.reason, reviewed_at=to_iso(candidate.reviewed_at), created_at=to_iso(candidate.created_at) or "", )