feat(backend): add ontology and orchestrator API endpoints
New endpoints: - server/src/app/api/v1/endpoints/ontology.py: ontology API - server/src/app/api/v1/endpoints/orchestrator.py: orchestrator API New schemas: - server/src/app/schemas/ontology.py: ontology data schemas - server/src/app/schemas/orchestrator.py: orchestrator data schemas - server/src/app/schemas/user_agent.py: user agent data schemas New services: - server/src/app/services/ontology.py: ontology business logic - server/src/app/services/orchestrator.py: orchestrator business logic - server/src/app/services/runtime_chat.py: runtime chat service - server/src/app/services/user_agent.py: user agent service New tests: - server/tests/test_ontology_service.py - server/tests/test_orchestrator_service.py - server/tests/test_user_agent_service.py
This commit is contained in:
887
server/src/app/services/orchestrator.py
Normal file
887
server/src/app/services/orchestrator.py
Normal file
@@ -0,0 +1,887 @@
|
||||
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_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.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()
|
||||
route_json: dict[str, Any] = {
|
||||
"orchestrated_by": AgentName.ORCHESTRATOR.value,
|
||||
"stage": "created",
|
||||
}
|
||||
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)
|
||||
ontology = self.ontology_service.parse_for_run(
|
||||
OntologyParseRequest(
|
||||
query=message,
|
||||
user_id=payload.user_id,
|
||||
context_json=payload.context_json,
|
||||
),
|
||||
run_id=run.run_id,
|
||||
)
|
||||
if payload.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:
|
||||
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,
|
||||
)
|
||||
else:
|
||||
outcome = self._execute_user_agent(
|
||||
payload=payload,
|
||||
run_id=run.run_id,
|
||||
ontology=ontology,
|
||||
capabilities=capabilities,
|
||||
requires_confirmation=requires_confirmation,
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
result_message = (
|
||||
str(outcome.result.get("message", "")).strip()
|
||||
or "Orchestrator 执行完成。"
|
||||
)
|
||||
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),
|
||||
)
|
||||
return OrchestratorResponse(
|
||||
run_id=run.run_id,
|
||||
selected_agent=selected_agent,
|
||||
route_reason=route_reason,
|
||||
permission_level=ontology.permission.level,
|
||||
status=self._normalize_response_status(final_status),
|
||||
result=outcome.result,
|
||||
requires_confirmation=requires_confirmation,
|
||||
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,
|
||||
),
|
||||
)
|
||||
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),
|
||||
)
|
||||
return OrchestratorResponse(
|
||||
run_id=run.run_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,
|
||||
) -> 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=payload.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=payload.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=payload.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=payload.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=payload.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=payload.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=payload.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_payload, degraded = self._invoke_tool(
|
||||
run_id=run_id,
|
||||
tool_type=AgentToolType.LLM.value,
|
||||
tool_name="user_agent.draft_placeholder",
|
||||
request_json=self._build_ontology_json(ontology),
|
||||
context_json=payload.context_json,
|
||||
executor=lambda: {
|
||||
"message": (
|
||||
f"已生成 {ontology.scenario} 场景草稿,"
|
||||
"占位能力后续由 Day 5 User Agent 接管。"
|
||||
),
|
||||
"draft_only": True,
|
||||
},
|
||||
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=payload.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,
|
||||
) -> 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=payload.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=payload.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()
|
||||
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"
|
||||
Reference in New Issue
Block a user