feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -13,12 +13,7 @@ from app.schemas.agent_asset import AgentAssetRiskRuleGenerateRequest
|
||||
from app.services.agent_asset_rule_library import AgentAssetRuleLibraryManager
|
||||
from app.services.agent_asset_spreadsheet import RISK_RULES_LIBRARY
|
||||
from app.services.audit import AuditLogService
|
||||
from app.services.risk_rule_flow_diagram import (
|
||||
RiskRuleFlowDiagramField,
|
||||
RiskRuleFlowDiagramRenderer,
|
||||
RiskRuleFlowDiagramSpec,
|
||||
build_risk_rule_flow_diagram_details,
|
||||
)
|
||||
from app.services.risk_rule_explainability import build_risk_rule_explainability_artifacts
|
||||
from app.services.risk_rule_generation_ontology import (
|
||||
BUSINESS_DOMAIN_LABELS,
|
||||
DOMAIN_FIELD_PREFIXES,
|
||||
@@ -38,6 +33,8 @@ from app.services.risk_rule_generation_semantics import (
|
||||
build_city_consistency_draft,
|
||||
build_city_consistency_params,
|
||||
)
|
||||
from app.services.risk_rule_generation_semantic_plan import unwrap_semantic_plan_payload
|
||||
from app.services.risk_rule_dsl_validator import validate_risk_rule_draft
|
||||
from app.services.risk_rule_scoring import apply_risk_score_to_draft, calculate_risk_rule_score
|
||||
from app.services.runtime_chat import RuntimeChatService
|
||||
|
||||
@@ -54,7 +51,6 @@ class RiskRuleGenerationService:
|
||||
self.rule_library_manager = rule_library_manager or AgentAssetRuleLibraryManager()
|
||||
self.runtime_chat_service = runtime_chat_service or RuntimeChatService(db)
|
||||
self.audit_service = AuditLogService(db)
|
||||
self.flow_diagram_renderer = RiskRuleFlowDiagramRenderer()
|
||||
|
||||
def generate_rule_asset(
|
||||
self,
|
||||
@@ -98,12 +94,14 @@ class RiskRuleGenerationService:
|
||||
risk_level="medium",
|
||||
fields=fields,
|
||||
)
|
||||
draft = validate_risk_rule_draft(draft, fields=fields, natural_language=natural_language)
|
||||
draft = self._align_draft_fields(
|
||||
draft,
|
||||
natural_language=natural_language,
|
||||
risk_level="medium",
|
||||
fields=fields,
|
||||
)
|
||||
draft = validate_risk_rule_draft(draft, fields=fields, natural_language=natural_language)
|
||||
risk_score = calculate_risk_rule_score(
|
||||
natural_language=natural_language,
|
||||
draft=draft,
|
||||
@@ -261,6 +259,7 @@ class RiskRuleGenerationService:
|
||||
return None
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
payload = unwrap_semantic_plan_payload(payload)
|
||||
return self._sanitize_model_draft(payload, fields=fields)
|
||||
|
||||
def _sanitize_model_draft(
|
||||
@@ -341,6 +340,8 @@ class RiskRuleGenerationService:
|
||||
scoring_evidence = payload.get("risk_scoring_evidence")
|
||||
if isinstance(scoring_evidence, dict):
|
||||
draft["risk_scoring_evidence"] = scoring_evidence
|
||||
if isinstance(payload.get("model_semantic_plan"), dict):
|
||||
draft["model_semantic_plan"] = payload["model_semantic_plan"]
|
||||
for key in ("formula", "message_template"):
|
||||
value = self._clean_text(payload.get(key))
|
||||
if value:
|
||||
@@ -435,6 +436,8 @@ class RiskRuleGenerationService:
|
||||
semantic_type = str(draft.get("semantic_type") or "").strip()
|
||||
if semantic_type:
|
||||
params["semantic_type"] = semantic_type
|
||||
if isinstance(draft.get("dsl_validation"), dict):
|
||||
params["dsl_validation"] = draft["dsl_validation"]
|
||||
if template_key == COMPOSITE_RULE_TEMPLATE_KEY and isinstance(draft.get("rule_ir"), dict):
|
||||
params["rule_ir"] = draft["rule_ir"]
|
||||
for key in ("conditions", "hit_logic", "field_groups", "formula", "message_template"):
|
||||
@@ -516,60 +519,28 @@ class RiskRuleGenerationService:
|
||||
"business_explanation": self._clean_text(draft.get("description")),
|
||||
"condition_summary": condition_summary,
|
||||
"rule_ir": draft.get("rule_ir") if isinstance(draft.get("rule_ir"), dict) else {},
|
||||
"model_semantic_plan": draft.get("model_semantic_plan") if isinstance(draft.get("model_semantic_plan"), dict) else {},
|
||||
"flow": draft.get("flow") if isinstance(draft.get("flow"), dict) else {},
|
||||
},
|
||||
}
|
||||
payload["flow_diagram_svg"] = self._build_flow_diagram_svg(
|
||||
explainability = build_risk_rule_explainability_artifacts(
|
||||
payload,
|
||||
fields=[field_by_key[key] for key in field_keys if key in field_by_key],
|
||||
domain=domain,
|
||||
domain_label=risk_category,
|
||||
risk_level=risk_level,
|
||||
risk_level_label=risk_level_label,
|
||||
)
|
||||
payload.update(explainability)
|
||||
payload["metadata"].update(
|
||||
{
|
||||
"semantic_plan": explainability["semantic_plan"],
|
||||
"flow_model": explainability["flow_model"],
|
||||
"flow_explanation": explainability["flow_explanation"],
|
||||
"flow_diagram_svg": explainability["flow_diagram_svg"],
|
||||
}
|
||||
)
|
||||
return payload
|
||||
|
||||
def _build_flow_diagram_svg(
|
||||
self,
|
||||
payload: dict[str, Any],
|
||||
*,
|
||||
fields: list[RiskRuleField],
|
||||
domain: str,
|
||||
domain_label: str | None = None,
|
||||
risk_level: str,
|
||||
) -> str:
|
||||
metadata = payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {}
|
||||
flow = metadata.get("flow") if isinstance(metadata.get("flow"), dict) else {}
|
||||
condition_summary = self._clean_text(metadata.get("condition_summary"))
|
||||
diagram_fields = [
|
||||
RiskRuleFlowDiagramField(key=field.key, label=field.label) for field in fields
|
||||
]
|
||||
details = build_risk_rule_flow_diagram_details(payload, diagram_fields)
|
||||
return self.flow_diagram_renderer.render(
|
||||
RiskRuleFlowDiagramSpec(
|
||||
title=self._clean_text(payload.get("name")) or "风险规则判断流程",
|
||||
domain_label=domain_label or BUSINESS_DOMAIN_LABELS.get(domain, "业务"),
|
||||
severity=risk_level,
|
||||
severity_label=RISK_LEVEL_LABELS.get(risk_level, "中风险"),
|
||||
fields=tuple(diagram_fields),
|
||||
start=self._clean_text(flow.get("start")) or "业务单据提交",
|
||||
evidence=self._clean_text(flow.get("evidence")) or "读取规则字段",
|
||||
decision=self._clean_text(flow.get("decision"))
|
||||
or condition_summary
|
||||
or "判断是否命中风险",
|
||||
basis=(
|
||||
condition_summary
|
||||
or self._clean_text(flow.get("decision"))
|
||||
or "根据规则字段判断"
|
||||
),
|
||||
pass_text=self._clean_text(flow.get("pass")) or "未命中风险,继续流转",
|
||||
fail_text=self._clean_text(flow.get("fail"))
|
||||
or f"命中{RISK_LEVEL_LABELS.get(risk_level, '风险')},进入人工复核",
|
||||
fact_lines=details["fact_lines"],
|
||||
condition_lines=details["condition_lines"],
|
||||
hit_logic=str(details["hit_logic"] or ""),
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_expense_category(value: str | None, domain: str) -> str | None:
|
||||
if domain != AgentAssetDomain.EXPENSE.value:
|
||||
@@ -759,6 +730,8 @@ class RiskRuleGenerationService:
|
||||
|
||||
@staticmethod
|
||||
def _infer_template_key(text: str) -> str:
|
||||
if any(keyword in text for keyword in ("超过", "超出", "超预算", "预算", "阈值", "早于", "晚于", "范围")):
|
||||
return COMPOSITE_RULE_TEMPLATE_KEY
|
||||
if any(
|
||||
keyword in text
|
||||
for keyword in ("一致", "匹配", "相同", "不一致", "不符", "对应", "出现在")
|
||||
|
||||
Reference in New Issue
Block a user