Files
hunjing/backend/app/services/serializers.py
T
chenxin 684261f4b4 M5:百工与司命——能力注册表、授契行契请命、禁域裁决与观察标证
- 百工域 domains/baigong:capabilities(统一能力契约:执行类型 builtin/qiankun/mcp/http + 四维风险标注 + 来源版本)、mandates(授契 grant / 行契 charter 预算时间窗,撤销立即生效)、tool_calls(净化摘要 + 观察标证);请命复用 pending_confirmations 加 expires_at/brief_json(超时=拒绝执行 HJ-1302,六要素说明)
- 司命裁决器 siming.py:四维取最高档(能力/数据/后果/情境);禁域为代码常量不可配置(递归删根/格式化/写裸设备/fork 炸弹、file:// 协议、密钥材料外发)——专项测试断言恶意标注与篡改对象仍拒 HJ-1301
- 执行链:禁域→裁决(HJ-1303 未授契拒且双留痕)→请命→执行(乾坤经 SandboxClient HMAC;MCP 经 call_tool;内置无副作用)→净化标证(观察非事实,敏感遮蔽+体量截断)→行动账
- MCP 登记/启停/刷新自动派生与撤下 capabilities(博闻登记接入点);内置能力种子(echo/datetime/qiankun.shell/web.fetch)
- 端点:/capabilities /mandates /petitions /tool-calls + /tools 兼容投影;前端百工页四区(能力目录四维标注/授契行契/请命批准拒绝/行动账观察标证)
- 退役:modules/safety|skills|tool_runtime|agent、application/tool_runtime、services/skills|safety、tool_policies/skills/model_endpoint_overrides 表(迁移 006)、前端 SkillManager/ToolsConsole/旧 tools 页
- 验收(test_baigong_siming 四用例):未授契被拒留痕、请命超时不执行、禁域不可关闭、结果为观察不入忆;后端 454、前端 220、BFF 4、神识 5 包全绿(2 个预存环境失败不变)
2026-08-27 19:17:21 +08:00

364 lines
12 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.governance import (
AgentRosterEntry,
GenerationPreferences,
Goal,
PerceptionChannel,
Plan,
WorldEntity,
)
from app.models.imports import CharacterImportJob, ImportItem
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.imports import ImportItemResponse, ImportJobResponse
from app.schemas.personality import PersonalityProposalResponse, PersonalityResponse
from app.schemas.preferences import GenerationPreferencesResponse
from app.schemas.runtime import EventResponse, RunResponse
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 run_to_response(run: Run) -> RunResponse:
return RunResponse(
id=run.id,
character_id=run.character_id,
status=run.status,
kind=run.kind,
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 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): # noqa: ANN001 — 旧导入候选(M6 退役)
from app.schemas.candidates import MemoryCandidateResponse
return MemoryCandidateResponse.model_validate(candidate, from_attributes=True)
def profile_candidate_to_response(candidate): # noqa: ANN001 — 旧导入候选(M6 退役)
from app.schemas.candidates import ProfileCandidateResponse
return ProfileCandidateResponse.model_validate(candidate, from_attributes=True)