- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位 - 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起) - 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数 - 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离 - 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换 - 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库 - 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id - 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继 - 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
454 lines
15 KiB
Python
454 lines
15 KiB
Python
"""司命台治理对象 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,
|
||
)
|
||
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,
|
||
GoalNode,
|
||
PerceptionChannelResponse,
|
||
PlanSummaryResponse,
|
||
UpdateAgentRequest,
|
||
UpdateGoalRequest,
|
||
UpdatePerceptionChannelRequest,
|
||
UpdatePlanRequest,
|
||
)
|
||
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,
|
||
)
|
||
|
||
# —— 太一枢 · 目标 ——
|
||
|
||
|
||
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_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),
|
||
)
|