Files
X-Financial/server/src/app/services/risk_ontology_bridge.py
caoxiaozhu d460ee0fe7 fix(agent): 修复规则中心表格版本和修改记录
补齐规则资产 JSON 读写接口和前端调用,修复 AuditView 导入缺失。

Excel 在线编辑改为比对所有页签并生成最近修改记录,版本快照统一保存到 rules/finance-rules/.versions。

隔离规则表测试存储,避免测试或旧入口写入真实规则目录与 storage/agent_assets。
2026-05-19 15:41:53 +00:00

78 lines
3.4 KiB
Python

from __future__ import annotations
from app.schemas.ontology import OntologyParseResult
RISK_SIGNAL_TO_RULE_CODES: dict[str, list[str]] = {
"location_mismatch": ["risk.travel.destination_receipt_location"],
"base_location_overlap": ["risk.travel.base_location_overlap"],
"intracity_travel": ["risk.travel.intracity_travel_claim"],
"multi_city_itinerary": ["risk.travel.multi_city_reason_required"],
"hotel_itinerary_mismatch": ["risk.travel.hotel_without_itinerary"],
"duplicate_invoice": ["risk.invoice.duplicate_invoice"],
"buyer_name_mismatch": ["risk.invoice.claimant_buyer_name_match"],
"document_expense_mismatch": ["risk.invoice.document_expense_mismatch"],
"cross_year_invoice": ["risk.invoice.cross_year_invoice"],
"void_or_red_invoice": ["risk.invoice.void_or_red_invoice"],
"vague_goods_description": ["risk.invoice.vague_goods_description"],
"entertainment_missing_detail": ["risk.expense.entertainment_missing_detail"],
"meal_as_travel": ["risk.expense.meal_localized_as_travel"],
"consecutive_transport_receipts": ["risk.expense.consecutive_transport_receipts"],
"reason_too_brief": ["risk.expense.reason_too_brief"],
}
TEXT_SIGNAL_KEYWORDS: dict[str, tuple[str, ...]] = {
"location_mismatch": ("地点", "行程", "出差地", "票据地", "城市不一致"),
"duplicate_invoice": ("重复", "同一张票", "重复报销", "发票重复"),
"buyer_name_mismatch": ("购买方", "抬头", "开票单位"),
"document_expense_mismatch": ("附件", "票据", "单据", "材料不一致"),
"cross_year_invoice": ("跨年", "以前年度", "去年发票"),
"void_or_red_invoice": ("作废", "红冲", "红字"),
"vague_goods_description": ("商品名称", "品名", "笼统"),
"entertainment_missing_detail": ("招待", "宴请", "陪同", "客户餐"),
"meal_as_travel": ("餐费", "差旅餐", "本地餐"),
"consecutive_transport_receipts": ("连续交通", "多张车票", "打车"),
"reason_too_brief": ("事由", "说明太短", "理由不足"),
}
def list_all_platform_risk_rule_codes() -> list[str]:
return sorted({code for codes in RISK_SIGNAL_TO_RULE_CODES.values() for code in codes})
def resolve_rule_codes_from_ontology(ontology: OntologyParseResult) -> list[str]:
resolved: list[str] = []
for signal in ontology.risk_flags:
for rule_code in RISK_SIGNAL_TO_RULE_CODES.get(str(signal or "").strip(), []):
if rule_code not in resolved:
resolved.append(rule_code)
return resolved
def infer_risk_signals_from_text(text: str) -> list[str]:
normalized = str(text or "").strip().lower()
if not normalized:
return []
signals: list[str] = []
for signal, keywords in TEXT_SIGNAL_KEYWORDS.items():
if any(keyword.lower() in normalized for keyword in keywords):
signals.append(signal)
return signals
def resolve_rule_codes_for_risk_check(
ontology: OntologyParseResult,
*,
query_text: str = "",
) -> list[str]:
if ontology.intent != "risk_check":
return []
resolved = resolve_rule_codes_from_ontology(ontology)
for signal in infer_risk_signals_from_text(query_text):
for rule_code in RISK_SIGNAL_TO_RULE_CODES.get(signal, []):
if rule_code not in resolved:
resolved.append(rule_code)
return resolved or list_all_platform_risk_rule_codes()