feat: 增强规则资产管理与审计页面运行时调试
后端新增规则资产版本管理和规则文件 CRUD 接口,优化风险 规则生成模板执行和员工数据模型字段,知识库 RAG 增强本 地回退和文档提取能力,清理旧风险规则文件统一由生成引擎 管理,前端审计页面增加运行时调试面板和规则资产编辑交互, 补充单元测试覆盖。
This commit is contained in:
@@ -5,6 +5,38 @@ from zipfile import ZipFile
|
||||
from app.services.knowledge_document_extractors import _extract_document_text_from_path
|
||||
|
||||
|
||||
def test_extract_docx_document_text_preserves_tables_as_markdown(tmp_path) -> None:
|
||||
file_path = tmp_path / "financial-basic.docx"
|
||||
_write_minimal_docx_with_table(
|
||||
file_path,
|
||||
paragraphs=[
|
||||
"远光软件股份有限公司",
|
||||
"财务基础知识手册",
|
||||
"二、常用会计科目",
|
||||
],
|
||||
table=[
|
||||
["科目类别", "科目名称", "说明"],
|
||||
["资产类", "库存现金", "公司持有的现金"],
|
||||
["负债类", "应付账款", "因购买商品或接受劳务应付的款项"],
|
||||
["损益类", "销售费用", "为销售产品发生的费用"],
|
||||
],
|
||||
)
|
||||
|
||||
text = _extract_document_text_from_path(
|
||||
file_path=file_path,
|
||||
original_name="远光软件财务基础知识手册.docx",
|
||||
mime_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
)
|
||||
|
||||
assert "二、常用会计科目" in text
|
||||
assert "| 科目类别 | 科目名称 | 说明 |" in text
|
||||
assert "| 资产类 | 库存现金 | 公司持有的现金 |" in text
|
||||
assert "| 负债类 | 应付账款 | 因购买商品或接受劳务应付的款项 |" in text
|
||||
assert "| 损益类 | 销售费用 | 为销售产品发生的费用 |" in text
|
||||
assert "表格第 2 行:科目类别=资产类;科目名称=库存现金;说明=公司持有的现金" in text
|
||||
assert "科目类别\n科目名称\n说明" not in text
|
||||
|
||||
|
||||
def test_extract_xlsx_document_text_builds_markdown_with_row_clues(tmp_path) -> None:
|
||||
file_path = tmp_path / "company-expense-rules.xlsx"
|
||||
_write_minimal_xlsx(
|
||||
@@ -58,6 +90,39 @@ def test_extract_pptx_document_text_builds_markdown_slides(tmp_path) -> None:
|
||||
assert "- 发票、审批、预算三项要素必须齐全" in text
|
||||
|
||||
|
||||
def _write_minimal_docx_with_table(
|
||||
file_path,
|
||||
*,
|
||||
paragraphs: list[str],
|
||||
table: list[list[str]],
|
||||
) -> None:
|
||||
paragraph_xml = "\n".join(f"<w:p>{_docx_text_run(text)}</w:p>" for text in paragraphs)
|
||||
table_xml = (
|
||||
"<w:tbl>"
|
||||
+ "".join(
|
||||
"<w:tr>"
|
||||
+ "".join(f"<w:tc><w:p>{_docx_text_run(cell)}</w:p></w:tc>" for cell in row)
|
||||
+ "</w:tr>"
|
||||
for row in table
|
||||
)
|
||||
+ "</w:tbl>"
|
||||
)
|
||||
document_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:body>
|
||||
{paragraph_xml}
|
||||
{table_xml}
|
||||
</w:body>
|
||||
</w:document>
|
||||
"""
|
||||
with ZipFile(file_path, "w") as archive:
|
||||
archive.writestr("word/document.xml", document_xml)
|
||||
|
||||
|
||||
def _docx_text_run(text: str) -> str:
|
||||
return f"<w:r><w:t>{text}</w:t></w:r>"
|
||||
|
||||
|
||||
def _write_minimal_xlsx(file_path, *, sheet_name: str, rows: list[list[str]]) -> None:
|
||||
workbook_xml = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
|
||||
@@ -58,6 +58,34 @@ def test_build_hits_boosts_query_term_matches() -> None:
|
||||
assert [item["candidate_id"] for item in hits] == ["ent-1", "travel-1"]
|
||||
|
||||
|
||||
def test_build_hits_keeps_long_query_anchor_terms_for_accounting_table() -> None:
|
||||
hits = KnowledgeRagService._build_hits_from_query_data(
|
||||
query="远光软件财务基础知识手册里的常用会计科目是什么?",
|
||||
chunks=[
|
||||
{
|
||||
"chunk_id": "generic-1",
|
||||
"file_path": "/tmp/doc-1__远光软件财务制度培训手册.docx",
|
||||
"content": "远光软件股份有限公司财务培训内容,介绍费用报销和财务制度。",
|
||||
},
|
||||
{
|
||||
"chunk_id": "accounts-1",
|
||||
"file_path": "/tmp/doc-2__远光软件财务基础知识手册.docx",
|
||||
"content": (
|
||||
"二、常用会计科目\n\n"
|
||||
"| 科目类别 | 科目名称 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| 资产类 | 库存现金 | 公司持有的现金 |\n"
|
||||
"| 损益类 | 销售费用 | 为销售产品发生的费用 |"
|
||||
),
|
||||
},
|
||||
],
|
||||
entities=[],
|
||||
limit=2,
|
||||
)
|
||||
|
||||
assert [item["candidate_id"] for item in hits] == ["accounts-1", "generic-1"]
|
||||
|
||||
|
||||
def test_build_hits_prioritizes_answer_clue_appendix_for_rule_queries() -> None:
|
||||
hits = KnowledgeRagService._build_hits_from_query_data(
|
||||
query="报销时限是多少?",
|
||||
|
||||
@@ -589,6 +589,66 @@ def test_semantic_ontology_service_covers_common_expense_scene_keywords(
|
||||
)
|
||||
|
||||
|
||||
def test_semantic_ontology_service_connects_expense_application_to_ontology() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
result = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="申请2026-06-01 ~ 2026-06-03去北京做客户现场验收,差旅预算18000元",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"document_type": "expense_application",
|
||||
"application_stage": "pre_approval",
|
||||
"entry_source": "documents_application",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert result.scenario == "expense"
|
||||
assert result.intent == "draft"
|
||||
assert any(
|
||||
item.type == "document_type" and item.normalized_value == "expense_application"
|
||||
for item in result.entities
|
||||
)
|
||||
assert any(
|
||||
item.type == "workflow_stage" and item.normalized_value == "pre_approval"
|
||||
for item in result.entities
|
||||
)
|
||||
assert any(
|
||||
item.field == "document_type" and item.value == "expense_application"
|
||||
for item in result.constraints
|
||||
)
|
||||
assert any(
|
||||
item.type == "expense_type" and item.normalized_value == "travel"
|
||||
for item in result.entities
|
||||
)
|
||||
|
||||
|
||||
def test_semantic_ontology_service_requires_attachment_for_meeting_application() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
result = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="发起会务申请,2026-06-01 ~ 2026-06-02上海产品发布会,预算32000元",
|
||||
user_id="pytest",
|
||||
context_json={
|
||||
"document_type": "expense_application",
|
||||
"application_stage": "pre_approval",
|
||||
"entry_source": "documents_application",
|
||||
"attachment_count": 0,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
assert result.scenario == "expense"
|
||||
assert result.intent == "draft"
|
||||
assert any(
|
||||
item.type == "expense_type" and item.normalized_value == "meeting"
|
||||
for item in result.entities
|
||||
)
|
||||
assert "attachments" in result.missing_slots
|
||||
|
||||
|
||||
def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
|
||||
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus
|
||||
from app.core.agent_enums import AgentAssetDomain, AgentAssetStatus, AgentReviewStatus
|
||||
from app.db.base import Base
|
||||
from app.models.agent_asset import AgentAsset
|
||||
from app.schemas.agent_asset import AgentAssetRiskRuleGenerateRequest
|
||||
from app.models.financial_record import ExpenseClaim
|
||||
from app.schemas.agent_asset import (
|
||||
AgentAssetReviewCreate,
|
||||
AgentAssetRiskRuleGenerateRequest,
|
||||
AgentAssetRiskRuleReportRequest,
|
||||
AgentAssetRiskRuleSampleTestRequest,
|
||||
AgentAssetRiskRuleScenarioTestRequest,
|
||||
AgentAssetRiskRuleSimulationRequest,
|
||||
)
|
||||
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
|
||||
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
|
||||
from app.services.risk_rule_flow_diagram import RiskRuleFlowDiagramRenderer, RiskRuleFlowDiagramSpec
|
||||
from app.services.agent_assets import AgentAssetService
|
||||
from app.services.risk_rule_flow_diagram import (
|
||||
RiskRuleFlowDiagramRenderer,
|
||||
RiskRuleFlowDiagramSpec,
|
||||
)
|
||||
from app.services.risk_rule_generation import RiskRuleGenerationService
|
||||
|
||||
|
||||
@@ -43,6 +57,7 @@ def test_generate_risk_rule_asset_creates_draft_json_rule(tmp_path) -> None:
|
||||
asset_id = service.generate_rule_asset(
|
||||
AgentAssetRiskRuleGenerateRequest(
|
||||
business_domain=AgentAssetDomain.EXPENSE,
|
||||
expense_category="travel",
|
||||
risk_level="high",
|
||||
natural_language="住宿城市必须出现在本次差旅行程城市中,否则提示高风险。",
|
||||
),
|
||||
@@ -54,12 +69,18 @@ def test_generate_risk_rule_asset_creates_draft_json_rule(tmp_path) -> None:
|
||||
assert asset.status == AgentAssetStatus.DRAFT.value
|
||||
assert asset.config_json["detail_mode"] == "json_risk"
|
||||
assert asset.config_json["evaluator"] == "template_rule"
|
||||
assert asset.config_json["expense_category"] == "travel"
|
||||
assert asset.config_json["risk_category"] == "差旅费"
|
||||
assert asset.scenario_json == ["差旅费"]
|
||||
assert asset.current_version == "v0.1.0"
|
||||
|
||||
file_name = asset.config_json["rule_document"]["file_name"]
|
||||
rule_path = tmp_path / "rules" / RISK_RULES_LIBRARY / file_name
|
||||
payload = json.loads(rule_path.read_text(encoding="utf-8"))
|
||||
assert payload["rule_code"] == asset.code
|
||||
assert payload["applies_to"]["expense_categories"] == ["travel"]
|
||||
assert payload["risk_category"] == "差旅费"
|
||||
assert payload["metadata"]["expense_category"] == "travel"
|
||||
assert payload["outcomes"]["fail"]["severity"] == "high"
|
||||
assert payload["template_key"] == "field_compare_v1"
|
||||
assert payload["metadata"]["natural_language"].startswith("住宿城市")
|
||||
@@ -104,3 +125,206 @@ def test_risk_rule_flow_diagram_uses_risk_level_palette() -> None:
|
||||
assert "#dc2626" in high_svg
|
||||
assert high_svg.count("#dc2626") == 1
|
||||
assert "#10a37f" not in high_svg
|
||||
|
||||
|
||||
def test_risk_rule_requires_test_report_before_review_and_publish(tmp_path) -> None:
|
||||
with build_session() as db:
|
||||
manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
|
||||
generator = RiskRuleGenerationService(
|
||||
db,
|
||||
rule_library_manager=manager,
|
||||
runtime_chat_service=NullRuntimeChatService(),
|
||||
)
|
||||
asset_id = generator.generate_rule_asset(
|
||||
AgentAssetRiskRuleGenerateRequest(
|
||||
business_domain=AgentAssetDomain.EXPENSE,
|
||||
risk_level="high",
|
||||
natural_language="酒店发票城市必须与行程城市一致,不一致时标记高风险。",
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
service = AgentAssetService(db)
|
||||
service.rule_library_manager = manager
|
||||
|
||||
asset = db.get(AgentAsset, asset_id)
|
||||
assert asset is not None
|
||||
try:
|
||||
service.create_review(
|
||||
asset_id,
|
||||
AgentAssetReviewCreate(
|
||||
version=asset.working_version or "v0.1.0",
|
||||
reviewer="manager",
|
||||
review_status=AgentReviewStatus.PENDING,
|
||||
review_note="送审",
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
except PermissionError as exc:
|
||||
assert "测试通过" in str(exc)
|
||||
else:
|
||||
raise AssertionError("未测试通过的风险规则不应允许提交审核")
|
||||
|
||||
simulation = service.simulate_risk_rule_message(
|
||||
asset_id,
|
||||
AgentAssetRiskRuleSimulationRequest(
|
||||
message="我想仿真一张酒店报销单,酒店发票城市上海,申报目的地北京,金额580元。",
|
||||
),
|
||||
)
|
||||
assert simulation.execution_mode == "risk_rule_simulation"
|
||||
assert simulation.ready is True
|
||||
assert simulation.hit is True
|
||||
assert simulation.severity == "high"
|
||||
assert "不创建业务单据" in simulation.summary
|
||||
assert service.get_latest_risk_rule_test_summary(asset_id).sample is None
|
||||
|
||||
blocked_simulation = service.simulate_risk_rule_message(
|
||||
asset_id,
|
||||
AgentAssetRiskRuleSimulationRequest(
|
||||
message="请识别上传单据是否命中风险规则。",
|
||||
attachments=[{"name": "hotel-invoice.pdf", "content_type": "application/pdf"}],
|
||||
),
|
||||
)
|
||||
assert blocked_simulation.ready is False
|
||||
assert blocked_simulation.stage == "needs_recognition"
|
||||
assert blocked_simulation.hit is False
|
||||
assert "尚未完成识别" in blocked_simulation.summary
|
||||
|
||||
db.add(
|
||||
ExpenseClaim(
|
||||
claim_no="TEST-CLAIM-001",
|
||||
employee_name="张三",
|
||||
department_name="财务部",
|
||||
expense_type="住宿费",
|
||||
reason="北京出差住宿",
|
||||
location="北京",
|
||||
amount=Decimal("300.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime.now(UTC),
|
||||
created_at=datetime.now(UTC),
|
||||
status="draft",
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
sample = service.run_risk_rule_sample_test(
|
||||
asset_id,
|
||||
AgentAssetRiskRuleSampleTestRequest(),
|
||||
actor="pytest",
|
||||
)
|
||||
assert sample.passed is True
|
||||
|
||||
scenario = service.run_risk_rule_scenario_test(
|
||||
asset_id,
|
||||
AgentAssetRiskRuleScenarioTestRequest(intent="用最近30天的住宿报销单试运行"),
|
||||
actor="pytest",
|
||||
)
|
||||
assert scenario.passed is True
|
||||
assert scenario.result_json["total_count"] == 1
|
||||
|
||||
report = service.confirm_risk_rule_test_report(
|
||||
asset_id,
|
||||
AgentAssetRiskRuleReportRequest(confirm_passed=True),
|
||||
actor="pytest",
|
||||
)
|
||||
assert report.passed is True
|
||||
|
||||
review = service.create_review(
|
||||
asset_id,
|
||||
AgentAssetReviewCreate(
|
||||
version=asset.working_version or "v0.1.0",
|
||||
reviewer="manager",
|
||||
review_status=AgentReviewStatus.PENDING,
|
||||
review_note="送审",
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
assert review.review_status == AgentReviewStatus.PENDING.value
|
||||
published = service.publish_risk_rule(asset_id, actor="manager")
|
||||
assert published.status == AgentAssetStatus.ACTIVE.value
|
||||
assert published.published_version == asset.working_version
|
||||
|
||||
disabled = service.set_risk_rule_enabled(
|
||||
asset_id,
|
||||
enabled=False,
|
||||
actor="manager",
|
||||
)
|
||||
assert disabled.config_json["enabled"] is False
|
||||
rule_document = disabled.config_json["rule_document"]
|
||||
manifest = manager.read_rule_library_json(
|
||||
library=RISK_RULES_LIBRARY,
|
||||
file_name=rule_document["file_name"],
|
||||
)
|
||||
assert manifest["enabled"] is False
|
||||
|
||||
attachment_required_id = generator.generate_rule_asset(
|
||||
AgentAssetRiskRuleGenerateRequest(
|
||||
business_domain=AgentAssetDomain.EXPENSE,
|
||||
risk_level="medium",
|
||||
natural_language="发票号码不能为空,缺失时进入中风险复核。",
|
||||
requires_attachment=True,
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
attachment_required_asset = db.get(AgentAsset, attachment_required_id)
|
||||
assert attachment_required_asset is not None
|
||||
assert attachment_required_asset.config_json["requires_attachment"] is True
|
||||
attachment_rule_document = attachment_required_asset.config_json["rule_document"]
|
||||
attachment_manifest = manager.read_rule_library_json(
|
||||
library=RISK_RULES_LIBRARY,
|
||||
file_name=attachment_rule_document["file_name"],
|
||||
)
|
||||
assert attachment_manifest["requires_attachment"] is True
|
||||
no_attachment_simulation = service.simulate_risk_rule_message(
|
||||
attachment_required_id,
|
||||
AgentAssetRiskRuleSimulationRequest(message="请测试这条规则。"),
|
||||
)
|
||||
assert no_attachment_simulation.ready is False
|
||||
assert no_attachment_simulation.stage == "needs_attachment"
|
||||
|
||||
attachment_only_simulation = service.simulate_risk_rule_message(
|
||||
attachment_required_id,
|
||||
AgentAssetRiskRuleSimulationRequest(
|
||||
message="请识别上传单据是否命中风险规则。",
|
||||
attachments=[
|
||||
{
|
||||
"name": "invoice.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"document_fields": [
|
||||
{"key": "invoice_no", "label": "发票号码", "value": "INV-001"}
|
||||
],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
assert attachment_only_simulation.ready is False
|
||||
assert attachment_only_simulation.stage == "needs_test_intent"
|
||||
|
||||
|
||||
def test_delete_unpublished_risk_rule_removes_asset_and_json_file(tmp_path) -> None:
|
||||
with build_session() as db:
|
||||
manager = AgentAssetRuleLibraryManager(rule_root=tmp_path / "rules")
|
||||
asset_id = RiskRuleGenerationService(
|
||||
db,
|
||||
rule_library_manager=manager,
|
||||
runtime_chat_service=NullRuntimeChatService(),
|
||||
).generate_rule_asset(
|
||||
AgentAssetRiskRuleGenerateRequest(
|
||||
business_domain=AgentAssetDomain.EXPENSE,
|
||||
risk_level="medium",
|
||||
natural_language="报销事由不能为空,缺失时进入中风险复核。",
|
||||
),
|
||||
actor="pytest",
|
||||
)
|
||||
asset = db.get(AgentAsset, asset_id)
|
||||
assert asset is not None
|
||||
file_name = asset.config_json["rule_document"]["file_name"]
|
||||
rule_path = tmp_path / "rules" / RISK_RULES_LIBRARY / file_name
|
||||
assert rule_path.exists()
|
||||
|
||||
service = AgentAssetService(db)
|
||||
service.rule_library_manager = manager
|
||||
service.delete_unpublished_asset(asset_id, actor="pytest")
|
||||
|
||||
assert db.get(AgentAsset, asset_id) is None
|
||||
assert not rule_path.exists()
|
||||
|
||||
@@ -131,6 +131,8 @@ def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None:
|
||||
assert "不能用常识、外部知识或主观推断补齐缺失条件" in messages[0]["content"]
|
||||
assert "不能只依赖排在最前面的片段" in messages[0]["content"]
|
||||
assert "不能把第一列的数值直接套给后面的列名" in messages[0]["content"]
|
||||
assert "最终答复必须像助手在认真回答问题" in messages[0]["content"]
|
||||
assert "禁止使用“已命中”“答案整理阶段”“稍后重试”" in messages[0]["content"]
|
||||
assert "knowledge_evidence_blocks" in messages[0]["content"]
|
||||
assert '"knowledge_answer_evidence": []' in messages[1]["content"]
|
||||
|
||||
@@ -162,8 +164,9 @@ def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None:
|
||||
)
|
||||
|
||||
assert answer.startswith("张三,您好。")
|
||||
assert "答案整理阶段本轮没有及时返回" in answer
|
||||
assert "先给你当前最直接的依据" in answer
|
||||
assert "我先根据当前制度依据给出可以确认的部分" in answer
|
||||
assert "已命中" not in answer
|
||||
assert "答案整理阶段本轮没有及时返回" not in answer
|
||||
assert "《差旅费制度》" in answer
|
||||
|
||||
|
||||
@@ -241,6 +244,40 @@ def test_user_agent_prefers_relevant_raw_hit_over_generic_appendix() -> None:
|
||||
assert "组织人事部" in selected[0]["content"]
|
||||
|
||||
|
||||
def test_user_agent_model_hit_selection_keeps_later_relevant_hits() -> None:
|
||||
selected = UserAgentService._select_knowledge_model_hits(
|
||||
{
|
||||
"hits": [
|
||||
{"content": "一般说明一"},
|
||||
{"content": "一般说明二"},
|
||||
{"content": "一般说明三"},
|
||||
{"content": "一般说明四"},
|
||||
{"content": "一般说明五"},
|
||||
{"content": "一般说明六"},
|
||||
{"content": "一般说明七"},
|
||||
{
|
||||
"content": (
|
||||
"# 问答线索补充\n\n"
|
||||
"- 第二章 报销时限:差旅费应在行程结束三个月内提交;逾期不予报销出差补贴。"
|
||||
)
|
||||
},
|
||||
]
|
||||
},
|
||||
question="差旅费报销时限是多少?",
|
||||
)
|
||||
|
||||
assert "三个月内提交" in selected[0]["content"]
|
||||
|
||||
|
||||
def test_user_agent_knowledge_terms_keep_accounting_subject_in_long_query() -> None:
|
||||
terms = UserAgentService._extract_knowledge_query_terms(
|
||||
"远光软件财务基础知识手册里的常用会计科目是什么?"
|
||||
)
|
||||
|
||||
assert "常用会计科目" in terms
|
||||
assert "会计科目" in terms
|
||||
|
||||
|
||||
def test_user_agent_uses_fast_knowledge_answer_without_model(monkeypatch) -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
@@ -286,12 +323,170 @@ def test_user_agent_uses_fast_knowledge_answer_without_model(monkeypatch) -> Non
|
||||
)
|
||||
|
||||
assert response.answer.startswith("张三,您好。")
|
||||
assert "当前能直接确认的是" in response.answer
|
||||
assert "**结论**" in response.answer
|
||||
assert "30 日内提交报销申请" in response.answer
|
||||
assert "## 依据" not in response.answer
|
||||
assert "答案整理阶段本轮没有及时返回" not in response.answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_focuses_inline_section_items() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="主要税种介绍",
|
||||
user_id="pytest",
|
||||
context_json={"session_type": "knowledge"},
|
||||
)
|
||||
)
|
||||
service = UserAgentService(db)
|
||||
|
||||
answer = service._build_fast_knowledge_answer(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="主要税种介绍",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"session_type": "knowledge",
|
||||
"user_input_text": "主要税种介绍",
|
||||
},
|
||||
tool_payload={
|
||||
"result_type": "knowledge_search",
|
||||
"hits": [
|
||||
{
|
||||
"title": "财务基础知识",
|
||||
"content": (
|
||||
"资产类 银行存款 企业存放在银行的款项 负债类 应付账款 "
|
||||
"因购买商品或接受劳务应付的款项 负债类 应交税费 应缴纳的各种税费 "
|
||||
"第二部分 税务基础知识 三、主要税种介绍 "
|
||||
"(一)增值税:公司为一般纳税人,软件服务适用6%税率,软件产品销售适用13%税率。 "
|
||||
"(二)企业所得税:税率为25%,高新技术企业享受15%优惠税率。 "
|
||||
"(三)个人所得税:员工工资薪金由公司代扣代缴。 "
|
||||
"(四)印花税:购销合同、账簿等按规定缴纳。"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
citations=[],
|
||||
)
|
||||
|
||||
assert answer is not None
|
||||
assert "主要税种介绍包括:增值税、企业所得税、个人所得税、印花税" in answer
|
||||
assert "软件服务适用6%税率" in answer
|
||||
assert "软件产品销售适用13%税率" in answer
|
||||
assert "高新技术企业享受15%优惠税率" in answer
|
||||
assert "员工工资薪金由公司代扣代缴" in answer
|
||||
assert "购销合同、账簿等按规定缴纳" in answer
|
||||
assert "应付账款" not in answer
|
||||
assert "银行存款" not in answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_summarizes_financial_statements() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="三大财务报表 是什么?",
|
||||
user_id="pytest",
|
||||
context_json={"session_type": "knowledge"},
|
||||
)
|
||||
)
|
||||
service = UserAgentService(db)
|
||||
|
||||
answer = service._build_fast_knowledge_answer(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="三大财务报表 是什么?",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"session_type": "knowledge",
|
||||
"user_input_text": "三大财务报表 是什么?",
|
||||
},
|
||||
tool_payload={
|
||||
"result_type": "knowledge_search",
|
||||
"hits": [
|
||||
{
|
||||
"title": "财务基础知识",
|
||||
"content": (
|
||||
"第三部分 财务报表解读 四、三大财务报表 "
|
||||
"(一)资产负债表:反映企业在某一特定日期的财务状况。 "
|
||||
"(二)利润表:反映企业在一定期间的经营成果。 "
|
||||
"(三)现金流量表:反映企业在一定期间现金和现金等价物的流入和流出。"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
citations=[],
|
||||
)
|
||||
|
||||
assert answer is not None
|
||||
assert "三大财务报表包括:资产负债表、利润表、现金流量表" in answer
|
||||
assert "资产负债表:反映企业在某一特定日期的财务状况" in answer
|
||||
assert "利润表:反映企业在一定期间的经营成果" in answer
|
||||
assert "现金流量表:反映企业在一定期间现金和现金等价物的流入和流出" in answer
|
||||
assert "第三部分 财务报表解读" not in answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_expands_broad_accounting_table() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="常用会计科目是什么?",
|
||||
user_id="pytest",
|
||||
context_json={"session_type": "knowledge"},
|
||||
)
|
||||
)
|
||||
service = UserAgentService(db)
|
||||
|
||||
answer = service._build_fast_knowledge_answer(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="常用会计科目是什么?",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"session_type": "knowledge",
|
||||
"user_input_text": "常用会计科目是什么?",
|
||||
},
|
||||
tool_payload={
|
||||
"result_type": "knowledge_search",
|
||||
"hits": [
|
||||
{
|
||||
"title": "财务基础知识",
|
||||
"content": (
|
||||
"二、常用会计科目\n\n"
|
||||
"| 科目类别 | 科目名称 | 说明 |\n"
|
||||
"| --- | --- | --- |\n"
|
||||
"| 资产类 | 库存现金 | 公司持有的现金 |\n"
|
||||
"| 资产类 | 银行存款 | 存放在银行的资金 |\n"
|
||||
"| 资产类 | 应收账款 | 因销售商品或提供劳务应收的款项 |\n"
|
||||
"| 资产类 | 固定资产 | 使用年限超过一年的有形资产 |\n"
|
||||
"| 负债类 | 应付账款 | 因购买商品或接受劳务应付的款项 |\n"
|
||||
"| 负债类 | 应交税费 | 应缴纳的各种税费 |\n"
|
||||
"| 负债类 | 应付职工薪酬 | 应付给职工的工资、福利等 |\n"
|
||||
"| 损益类 | 主营业务收入 | 主要经营业务产生的收入 |\n"
|
||||
"| 损益类 | 管理费用 | 为管理生产经营发生的费用 |\n"
|
||||
"| 损益类 | 销售费用 | 为销售产品发生的费用 |\n"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
citations=[],
|
||||
)
|
||||
|
||||
assert answer is not None
|
||||
assert "| 科目类别 | 科目名称 | 说明 |" in answer
|
||||
assert "| 资产类 | 库存现金 | 公司持有的现金 |" in answer
|
||||
assert "| 负债类 | 应付职工薪酬 | 应付给职工的工资、福利等 |" in answer
|
||||
assert "| 损益类 | 销售费用 | 为销售产品发生的费用 |" in answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_renders_relevant_table_preview() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
@@ -337,9 +532,65 @@ def test_user_agent_fast_knowledge_answer_renders_relevant_table_preview() -> No
|
||||
assert answer is not None
|
||||
assert "| 项目 | 港澳台 | 其他地区 | 国外 |" in answer
|
||||
assert "| 餐补 | 75 | 55 | 140 |" in answer
|
||||
assert "餐补的标准为" in answer
|
||||
assert "## 依据" not in answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_uses_user_grade_for_table_row() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
ontology = SemanticOntologyService(db).parse(
|
||||
OntologyParseRequest(
|
||||
query="我的住宿费标准是多少?",
|
||||
user_id="pytest",
|
||||
context_json={"session_type": "knowledge"},
|
||||
)
|
||||
)
|
||||
service = UserAgentService(db)
|
||||
|
||||
answer = service._build_fast_knowledge_answer(
|
||||
UserAgentRequest(
|
||||
run_id=ontology.run_id,
|
||||
user_id="pytest",
|
||||
message="我的住宿费标准是多少?",
|
||||
ontology=ontology,
|
||||
context_json={
|
||||
"name": "张三",
|
||||
"grade": "P5",
|
||||
"position": "实施经理",
|
||||
"session_type": "knowledge",
|
||||
"user_input_text": "我的住宿费标准是多少?",
|
||||
},
|
||||
tool_payload={
|
||||
"result_type": "knowledge_search",
|
||||
"hits": [
|
||||
{
|
||||
"title": "费用报销制度",
|
||||
"content": (
|
||||
"# 结构化表格补充\n\n"
|
||||
"## 国内住宿限额标准\n\n"
|
||||
"| 职级 | 直辖市/特区/港澳台 | 省会城市 | 其他地区 |\n"
|
||||
"| --- | --- | --- | --- |\n"
|
||||
"| 公司领导(P8及以上) | 800 | 500 | 400 |\n"
|
||||
"| 高层经理(P7) | 700 | 450 | 400 |\n"
|
||||
"| 中层经理、基层经理(P4~P6、外聘专家) | 600 | 400 | 350 |\n"
|
||||
"| 其他员工 | 500 | 350 | 300 |\n"
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
),
|
||||
citations=[],
|
||||
)
|
||||
|
||||
assert answer is not None
|
||||
assert answer.startswith("张三,您好。")
|
||||
assert "中层经理、基层经理(P4~P6、外聘专家)的标准为" in answer
|
||||
assert "| 中层经理、基层经理(P4~P6、外聘专家) | 600 | 400 | 350 |" in answer
|
||||
assert "| 公司领导(P8及以上) | 800 | 500 | 400 |" not in answer
|
||||
assert "| 高层经理(P7) | 700 | 450 | 400 |" not in answer
|
||||
|
||||
|
||||
def test_user_agent_fast_knowledge_answer_notes_missing_location_grounding() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
@@ -384,6 +635,7 @@ def test_user_agent_fast_knowledge_answer_notes_missing_location_grounding() ->
|
||||
|
||||
assert answer is not None
|
||||
assert "没有直接写出“北京”对应的地区档位或映射关系" in answer
|
||||
assert "**说明**" in answer
|
||||
assert "## 依据" not in answer
|
||||
|
||||
|
||||
@@ -429,7 +681,7 @@ def test_user_agent_fast_knowledge_answer_expands_lead_in_list_items() -> None:
|
||||
)
|
||||
|
||||
assert answer is not None
|
||||
assert "当前能直接确认的是" in answer
|
||||
assert "**结论**" in answer
|
||||
assert "登机牌、高速道路通行记录" in answer
|
||||
assert "支付记录" in answer
|
||||
assert "出差审批邮件、短信、微信等" in answer
|
||||
|
||||
Reference in New Issue
Block a user