feat: 重构报销单AI预审流程并添加平台风险规则引擎
- 将AI验审改为AI预审,高风险不再拦截而是随单流转给审批人复核 - 新增平台风险规则评估引擎,支持事由过短、票据异常、重复发票等多种评估器 - 用户上下文增加部门信息(department_name),认证流程同步关联组织架构 - 规则scenario_json改为中文标签(差旅/费用科目),统一场景分类 - 新增orchestrator审核流程测试用例 - 前端更新审计视图、差旅报销等相关页面
This commit is contained in:
@@ -17,11 +17,12 @@ def get_db() -> Generator[Session, None, None]:
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CurrentUserContext:
|
||||
username: str
|
||||
name: str
|
||||
role_codes: list[str]
|
||||
is_admin: bool
|
||||
class CurrentUserContext:
|
||||
username: str
|
||||
name: str
|
||||
role_codes: list[str]
|
||||
is_admin: bool
|
||||
department_name: str = ""
|
||||
|
||||
|
||||
def get_current_user(
|
||||
@@ -41,6 +42,10 @@ def get_current_user(
|
||||
str | None,
|
||||
Header(description="是否管理员,支持 `true/false/1/0`。"),
|
||||
] = None,
|
||||
x_auth_department: Annotated[
|
||||
str | None,
|
||||
Header(description="当前登录人的所属部门。"),
|
||||
] = None,
|
||||
) -> CurrentUserContext:
|
||||
role_codes = [item.strip() for item in (x_auth_role_codes or "").split(",") if item.strip()]
|
||||
is_admin = str(x_auth_is_admin or "").strip().lower() in {"1", "true", "yes", "on"}
|
||||
@@ -56,10 +61,11 @@ def get_current_user(
|
||||
|
||||
return CurrentUserContext(
|
||||
username=username or name,
|
||||
name=name or username,
|
||||
role_codes=role_codes,
|
||||
is_admin=is_admin,
|
||||
)
|
||||
name=name or username,
|
||||
role_codes=role_codes,
|
||||
is_admin=is_admin,
|
||||
department_name=(x_auth_department or "").strip(),
|
||||
)
|
||||
|
||||
|
||||
def require_admin_user(
|
||||
|
||||
@@ -12,6 +12,8 @@ class AuthUserRead(BaseModel):
|
||||
username: str
|
||||
name: str
|
||||
role: str
|
||||
department: str = ""
|
||||
departmentName: str = ""
|
||||
position: str = ""
|
||||
grade: str = ""
|
||||
roleCodes: list[str] = Field(default_factory=list)
|
||||
|
||||
@@ -93,9 +93,11 @@ LEGACY_RULE_CODES = (
|
||||
"rule.ap.payment_dual_review",
|
||||
)
|
||||
|
||||
ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements"
|
||||
COMPANY_TRAVEL_RULE_VERSION = "v1.0.0"
|
||||
COMPANY_COMMUNICATION_RULE_VERSION = "v1.0.0"
|
||||
ATTACHMENT_RULE_ASSET_CODE = "rule.expense.attachment_submission_requirements"
|
||||
COMPANY_TRAVEL_RULE_VERSION = "v1.0.0"
|
||||
COMPANY_COMMUNICATION_RULE_VERSION = "v1.0.0"
|
||||
COMPANY_TRAVEL_RULE_SCENARIO_JSON = ("差旅",)
|
||||
COMPANY_COMMUNICATION_RULE_SCENARIO_JSON = ("费用科目",)
|
||||
|
||||
ATTACHMENT_RULE_RUNTIME_CONFIG = {
|
||||
"kind": "policy_rule_draft",
|
||||
@@ -267,49 +269,53 @@ class AgentFoundationService:
|
||||
)
|
||||
company_travel_rule = AgentAsset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||
name="公司差旅费报销规则",
|
||||
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=["expense", "travel_policy", "travel_standard"],
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||
name="公司差旅费报销规则",
|
||||
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=list(COMPANY_TRAVEL_RULE_SCENARIO_JSON),
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||
published_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||
working_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||
config_json={
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
platform_risk_assets = self._build_platform_risk_seed_assets()
|
||||
company_communication_rule = AgentAsset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
||||
name="公司通信费报销规则",
|
||||
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=["expense", "communication_expense", "expense_standard"],
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
||||
name="公司通信费报销规则",
|
||||
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON),
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
current_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
||||
published_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
||||
working_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
||||
config_json={
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
skill_expense_asset = AgentAsset(
|
||||
asset_type=AgentAssetType.SKILL.value,
|
||||
code="skill.expense.summary_lookup",
|
||||
@@ -1266,47 +1272,52 @@ class AgentFoundationService:
|
||||
if COMPANY_TRAVEL_EXPENSE_RULE_CODE not in existing_codes:
|
||||
company_travel_rule = self._create_seed_asset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||
name="公司差旅费报销规则",
|
||||
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=["expense", "travel_policy", "travel_standard"],
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
code=COMPANY_TRAVEL_EXPENSE_RULE_CODE,
|
||||
name="公司差旅费报销规则",
|
||||
description="通过 Excel 明细表维护差旅费报销标准、票据要求和审批口径。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=list(COMPANY_TRAVEL_RULE_SCENARIO_JSON),
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
current_version=COMPANY_TRAVEL_RULE_VERSION,
|
||||
config_json={
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
if COMPANY_COMMUNICATION_EXPENSE_RULE_CODE not in existing_codes:
|
||||
company_communication_rule = self._create_seed_asset(
|
||||
asset_type=AgentAssetType.RULE.value,
|
||||
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
||||
name="公司通信费报销规则",
|
||||
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=["expense", "communication_expense", "expense_standard"],
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
code=COMPANY_COMMUNICATION_EXPENSE_RULE_CODE,
|
||||
name="公司通信费报销规则",
|
||||
description="通过 Excel 明细表维护员工通信费报销标准、专项补充口径和审批要求。",
|
||||
domain=AgentAssetDomain.EXPENSE.value,
|
||||
scenario_json=list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON),
|
||||
owner="财务制度管理组",
|
||||
reviewer="顾承宇",
|
||||
status=AgentAssetStatus.ACTIVE.value,
|
||||
current_version=COMPANY_COMMUNICATION_RULE_VERSION,
|
||||
config_json={
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
|
||||
if company_travel_rule is not None:
|
||||
if not str(company_travel_rule.current_version or "").strip():
|
||||
company_travel_rule.current_version = COMPANY_TRAVEL_RULE_VERSION
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
},
|
||||
)
|
||||
|
||||
if company_travel_rule is not None:
|
||||
company_travel_rule.scenario_json = list(COMPANY_TRAVEL_RULE_SCENARIO_JSON)
|
||||
if not str(company_travel_rule.current_version or "").strip():
|
||||
company_travel_rule.current_version = COMPANY_TRAVEL_RULE_VERSION
|
||||
if not str(company_travel_rule.working_version or "").strip():
|
||||
company_travel_rule.working_version = company_travel_rule.current_version
|
||||
if not str(company_travel_rule.published_version or "").strip():
|
||||
@@ -1318,11 +1329,13 @@ class AgentFoundationService:
|
||||
**(company_travel_rule.config_json or {}),
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
}
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"scenario_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_TRAVEL_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "差旅报销 Excel 模板",
|
||||
}
|
||||
company_travel_rule_meta = self._ensure_company_travel_rule_spreadsheet_seed(
|
||||
company_travel_rule,
|
||||
version=str(company_travel_rule.current_version or COMPANY_TRAVEL_RULE_VERSION),
|
||||
@@ -1350,9 +1363,10 @@ class AgentFoundationService:
|
||||
reviewed_at=datetime.now(UTC),
|
||||
)
|
||||
|
||||
if company_communication_rule is not None:
|
||||
if not str(company_communication_rule.current_version or "").strip():
|
||||
company_communication_rule.current_version = COMPANY_COMMUNICATION_RULE_VERSION
|
||||
if company_communication_rule is not None:
|
||||
company_communication_rule.scenario_json = list(COMPANY_COMMUNICATION_RULE_SCENARIO_JSON)
|
||||
if not str(company_communication_rule.current_version or "").strip():
|
||||
company_communication_rule.current_version = COMPANY_COMMUNICATION_RULE_VERSION
|
||||
if not str(company_communication_rule.working_version or "").strip():
|
||||
company_communication_rule.working_version = company_communication_rule.current_version
|
||||
if not str(company_communication_rule.published_version or "").strip():
|
||||
@@ -1364,11 +1378,13 @@ class AgentFoundationService:
|
||||
**(company_communication_rule.config_json or {}),
|
||||
"severity": "medium",
|
||||
"enabled": True,
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
}
|
||||
"tag": "财务规则",
|
||||
"detail_mode": "spreadsheet",
|
||||
"rule_library": FINANCE_RULES_LIBRARY,
|
||||
"scenario_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"ai_review_category": COMPANY_COMMUNICATION_RULE_SCENARIO_JSON[0],
|
||||
"rule_template_label": "通信费报销 Excel 模板",
|
||||
}
|
||||
company_communication_rule_meta = self._ensure_company_communication_rule_spreadsheet_seed(
|
||||
company_communication_rule,
|
||||
version=str(company_communication_rule.current_version or COMPANY_COMMUNICATION_RULE_VERSION),
|
||||
|
||||
@@ -31,6 +31,7 @@ class AuthenticatedUser:
|
||||
username: str
|
||||
name: str
|
||||
role: str
|
||||
department: str
|
||||
position: str
|
||||
grade: str
|
||||
role_codes: list[str]
|
||||
@@ -78,6 +79,7 @@ class AuthService:
|
||||
username=admin_username or admin_email,
|
||||
name=display_name,
|
||||
role="管理员",
|
||||
department="",
|
||||
position="系统管理员",
|
||||
grade="",
|
||||
role_codes=["manager"],
|
||||
@@ -94,7 +96,7 @@ class AuthService:
|
||||
|
||||
stmt = (
|
||||
select(Employee)
|
||||
.options(selectinload(Employee.roles))
|
||||
.options(selectinload(Employee.organization_unit), selectinload(Employee.roles))
|
||||
.where(func.lower(Employee.email) == identifier.lower())
|
||||
)
|
||||
employee = self.db.execute(stmt).scalars().first()
|
||||
@@ -120,6 +122,7 @@ class AuthService:
|
||||
username=employee.email,
|
||||
name=employee.name,
|
||||
role=ROLE_LABELS.get(primary_role_code, "使用者"),
|
||||
department=employee.organization_unit.name if employee.organization_unit is not None else "",
|
||||
position=employee.position,
|
||||
grade=employee.grade,
|
||||
role_codes=role_codes or ["user"],
|
||||
@@ -134,6 +137,8 @@ class AuthService:
|
||||
username=user.username,
|
||||
name=user.name,
|
||||
role=user.role,
|
||||
department=user.department,
|
||||
departmentName=user.department,
|
||||
position=user.position,
|
||||
grade=user.grade,
|
||||
roleCodes=user.role_codes,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -120,11 +120,13 @@ EXPLAIN_KEYWORDS = ("为什么", "依据", "原因", "怎么处理", "是否可
|
||||
COMPARE_KEYWORDS = ("对比", "比较", "相比", "差异", "变化")
|
||||
RISK_KEYWORDS = ("风险", "异常", "重复", "超标", "超预算", "逾期", "验真", "巡检")
|
||||
DRAFT_KEYWORDS = ("生成", "草稿", "起草", "拟一份", "创建", "发起", "准备")
|
||||
DRAFT_FOLLOW_UP_KEYWORDS = (
|
||||
"继续",
|
||||
"补充",
|
||||
"补一下",
|
||||
"修改",
|
||||
DRAFT_FOLLOW_UP_KEYWORDS = (
|
||||
"继续",
|
||||
"下一步",
|
||||
"核对",
|
||||
"补充",
|
||||
"补一下",
|
||||
"修改",
|
||||
"改成",
|
||||
"改为",
|
||||
"换成",
|
||||
@@ -136,9 +138,16 @@ DRAFT_FOLLOW_UP_KEYWORDS = (
|
||||
"地点是",
|
||||
"金额是",
|
||||
"日期是",
|
||||
"时间是",
|
||||
)
|
||||
OPERATE_KEYWORDS = (
|
||||
"时间是",
|
||||
)
|
||||
EXPENSE_REVIEW_ACTIONS = {
|
||||
"save_draft",
|
||||
"next_step",
|
||||
"edit_review",
|
||||
"link_to_existing_draft",
|
||||
"create_new_claim_from_documents",
|
||||
}
|
||||
OPERATE_KEYWORDS = (
|
||||
"直接付款",
|
||||
"帮我付款",
|
||||
"安排付款",
|
||||
@@ -636,12 +645,17 @@ class SemanticOntologyService:
|
||||
def _compact(text: str) -> str:
|
||||
return re.sub(r"\s+", "", text).lower()
|
||||
|
||||
@staticmethod
|
||||
def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None:
|
||||
value = str(context_json.get("conversation_scenario") or "").strip()
|
||||
if value in CONTEXTUAL_SCENARIOS:
|
||||
return value
|
||||
return None
|
||||
@staticmethod
|
||||
def _resolve_context_scenario(context_json: dict[str, Any]) -> str | None:
|
||||
value = str(context_json.get("conversation_scenario") or "").strip()
|
||||
if value in CONTEXTUAL_SCENARIOS:
|
||||
return value
|
||||
review_action = str(context_json.get("review_action") or "").strip()
|
||||
if review_action in EXPENSE_REVIEW_ACTIONS:
|
||||
return "expense"
|
||||
if str(context_json.get("draft_claim_id") or "").strip():
|
||||
return "expense"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _resolve_session_type_scenario(context_json: dict[str, Any]) -> str | None:
|
||||
@@ -728,19 +742,22 @@ class SemanticOntologyService:
|
||||
)
|
||||
return len(compact_query) <= 12 and not has_domain_keyword
|
||||
|
||||
def _should_inherit_expense_draft(
|
||||
self,
|
||||
compact_query: str,
|
||||
def _should_inherit_expense_draft(
|
||||
self,
|
||||
compact_query: str,
|
||||
*,
|
||||
scenario: str,
|
||||
entities: list[OntologyEntity],
|
||||
time_range: OntologyTimeRange,
|
||||
context_json: dict[str, Any],
|
||||
) -> bool:
|
||||
context_scenario = self._resolve_context_scenario(context_json)
|
||||
draft_claim_id = str(context_json.get("draft_claim_id") or "").strip()
|
||||
if context_scenario != "expense" and not draft_claim_id:
|
||||
return False
|
||||
context_json: dict[str, Any],
|
||||
) -> bool:
|
||||
context_scenario = self._resolve_context_scenario(context_json)
|
||||
draft_claim_id = str(context_json.get("draft_claim_id") or "").strip()
|
||||
review_action = str(context_json.get("review_action") or "").strip()
|
||||
if review_action in EXPENSE_REVIEW_ACTIONS:
|
||||
return True
|
||||
if context_scenario != "expense" and not draft_claim_id:
|
||||
return False
|
||||
|
||||
if any(keyword in compact_query for keyword in DRAFT_FOLLOW_UP_KEYWORDS):
|
||||
return True
|
||||
@@ -1674,15 +1691,16 @@ class SemanticOntologyService:
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def _allow_incomplete_draft(
|
||||
context_json: dict[str, Any],
|
||||
*,
|
||||
scenario: str,
|
||||
intent: str,
|
||||
def _allow_incomplete_draft(
|
||||
context_json: dict[str, Any],
|
||||
*,
|
||||
scenario: str,
|
||||
intent: str,
|
||||
) -> bool:
|
||||
if scenario != "expense" or intent != "draft":
|
||||
return False
|
||||
return str(context_json.get("review_action") or "").strip() == "save_draft"
|
||||
if scenario != "expense" or intent != "draft":
|
||||
return False
|
||||
review_action = str(context_json.get("review_action") or "").strip()
|
||||
return review_action in EXPENSE_REVIEW_ACTIONS
|
||||
|
||||
@staticmethod
|
||||
def _display_slot_label(slot: str) -> str:
|
||||
|
||||
@@ -173,9 +173,11 @@ class OrchestratorService:
|
||||
task_asset=task_asset,
|
||||
)
|
||||
selected_capability_codes = self._flatten_capability_codes(capabilities)
|
||||
requires_confirmation = (
|
||||
ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value
|
||||
)
|
||||
is_expense_review_action = self._is_expense_review_action(context_json)
|
||||
requires_confirmation = (
|
||||
ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value
|
||||
and not is_expense_review_action
|
||||
)
|
||||
|
||||
route_json = {
|
||||
"orchestrated_by": AgentName.ORCHESTRATOR.value,
|
||||
@@ -526,7 +528,11 @@ class OrchestratorService:
|
||||
failed_tool_count=1 if degraded else 0,
|
||||
)
|
||||
|
||||
next_step = self._resolve_next_step(ontology, payload.source)
|
||||
next_step = self._resolve_next_step(
|
||||
ontology,
|
||||
payload.source,
|
||||
context_json=context_json,
|
||||
)
|
||||
if next_step == "query_database":
|
||||
tool_payload, degraded = self._invoke_tool(
|
||||
run_id=run_id,
|
||||
@@ -662,9 +668,9 @@ class OrchestratorService:
|
||||
"degraded": True,
|
||||
}
|
||||
|
||||
if ontology.scenario == "expense":
|
||||
tool_type = AgentToolType.DATABASE.value
|
||||
tool_name = "database.expense_claims.save_or_submit"
|
||||
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,
|
||||
@@ -781,10 +787,17 @@ class OrchestratorService:
|
||||
failed_tool_count=failed_tool_count,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_next_step(ontology: OntologyParseResult, source: str) -> str:
|
||||
if ontology.clarification_required:
|
||||
return "ask_clarification"
|
||||
@staticmethod
|
||||
def _resolve_next_step(
|
||||
ontology: OntologyParseResult,
|
||||
source: str,
|
||||
*,
|
||||
context_json: dict[str, Any] | None = None,
|
||||
) -> str:
|
||||
if OrchestratorService._is_expense_review_action(context_json or {}):
|
||||
return "create_draft"
|
||||
if ontology.clarification_required:
|
||||
return "ask_clarification"
|
||||
if ontology.intent == "draft":
|
||||
return "create_draft"
|
||||
if ontology.scenario == "knowledge" or ontology.intent == "explain":
|
||||
@@ -793,7 +806,18 @@ class OrchestratorService:
|
||||
return "run_rule"
|
||||
if ontology.intent in {"query", "compare"}:
|
||||
return "query_database"
|
||||
return "create_draft"
|
||||
return "create_draft"
|
||||
|
||||
@staticmethod
|
||||
def _is_expense_review_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",
|
||||
"edit_review",
|
||||
"link_to_existing_draft",
|
||||
"create_new_claim_from_documents",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _flatten_capability_codes(
|
||||
|
||||
@@ -255,7 +255,7 @@ class UserAgentService:
|
||||
query_payload = self._build_query_payload(payload)
|
||||
draft_payload = (
|
||||
self._build_draft_payload(payload)
|
||||
if payload.ontology.intent == "draft"
|
||||
if self._should_build_draft_payload(payload)
|
||||
else None
|
||||
)
|
||||
review_payload = self._build_review_payload(
|
||||
@@ -1683,7 +1683,10 @@ class UserAgentService:
|
||||
if not risk_flags and not platform_messages:
|
||||
return "当前未识别到明确风险标签,建议继续查看原始明细或补充更多上下文。"
|
||||
|
||||
reasons = [RISK_REASON_MAP.get(flag, f"{flag} 需要人工进一步确认。") for flag in risk_flags]
|
||||
reasons = [
|
||||
f"{flag}:{RISK_REASON_MAP.get(flag, f'{flag} 需要人工进一步确认。')}"
|
||||
for flag in risk_flags
|
||||
]
|
||||
if platform_messages:
|
||||
reasons.extend(platform_messages)
|
||||
citation_text = (
|
||||
@@ -1764,6 +1767,17 @@ class UserAgentService:
|
||||
approval_stage=approval_stage,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _should_build_draft_payload(payload: UserAgentRequest) -> bool:
|
||||
if payload.ontology.intent == "draft":
|
||||
return True
|
||||
if payload.ontology.scenario != "expense":
|
||||
return False
|
||||
return any(
|
||||
str(payload.tool_payload.get(key) or "").strip()
|
||||
for key in ("claim_id", "claim_no", "status")
|
||||
)
|
||||
|
||||
def _build_suggested_actions(
|
||||
self,
|
||||
payload: UserAgentRequest,
|
||||
@@ -1868,6 +1882,7 @@ class UserAgentService:
|
||||
payload,
|
||||
slot_cards=slot_cards,
|
||||
)
|
||||
submission_blocked = bool(payload.tool_payload.get("submission_blocked"))
|
||||
risk_briefs = self._build_review_risk_briefs(
|
||||
payload,
|
||||
citations=citations,
|
||||
@@ -1877,7 +1892,7 @@ class UserAgentService:
|
||||
association_choice_pending = self._is_review_association_choice_pending(payload)
|
||||
can_proceed = (
|
||||
False
|
||||
if association_choice_pending
|
||||
if association_choice_pending or submission_blocked
|
||||
else self._can_proceed_review(
|
||||
payload,
|
||||
missing_slot_keys=missing_slot_keys,
|
||||
@@ -2157,6 +2172,15 @@ class UserAgentService:
|
||||
claim_groups: list[UserAgentReviewClaimGroup],
|
||||
) -> list[UserAgentReviewRiskBrief]:
|
||||
briefs: list[UserAgentReviewRiskBrief] = []
|
||||
for reason in self._resolve_submission_blocked_reasons(payload):
|
||||
briefs.append(
|
||||
UserAgentReviewRiskBrief(
|
||||
title="AI预审未通过",
|
||||
level="high",
|
||||
content=reason,
|
||||
)
|
||||
)
|
||||
|
||||
employee_name = self._collect_entity_values(payload).get("employee_name") or str(
|
||||
payload.context_json.get("name") or ""
|
||||
).strip()
|
||||
@@ -2229,6 +2253,36 @@ class UserAgentService:
|
||||
|
||||
return briefs[:4]
|
||||
|
||||
@staticmethod
|
||||
def _resolve_submission_blocked_reasons(payload: UserAgentRequest) -> list[str]:
|
||||
raw_reasons = payload.tool_payload.get("submission_blocked_reasons")
|
||||
if raw_reasons is None:
|
||||
raw_reasons = payload.tool_payload.get("missing_fields")
|
||||
|
||||
reasons: list[str] = []
|
||||
if isinstance(raw_reasons, list):
|
||||
reasons.extend(str(item or "").strip() for item in raw_reasons)
|
||||
elif isinstance(raw_reasons, str):
|
||||
reasons.extend(
|
||||
item.strip()
|
||||
for item in re.split(r"[;;\n]+", raw_reasons)
|
||||
if item.strip()
|
||||
)
|
||||
|
||||
if not reasons:
|
||||
message = str(payload.tool_payload.get("message") or "").strip()
|
||||
prefix = "提交前请先补全信息:"
|
||||
if message.startswith(prefix):
|
||||
message = message[len(prefix):].strip()
|
||||
if message:
|
||||
reasons.extend(
|
||||
item.strip()
|
||||
for item in re.split(r"[;;\n]+", message)
|
||||
if item.strip() and not item.strip().startswith("AI预审暂未通过")
|
||||
)
|
||||
|
||||
return list(dict.fromkeys(reason for reason in reasons if reason))
|
||||
|
||||
def _build_review_confirmation_actions(
|
||||
self,
|
||||
payload: UserAgentRequest,
|
||||
@@ -2383,6 +2437,16 @@ class UserAgentService:
|
||||
stage_text = draft_payload.approval_stage or "审批中"
|
||||
return f"报销单 {draft_payload.claim_no or ''} 已提交,当前节点为 {stage_text}。".strip()
|
||||
if payload.tool_payload.get("submission_blocked"):
|
||||
reasons = self._resolve_submission_blocked_reasons(payload)
|
||||
if reasons:
|
||||
reason_lines = "\n".join(
|
||||
f"{index}. {reason}" for index, reason in enumerate(reasons, start=1)
|
||||
)
|
||||
return (
|
||||
"AI预审暂未通过,所以还没有提交到审批人。\n"
|
||||
f"{reason_lines}\n"
|
||||
"请先处理以上项目;处理完成后再点继续下一步。"
|
||||
)
|
||||
return str(payload.tool_payload.get("message") or "").strip() or "当前报销单暂时还不能提交审批。"
|
||||
return (
|
||||
f"{self._build_review_intent_summary(payload, slot_cards=review_payload.slot_cards, claim_groups=review_payload.claim_groups)} "
|
||||
|
||||
Reference in New Issue
Block a user