feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
@@ -24,6 +25,34 @@ logger = get_logger("app.services.agent_runs")
|
||||
|
||||
KNOWLEDGE_SYNC_HEARTBEAT_TIMEOUT = timedelta(minutes=30)
|
||||
KNOWLEDGE_SYNC_JOB_TYPES = {"knowledge_index_sync", "llm_wiki_sync"}
|
||||
LIST_ROUTE_FIELDS = (
|
||||
("route_job_type", "job_type"),
|
||||
("route_task_type", "task_type"),
|
||||
("route_task_code", "task_code"),
|
||||
("route_task_name", "task_name"),
|
||||
("route_task_title", "task_title"),
|
||||
("route_asset_name", "asset_name"),
|
||||
("route_selected_agent", "selected_agent"),
|
||||
("route_phase", "phase"),
|
||||
("route_stage", "stage"),
|
||||
("route_report_type", "report_type"),
|
||||
("route_snapshot_key", "snapshot_key"),
|
||||
("route_folder", "folder"),
|
||||
("route_heartbeat_at", "heartbeat_at"),
|
||||
)
|
||||
LIST_ONTOLOGY_FIELDS = (
|
||||
("ontology_scenario", "scenario"),
|
||||
("ontology_intent", "intent"),
|
||||
("ontology_parse_strategy", "parse_strategy"),
|
||||
)
|
||||
LIST_PROGRESS_FIELDS = {
|
||||
"percent",
|
||||
"total_documents",
|
||||
"completed_documents",
|
||||
"failed_documents",
|
||||
"skipped_documents",
|
||||
"current_stage",
|
||||
}
|
||||
|
||||
|
||||
class AgentRunService:
|
||||
@@ -41,8 +70,22 @@ class AgentRunService:
|
||||
) -> list[AgentRunRead]:
|
||||
self._ensure_ready()
|
||||
self._reconcile_stale_knowledge_index_runs()
|
||||
runs = self.repository.list(agent=agent, status=status, source=source, limit=limit)
|
||||
return [self._serialize_run(item) for item in runs]
|
||||
rows = self.repository.list_light(
|
||||
agent=agent,
|
||||
status=status,
|
||||
source=source,
|
||||
limit=limit,
|
||||
)
|
||||
tool_calls_by_run_id = self._group_light_tool_calls(
|
||||
self.repository.list_light_tool_calls([str(item["run_id"]) for item in rows])
|
||||
)
|
||||
return [
|
||||
self._serialize_run_list_item(
|
||||
item,
|
||||
tool_calls_by_run_id.get(str(item["run_id"]), []),
|
||||
)
|
||||
for item in rows
|
||||
]
|
||||
|
||||
def get_run(self, run_id: str) -> AgentRunRead | None:
|
||||
self._ensure_ready()
|
||||
@@ -435,3 +478,99 @@ class AgentRunService:
|
||||
if semantic_parse
|
||||
else None,
|
||||
)
|
||||
|
||||
def _serialize_run_list_item(
|
||||
self,
|
||||
row: dict[str, Any],
|
||||
tool_calls: list[dict[str, Any]],
|
||||
) -> AgentRunRead:
|
||||
return AgentRunRead(
|
||||
id=str(row["id"]),
|
||||
run_id=str(row["run_id"]),
|
||||
agent=str(row["agent"]),
|
||||
source=str(row["source"]),
|
||||
user_id=row.get("user_id"),
|
||||
task_id=row.get("task_id"),
|
||||
ontology_json=self._build_list_ontology_json(row),
|
||||
route_json=self._build_list_route_json(row),
|
||||
permission_level=str(row["permission_level"]),
|
||||
status=str(row["status"]),
|
||||
result_summary=row.get("result_summary"),
|
||||
error_message=row.get("error_message"),
|
||||
started_at=row["started_at"],
|
||||
finished_at=row.get("finished_at"),
|
||||
tool_calls=[self._serialize_light_tool_call(item) for item in tool_calls],
|
||||
semantic_parse=None,
|
||||
)
|
||||
|
||||
def _build_list_route_json(self, row: dict[str, Any]) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {}
|
||||
for source_key, target_key in LIST_ROUTE_FIELDS:
|
||||
self._set_if_present(payload, target_key, row.get(source_key))
|
||||
|
||||
progress = self._coerce_json_object(row.get("route_progress"))
|
||||
compact_progress = {
|
||||
key: value
|
||||
for key, value in progress.items()
|
||||
if key in LIST_PROGRESS_FIELDS and self._is_scalar_json_value(value)
|
||||
}
|
||||
if compact_progress:
|
||||
payload["progress"] = compact_progress
|
||||
return payload
|
||||
|
||||
def _build_list_ontology_json(self, row: dict[str, Any]) -> dict[str, Any]:
|
||||
payload: dict[str, Any] = {}
|
||||
for source_key, target_key in LIST_ONTOLOGY_FIELDS:
|
||||
self._set_if_present(payload, target_key, row.get(source_key))
|
||||
return payload
|
||||
|
||||
def _serialize_light_tool_call(self, row: dict[str, Any]) -> AgentToolCallRead:
|
||||
return AgentToolCallRead(
|
||||
id=str(row["id"]),
|
||||
run_id=str(row["run_id"]),
|
||||
tool_type=str(row["tool_type"]),
|
||||
tool_name=str(row["tool_name"]),
|
||||
request_json={},
|
||||
response_json={},
|
||||
status=str(row["status"]),
|
||||
duration_ms=int(row.get("duration_ms") or 0),
|
||||
error_message=row.get("error_message"),
|
||||
created_at=row["created_at"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _group_light_tool_calls(
|
||||
tool_calls: list[dict[str, Any]],
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = {}
|
||||
for tool_call in tool_calls:
|
||||
grouped.setdefault(str(tool_call.get("run_id") or ""), []).append(tool_call)
|
||||
return grouped
|
||||
|
||||
@staticmethod
|
||||
def _coerce_json_object(value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip()
|
||||
if normalized.startswith("{") and normalized.endswith("}"):
|
||||
try:
|
||||
loaded = json.loads(normalized)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
return loaded if isinstance(loaded, dict) else {}
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def _set_if_present(payload: dict[str, Any], key: str, value: Any) -> None:
|
||||
if value is None:
|
||||
return
|
||||
if isinstance(value, str) and not value.strip():
|
||||
return
|
||||
if not AgentRunService._is_scalar_json_value(value):
|
||||
return
|
||||
payload[key] = value
|
||||
|
||||
@staticmethod
|
||||
def _is_scalar_json_value(value: Any) -> bool:
|
||||
return value is None or isinstance(value, str | int | float | bool)
|
||||
|
||||
Reference in New Issue
Block a user