feat: 完善文档中心与报销申请交互及侧边栏重构

后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-25 13:35:39 +08:00
parent 50b1c3f9a9
commit d0e946cf47
59 changed files with 5117 additions and 416 deletions

View File

@@ -29,6 +29,41 @@ def build_session_factory() -> sessionmaker[Session]:
return sessionmaker(bind=engine, autoflush=False, autocommit=False)
def build_application_user_agent_response(
db: Session,
message: str,
*,
history: list[dict[str, object]] | None = None,
context_overrides: dict[str, object] | None = None,
):
context_json = {
"session_type": "application",
"entry_source": "application",
"attachment_count": 0,
}
if context_overrides:
context_json.update(context_overrides)
if history is not None:
context_json["conversation_history"] = history
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
return UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": ontology.clarification_required},
)
)
def test_user_agent_query_returns_readable_answer_and_actions() -> None:
session_factory = build_session_factory()
with session_factory() as db:
@@ -137,6 +172,216 @@ def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None:
assert '"knowledge_answer_evidence": []' in messages[1]["content"]
def test_user_agent_application_context_uses_application_language() -> None:
session_factory = build_session_factory()
message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
context_json = {
"session_type": "application",
"entry_source": "application",
"attachment_count": 0,
}
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": True},
)
)
assert "费用申请" in response.answer
assert "| 字段 | 内容 |" in response.answer
assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer
assert "支持上海国网服务器部署" in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert "请先在下面选择报销场景" not in response.answer
assert response.review_payload is None
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:"
def test_user_agent_application_infers_natural_reason_and_expands_single_date() -> None:
session_factory = build_session_factory()
message = "发生时间2026-05-25\n去上海出差3天支撑上海国网服务器部署"
with session_factory() as db:
response = build_application_user_agent_response(db, message)
assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer
assert "| 地点 | 上海 |" in response.answer
assert "| 事由 | 支撑上海国网服务器部署 |" in response.answer
assert "当前还需要先补充:申请事由" not in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
def test_user_agent_application_uses_selected_time_and_natural_language_fields() -> None:
session_factory = build_session_factory()
message = "出差上海,支撑国网服务器上线部署"
context_json = {
"session_type": "application",
"entry_source": "application",
"business_time_context": {
"mode": "single",
"start_date": "2026-05-25",
"end_date": "2026-05-25",
"display_value": "2026-05-25",
},
}
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=message,
user_id="pytest",
context_json=context_json,
)
)
response = UserAgentService(db).respond(
UserAgentRequest(
run_id=ontology.run_id,
user_id="pytest",
message=message,
ontology=ontology,
context_json=context_json,
tool_payload={"clarification_required": True},
)
)
assert "| 发生时间 | 2026-05-25 |" in response.answer
assert "| 地点 | 上海 |" in response.answer
assert "| 事由 | 支撑国网服务器上线部署 |" in response.answer
assert "当前还需要补充:出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:"
def test_user_agent_application_asks_amount_after_transport_choice() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"飞机",
history=[{"role": "user", "content": initial_message}],
)
assert "| 出行方式 | 飞机 |" in response.answer
assert "当前还需要补充:预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "预计总费用:"
def test_user_agent_application_missing_base_actions_prefill_composer() -> None:
session_factory = build_session_factory()
with session_factory() as db:
response = build_application_user_agent_response(
db,
"地点:上海\n事由:支撑国网服务器部署\n天数3天",
)
assert "当前还需要补充:发生时间、出行方式、预计金额/预算" in response.answer
assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"]
assert response.suggested_actions[0].action_type == "prefill_composer"
assert response.suggested_actions[0].payload["prompt_prefill"] == "申请时间段:\n出行方式:\n预计总费用:"
def test_user_agent_application_builds_preview_when_amount_is_ready() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"预计总费用12000元",
history=[
{"role": "user", "content": initial_message},
{"role": "user", "content": "飞机"},
],
)
assert "这是模拟的费用申请结果" in response.answer
assert "| 字段 | 内容 |" in response.answer
assert "| 事由 | 支持上海国网服务器部署 |" in response.answer
assert "| 出行方式 | 飞机 |" in response.answer
assert "| 预计总费用 | 12000元 |" in response.answer
assert "请核对上述信息无误" in response.answer
assert "[确认](#application-submit)" in response.answer
assert response.requires_confirmation is True
assert response.suggested_actions == []
def test_user_agent_application_submit_enters_leader_review() -> None:
session_factory = build_session_factory()
initial_message = (
"发生时间2026-05-25\n"
"地点:上海\n"
"事由:支持上海国网服务器部署\n"
"天数3天"
)
preview_answer = (
"这是模拟的费用申请结果,请核对:\n"
"| 字段 | 内容 |\n"
"| --- | --- |\n"
"| 申请类型 | 差旅费用申请 |\n"
"| 发生时间 | 2026-05-25 |\n"
"| 地点 | 上海 |\n"
"| 事由 | 支持上海国网服务器部署 |\n"
"| 天数 | 3天 |\n"
"| 出行方式 | 飞机 |\n"
"| 预计总费用 | 12000元 |\n\n"
"请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。"
)
with session_factory() as db:
response = build_application_user_agent_response(
db,
"确认提交",
context_overrides={"manager_name": "陈硕"},
history=[
{"role": "user", "content": initial_message},
{"role": "user", "content": "飞机"},
{"role": "user", "content": "预计总费用12000元"},
{"role": "assistant", "content": preview_answer},
],
)
assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in response.answer
assert "当前状态:陈硕审核中" in response.answer
assert "预算占用参考" in response.answer
assert "APP-20260525-" in response.answer
assert response.suggested_actions == []
claim = db.query(ExpenseClaim).filter(ExpenseClaim.claim_no.like("APP-20260525-%")).one()
assert claim.status == "submitted"
assert claim.approval_stage == "直属领导审批"
assert claim.expense_type == "travel_application"
assert claim.amount == Decimal("12000.00")
assert claim.employee_name == "pytest"
def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None:
session_factory = build_session_factory()
with session_factory() as db: