fix(ai): recognize conversational document filters
This commit is contained in:
@@ -83,6 +83,13 @@ ENGLISH_FINANCE_BUSINESS_KEYWORDS = (
|
||||
|
||||
|
||||
class OntologyDetectionMixin:
|
||||
@staticmethod
|
||||
def _has_document_query_signal(compact_query: str) -> bool:
|
||||
return "单据" in compact_query and (
|
||||
any(keyword in compact_query for keyword in QUERY_KEYWORDS)
|
||||
or any(keyword in compact_query for keyword in STATUS_KEYWORDS)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_expense_application_context(context_json: dict[str, Any]) -> bool:
|
||||
document_type = str(context_json.get("document_type") or "").strip()
|
||||
@@ -141,9 +148,7 @@ class OntologyDetectionMixin:
|
||||
)
|
||||
):
|
||||
return True
|
||||
if "单据" in compact_query and (
|
||||
"状态" in compact_query or any(keyword in compact_query for keyword in STATUS_KEYWORDS)
|
||||
):
|
||||
if self._has_document_query_signal(compact_query):
|
||||
return True
|
||||
if any(keyword in compact_query for keyword in EXPENSE_NARRATIVE_KEYWORDS):
|
||||
return True
|
||||
@@ -224,9 +229,7 @@ class OntologyDetectionMixin:
|
||||
best_scenario = "budget"
|
||||
best_score = scores["budget"]
|
||||
if best_score <= 0:
|
||||
if "单据" in compact_query and any(
|
||||
keyword in compact_query for keyword in STATUS_KEYWORDS
|
||||
):
|
||||
if self._has_document_query_signal(compact_query):
|
||||
return "expense", 0.14
|
||||
return "unknown", 0.0
|
||||
|
||||
@@ -539,7 +542,7 @@ class OntologyDetectionMixin:
|
||||
"出现“客户”不等于应收,出现“供应商”不等于应付,必须结合动作词和业务目标判断。"
|
||||
"预算编制、预算金额、成本中心、预算科目、预算预警、预算占用、"
|
||||
"剩余预算、可用预算、超预算、预算不足等问题必须使用 budget 场景。"
|
||||
"只有明确查询、统计、列出、多少、明细、对比时才优先使用 query 或 compare。"
|
||||
"只有明确查询、筛选、过滤、统计、列出、多少、明细、对比时才优先使用 query 或 compare。"
|
||||
"附件名称和 OCR 摘要只作为辅助证据,不能编造未出现的事实。"
|
||||
"如果用户明确提到打车、的士票、出租车票、网约车、乘车费、车费等交通票据,"
|
||||
"即使句子里出现“客户”,也必须优先识别为 transport,不要推断为 entertainment。"
|
||||
|
||||
@@ -36,6 +36,7 @@ SCENARIO_KEYWORDS = {
|
||||
("单据报销", 0.18),
|
||||
("报账", 0.20),
|
||||
("差旅", 0.20),
|
||||
("出差", 0.20),
|
||||
("费用", 0.14),
|
||||
("发票", 0.14),
|
||||
("票据", 0.12),
|
||||
@@ -99,6 +100,8 @@ QUERY_KEYWORDS = (
|
||||
"查",
|
||||
"查询",
|
||||
"查看",
|
||||
"筛选",
|
||||
"过滤",
|
||||
"列出",
|
||||
"统计",
|
||||
"汇总",
|
||||
@@ -158,6 +161,7 @@ EXPENSE_TYPE_KEYWORDS = build_expense_type_keyword_map()
|
||||
EXPENSE_NARRATIVE_KEYWORDS = (
|
||||
"报销",
|
||||
"报账",
|
||||
"出差",
|
||||
"招待",
|
||||
"招待费",
|
||||
"花销",
|
||||
|
||||
@@ -266,6 +266,46 @@ def test_semantic_ontology_service_extracts_entities_time_and_constraints() -> N
|
||||
assert result.time_range.end_date == "2026-04-30"
|
||||
|
||||
|
||||
def test_semantic_ontology_service_recognizes_colloquial_travel_city_filter(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
service = SemanticOntologyService(db)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_parse_with_model",
|
||||
lambda **_kwargs: (None, [], "model_unavailable_for_rule_regression"),
|
||||
)
|
||||
|
||||
result = service.parse(
|
||||
OntologyParseRequest(
|
||||
query="出差上海的单据请筛选一下",
|
||||
user_id="pytest",
|
||||
)
|
||||
)
|
||||
|
||||
entity_map = {item.type: item.normalized_value for item in result.entities}
|
||||
assert result.scenario == "expense"
|
||||
assert result.intent == "query"
|
||||
assert result.permission.level == "read"
|
||||
assert entity_map["expense_type"] == "travel"
|
||||
assert entity_map["location"] == "上海"
|
||||
|
||||
generic_result = service.parse(
|
||||
OntologyParseRequest(
|
||||
query="上海的单据请筛选一下",
|
||||
user_id="pytest",
|
||||
)
|
||||
)
|
||||
generic_entities = {
|
||||
item.type: item.normalized_value for item in generic_result.entities
|
||||
}
|
||||
assert generic_result.scenario == "expense"
|
||||
assert generic_result.intent == "query"
|
||||
assert generic_entities["location"] == "上海"
|
||||
|
||||
|
||||
def test_semantic_ontology_service_extracts_budget_query_fields() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
|
||||
@@ -562,6 +562,77 @@ def test_orchestrator_history_query_filters_location_time_and_returns_real_amoun
|
||||
assert "321.45" in response.result["answer"]
|
||||
|
||||
|
||||
def test_orchestrator_colloquial_travel_city_filter_returns_only_matching_documents(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
id="emp-colloquial-filter",
|
||||
employee_no="E9021",
|
||||
name="张三",
|
||||
email="colloquial-filter@example.com",
|
||||
)
|
||||
db.add_all(
|
||||
[
|
||||
employee,
|
||||
ExpenseClaim(
|
||||
id="claim-colloquial-shanghai",
|
||||
claim_no="EXP-SHANGHAI-001",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="前往上海支持客户项目",
|
||||
location="上海",
|
||||
amount=Decimal("888.00"),
|
||||
currency="CNY",
|
||||
occurred_at=datetime(2026, 7, 8, 9, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
),
|
||||
ExpenseClaim(
|
||||
id="claim-colloquial-beijing",
|
||||
claim_no="EXP-BEIJING-001",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="张三",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="前往北京支持客户项目",
|
||||
location="北京",
|
||||
amount=Decimal("666.00"),
|
||||
currency="CNY",
|
||||
occurred_at=datetime(2026, 7, 9, 9, 0, tzinfo=UTC),
|
||||
status="paid",
|
||||
),
|
||||
]
|
||||
)
|
||||
db.commit()
|
||||
|
||||
response = run_for_fixture_tenant(
|
||||
OrchestratorService(db),
|
||||
OrchestratorRequest(
|
||||
source="user_message",
|
||||
user_id="colloquial-filter@example.com",
|
||||
message="出差上海的单据请筛选一下",
|
||||
),
|
||||
)
|
||||
|
||||
query_payload = response.result["query_payload"]
|
||||
assert response.status == "succeeded"
|
||||
assert response.trace_summary.scenario == "expense"
|
||||
assert response.trace_summary.intent == "query"
|
||||
assert query_payload["record_count"] == 1
|
||||
assert [item["claim_no"] for item in query_payload["records"]] == [
|
||||
"EXP-SHANGHAI-001"
|
||||
]
|
||||
|
||||
|
||||
def test_orchestrator_archive_query_filters_archived_claims_and_limits_preview(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user