feat: 增强差旅报销审核流程与票据智能推理

优化本体解析和编排器的差旅场景处理能力,完善报销单草稿
保存和费用明细同步逻辑,前端报销创建页面增加行程推理和
票据审核交互,新增助手会话快照工具函数,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-21 16:09:47 +08:00
parent f28d7e6d16
commit e701fa01da
33 changed files with 3033 additions and 337 deletions

View File

@@ -97,6 +97,15 @@ GROUP_SCENE_LABELS = {
"other": "其他费用",
}
EXPENSE_SCENE_SELECTION_OPTIONS = (
("travel", "差旅费", "出差、长途交通、住宿、差旅补贴等场景。"),
("transport", "交通费", "市内打车、停车、过路费等日常交通场景。"),
("hotel", "住宿费", "单独住宿、酒店发票等场景。"),
("entertainment", "业务招待费", "客户接待、宴请、招待等场景。"),
("office", "办公费", "办公用品、耗材、办公设备等采购场景。"),
("other", "其他费用", "暂不属于以上分类的报销场景。"),
)
KNOWLEDGE_MODEL_MAIN_TIMEOUT_SECONDS = 3
KNOWLEDGE_MODEL_BACKUP_TIMEOUT_SECONDS = 5
KNOWLEDGE_MODEL_TIMEOUT_SECONDS = KNOWLEDGE_MODEL_BACKUP_TIMEOUT_SECONDS
@@ -275,6 +284,17 @@ class UserAgentService:
AgentFoundationService(self.db).ensure_foundation_ready()
citations = self._build_citations(payload)
suggested_actions = self._build_suggested_actions(payload)
if self._should_prompt_expense_scene_selection(payload):
return UserAgentResponse(
answer=self._build_expense_scene_selection_answer(payload),
citations=citations,
suggested_actions=suggested_actions,
query_payload=None,
draft_payload=None,
review_payload=None,
risk_flags=[],
requires_confirmation=False,
)
risk_flags = self._resolve_risk_flags(payload)
query_payload = self._build_query_payload(payload)
draft_payload = (
@@ -1801,6 +1821,11 @@ class UserAgentService:
@staticmethod
def _should_build_draft_payload(payload: UserAgentRequest) -> bool:
if payload.ontology.scenario == "expense" and payload.tool_payload.get("preview_only"):
return any(
str(payload.tool_payload.get(key) or "").strip()
for key in ("claim_id", "claim_no")
)
if payload.ontology.intent == "draft":
return True
if payload.ontology.scenario != "expense":
@@ -1817,6 +1842,21 @@ class UserAgentService:
if payload.ontology.scenario == "knowledge":
return []
if self._should_prompt_expense_scene_selection(payload):
return [
UserAgentSuggestedAction(
label=label,
action_type="select_expense_type",
description=description,
payload={
"expense_type": code,
"expense_type_label": label,
"original_message": payload.message,
},
)
for code, label, description in EXPENSE_SCENE_SELECTION_OPTIONS
]
if self._is_generic_expense_prompt(payload):
return [
UserAgentSuggestedAction(
@@ -1886,6 +1926,35 @@ class UserAgentService:
),
]
def _should_prompt_expense_scene_selection(self, payload: UserAgentRequest) -> bool:
if payload.ontology.scenario != "expense":
return False
if payload.ontology.intent not in {"draft", "operate"}:
return False
if str(payload.context_json.get("review_action") or "").strip():
return False
review_form_values = self._resolve_review_form_values(payload)
if str(review_form_values.get("expense_type") or review_form_values.get("reimbursement_type") or "").strip():
return False
if self._resolve_attachment_count(payload) > 0 or self._resolve_ocr_documents(payload):
return False
return not any(
item.type == "expense_type" and str(item.normalized_value or item.value or "").strip()
for item in payload.ontology.entities
)
@staticmethod
def _build_expense_scene_selection_answer(payload: UserAgentRequest) -> str:
has_time = bool(payload.ontology.time_range.start_date or payload.ontology.time_range.raw)
context_hint = "我先识别到这是一次报销申请"
if has_time:
context_hint += ",并看到了业务发生时间"
return (
f"{context_hint}。但你还没有明确这笔单据属于哪类报销。"
"请先在下面选择报销场景,我会按你选择的场景再继续识别时间、地点、事由、金额和所需票据,"
"避免系统先入为主把项目支持、部署等描述误判成差旅。"
)
def _build_review_payload(
self,
payload: UserAgentRequest,
@@ -3363,6 +3432,17 @@ class UserAgentService:
)
review_action = str(payload.context_json.get("review_action") or "").strip()
if payload.tool_payload.get("preview_only") and not review_action:
base_message = review_payload.body_message or self._build_review_intent_summary(
payload,
slot_cards=review_payload.slot_cards,
claim_groups=review_payload.claim_groups,
)
return (
f"{base_message} "
"本次只是核对预览,尚未保存为草稿;需要暂存时请点击“保存为草稿”,"
"需要正式提交时再点击“继续下一步”。"
)
if review_action == "save_draft":
if draft_payload is not None and draft_payload.claim_no:
return (