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