feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user