feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -0,0 +1,136 @@
from __future__ import annotations
from app.schemas.steward import StewardSlotDecisionRequest
from app.services.runtime_chat import RuntimeChatCallTrace, RuntimeChatToolCall, RuntimeToolCallResult
from app.services.steward_slot_decision_agent import (
STEWARD_SLOT_DECISION_FUNCTION_NAME,
StewardSlotDecisionAgent,
)
class FakeSlotRuntime:
def __init__(self, arguments=None):
self.arguments = arguments
self.messages = None
def complete_with_tool_call(self, messages, **kwargs):
self.messages = messages
if self.arguments is None:
return RuntimeToolCallResult(tool_call=None, calls=[])
return RuntimeToolCallResult(
tool_call=RuntimeChatToolCall(
name=STEWARD_SLOT_DECISION_FUNCTION_NAME,
arguments=self.arguments,
),
calls=[
RuntimeChatCallTrace(
slot="main",
provider="OpenAI Compatible",
model="fake",
attempt=1,
status="succeeded",
)
],
)
def test_steward_slot_decision_uses_function_calling_result() -> None:
runtime = FakeSlotRuntime(
{
"next_action": "ask_user",
"required_fields": ["expense_type", "time_range", "location", "reason", "transport_mode"],
"missing_fields": ["transport_mode"],
"question": "请问你这次打算怎么出行?",
"options": [
{"field_key": "transport_mode", "label": "飞机", "value": "飞机"},
{"field_key": "transport_mode", "label": "火车", "value": "火车"},
],
"rationale": "出行方式会影响交通费用测算。",
}
)
result = StewardSlotDecisionAgent(runtime).decide(
StewardSlotDecisionRequest(
task_type="expense_application",
user_message="明天出差北京3天支撑国网仿生产部署",
ontology_fields={
"expense_type": "travel",
"time_range": "2026-06-05 至 2026-06-07",
"location": "北京",
"reason": "支撑国网仿生产部署",
},
missing_fields=["transport_mode"],
)
)
assert result.decision_source == "llm_function_call"
assert result.next_action == "ask_user"
assert result.missing_fields == ["transport_mode"]
assert [item.value for item in result.options] == ["飞机", "火车"]
assert "出行方式会影响" in result.rationale
def test_steward_slot_decision_falls_back_to_intent_missing_fields_only() -> None:
runtime = FakeSlotRuntime(arguments=None)
result = StewardSlotDecisionAgent(runtime).decide(
StewardSlotDecisionRequest(
task_type="expense_application",
user_message="还需要补充:出行方式(例如高铁、飞机、自驾、出租车)",
ontology_fields={
"expense_type": "travel",
"location": "北京",
"reason": "支撑国网仿生产部署",
},
missing_fields=["transport_mode"],
)
)
assert result.decision_source == "rule_fallback"
assert result.next_action == "ask_user"
assert result.missing_fields == ["transport_mode"]
assert [item.value for item in result.options] == ["火车", "飞机", "轮船"]
assert "高铁" not in result.required_fields
def test_steward_slot_decision_does_not_ask_user_for_application_profile_or_attachments() -> None:
runtime = FakeSlotRuntime(
{
"next_action": "ask_user",
"required_fields": [
"expense_type",
"time_range",
"location",
"reason",
"amount",
"attachments",
"employee_no",
],
"missing_fields": ["attachments", "employee_no"],
"question": "请补充附件和员工编号。",
"options": [],
"rationale": "附件/凭证和员工编号为合规必需字段。",
}
)
result = StewardSlotDecisionAgent(runtime).decide(
StewardSlotDecisionRequest(
task_type="expense_application",
user_message="明天出差北京3天支撑国网仿生产部署",
ontology_fields={
"expense_type": "travel",
"time_range": "2026-06-05 至 2026-06-07",
"location": "北京",
"reason": "支撑国网仿生产部署",
},
missing_fields=["attachments", "employee_no"],
)
)
assert result.decision_source == "llm_function_call"
assert result.next_action == "render_preview"
assert result.missing_fields == []
assert "attachments" not in result.required_fields
assert "employee_no" not in result.required_fields
assert result.options == []
assert "合规必需字段" not in result.rationale