"""司命台治理对象 CRUD 服务(api-spec §19.4)。 五组开放集合(目标 / 感知通道 / 世界实体 / 计划 / 天工阁角色)的增删改查; 治理写操作一律记审计事件(audit_),并入起居注(§19.3)。pivot 由 action 前缀推断。 所有函数自管 ``AsyncSessionLocal``(路由直接调用,不传入 session)。 """ from app.core.errors import AppError from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.models.governance import ( AgentRosterEntry, Goal, PerceptionChannel, Plan, WorldEntity, ) from app.modules.runtime.events import Event from app.repositories import governance as governance_repo from app.repositories import runtime as runtime_repo from app.repositories.json_utils import dumps, loads_dict from app.schemas.admin import ( AgentRosterResponse, CreateAgentRequest, CreateGoalRequest, CreatePerceptionChannelRequest, CreatePlanRequest, CreateWorldEntityRequest, GoalNode, PerceptionChannelResponse, PlanSummaryResponse, UpdateAgentRequest, UpdateGoalRequest, UpdatePerceptionChannelRequest, UpdatePlanRequest, UpdateWorldEntityRequest, WorldEntityResponse, ) from app.schemas.safety import ( ToolCallDetail, ) from app.services.audit import record_audit_event from app.services.serializers import ( agent_to_response, goal_to_response, perception_channel_to_response, plan_to_response, world_entity_to_response, ) # —— 太一枢 · 目标 —— async def list_goals() -> list[GoalNode]: async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals(session) return [goal_to_response(goal) for goal in goals] async def list_goals_by_character( character_id: str, status_filter: str | None = None, ) -> list[GoalNode]: async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals_by_character( session, character_id, status_filter=status_filter ) return [goal_to_response(goal) for goal in goals] async def load_active_goals_context(character_id: str) -> str | None: """加载活跃目标并格式化为 system prompt 片段,供运行时注入。 目标按优先级排序(``prioritize_active_goals``),最高优先级在前。 """ async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals_by_character( session, character_id, status_filter="active" ) if not goals: return None lines = [ f"- [{g.level}] {g.title}(优先级 {g.priority},进度 {g.progress}%)" for g in sorted(goals, key=lambda g: -(g.priority or 0)) ] return "【当前目标】\n" + "\n".join(lines) async def emit_goal_event( goal: Goal, event_type: str, extra_payload: dict | None = None, ) -> None: """向事件存储写入 goal.* 事件。""" from app.main import app # 惰性 import 避免启动循环 event_store = getattr(app.state, "event_store", None) if event_store is None: return payload = { "goal_id": goal.id, "character_id": goal.character_id, "title": goal.title, "level": goal.level, "type": goal.type, "status": goal.status, "priority": goal.priority, "progress": goal.progress, } if extra_payload: payload.update(extra_payload) event = Event( type=f"goal.{event_type}", source="goal_orchestrator", payload=payload, character_id=goal.character_id, visibility="audit", ) await event_store.append(event) async def create_goal(payload: CreateGoalRequest) -> GoalNode: async with AsyncSessionLocal() as session: goal = Goal( title=payload.title, description=payload.description, level=payload.level, type=payload.type, character_id=payload.character_id, status="pending", priority=payload.priority, constraints_json=dumps(payload.constraints), parent_id=payload.parent_id, success_criteria_json=dumps(payload.success_criteria), ) await governance_repo.add_goal(session, goal) await record_audit_event( session, action="goal.created", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() await session.refresh(goal) await emit_goal_event(goal, "created") return goal_to_response(goal) async def update_goal(goal_id: str, payload: UpdateGoalRequest) -> GoalNode: async with AsyncSessionLocal() as session: goal = await governance_repo.get_goal(session, goal_id) if goal is None: raise AppError("not_found", "目标不存在。") old_status = goal.status updates = payload.model_dump(exclude_unset=True) constraints = updates.pop("constraints", None) for key, value in updates.items(): setattr(goal, key, value) if constraints is not None: goal.constraints_json = dumps(constraints) goal.updated_at = utc_now() await record_audit_event( session, action="goal.updated", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() await session.refresh(goal) if old_status != goal.status: if goal.status == "completed": await emit_goal_event(goal, "completed", {"previous_status": old_status}) else: await emit_goal_event(goal, "updated", {"previous_status": old_status}) else: await emit_goal_event(goal, "updated", {"previous_status": old_status}) return goal_to_response(goal) async def delete_goal(goal_id: str) -> None: async with AsyncSessionLocal() as session: goal = await governance_repo.get_goal(session, goal_id) if goal is None: raise AppError("not_found", "目标不存在。") if goal.level == "highest": raise AppError("invalid_request", "最高目标(使命级)不可删除。") await session.delete(goal) await record_audit_event( session, action="goal.deleted", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() # —— 烛照枢 · 感知通道 —— async def list_perception_channels() -> list[PerceptionChannelResponse]: async with AsyncSessionLocal() as session: channels = await governance_repo.list_perception_channels(session) return [perception_channel_to_response(channel) for channel in channels] async def create_perception_channel( payload: CreatePerceptionChannelRequest, ) -> PerceptionChannelResponse: async with AsyncSessionLocal() as session: if await governance_repo.get_perception_channel(session, payload.channel) is not None: raise AppError("conflict", "感知通道已存在。") channel = PerceptionChannel( channel=payload.channel, name=payload.name, description=payload.description, risk_level=payload.risk_level, ) await governance_repo.add_perception_channel(session, channel) await record_audit_event( session, action="perception.created", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() await session.refresh(channel) return perception_channel_to_response(channel) async def update_perception_channel( channel_id: str, payload: UpdatePerceptionChannelRequest ) -> PerceptionChannelResponse: async with AsyncSessionLocal() as session: channel = await governance_repo.get_perception_channel(session, channel_id) if channel is None: raise AppError("not_found", "感知通道不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(channel, key, value) channel.updated_at = utc_now() await record_audit_event( session, action="perception.updated", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() await session.refresh(channel) return perception_channel_to_response(channel) async def delete_perception_channel(channel_id: str) -> None: async with AsyncSessionLocal() as session: channel = await governance_repo.get_perception_channel(session, channel_id) if channel is None: raise AppError("not_found", "感知通道不存在。") await session.delete(channel) await record_audit_event( session, action="perception.deleted", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() # —— 河图枢 · 世界实体 —— async def list_world_entities() -> list[WorldEntityResponse]: async with AsyncSessionLocal() as session: entities = await governance_repo.list_world_entities(session) return [world_entity_to_response(entity) for entity in entities] async def create_world_entity( payload: CreateWorldEntityRequest, ) -> WorldEntityResponse: async with AsyncSessionLocal() as session: entity = WorldEntity( type=payload.type, name=payload.name, attributes_json=dumps(payload.attributes), state_type=payload.state_type, confidence=payload.confidence, sources_json=dumps(payload.sources), ) await governance_repo.add_world_entity(session, entity) await record_audit_event( session, action="world.created", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() await session.refresh(entity) return world_entity_to_response(entity) async def update_world_entity( entity_id: str, payload: UpdateWorldEntityRequest ) -> WorldEntityResponse: async with AsyncSessionLocal() as session: entity = await governance_repo.get_world_entity(session, entity_id) if entity is None: raise AppError("not_found", "世界实体不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(entity, key, value) entity.updated_at = utc_now() await record_audit_event( session, action="world.updated", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() await session.refresh(entity) return world_entity_to_response(entity) async def delete_world_entity(entity_id: str) -> None: async with AsyncSessionLocal() as session: entity = await governance_repo.get_world_entity(session, entity_id) if entity is None: raise AppError("not_found", "世界实体不存在。") await session.delete(entity) await record_audit_event( session, action="world.deleted", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() # —— 洛书枢 · 计划 —— async def list_plans() -> list[PlanSummaryResponse]: async with AsyncSessionLocal() as session: plans = await governance_repo.list_plans(session) return [plan_to_response(plan) for plan in plans] async def create_plan(payload: CreatePlanRequest) -> PlanSummaryResponse: async with AsyncSessionLocal() as session: plan = Plan( goal_id=payload.goal_id, title=payload.title, assumptions_json=dumps(payload.assumptions), risks_json=dumps(payload.risks), ) await governance_repo.add_plan(session, plan) await record_audit_event( session, action="plan.created", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() await session.refresh(plan) return plan_to_response(plan) async def update_plan(plan_id: str, payload: UpdatePlanRequest) -> PlanSummaryResponse: async with AsyncSessionLocal() as session: plan = await governance_repo.get_plan(session, plan_id) if plan is None: raise AppError("not_found", "计划不存在。") updates = payload.model_dump(exclude_unset=True) assumptions = updates.pop("assumptions", None) risks = updates.pop("risks", None) for key, value in updates.items(): setattr(plan, key, value) if assumptions is not None: plan.assumptions_json = dumps(assumptions) if risks is not None: plan.risks_json = dumps(risks) plan.updated_at = utc_now() await record_audit_event( session, action="plan.updated", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() await session.refresh(plan) return plan_to_response(plan) async def delete_plan(plan_id: str) -> None: async with AsyncSessionLocal() as session: plan = await governance_repo.get_plan(session, plan_id) if plan is None: raise AppError("not_found", "计划不存在。") await session.delete(plan) await record_audit_event( session, action="plan.deleted", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() # —— 天工阁枢 · 角色在岗 —— async def list_agent_roster() -> list[AgentRosterResponse]: async with AsyncSessionLocal() as session: agents = await governance_repo.list_agent_roster(session) return [agent_to_response(agent) for agent in agents] async def create_agent(payload: CreateAgentRequest) -> AgentRosterResponse: async with AsyncSessionLocal() as session: if await governance_repo.get_agent(session, payload.role) is not None: raise AppError("conflict", "角色已存在。") agent = AgentRosterEntry( role=payload.role, name=payload.name, responsibility=payload.responsibility, ) await governance_repo.add_agent(session, agent) await record_audit_event( session, action="agent.created", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() await session.refresh(agent) return agent_to_response(agent) async def update_agent(role: str, payload: UpdateAgentRequest) -> AgentRosterResponse: async with AsyncSessionLocal() as session: agent = await governance_repo.get_agent(session, role) if agent is None: raise AppError("not_found", "角色不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(agent, key, value) agent.updated_at = utc_now() await record_audit_event( session, action="agent.updated", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() await session.refresh(agent) return agent_to_response(agent) async def delete_agent(role: str) -> None: async with AsyncSessionLocal() as session: agent = await governance_repo.get_agent(session, role) if agent is None: raise AppError("not_found", "角色不存在。") await session.delete(agent) await record_audit_event( session, action="agent.deleted", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() # —— 百工枢 · 工具策略(§15 / §16 共享)—— async def get_tool_call(tool_call_id: str) -> ToolCallDetail: """GET /tools/calls/{tool_call_id}:单条 tool.* 事件详情。 tool_call_id 对应一炁事件存储的事件主键;仅返回 tool.* 类型事件,否则 404。 """ from app.core.time import to_iso async with AsyncSessionLocal() as session: event = await runtime_repo.get_event(session, tool_call_id) if event is None or not event.type.startswith("tool."): raise AppError("not_found", f"工具调用 {tool_call_id} 不存在。") payload = loads_dict(event.payload_json) return ToolCallDetail( event_id=event.id, type=event.type, tool=payload.get("tool"), run_id=event.run_id, character_id=event.character_id, trace_id=event.trace_id, payload=payload, created_at=to_iso(event.created_at), )