feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -8,10 +8,12 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.core.agent_enums import AgentName, AgentRunSource, AgentRunStatus
from app.api.deps import get_db
from app.db.base import Base
from app.schemas.ontology import OntologyParseRequest
from app.services.ontology import LlmOntologyParseResult, SemanticOntologyService
from app.services.runtime_chat import RuntimeChatCallTrace, RuntimeChatResult
def build_session_factory() -> sessionmaker[Session]:
@@ -283,6 +285,61 @@ def test_semantic_ontology_service_extracts_budget_query_fields() -> None:
assert {"available_amount", "reserved_amount"}.issubset(metric_names)
@pytest.mark.parametrize(
"query",
[
"申请出差",
"申请差旅",
"去国网出差3天协助仿生产环境部署",
"去北京出差3天支撑国网仿生产环境部署",
"下周去上海出差支撑客户系统上线预计3天",
"安排去深圳客户现场验收项目,出差两天",
"准备去国网现场做仿生产环境部署差旅3天",
],
)
def test_semantic_ontology_service_treats_apply_for_travel_as_expense_application(query: str) -> None:
session_factory = build_session_factory()
with session_factory() as db:
result = SemanticOntologyService(db).parse(
OntologyParseRequest(
query=query,
user_id="pytest",
)
)
entity_map = {item.type: item.normalized_value for item in result.entities}
entity_types = {item.type for item in result.entities}
assert result.scenario == "expense"
assert result.intent == "draft"
assert result.permission.level == "draft_write"
assert entity_map["document_type"] == "expense_application"
assert entity_map["workflow_stage"] == "pre_approval"
assert entity_map["expense_type"] == "travel"
assert "employee" not in entity_types
assert "amount" in result.missing_slots
assert "time_range" in result.missing_slots
def test_semantic_ontology_service_keeps_explicit_travel_reimbursement_as_reimbursement_draft() -> None:
session_factory = build_session_factory()
with session_factory() as db:
result = SemanticOntologyService(db).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 == "draft"
assert entity_map["expense_type"] == "travel"
assert "document_type" not in entity_map
assert "workflow_stage" not in entity_map
def test_semantic_ontology_service_extracts_budget_edit_fields() -> None:
session_factory = build_session_factory()
with session_factory() as db:
@@ -438,20 +495,24 @@ def test_semantic_ontology_service_rejects_draft_intent_inside_knowledge_session
session_factory = build_session_factory()
with session_factory() as db:
service = SemanticOntologyService(db)
monkeypatch.setattr(
service,
"_parse_with_model",
lambda **kwargs: LlmOntologyParseResult(
scenario="expense",
intent="draft",
confidence=0.91,
clarification_required=True,
clarification_question="请补充招待对象和票据附件。",
missing_slots=["participants", "attachments"],
ambiguity=[],
entity_hints=[],
),
)
monkeypatch.setattr(
service,
"_parse_with_model",
lambda **kwargs: (
LlmOntologyParseResult(
scenario="expense",
intent="draft",
confidence=0.91,
clarification_required=True,
clarification_question="请补充招待对象和票据附件。",
missing_slots=["participants", "attachments"],
ambiguity=[],
entity_hints=[],
),
[],
None,
),
)
result = service.parse(
OntologyParseRequest(
@@ -809,20 +870,33 @@ def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch)
session_factory = build_session_factory()
with session_factory() as db:
service = SemanticOntologyService(db)
monkeypatch.setattr(
service,
"_parse_with_model",
lambda **kwargs: LlmOntologyParseResult(
scenario="expense",
intent="draft",
confidence=0.91,
clarification_required=True,
clarification_question="请补充费用类型、金额和票据附件。",
missing_slots=["expense_type", "amount", "attachments"],
ambiguity=[],
entity_hints=[],
),
)
monkeypatch.setattr(
service,
"_parse_with_model",
lambda **kwargs: (
LlmOntologyParseResult(
scenario="expense",
intent="draft",
confidence=0.91,
clarification_required=True,
clarification_question="请补充费用类型、金额和票据附件。",
missing_slots=["expense_type", "amount", "attachments"],
ambiguity=[],
entity_hints=[],
),
[
{
"slot": "main",
"provider": "MiniMax",
"model": "intent-model",
"attempt": 1,
"status": "succeeded",
"duration_ms": 8,
}
],
None,
),
)
result = service.parse(
OntologyParseRequest(
@@ -836,7 +910,103 @@ def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch)
assert result.parse_strategy == "llm_primary"
assert result.clarification_required is True
assert "expense_type" in result.missing_slots
assert result.clarification_question == "请补充费用类型、金额和票据附件。"
assert result.clarification_question == "请补充费用类型、金额和票据附件。"
def test_semantic_ontology_service_falls_back_when_model_conflicts_with_application_signal(
monkeypatch,
) -> None:
session_factory = build_session_factory()
with session_factory() as db:
service = SemanticOntologyService(db)
monkeypatch.setattr(
service.runtime_chat_service,
"complete_with_trace",
lambda *args, **kwargs: RuntimeChatResult(
text=(
'{"scenario":"knowledge","intent":"query","confidence":0.91,'
'"clarification_required":false,"missing_slots":[],'
'"ambiguity":[],"entity_hints":[]}'
),
calls=[
RuntimeChatCallTrace(
slot="main",
provider="MiniMax",
model="intent-model",
attempt=1,
status="succeeded",
duration_ms=11,
)
],
),
)
result = service.parse(
OntologyParseRequest(
query="去国网出差3天协助仿生产环境部署",
user_id="pytest",
)
)
fetched = service.run_service.get_run(result.run_id)
entity_map = {item.type: item.normalized_value for item in result.entities}
assert result.scenario == "expense"
assert result.intent == "draft"
assert result.parse_strategy == "rule_fallback"
assert entity_map["document_type"] == "expense_application"
assert fetched is not None
assert fetched.tool_calls[0].status == "failed"
assert fetched.tool_calls[0].error_message == "model_conflicts_with_application_stage_signal"
def test_semantic_ontology_service_records_model_call_errors_for_statistics(monkeypatch) -> None:
session_factory = build_session_factory()
with session_factory() as db:
service = SemanticOntologyService(db)
run = service.run_service.create_run(
agent=AgentName.ORCHESTRATOR.value,
source=AgentRunSource.USER_MESSAGE.value,
status=AgentRunStatus.RUNNING.value,
)
monkeypatch.setattr(
service.runtime_chat_service,
"complete_with_trace",
lambda *args, **kwargs: RuntimeChatResult(
text=None,
calls=[
RuntimeChatCallTrace(
slot="main",
provider="MiniMax",
model="intent-model",
attempt=1,
status="failed",
duration_ms=15,
error_message="incorrect api key",
)
],
),
)
result = service.parse_for_run(
OntologyParseRequest(
query="去北京出差3天支撑国网仿生产环境部署",
user_id="pytest",
),
run_id=run.run_id,
)
fetched = service.run_service.get_run(run.run_id)
stats = service.run_service.summarize_runs(limit=20)
assert result.parse_strategy == "rule_fallback"
assert fetched is not None
assert len(fetched.tool_calls) == 1
assert fetched.tool_calls[0].tool_name == "semantic_ontology.main"
assert fetched.tool_calls[0].status == "failed"
assert fetched.tool_calls[0].error_message == "incorrect api key"
assert stats.failed_llm_call_count >= 1
def test_parse_ontology_endpoint_returns_eight_fields_and_writes_trace() -> None: