Files
X-Financial/server/src/app/services/orchestrator.py
caoxiaozhu a3d40ad9f5 refactor(backend): update service layers
- services/ontology.py: update ontology service logic
- services/orchestrator.py: update orchestrator service logic
- services/user_agent.py: update user agent service logic
2026-05-12 07:19:21 +00:00

1037 lines
41 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from time import perf_counter
from typing import Any
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from app.core.agent_enums import (
AgentAssetStatus,
AgentAssetType,
AgentName,
AgentPermissionLevel,
AgentRunSource,
AgentRunStatus,
AgentToolType,
)
from app.core.logging import get_logger
from app.models.financial_record import (
AccountsPayableRecord,
AccountsReceivableRecord,
ExpenseClaim,
)
from app.schemas.agent_asset import AgentAssetListItem, AgentAssetRead
from app.schemas.ontology import OntologyParseRequest, OntologyParseResult
from app.schemas.orchestrator import (
OrchestratorRequest,
OrchestratorResponse,
OrchestratorTraceSummary,
)
from app.schemas.user_agent import UserAgentRequest, UserAgentResponse
from app.services.agent_assets import AgentAssetService
from app.services.agent_conversations import AgentConversationService
from app.services.expense_claims import ExpenseClaimService
from app.services.agent_foundation import AgentFoundationService
from app.services.agent_runs import AgentRunService
from app.services.ontology import SemanticOntologyService
from app.services.user_agent import UserAgentService
logger = get_logger("app.services.orchestrator")
SCENARIO_TO_DOMAIN = {
"expense": "expense",
"accounts_receivable": "ar",
"accounts_payable": "ap",
"knowledge": "knowledge",
"unknown": "system",
}
@dataclass(slots=True)
class ExecutionOutcome:
status: str
result: dict[str, Any]
degraded: bool
tool_count: int
failed_tool_count: int
class OrchestratorService:
def __init__(self, db: Session) -> None:
self.db = db
self.asset_service = AgentAssetService(db)
self.conversation_service = AgentConversationService(db)
self.expense_claim_service = ExpenseClaimService(db)
self.run_service = AgentRunService(db)
self.ontology_service = SemanticOntologyService(db)
self.user_agent_service = UserAgentService(db)
def run(self, payload: OrchestratorRequest) -> OrchestratorResponse:
AgentFoundationService(self.db).ensure_foundation_ready()
context_json = dict(payload.context_json or {})
conversation_id = str(payload.conversation_id or "").strip() or None
conversation = None
if payload.source == AgentRunSource.USER_MESSAGE.value:
conversation = self.conversation_service.get_or_create_conversation(
conversation_id=conversation_id,
user_id=payload.user_id,
source=payload.source,
context_json=context_json,
)
conversation_id = conversation.conversation_id
context_json = self.conversation_service.hydrate_context_json(
conversation=conversation,
context_json=context_json,
)
route_json: dict[str, Any] = {
"orchestrated_by": AgentName.ORCHESTRATOR.value,
"stage": "created",
}
if conversation_id:
route_json["conversation_id"] = conversation_id
run = self.run_service.create_run(
agent=AgentName.ORCHESTRATOR.value,
source=payload.source,
user_id=payload.user_id,
task_id=payload.task_id,
ontology_json={},
route_json=route_json,
permission_level=AgentPermissionLevel.READ.value,
status=AgentRunStatus.RUNNING.value,
result_summary="Orchestrator 已接收请求。",
)
try:
message, task_asset = self._resolve_message(payload)
if conversation is not None:
self.conversation_service.append_message(
conversation_id=conversation.conversation_id,
role="user",
content=message,
run_id=run.run_id,
message_json={
"attachment_names": context_json.get("attachment_names", []),
"attachment_count": context_json.get("attachment_count", 0),
"ocr_summary": context_json.get("ocr_summary", ""),
},
)
ontology = self.ontology_service.parse_for_run(
OntologyParseRequest(
query=message,
user_id=payload.user_id,
context_json=context_json,
),
run_id=run.run_id,
)
if context_json.get("simulate_orchestrator_exception"):
raise RuntimeError("simulated orchestrator exception")
selected_agent, route_reason = self._select_agent(payload, ontology)
capabilities = self._select_capabilities(
payload=payload,
ontology=ontology,
task_asset=task_asset,
)
selected_capability_codes = self._flatten_capability_codes(capabilities)
requires_confirmation = (
ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value
)
route_json = {
"orchestrated_by": AgentName.ORCHESTRATOR.value,
"stage": "routed",
"selected_agent": selected_agent,
"route_reason": route_reason,
"selected_capability_codes": selected_capability_codes,
"ontology_run_id": ontology.run_id,
}
if ontology.permission.level == AgentPermissionLevel.FORBIDDEN.value:
outcome = ExecutionOutcome(
status=AgentRunStatus.BLOCKED.value,
result={
"message": ontology.permission.reason,
"clarification_question": ontology.clarification_question,
"degraded": False,
},
degraded=False,
tool_count=0,
failed_tool_count=0,
)
selected_agent = None
route_reason = "permission_forbidden"
route_json["stage"] = "blocked"
route_json["route_reason"] = route_reason
elif ontology.clarification_required:
if selected_agent == AgentName.USER_AGENT.value and ontology.scenario == "expense":
clarification_response = self.user_agent_service.respond(
UserAgentRequest(
run_id=run.run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": True},
selected_capability_codes=selected_capability_codes,
degraded=False,
requires_confirmation=requires_confirmation,
)
)
clarification_result = self._build_user_agent_result(
clarification_response,
degraded=False,
)
clarification_result.update(
{
"clarification_required": True,
"missing_slots": ontology.missing_slots,
"ambiguity": ontology.ambiguity,
"parse_strategy": ontology.parse_strategy,
}
)
outcome = ExecutionOutcome(
status=AgentRunStatus.BLOCKED.value,
result=clarification_result,
degraded=False,
tool_count=0,
failed_tool_count=0,
)
else:
outcome = ExecutionOutcome(
status=AgentRunStatus.BLOCKED.value,
result={
"message": ontology.clarification_question or "需要补充更多上下文。",
"clarification_required": True,
"missing_slots": ontology.missing_slots,
"ambiguity": ontology.ambiguity,
"parse_strategy": ontology.parse_strategy,
"degraded": False,
},
degraded=False,
tool_count=0,
failed_tool_count=0,
)
route_reason = "clarification_required"
route_json["stage"] = "clarification"
route_json["route_reason"] = route_reason
elif selected_agent == AgentName.HERMES.value:
outcome = self._execute_hermes(
payload=payload,
run_id=run.run_id,
ontology=ontology,
capabilities=capabilities,
requires_confirmation=requires_confirmation,
task_asset=task_asset,
context_json=context_json,
)
else:
outcome = self._execute_user_agent(
payload=payload,
run_id=run.run_id,
ontology=ontology,
capabilities=capabilities,
requires_confirmation=requires_confirmation,
context_json=context_json,
)
final_status = (
AgentRunStatus.BLOCKED.value
if requires_confirmation
and outcome.status == AgentRunStatus.SUCCEEDED.value
and ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value
else outcome.status
)
response_status = self._normalize_response_status(final_status)
result_message = (
str(outcome.result.get("message", "")).strip()
or "Orchestrator 执行完成。"
)
trace_summary = OrchestratorTraceSummary(
scenario=ontology.scenario,
intent=ontology.intent,
tool_count=outcome.tool_count,
failed_tool_count=outcome.failed_tool_count,
selected_capability_codes=selected_capability_codes,
degraded=outcome.degraded,
)
self.run_service.update_run(
run.run_id,
agent=selected_agent or AgentName.ORCHESTRATOR.value,
ontology_json=self._build_ontology_json(ontology),
route_json={
**route_json,
"requires_confirmation": requires_confirmation,
"degraded": outcome.degraded,
},
permission_level=ontology.permission.level,
status=final_status,
result_summary=result_message,
error_message=None,
finished_at=datetime.now(UTC),
)
if conversation is not None and conversation_id:
draft_payload = outcome.result.get("draft_payload")
self.conversation_service.update_state(
conversation_id=conversation_id,
run_id=run.run_id,
scenario=ontology.scenario,
intent=ontology.intent,
context_json=context_json,
draft_payload=draft_payload if isinstance(draft_payload, dict) else None,
)
self.conversation_service.append_message(
conversation_id=conversation_id,
role="assistant",
content=result_message,
run_id=run.run_id,
message_json={
"status": final_status,
"scenario": ontology.scenario,
"intent": ontology.intent,
"attachment_names": context_json.get("attachment_names", []),
"attachment_count": context_json.get("attachment_count", 0),
"draft_payload": draft_payload if isinstance(draft_payload, dict) else None,
"orchestrator_payload": {
"run_id": run.run_id,
"conversation_id": conversation_id,
"selected_agent": selected_agent,
"route_reason": route_reason,
"permission_level": ontology.permission.level,
"status": response_status,
"requires_confirmation": requires_confirmation,
"trace_summary": trace_summary.model_dump(),
"result": outcome.result,
},
},
)
return OrchestratorResponse(
run_id=run.run_id,
conversation_id=conversation_id,
selected_agent=selected_agent,
route_reason=route_reason,
permission_level=ontology.permission.level,
status=response_status,
result=outcome.result,
requires_confirmation=requires_confirmation,
trace_summary=trace_summary,
)
except Exception as exc:
logger.exception("Orchestrator run failed run_id=%s", run.run_id)
self.run_service.update_run(
run.run_id,
agent=AgentName.ORCHESTRATOR.value,
route_json={**route_json, "stage": "failed"},
status=AgentRunStatus.FAILED.value,
result_summary="Orchestrator 执行失败。",
error_message=str(exc),
finished_at=datetime.now(UTC),
)
if conversation is not None and conversation_id:
self.conversation_service.update_state(
conversation_id=conversation_id,
run_id=run.run_id,
scenario=None,
intent=None,
context_json=context_json,
draft_payload=None,
)
self.conversation_service.append_message(
conversation_id=conversation_id,
role="assistant",
content=f"Orchestrator 执行失败:{exc}",
run_id=run.run_id,
message_json={"status": AgentRunStatus.FAILED.value},
)
return OrchestratorResponse(
run_id=run.run_id,
conversation_id=conversation_id,
selected_agent=None,
route_reason="orchestrator_exception",
permission_level=AgentPermissionLevel.READ.value,
status="failed",
result={"message": f"Orchestrator 执行失败:{exc}"},
requires_confirmation=False,
trace_summary=OrchestratorTraceSummary(
scenario="unknown",
intent="query",
tool_count=0,
failed_tool_count=0,
selected_capability_codes=[],
degraded=False,
),
)
def _resolve_message(
self,
payload: OrchestratorRequest,
) -> tuple[str, AgentAssetRead | None]:
task_asset = None
if payload.task_id:
task_asset = self.asset_service.get_asset(payload.task_id)
if payload.message and payload.message.strip():
return payload.message.strip(), task_asset
if task_asset is not None:
description = str(task_asset.description or "").strip()
scenario_text = " ".join(str(item) for item in task_asset.scenario_json)
message = f"{task_asset.name} {description} {scenario_text}".strip()
return message, task_asset
if payload.source == AgentRunSource.SCHEDULE.value:
return "定时风险巡检任务", task_asset
raise ValueError("message 或 task_id 至少需要提供一个。")
@staticmethod
def _select_agent(
payload: OrchestratorRequest,
ontology: OntologyParseResult,
) -> tuple[str, str]:
if payload.source == AgentRunSource.SCHEDULE.value:
return AgentName.HERMES.value, "schedule_source_defaults_to_hermes"
if payload.source == AgentRunSource.SYSTEM_EVENT.value and ontology.intent == "risk_check":
return AgentName.HERMES.value, "system_event_risk_check_routes_to_hermes"
if ontology.intent == "risk_check" and payload.source == AgentRunSource.SCHEDULE.value:
return AgentName.HERMES.value, "scheduled_risk_check_routes_to_hermes"
if ontology.intent in {"query", "explain", "draft", "compare", "operate"}:
return AgentName.USER_AGENT.value, f"{ontology.intent}_routes_to_user_agent"
return AgentName.USER_AGENT.value, "user_message_defaults_to_user_agent"
def _select_capabilities(
self,
*,
payload: OrchestratorRequest,
ontology: OntologyParseResult,
task_asset: AgentAssetRead | None,
) -> dict[str, list[AgentAssetListItem | AgentAssetRead]]:
domain_value = SCENARIO_TO_DOMAIN.get(ontology.scenario)
rules = self._rank_assets(
self.asset_service.list_assets(
asset_type=AgentAssetType.RULE.value,
status=AgentAssetStatus.ACTIVE.value,
domain=domain_value if domain_value not in {"knowledge", "system"} else None,
),
ontology,
)
skills = self._rank_assets(
self.asset_service.list_assets(
asset_type=AgentAssetType.SKILL.value,
status=AgentAssetStatus.ACTIVE.value,
domain=domain_value if domain_value not in {"system"} else None,
),
ontology,
)
mcps = self._rank_assets(
self.asset_service.list_assets(
asset_type=AgentAssetType.MCP.value,
status=AgentAssetStatus.ACTIVE.value,
),
ontology,
)
tasks: list[AgentAssetListItem | AgentAssetRead] = []
if task_asset is not None and task_asset.status == AgentAssetStatus.ACTIVE.value:
tasks.append(task_asset)
elif payload.source == AgentRunSource.SCHEDULE.value:
tasks = self._rank_assets(
self.asset_service.list_assets(
asset_type=AgentAssetType.TASK.value,
status=AgentAssetStatus.ACTIVE.value,
),
ontology,
)
return {
"rules": rules,
"skills": skills,
"mcps": mcps,
"tasks": tasks,
}
def _execute_user_agent(
self,
*,
payload: OrchestratorRequest,
run_id: str,
ontology: OntologyParseResult,
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
requires_confirmation: bool,
context_json: dict[str, Any],
) -> ExecutionOutcome:
selected_capability_codes = self._flatten_capability_codes(capabilities)
if requires_confirmation:
response, degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.LLM.value,
tool_name="user_agent.confirmation_placeholder",
request_json={
"message": payload.message,
"permission_level": ontology.permission.level,
},
context_json=context_json,
executor=lambda: {
"confirmation_title": "操作需要确认",
"message": f"{ontology.permission.reason} 当前仅返回确认摘要,不直接执行动作。",
},
fallback_factory=lambda exc: {
"confirmation_title": "操作需要确认",
"message": f"确认摘要生成失败,已阻断自动执行:{exc}",
},
)
return ExecutionOutcome(
status=AgentRunStatus.BLOCKED.value,
result={**response, "degraded": degraded},
degraded=degraded,
tool_count=1,
failed_tool_count=1 if degraded else 0,
)
next_step = self._resolve_next_step(ontology, payload.source)
if next_step == "query_database":
tool_payload, degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.DATABASE.value,
tool_name=self._database_tool_name(ontology.scenario),
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=lambda: self._build_database_answer(ontology),
fallback_factory=lambda exc: {
"message": f"数据库查询暂时不可用,已返回降级说明:{exc}",
"degraded": True,
},
)
result = self._build_user_agent_result(
self.user_agent_service.respond(
UserAgentRequest(
run_id=run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
requires_confirmation=requires_confirmation,
)
),
degraded=degraded,
)
return ExecutionOutcome(
status=AgentRunStatus.SUCCEEDED.value,
result=result,
degraded=degraded,
tool_count=1,
failed_tool_count=1 if degraded else 0,
)
if next_step == "search_knowledge":
tool_payload, degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.DATABASE.value,
tool_name="knowledge.search",
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=lambda: self._build_knowledge_answer(ontology, capabilities),
fallback_factory=lambda exc: {
"message": f"知识检索暂时不可用,建议稍后重试:{exc}",
"degraded": True,
},
)
result = self._build_user_agent_result(
self.user_agent_service.respond(
UserAgentRequest(
run_id=run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
requires_confirmation=requires_confirmation,
)
),
degraded=degraded,
)
return ExecutionOutcome(
status=AgentRunStatus.SUCCEEDED.value,
result=result,
degraded=degraded,
tool_count=1,
failed_tool_count=1 if degraded else 0,
)
if next_step == "run_rule":
tool_payload, degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.RULE_ENGINE.value,
tool_name=self._rule_tool_name(capabilities),
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=lambda: self._build_rule_answer(ontology),
fallback_factory=lambda exc: {
"message": f"规则检查暂时不可用,已返回人工复核建议:{exc}",
"degraded": True,
},
)
result = self._build_user_agent_result(
self.user_agent_service.respond(
UserAgentRequest(
run_id=run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
requires_confirmation=requires_confirmation,
)
),
degraded=degraded,
)
return ExecutionOutcome(
status=AgentRunStatus.SUCCEEDED.value,
result=result,
degraded=degraded,
tool_count=1,
failed_tool_count=1 if degraded else 0,
)
tool_type = AgentToolType.LLM.value
tool_name = "user_agent.draft_placeholder"
executor = lambda: {
"message": (
f"已生成 {ontology.scenario} 场景草稿,"
"占位能力后续由 Day 5 User Agent 接管。"
),
"draft_only": True,
}
fallback_factory = lambda exc: {
"message": f"草稿生成暂时不可用,请稍后再试:{exc}",
"degraded": True,
}
if ontology.scenario == "expense":
tool_type = AgentToolType.DATABASE.value
tool_name = "database.expense_claims.upsert_draft"
executor = lambda: self.expense_claim_service.upsert_draft_from_ontology(
run_id=run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
)
fallback_factory = lambda exc: {
"message": f"报销草稿落库失败,请稍后再试:{exc}",
"degraded": True,
}
tool_payload, degraded = self._invoke_tool(
run_id=run_id,
tool_type=tool_type,
tool_name=tool_name,
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=executor,
fallback_factory=fallback_factory,
)
result = self._build_user_agent_result(
self.user_agent_service.respond(
UserAgentRequest(
run_id=run_id,
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
requires_confirmation=requires_confirmation,
)
),
degraded=degraded,
)
return ExecutionOutcome(
status=AgentRunStatus.SUCCEEDED.value,
result=result,
degraded=degraded,
tool_count=1,
failed_tool_count=1 if degraded else 0,
)
def _execute_hermes(
self,
*,
payload: OrchestratorRequest,
run_id: str,
ontology: OntologyParseResult,
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
requires_confirmation: bool,
task_asset: AgentAssetRead | None,
context_json: dict[str, Any],
) -> ExecutionOutcome:
if requires_confirmation:
return ExecutionOutcome(
status=AgentRunStatus.BLOCKED.value,
result={
"message": "Hermes 不会自动执行需要确认的高风险动作,已阻断。",
"degraded": False,
},
degraded=False,
tool_count=0,
failed_tool_count=0,
)
rule_response, rule_degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.RULE_ENGINE.value,
tool_name=self._rule_tool_name(capabilities),
request_json=self._build_ontology_json(ontology),
context_json=context_json,
executor=lambda: self._build_rule_answer(ontology),
fallback_factory=lambda exc: {
"message": f"规则巡检失败,已降级为待人工复核:{exc}",
"degraded": True,
},
)
mcp_response, mcp_degraded = self._invoke_tool(
run_id=run_id,
tool_type=AgentToolType.MCP.value,
tool_name=self._mcp_tool_name(capabilities),
request_json={
"task_code": task_asset.code if task_asset is not None else "",
"scenario": ontology.scenario,
},
context_json=context_json,
executor=lambda: self._build_mcp_answer(task_asset, ontology),
fallback_factory=lambda exc: {
"message": f"MCP 调用失败,已使用缓存快照降级:{exc}",
"fallback": "used_cached_snapshot",
},
)
degraded = rule_degraded or mcp_degraded
failed_tool_count = int(rule_degraded) + int(mcp_degraded)
result = {
"message": self._build_hermes_message(
task_asset=task_asset,
ontology=ontology,
rule_response=rule_response,
mcp_response=mcp_response,
degraded=degraded,
),
"report_type": task_asset.code if task_asset is not None else "hermes_runtime",
"degraded": degraded,
}
return ExecutionOutcome(
status=AgentRunStatus.SUCCEEDED.value,
result=result,
degraded=degraded,
tool_count=2,
failed_tool_count=failed_tool_count,
)
@staticmethod
def _resolve_next_step(ontology: OntologyParseResult, source: str) -> str:
if ontology.clarification_required:
return "ask_clarification"
if ontology.intent == "draft":
return "create_draft"
if ontology.scenario == "knowledge" or ontology.intent == "explain":
return "search_knowledge"
if ontology.intent == "risk_check" or source == AgentRunSource.SCHEDULE.value:
return "run_rule"
if ontology.intent in {"query", "compare"}:
return "query_database"
return "create_draft"
@staticmethod
def _flatten_capability_codes(
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
) -> list[str]:
codes: list[str] = []
for items in capabilities.values():
for item in items[:2]:
if item.code not in codes:
codes.append(item.code)
return codes
def _rank_assets(
self,
items: list[AgentAssetListItem],
ontology: OntologyParseResult,
) -> list[AgentAssetListItem]:
def score(item: AgentAssetListItem) -> tuple[int, str]:
item_tags = {str(value) for value in item.scenario_json or []}
weight = 0
if ontology.scenario in item_tags:
weight += 3
if ontology.intent in item_tags:
weight += 2
for risk_flag in ontology.risk_flags:
if risk_flag in item_tags:
weight += 4
return weight, item.code
ranked = sorted(items, key=score, reverse=True)
if not ranked:
return []
scored = [item for item in ranked if score(item)[0] > 0]
return scored or ranked[:1]
def _invoke_tool(
self,
*,
run_id: str,
tool_type: str,
tool_name: str,
request_json: dict[str, Any],
context_json: dict[str, Any],
executor,
fallback_factory,
) -> tuple[dict[str, Any], bool]:
started = perf_counter()
try:
self._maybe_raise_simulated_failure(tool_type, context_json)
response = executor()
duration_ms = int((perf_counter() - started) * 1000)
self.run_service.record_tool_call(
run_id=run_id,
tool_type=tool_type,
tool_name=tool_name,
request_json=request_json,
response_json=response,
status="succeeded",
duration_ms=duration_ms,
)
return response, False
except Exception as exc:
duration_ms = int((perf_counter() - started) * 1000)
response = fallback_factory(exc)
self.run_service.record_tool_call(
run_id=run_id,
tool_type=tool_type,
tool_name=tool_name,
request_json=request_json,
response_json=response,
status="failed",
duration_ms=duration_ms,
error_message=str(exc),
)
return response, True
@staticmethod
def _maybe_raise_simulated_failure(tool_type: str, context_json: dict[str, Any]) -> None:
expected = str(context_json.get("simulate_tool_failure") or "").strip().lower()
if not expected:
return
if expected == tool_type.lower():
raise RuntimeError(f"simulated {tool_type} failure")
def _build_database_answer(self, ontology: OntologyParseResult) -> dict[str, Any]:
if ontology.scenario == "expense":
count_stmt = select(func.count()).select_from(ExpenseClaim)
amount_stmt = select(
func.coalesce(func.sum(ExpenseClaim.amount), 0)
).select_from(ExpenseClaim)
employee_names = [
item.normalized_value
for item in ontology.entities
if item.type == "employee"
]
if employee_names:
count_stmt = count_stmt.where(ExpenseClaim.employee_name.in_(employee_names))
amount_stmt = amount_stmt.where(ExpenseClaim.employee_name.in_(employee_names))
total_count = int(self.db.scalar(count_stmt) or 0)
total_amount = float(self.db.scalar(amount_stmt) or 0)
return {
"record_count": total_count,
"total_amount": round(total_amount, 2),
}
if ontology.scenario == "accounts_receivable":
total_count = int(
self.db.scalar(
select(func.count()).select_from(AccountsReceivableRecord)
)
or 0
)
total_amount = float(
self.db.scalar(
select(func.coalesce(func.sum(AccountsReceivableRecord.amount_outstanding), 0))
)
or 0
)
return {
"record_count": total_count,
"outstanding_amount": round(total_amount, 2),
}
total_count = int(
self.db.scalar(select(func.count()).select_from(AccountsPayableRecord))
or 0
)
total_amount = float(
self.db.scalar(
select(func.coalesce(func.sum(AccountsPayableRecord.amount_outstanding), 0))
)
or 0
)
return {
"record_count": total_count,
"outstanding_amount": round(total_amount, 2),
}
@staticmethod
def _build_user_query_result(
ontology: OntologyParseResult,
response: dict[str, Any],
) -> dict[str, Any]:
if ontology.scenario == "expense":
return {
"message": (
f"已路由到 User Agent占位查询结果命中 {response['record_count']} 笔报销,"
f"金额合计 {response['total_amount']} 元。"
),
"data": response,
}
if ontology.scenario == "accounts_receivable":
return {
"message": (
f"已路由到 User Agent占位查询结果命中 {response['record_count']} 条应收,"
f"未回款金额 {response['outstanding_amount']} 元。"
),
"data": response,
}
return {
"message": (
f"已路由到 User Agent占位查询结果命中 {response['record_count']} 条应付,"
f"待付金额 {response['outstanding_amount']} 元。"
),
"data": response,
}
@staticmethod
def _build_user_agent_result(
response: UserAgentResponse,
*,
degraded: bool,
) -> dict[str, Any]:
result = {
"message": response.answer,
"answer": response.answer,
"citations": [item.model_dump() for item in response.citations],
"suggested_actions": [item.model_dump() for item in response.suggested_actions],
"risk_flags": response.risk_flags,
"requires_confirmation": response.requires_confirmation,
"degraded": degraded,
}
if response.draft_payload is not None:
result["draft_payload"] = response.draft_payload.model_dump()
if response.review_payload is not None:
result["review_payload"] = response.review_payload.model_dump()
return result
@staticmethod
def _build_knowledge_answer(
ontology: OntologyParseResult,
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
) -> dict[str, Any]:
referenced = [item.code for item in capabilities["rules"][:1]] or [
"knowledge.policy.default"
]
return {
"message": f"已路由到 User Agent占位知识结果建议先查看 {', '.join(referenced)}",
"references": referenced,
}
@staticmethod
def _build_rule_answer(ontology: OntologyParseResult) -> dict[str, Any]:
risk_text = (
"".join(ontology.risk_flags)
if ontology.risk_flags
else "未识别到明确风险标签"
)
return {
"message": f"已完成占位规则检查,风险标签:{risk_text}",
"risk_flags": ontology.risk_flags,
}
@staticmethod
def _build_mcp_answer(
task_asset: AgentAssetRead | None,
ontology: OntologyParseResult,
) -> dict[str, Any]:
return {
"message": (
f"已调用占位 MCP 快照,任务={task_asset.code if task_asset else 'none'}"
f"scenario={ontology.scenario}"
),
"snapshot": "stubbed",
}
@staticmethod
def _build_hermes_message(
*,
task_asset: AgentAssetRead | None,
ontology: OntologyParseResult,
rule_response: dict[str, Any],
mcp_response: dict[str, Any],
degraded: bool,
) -> str:
task_code = task_asset.code if task_asset is not None else "task.unspecified"
suffix = ",其中部分能力已降级。" if degraded else ""
return (
f"Hermes 占位执行完成:任务 {task_code}"
f"场景 {ontology.scenario},规则结果={rule_response.get('message', '')}"
f"MCP 结果={mcp_response.get('message', '')}{suffix}"
)
@staticmethod
def _database_tool_name(scenario: str) -> str:
if scenario == "expense":
return "database.expense_claims.lookup"
if scenario == "accounts_receivable":
return "database.accounts_receivable.lookup"
return "database.accounts_payable.lookup"
@staticmethod
def _rule_tool_name(
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
) -> str:
if capabilities["rules"]:
return capabilities["rules"][0].code
return "rule_engine.default_risk_check"
@staticmethod
def _mcp_tool_name(
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
) -> str:
if capabilities["mcps"]:
return capabilities["mcps"][0].code
return "mcp.default_snapshot"
@staticmethod
def _build_ontology_json(ontology: OntologyParseResult) -> dict[str, Any]:
return {
"scenario": ontology.scenario,
"intent": ontology.intent,
"entities": [item.model_dump() for item in ontology.entities],
"time_range": ontology.time_range.model_dump(),
"metrics": [item.model_dump() for item in ontology.metrics],
"constraints": [item.model_dump() for item in ontology.constraints],
"risk_flags": ontology.risk_flags,
"permission": ontology.permission.model_dump(),
}
@staticmethod
def _normalize_response_status(status: str) -> str:
if status == AgentRunStatus.FAILED.value:
return "failed"
if status == AgentRunStatus.BLOCKED.value:
return "blocked"
return "succeeded"