Files
hunjing/backend/app/services/serializers.py
T
2026-08-21 16:00:13 +08:00

460 lines
16 KiB
Python

"""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 "",
)