初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,369 @@
|
||||
"""司命台管理聚合服务(api-spec §19.1–§19.3)。
|
||||
|
||||
九枢健康度由各枢运行/治理数据派生(不落长期状态,§19.1);朱批待办复用
|
||||
ConfirmationStore(§19.2);起居注审计复用 audit_events(§19.3)。
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.application.tool_runtime.runtime import ToolRuntime
|
||||
from app.core.config import settings
|
||||
from app.core.security import redact_text
|
||||
from app.core.time import to_iso, utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.governance import (
|
||||
AgentRosterEntry,
|
||||
Goal,
|
||||
PerceptionChannel,
|
||||
Plan,
|
||||
WorldEntity,
|
||||
)
|
||||
from app.models.idempotency import IdempotencyKey
|
||||
from app.models.memory import Memory
|
||||
from app.models.runtime import Event, Run
|
||||
from app.models.safety import AuditEvent
|
||||
from app.modules.safety.confirmation import ConfirmationStore, PendingConfirmation
|
||||
from app.repositories import audit as audit_repo
|
||||
from app.repositories import characters as character_repo
|
||||
from app.repositories import governance as governance_repo
|
||||
from app.repositories import idempotency as idempotency_repo
|
||||
from app.repositories import runtime as runtime_repo
|
||||
from app.schemas.admin import (
|
||||
AuditEntry,
|
||||
PendingApproval,
|
||||
PivotHealthReport,
|
||||
PivotHealthSummary,
|
||||
TraceDetail,
|
||||
TraceItem,
|
||||
TraceListResponse,
|
||||
TraceSummary,
|
||||
)
|
||||
from app.schemas.common import Page
|
||||
from app.services.serializers import audit_to_response
|
||||
|
||||
# 九枢全集(契约字典 §5.9):枢标识、中文名、工程模块名。
|
||||
_PIVOT_REGISTRY: list[tuple[str, str, str]] = [
|
||||
("personality_core", "灵核枢", "Personality Core"),
|
||||
("goal_orchestrator", "太一枢", "Goal Orchestrator"),
|
||||
("safety_governor", "司命枢", "Safety Governor"),
|
||||
("perception", "烛照枢", "Perception Layer"),
|
||||
("world_model", "河图枢", "World Model"),
|
||||
("planner", "洛书枢", "Planner"),
|
||||
("memory", "息壤枢", "Memory System"),
|
||||
("tool_runtime", "百工枢", "Tool Runtime"),
|
||||
("agent_society", "天工阁枢", "Agent Society"),
|
||||
]
|
||||
|
||||
|
||||
def _summarize_input(value: dict | None) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
text = json.dumps(value, ensure_ascii=False, default=str)
|
||||
return redact_text(text)[:200]
|
||||
|
||||
|
||||
async def get_pivots_health(
|
||||
*,
|
||||
confirmation_store: ConfirmationStore,
|
||||
tool_runtime: ToolRuntime | None,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> PivotHealthReport:
|
||||
async with AsyncSessionLocal() as session:
|
||||
pending_count = len(await confirmation_store.list_pending())
|
||||
goals_n = await governance_repo.count(session, Goal)
|
||||
world_n = await governance_repo.count(session, WorldEntity)
|
||||
plans_n = await governance_repo.count(session, Plan)
|
||||
perception_n = await governance_repo.count(session, PerceptionChannel)
|
||||
agents_n = await governance_repo.count(session, AgentRosterEntry)
|
||||
memories_n = await governance_repo.count(session, Memory)
|
||||
character = await character_repo.get_active_character(session, owner_scope)
|
||||
has_personality = bool(character and character.current_personality_version_id)
|
||||
tool_enabled = settings.enable_tool_runtime and tool_runtime is not None
|
||||
now = to_iso(utc_now())
|
||||
|
||||
raw: list[tuple[str, str, str, bool, str]] = [
|
||||
(
|
||||
"personality_core",
|
||||
"steady" if has_personality else "void",
|
||||
"人格版本已就绪" if has_personality else "人格未初始化",
|
||||
),
|
||||
("goal_orchestrator", "steady" if goals_n else "void", f"目标 {goals_n} 项"),
|
||||
(
|
||||
"safety_governor",
|
||||
"cracked" if pending_count else "steady",
|
||||
f"{pending_count} 项工具调用待裁定" if pending_count else "无待裁定",
|
||||
),
|
||||
("perception", "steady" if perception_n else "void", f"感知通道 {perception_n} 项"),
|
||||
("world_model", "steady" if world_n else "void", f"世界实体 {world_n} 项"),
|
||||
("planner", "steady" if plans_n else "void", f"计划 {plans_n} 项"),
|
||||
("memory", "steady" if memories_n else "void", f"记忆 {memories_n} 条"),
|
||||
(
|
||||
"tool_runtime",
|
||||
"breathing" if tool_enabled else "void",
|
||||
"沙箱运行时在线" if tool_enabled else "工具运行时未启用",
|
||||
),
|
||||
("agent_society", "steady" if agents_n else "void", f"在岗角色 {agents_n} 位"),
|
||||
]
|
||||
|
||||
name_map = {pivot: (name, module) for pivot, name, module in _PIVOT_REGISTRY}
|
||||
pivots: list[PivotHealthSummary] = []
|
||||
for pivot, health, summary in raw:
|
||||
name, module = name_map[pivot]
|
||||
pivots.append(
|
||||
PivotHealthSummary(
|
||||
pivot=pivot,
|
||||
name=name,
|
||||
module=module,
|
||||
health=health,
|
||||
summary=summary,
|
||||
updated_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
alerts = sum(1 for item in pivots if item.health == "cracked")
|
||||
return PivotHealthReport(
|
||||
overall="cracked" if alerts else "steady", alerts=alerts, pivots=pivots
|
||||
)
|
||||
|
||||
|
||||
async def get_pending_approvals(
|
||||
*,
|
||||
confirmation_store: ConfirmationStore,
|
||||
tool_runtime: ToolRuntime | None,
|
||||
) -> list[PendingApproval]:
|
||||
specs = {spec.name: spec for spec in tool_runtime.list_specs()} if tool_runtime else {}
|
||||
approvals: list[PendingApproval] = []
|
||||
for pending in await confirmation_store.list_pending():
|
||||
spec = specs.get(pending.tool)
|
||||
permission_policy = str(spec.permission_policy) if spec else "ask"
|
||||
possible_impact = "可能产生副作用" if (spec and spec.side_effect) else None
|
||||
approvals.append(_pending_to_approval(pending, permission_policy, possible_impact))
|
||||
return approvals
|
||||
|
||||
|
||||
def _pending_to_approval(
|
||||
pending: PendingConfirmation, permission_policy: str, possible_impact: str | None
|
||||
) -> PendingApproval:
|
||||
return PendingApproval(
|
||||
confirmation_id=pending.confirmation_id,
|
||||
character_id=pending.character_id,
|
||||
tool=pending.tool,
|
||||
risk_level=pending.risk_level,
|
||||
permission_policy=permission_policy,
|
||||
action_summary=f"调用工具 {pending.tool}",
|
||||
input_summary=_summarize_input(pending.input),
|
||||
possible_impact=possible_impact,
|
||||
reason=pending.reason,
|
||||
created_at=pending.created_at,
|
||||
)
|
||||
|
||||
|
||||
async def get_audit_timeline(
|
||||
*,
|
||||
pivot: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Page[AuditEntry]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
events, total = await audit_repo.list_audit_events(
|
||||
session, pivot=pivot, page=page, page_size=page_size
|
||||
)
|
||||
return Page(
|
||||
items=[audit_to_response(event) for event in events],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
# —— §19.5 链路追踪(按 trace_id 聚合 events / audit_events / idempotency_keys)——
|
||||
|
||||
|
||||
def _event_to_item(event: Event) -> TraceItem:
|
||||
try:
|
||||
payload = json.loads(event.payload_json) if event.payload_json else {}
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
payload = {}
|
||||
return TraceItem(
|
||||
kind="event",
|
||||
id=event.id,
|
||||
created_at=event.created_at,
|
||||
source_label=f"运行事件 / {event.type}",
|
||||
summary=event.type,
|
||||
detail={
|
||||
"run_id": event.run_id,
|
||||
"source": event.source,
|
||||
"type": event.type,
|
||||
"payload": payload,
|
||||
"sequence": event.sequence,
|
||||
"parent_id": event.parent_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _audit_to_item(audit: AuditEvent) -> TraceItem:
|
||||
parts = [audit.decision, audit.risk_level]
|
||||
if audit.reason:
|
||||
parts.append(audit.reason[:60])
|
||||
return TraceItem(
|
||||
kind="audit",
|
||||
id=audit.id,
|
||||
created_at=audit.created_at,
|
||||
source_label=f"安全审计 / {audit.action}",
|
||||
summary=" · ".join(parts),
|
||||
detail={
|
||||
"pivot": audit.pivot,
|
||||
"action": audit.action,
|
||||
"actor": audit.actor,
|
||||
"decision": audit.decision,
|
||||
"risk_level": audit.risk_level,
|
||||
"reason": audit.reason,
|
||||
"resource_type": audit.resource_type,
|
||||
"resource_id": audit.resource_id,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _idempotency_to_item(idem: IdempotencyKey) -> TraceItem:
|
||||
return TraceItem(
|
||||
kind="idempotency",
|
||||
id=idem.id,
|
||||
created_at=idem.created_at,
|
||||
source_label=f"请求幂等 / {idem.method}",
|
||||
summary=f"{idem.method} {idem.path} → {idem.response_status_code}",
|
||||
detail={
|
||||
"method": idem.method,
|
||||
"path": idem.path,
|
||||
"status": idem.status,
|
||||
"response_status_code": idem.response_status_code,
|
||||
"scope": idem.scope,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def get_trace(trace_id: str) -> TraceDetail:
|
||||
"""按 trace_id 聚合 events / audit_events / idempotency_keys 三表为升序时间线。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
events = await runtime_repo.list_events_by_trace(session, trace_id)
|
||||
audits = await audit_repo.list_audit_events_by_trace(session, trace_id)
|
||||
idempotencies = await idempotency_repo.list_by_trace(session, trace_id)
|
||||
|
||||
items: list[TraceItem] = [_event_to_item(event) for event in events]
|
||||
items.extend(_audit_to_item(audit) for audit in audits)
|
||||
items.extend(_idempotency_to_item(idem) for idem in idempotencies)
|
||||
items.sort(key=lambda item: item.created_at)
|
||||
|
||||
run_ids = sorted({event.run_id for event in events if event.run_id})
|
||||
character_ids = sorted(
|
||||
{
|
||||
*[event.character_id for event in events if event.character_id],
|
||||
*[audit.character_id for audit in audits if audit.character_id],
|
||||
}
|
||||
)
|
||||
session_ids = (
|
||||
sorted(await runtime_repo.list_session_ids_by_runs(session, run_ids)) if run_ids else []
|
||||
)
|
||||
return TraceDetail(
|
||||
trace_id=trace_id,
|
||||
character_ids=character_ids,
|
||||
run_ids=run_ids,
|
||||
session_ids=session_ids,
|
||||
items=items,
|
||||
)
|
||||
|
||||
|
||||
async def list_time_events(*, limit: int = 100, offset: int = 0) -> tuple[list[Event], int]:
|
||||
"""列出辰正 time.* 事件(source='time_sense'),供起居注时间河展示(设计 §8 司命台)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
result = await session.execute(
|
||||
select(Event)
|
||||
.where(Event.source == "time_sense")
|
||||
.order_by(Event.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
items = list(result.scalars().all())
|
||||
count_result = await session.execute(
|
||||
select(func.count()).select_from(Event).where(Event.source == "time_sense")
|
||||
)
|
||||
total = int(count_result.scalar_one())
|
||||
return items, total
|
||||
|
||||
|
||||
async def list_traces(*, limit: int = 50) -> TraceListResponse:
|
||||
"""列出最近活动的 trace_id 概览(跨 events / audit_events / idempotency_keys 聚合),
|
||||
含每条链路涉及的人物与对话归属。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
merged: dict[str, dict[str, object]] = {}
|
||||
|
||||
def _entry(tid: str) -> dict[str, object]:
|
||||
return merged.setdefault(
|
||||
tid,
|
||||
{
|
||||
"trace_id": tid,
|
||||
"last_seen": None,
|
||||
"event_count": 0,
|
||||
"audit_count": 0,
|
||||
"idempotency_count": 0,
|
||||
"character_ids": set(),
|
||||
"session_ids": set(),
|
||||
},
|
||||
)
|
||||
|
||||
def _absorb(stats: list[tuple[str, int, object]], count_key: str) -> None:
|
||||
for tid, cnt, last in stats:
|
||||
entry = _entry(tid)
|
||||
entry[count_key] = cnt
|
||||
if last is not None and (entry["last_seen"] is None or last > entry["last_seen"]):
|
||||
entry["last_seen"] = last
|
||||
|
||||
_absorb(await runtime_repo.summarize_by_trace(session), "event_count")
|
||||
_absorb(await audit_repo.summarize_by_trace(session), "audit_count")
|
||||
_absorb(await idempotency_repo.summarize_by_trace(session), "idempotency_count")
|
||||
|
||||
# 人物归属(events + audits 的 character_id)
|
||||
for tid, cid in (
|
||||
await session.execute(
|
||||
select(Event.trace_id, Event.character_id).where(
|
||||
Event.trace_id.is_not(None), Event.character_id.is_not(None)
|
||||
)
|
||||
)
|
||||
).all():
|
||||
_entry(tid)["character_ids"].add(cid)
|
||||
for tid, cid in (
|
||||
await session.execute(
|
||||
select(AuditEvent.trace_id, AuditEvent.character_id).where(
|
||||
AuditEvent.trace_id.is_not(None),
|
||||
AuditEvent.character_id.is_not(None),
|
||||
)
|
||||
)
|
||||
).all():
|
||||
_entry(tid)["character_ids"].add(cid)
|
||||
|
||||
# 对话归属(events.run_id → runs.session_id)
|
||||
for tid, sid in (
|
||||
await session.execute(
|
||||
select(Event.trace_id, Run.session_id)
|
||||
.join(Run, Event.run_id == Run.id)
|
||||
.where(Event.trace_id.is_not(None), Run.session_id.is_not(None))
|
||||
)
|
||||
).all():
|
||||
_entry(tid)["session_ids"].add(sid)
|
||||
|
||||
total = len(merged)
|
||||
ordered = sorted(merged.values(), key=lambda e: e["last_seen"], reverse=True)
|
||||
items = [
|
||||
TraceSummary(
|
||||
trace_id=str(entry["trace_id"]),
|
||||
last_seen=entry["last_seen"], # type: ignore[arg-type]
|
||||
character_ids=sorted(entry["character_ids"]), # type: ignore[arg-type]
|
||||
session_ids=sorted(entry["session_ids"]), # type: ignore[arg-type]
|
||||
event_count=int(entry["event_count"]),
|
||||
audit_count=int(entry["audit_count"]),
|
||||
idempotency_count=int(entry["idempotency_count"]),
|
||||
)
|
||||
for entry in ordered[:limit]
|
||||
]
|
||||
return TraceListResponse(items=items, total=total)
|
||||
Reference in new issue
Block a user