feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
170
server/src/app/models/risk_observation.py
Normal file
170
server/src/app/models/risk_observation.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.types import JSON
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class RiskObservation(Base):
|
||||
__tablename__ = "risk_observations"
|
||||
__table_args__ = (
|
||||
Index("ix_risk_observations_subject", "subject_type", "subject_key"),
|
||||
Index("ix_risk_observations_signal_level", "risk_signal", "risk_level"),
|
||||
Index("ix_risk_observations_status_created", "status", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
observation_key: Mapped[str] = mapped_column(String(160), unique=True, index=True)
|
||||
subject_type: Mapped[str] = mapped_column(String(50), index=True)
|
||||
subject_key: Mapped[str] = mapped_column(String(160), index=True)
|
||||
subject_label: Mapped[str] = mapped_column(String(160), default="")
|
||||
claim_id: Mapped[str | None] = mapped_column(
|
||||
ForeignKey("expense_claims.id"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
claim_no: Mapped[str] = mapped_column(String(80), default="", index=True)
|
||||
run_id: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
|
||||
execution_log_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
|
||||
|
||||
risk_type: Mapped[str] = mapped_column(String(80), index=True)
|
||||
risk_signal: Mapped[str] = mapped_column(String(100), index=True)
|
||||
title: Mapped[str] = mapped_column(String(200), default="")
|
||||
description: Mapped[str] = mapped_column(Text(), default="")
|
||||
risk_score: Mapped[int] = mapped_column(Integer, default=0, index=True)
|
||||
risk_level: Mapped[str] = mapped_column(String(20), index=True)
|
||||
confidence_score: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
control_stage: Mapped[str] = mapped_column(String(50), default="")
|
||||
control_mode: Mapped[str] = mapped_column(String(50), default="")
|
||||
automation_mode: Mapped[str] = mapped_column(String(50), default="")
|
||||
source: Mapped[str] = mapped_column(String(60), default="", index=True)
|
||||
algorithm_version: Mapped[str] = mapped_column(String(80), default="", index=True)
|
||||
status: Mapped[str] = mapped_column(String(30), default="pending_review", index=True)
|
||||
feedback_status: Mapped[str] = mapped_column(String(30), default="unreviewed", index=True)
|
||||
|
||||
contribution_scores_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
baseline_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
evidence_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||
graph_node_keys_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||
graph_edge_keys_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||
policy_refs_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||
similar_case_claim_ids_json: Mapped[list[Any]] = mapped_column(JSON, default=list)
|
||||
ontology_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
decision_trace_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
)
|
||||
|
||||
claim = relationship("ExpenseClaim", foreign_keys=[claim_id])
|
||||
feedback_items = relationship(
|
||||
"RiskObservationFeedback",
|
||||
back_populates="observation",
|
||||
cascade="all, delete-orphan",
|
||||
order_by="desc(RiskObservationFeedback.created_at)",
|
||||
)
|
||||
|
||||
@property
|
||||
def sampling_strategy(self) -> dict[str, Any]:
|
||||
value = (self.decision_trace_json or {}).get("sampling_strategy")
|
||||
return dict(value) if isinstance(value, dict) else {}
|
||||
|
||||
@property
|
||||
def evaluation_case_id(self) -> str:
|
||||
return _json_text((self.decision_trace_json or {}).get("evaluation_case_id"))
|
||||
|
||||
@property
|
||||
def ontology_parse_id(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("ontology_parse_id"))
|
||||
|
||||
@property
|
||||
def ontology_version(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("ontology_version"))
|
||||
|
||||
@property
|
||||
def domain(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("domain"))
|
||||
|
||||
@property
|
||||
def scenario(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("scenario"))
|
||||
|
||||
@property
|
||||
def intent(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("intent"))
|
||||
|
||||
@property
|
||||
def ontology_entities_json(self) -> list[Any]:
|
||||
value = (self.ontology_json or {}).get("ontology_entities_json")
|
||||
if value is None:
|
||||
value = (self.ontology_json or {}).get("entities")
|
||||
return list(value) if isinstance(value, list) else []
|
||||
|
||||
@property
|
||||
def risk_signals_json(self) -> list[Any]:
|
||||
value = (self.ontology_json or {}).get("risk_signals_json")
|
||||
if value is None:
|
||||
value = (self.ontology_json or {}).get("risk_signals")
|
||||
return list(value) if isinstance(value, list) else []
|
||||
|
||||
@property
|
||||
def canonical_subject_key(self) -> str:
|
||||
return _json_text((self.ontology_json or {}).get("canonical_subject_key"))
|
||||
|
||||
|
||||
class RiskObservationFeedback(Base):
|
||||
__tablename__ = "risk_observation_feedback"
|
||||
__table_args__ = (
|
||||
Index("ix_risk_observation_feedback_type_created", "feedback_type", "created_at"),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
observation_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("risk_observations.id"),
|
||||
index=True,
|
||||
)
|
||||
feedback_type: Mapped[str] = mapped_column(String(30), index=True)
|
||||
action: Mapped[str] = mapped_column(String(50), default="")
|
||||
actor: Mapped[str] = mapped_column(String(100), default="")
|
||||
comment: Mapped[str | None] = mapped_column(Text(), nullable=True)
|
||||
payload_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
observation = relationship("RiskObservation", back_populates="feedback_items")
|
||||
|
||||
@property
|
||||
def decision(self) -> str:
|
||||
return _json_text((self.payload_json or {}).get("decision")) or self.feedback_type
|
||||
|
||||
@property
|
||||
def candidate_rule_source(self) -> str:
|
||||
return _json_text((self.payload_json or {}).get("candidate_rule_source"))
|
||||
|
||||
@property
|
||||
def confidence_score(self) -> float:
|
||||
try:
|
||||
return float((self.payload_json or {}).get("confidence_score") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def escalation_target(self) -> str:
|
||||
return _json_text((self.payload_json or {}).get("escalation_target"))
|
||||
|
||||
@property
|
||||
def supplement_required(self) -> bool:
|
||||
return bool((self.payload_json or {}).get("supplement_required"))
|
||||
|
||||
|
||||
def _json_text(value: Any) -> str:
|
||||
return str(value or "").strip()
|
||||
Reference in New Issue
Block a user