feat: 增强差旅报销审核流程与票据智能推理
优化本体解析和编排器的差旅场景处理能力,完善报销单草稿 保存和费用明细同步逻辑,前端报销创建页面增加行程推理和 票据审核交互,新增助手会话快照工具函数,补充单元测试。
This commit is contained in:
@@ -670,19 +670,32 @@ class OrchestratorService:
|
||||
}
|
||||
|
||||
if ontology.scenario == "expense" or self._is_expense_review_action(context_json):
|
||||
tool_type = AgentToolType.DATABASE.value
|
||||
tool_name = "database.expense_claims.save_or_submit"
|
||||
executor = lambda: self.expense_claim_service.save_or_submit_from_ontology(
|
||||
run_id=run_id,
|
||||
user_id=payload.user_id,
|
||||
message=payload.message or "",
|
||||
ontology=ontology,
|
||||
context_json=context_json,
|
||||
)
|
||||
fallback_factory = lambda exc: {
|
||||
"message": f"报销草稿落库失败,请稍后再试:{exc}",
|
||||
"degraded": True,
|
||||
}
|
||||
is_persistence_action = self._is_expense_persistence_action(context_json)
|
||||
tool_type = (
|
||||
AgentToolType.DATABASE.value
|
||||
if is_persistence_action
|
||||
else AgentToolType.LLM.value
|
||||
)
|
||||
tool_name = (
|
||||
"database.expense_claims.save_or_submit"
|
||||
if is_persistence_action
|
||||
else "user_agent.expense_review_preview"
|
||||
)
|
||||
executor = lambda: self.expense_claim_service.save_or_submit_from_ontology(
|
||||
run_id=run_id,
|
||||
user_id=payload.user_id,
|
||||
message=payload.message or "",
|
||||
ontology=ontology,
|
||||
context_json=context_json,
|
||||
)
|
||||
fallback_factory = lambda exc: {
|
||||
"message": (
|
||||
f"报销草稿落库失败,请稍后再试:{exc}"
|
||||
if is_persistence_action
|
||||
else f"报销内容预览生成失败,请稍后再试:{exc}"
|
||||
),
|
||||
"degraded": True,
|
||||
}
|
||||
|
||||
tool_payload, degraded = self._invoke_tool(
|
||||
run_id=run_id,
|
||||
@@ -819,6 +832,16 @@ class OrchestratorService:
|
||||
"link_to_existing_draft",
|
||||
"create_new_claim_from_documents",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_expense_persistence_action(context_json: dict[str, Any]) -> bool:
|
||||
review_action = str((context_json or {}).get("review_action") or "").strip()
|
||||
return review_action in {
|
||||
"save_draft",
|
||||
"next_step",
|
||||
"link_to_existing_draft",
|
||||
"create_new_claim_from_documents",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _flatten_capability_codes(
|
||||
@@ -1165,16 +1188,18 @@ class OrchestratorService:
|
||||
if item.type == "expense_claim" and str(item.normalized_value or item.value or "").strip()
|
||||
)
|
||||
)
|
||||
expense_types = list(
|
||||
dict.fromkeys(
|
||||
str(item.normalized_value or item.value or "").strip()
|
||||
for item in ontology.entities
|
||||
if item.type == "expense_type" and str(item.normalized_value or item.value or "").strip()
|
||||
)
|
||||
)
|
||||
status_values = list(
|
||||
dict.fromkeys(
|
||||
str(item.value).strip()
|
||||
expense_types = list(
|
||||
dict.fromkeys(
|
||||
str(item.normalized_value or item.value or "").strip()
|
||||
for item in ontology.entities
|
||||
if item.type == "expense_type" and str(item.normalized_value or item.value or "").strip()
|
||||
)
|
||||
)
|
||||
project_values = self._collect_expense_query_filter_values(ontology, "project")
|
||||
location_values = self._collect_expense_query_filter_values(ontology, "location")
|
||||
status_values = list(
|
||||
dict.fromkeys(
|
||||
str(item.value).strip()
|
||||
for item in ontology.constraints
|
||||
if item.field == "status" and item.operator == "=" and str(item.value).strip()
|
||||
)
|
||||
@@ -1189,10 +1214,24 @@ class OrchestratorService:
|
||||
|
||||
if expense_claim_nos:
|
||||
conditions.append(ExpenseClaim.claim_no.in_(expense_claim_nos))
|
||||
if expense_types:
|
||||
conditions.append(ExpenseClaim.expense_type.in_(expense_types))
|
||||
if status_values:
|
||||
conditions.append(ExpenseClaim.status.in_(status_values))
|
||||
if expense_types:
|
||||
conditions.append(ExpenseClaim.expense_type.in_(expense_types))
|
||||
if status_values:
|
||||
conditions.append(ExpenseClaim.status.in_(status_values))
|
||||
if project_values:
|
||||
project_conditions = []
|
||||
for value in project_values:
|
||||
pattern = f"%{value}%"
|
||||
project_conditions.append(ExpenseClaim.project_code.ilike(pattern))
|
||||
project_conditions.append(ExpenseClaim.reason.ilike(pattern))
|
||||
conditions.append(or_(*project_conditions))
|
||||
if location_values:
|
||||
location_conditions = []
|
||||
for value in location_values:
|
||||
pattern = f"%{value}%"
|
||||
location_conditions.append(ExpenseClaim.location.ilike(pattern))
|
||||
location_conditions.append(ExpenseClaim.reason.ilike(pattern))
|
||||
conditions.append(or_(*location_conditions))
|
||||
|
||||
for item in amount_constraints:
|
||||
amount_value = float(item.value)
|
||||
@@ -1251,11 +1290,31 @@ class OrchestratorService:
|
||||
scoped_to_current_user = True
|
||||
else:
|
||||
scope_label = "全部报销单"
|
||||
|
||||
return conditions, scope_label, scoped_to_current_user
|
||||
|
||||
def _build_current_user_claim_conditions(
|
||||
self,
|
||||
|
||||
return conditions, scope_label, scoped_to_current_user
|
||||
|
||||
@staticmethod
|
||||
def _collect_expense_query_filter_values(
|
||||
ontology: OntologyParseResult,
|
||||
field_name: str,
|
||||
) -> list[str]:
|
||||
values: list[str] = []
|
||||
for entity in ontology.entities:
|
||||
if entity.type != field_name:
|
||||
continue
|
||||
value = str(entity.normalized_value or entity.value or "").strip()
|
||||
if value:
|
||||
values.append(value)
|
||||
for constraint in ontology.constraints:
|
||||
if constraint.field != field_name or constraint.operator != "=":
|
||||
continue
|
||||
value = str(constraint.value or "").strip()
|
||||
if value:
|
||||
values.append(value)
|
||||
return list(dict.fromkeys(values))
|
||||
|
||||
def _build_current_user_claim_conditions(
|
||||
self,
|
||||
*,
|
||||
user_id: str | None,
|
||||
context_json: dict[str, Any],
|
||||
|
||||
Reference in New Issue
Block a user