feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -1,5 +1,6 @@
from app.models.agent_conversation import AgentConversation, AgentConversationMessage
from app.models.agent_asset import AgentAsset, AgentAssetReview, AgentAssetVersion
from app.models.agent_feedback import AgentOperationFeedback
from app.models.agent_run import AgentRun, AgentToolCall, SemanticParseLog
from app.models.approval import ApprovalRecord
from app.models.audit_log import AuditLog
@@ -17,10 +18,12 @@ from app.models.hermes_config import HermesTaskConfig, HermesTaskExecutionLog
from app.models.hermes_report import HermesRiskReport
from app.models.organization import OrganizationUnit
from app.models.reimbursement import ReimbursementRequest
from app.models.risk_observation import RiskObservation, RiskObservationFeedback
from app.models.role import Role
from app.models.system_model_setting import SystemModelSetting
from app.models.system_setting import SystemSetting
from app.models.system_setting_secret import SystemSettingSecret
from app.models.user_session_metric import UserSessionMetric
__all__ = [
"AccountsPayableRecord",
@@ -30,6 +33,7 @@ __all__ = [
"AgentAsset",
"AgentAssetReview",
"AgentAssetVersion",
"AgentOperationFeedback",
"AgentRun",
"AgentToolCall",
"ApprovalRecord",
@@ -47,9 +51,12 @@ __all__ = [
"HermesRiskReport",
"OrganizationUnit",
"ReimbursementRequest",
"RiskObservation",
"RiskObservationFeedback",
"Role",
"SemanticParseLog",
"SystemModelSetting",
"SystemSetting",
"SystemSettingSecret",
"UserSessionMetric",
]

View File

@@ -0,0 +1,39 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import DateTime, Index, Integer, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.types import JSON
from app.db.base_class import Base
class AgentOperationFeedback(Base):
__tablename__ = "agent_operation_feedback"
__table_args__ = (
Index("ix_agent_operation_feedback_user_created", "user_id", "created_at"),
Index("ix_agent_operation_feedback_run_rating", "run_id", "rating"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
feedback_id: Mapped[str] = mapped_column(
String(50),
unique=True,
index=True,
default=lambda: f"fb_{uuid.uuid4().hex[:16]}",
)
run_id: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
conversation_id: Mapped[str | None] = mapped_column(String(50), nullable=True, index=True)
user_id: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
agent: Mapped[str] = mapped_column(String(30), default="", index=True)
source: Mapped[str] = mapped_column(String(30), default="", index=True)
session_type: Mapped[str] = mapped_column(String(30), default="", index=True)
operation_type: Mapped[str] = mapped_column(String(50), default="assistant_round", index=True)
operation_status: Mapped[str] = mapped_column(String(20), default="", index=True)
rating: Mapped[int] = mapped_column(Integer, index=True)
reason: Mapped[str | None] = mapped_column(Text(), nullable=True)
context_json: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)

View 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()

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, Index, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.types import JSON
from app.db.base_class import Base
class UserSessionMetric(Base):
__tablename__ = "user_session_metrics"
__table_args__ = (
Index("ix_user_session_metrics_identity_window", "username", "employee_no", "login_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
session_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
username: Mapped[str] = mapped_column(String(255), index=True)
display_name: Mapped[str] = mapped_column(String(100), default="", index=True)
employee_no: Mapped[str] = mapped_column(String(80), default="", index=True)
email: Mapped[str] = mapped_column(String(255), default="", index=True)
is_admin: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
login_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), index=True)
logout_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
last_activity_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
duration_ms: Mapped[int] = mapped_column(Integer, default=0)
activity_event_count: Mapped[int] = mapped_column(Integer, default=0)
logout_reason: Mapped[str] = mapped_column(String(40), default="")
status: Mapped[str] = mapped_column(String(20), default="active", index=True)
event_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()
)