refactor(backend): update and add service layers

- services/ontology.py: update ontology service
- services/orchestrator.py: update orchestrator service
- services/user_agent.py: update user agent service
- services/settings.py: update settings service
- services/expense_claims.py: update expense claims service
- services/agent_conversations.py: add new agent conversations service
This commit is contained in:
caoxiaozhu
2026-05-12 06:36:09 +00:00
parent a6a28ba865
commit 01df3452fd
6 changed files with 1442 additions and 80 deletions

View File

@@ -32,6 +32,7 @@ from app.schemas.orchestrator import (
)
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
@@ -62,6 +63,7 @@ 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)
@@ -69,10 +71,28 @@ class OrchestratorService:
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,
@@ -87,15 +107,27 @@ class OrchestratorService:
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=payload.context_json,
context_json=context_json,
),
run_id=run.run_id,
)
if payload.context_json.get("simulate_orchestrator_exception"):
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(
@@ -159,6 +191,7 @@ class OrchestratorService:
capabilities=capabilities,
requires_confirmation=requires_confirmation,
task_asset=task_asset,
context_json=context_json,
)
else:
outcome = self._execute_user_agent(
@@ -167,6 +200,7 @@ class OrchestratorService:
ontology=ontology,
capabilities=capabilities,
requires_confirmation=requires_confirmation,
context_json=context_json,
)
final_status = (
@@ -176,10 +210,19 @@ class OrchestratorService:
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,
@@ -195,22 +238,51 @@ class OrchestratorService:
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=self._normalize_response_status(final_status),
status=response_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,
),
trace_summary=trace_summary,
)
except Exception as exc:
logger.exception("Orchestrator run failed run_id=%s", run.run_id)
@@ -223,8 +295,25 @@ class OrchestratorService:
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,
@@ -336,6 +425,7 @@ class OrchestratorService:
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:
@@ -347,7 +437,7 @@ class OrchestratorService:
"message": payload.message,
"permission_level": ontology.permission.level,
},
context_json=payload.context_json,
context_json=context_json,
executor=lambda: {
"confirmation_title": "操作需要确认",
"message": f"{ontology.permission.reason} 当前仅返回确认摘要,不直接执行动作。",
@@ -372,7 +462,7 @@ class OrchestratorService:
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,
context_json=context_json,
executor=lambda: self._build_database_answer(ontology),
fallback_factory=lambda exc: {
"message": f"数据库查询暂时不可用,已返回降级说明:{exc}",
@@ -386,7 +476,7 @@ class OrchestratorService:
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=payload.context_json,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
@@ -409,7 +499,7 @@ class OrchestratorService:
tool_type=AgentToolType.DATABASE.value,
tool_name="knowledge.search",
request_json=self._build_ontology_json(ontology),
context_json=payload.context_json,
context_json=context_json,
executor=lambda: self._build_knowledge_answer(ontology, capabilities),
fallback_factory=lambda exc: {
"message": f"知识检索暂时不可用,建议稍后重试:{exc}",
@@ -423,7 +513,7 @@ class OrchestratorService:
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=payload.context_json,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
@@ -446,7 +536,7 @@ class OrchestratorService:
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,
context_json=context_json,
executor=lambda: self._build_rule_answer(ontology),
fallback_factory=lambda exc: {
"message": f"规则检查暂时不可用,已返回人工复核建议:{exc}",
@@ -460,7 +550,7 @@ class OrchestratorService:
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=payload.context_json,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
@@ -499,7 +589,7 @@ class OrchestratorService:
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=payload.context_json,
context_json=context_json,
)
fallback_factory = lambda exc: {
"message": f"报销草稿落库失败,请稍后再试:{exc}",
@@ -511,7 +601,7 @@ class OrchestratorService:
tool_type=tool_type,
tool_name=tool_name,
request_json=self._build_ontology_json(ontology),
context_json=payload.context_json,
context_json=context_json,
executor=executor,
fallback_factory=fallback_factory,
)
@@ -522,7 +612,7 @@ class OrchestratorService:
user_id=payload.user_id,
message=payload.message or "",
ontology=ontology,
context_json=payload.context_json,
context_json=context_json,
tool_payload=tool_payload,
selected_capability_codes=selected_capability_codes,
degraded=degraded,
@@ -548,6 +638,7 @@ class OrchestratorService:
capabilities: dict[str, list[AgentAssetListItem | AgentAssetRead]],
requires_confirmation: bool,
task_asset: AgentAssetRead | None,
context_json: dict[str, Any],
) -> ExecutionOutcome:
if requires_confirmation:
return ExecutionOutcome(
@@ -566,7 +657,7 @@ class OrchestratorService:
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,
context_json=context_json,
executor=lambda: self._build_rule_answer(ontology),
fallback_factory=lambda exc: {
"message": f"规则巡检失败,已降级为待人工复核:{exc}",
@@ -581,7 +672,7 @@ class OrchestratorService:
"task_code": task_asset.code if task_asset is not None else "",
"scenario": ontology.scenario,
},
context_json=payload.context_json,
context_json=context_json,
executor=lambda: self._build_mcp_answer(task_asset, ontology),
fallback_factory=lambda exc: {
"message": f"MCP 调用失败,已使用缓存快照降级:{exc}",
@@ -806,6 +897,8 @@ class OrchestratorService:
}
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