feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
132
server/src/app/algorithem/risk_graph/decisioning.py
Normal file
132
server/src/app/algorithem/risk_graph/decisioning.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Decision trace and explanation helpers for risk graph observations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from .models import PeerBaseline, RiskEvidence
|
||||
|
||||
RISK_SCORE_FORMULA = (
|
||||
"0.35*S_rule + 0.25*S_anomaly + "
|
||||
"0.20*S_graph + 0.15*S_policy + 0.05*S_history"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DecisionTrace:
|
||||
formula: str
|
||||
algorithm_version: str
|
||||
input_scores: dict[str, int]
|
||||
output_score: int
|
||||
decision_row: str
|
||||
feature_contributions_json: list[dict[str, Any]]
|
||||
uncertainty_reasons_json: list[str]
|
||||
explanation_template_key: str
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"formula": self.formula,
|
||||
"algorithm_version": self.algorithm_version,
|
||||
"input_scores": dict(self.input_scores),
|
||||
"output_score": self.output_score,
|
||||
"decision_row": self.decision_row,
|
||||
"feature_contributions_json": list(self.feature_contributions_json),
|
||||
"uncertainty_reasons_json": list(self.uncertainty_reasons_json),
|
||||
"explanation_template_key": self.explanation_template_key,
|
||||
**self.metadata,
|
||||
}
|
||||
|
||||
|
||||
class DecisionTraceBuilder:
|
||||
def build(
|
||||
self,
|
||||
*,
|
||||
algorithm_version: str,
|
||||
risk_signal: str,
|
||||
risk_level: str,
|
||||
raw_risk_score: int,
|
||||
risk_score: int,
|
||||
contribution_scores: dict[str, int],
|
||||
evidence: list[RiskEvidence],
|
||||
baseline: PeerBaseline,
|
||||
confidence: Decimal,
|
||||
metadata: dict[str, Any],
|
||||
) -> DecisionTrace:
|
||||
return DecisionTrace(
|
||||
formula=RISK_SCORE_FORMULA,
|
||||
algorithm_version=algorithm_version,
|
||||
input_scores=contribution_scores,
|
||||
output_score=risk_score,
|
||||
decision_row=_decision_row(risk_score=risk_score, risk_level=risk_level),
|
||||
feature_contributions_json=_feature_contributions(contribution_scores),
|
||||
uncertainty_reasons_json=_uncertainty_reasons(
|
||||
raw_risk_score=raw_risk_score,
|
||||
risk_score=risk_score,
|
||||
evidence=evidence,
|
||||
baseline=baseline,
|
||||
confidence=confidence,
|
||||
metadata=metadata,
|
||||
),
|
||||
explanation_template_key=f"risk.{risk_signal}.{risk_level}",
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
def _decision_row(*, risk_score: int, risk_level: str) -> str:
|
||||
if risk_score >= 90:
|
||||
return f"{risk_level}:score>=90"
|
||||
if risk_score >= 70:
|
||||
return f"{risk_level}:70<=score<90"
|
||||
if risk_score >= 45:
|
||||
return f"{risk_level}:45<=score<70"
|
||||
return f"{risk_level}:score<45"
|
||||
|
||||
|
||||
def _feature_contributions(scores: dict[str, int]) -> list[dict[str, Any]]:
|
||||
weights = {
|
||||
"S_rule": Decimal("0.35"),
|
||||
"S_anomaly": Decimal("0.25"),
|
||||
"S_graph": Decimal("0.20"),
|
||||
"S_policy": Decimal("0.15"),
|
||||
"S_history": Decimal("0.05"),
|
||||
}
|
||||
rows = []
|
||||
for key, score in scores.items():
|
||||
weighted_score = Decimal(int(score or 0)) * weights.get(key, Decimal("0"))
|
||||
rows.append(
|
||||
{
|
||||
"feature": key,
|
||||
"score": int(score or 0),
|
||||
"weight": str(weights.get(key, Decimal("0"))),
|
||||
"weighted_score": float(weighted_score),
|
||||
}
|
||||
)
|
||||
return sorted(rows, key=lambda item: item["weighted_score"], reverse=True)
|
||||
|
||||
|
||||
def _uncertainty_reasons(
|
||||
*,
|
||||
raw_risk_score: int,
|
||||
risk_score: int,
|
||||
evidence: list[RiskEvidence],
|
||||
baseline: PeerBaseline,
|
||||
confidence: Decimal,
|
||||
metadata: dict[str, Any],
|
||||
) -> list[str]:
|
||||
reasons: list[str] = []
|
||||
if risk_score < raw_risk_score:
|
||||
reasons.append("score_capped_by_gate")
|
||||
if baseline.scope == "insufficient_sample" or baseline.sample_size <= 0:
|
||||
reasons.append("peer_baseline_insufficient")
|
||||
if confidence < Decimal("0.55"):
|
||||
reasons.append("low_confidence")
|
||||
if len({item.source for item in evidence if item.source}) < 2:
|
||||
reasons.append("single_evidence_source")
|
||||
if metadata.get("ontology_gate") == "candidate_only":
|
||||
reasons.append("ontology_candidate_only")
|
||||
if metadata.get("data_quality_gate") not in {"", "passed", None}:
|
||||
reasons.append("data_quality_gate_not_passed")
|
||||
return list(dict.fromkeys(reasons))
|
||||
Reference in New Issue
Block a user