feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
39
server/src/app/models/agent_feedback.py
Normal file
39
server/src/app/models/agent_feedback.py
Normal 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)
|
||||
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()
|
||||
38
server/src/app/models/user_session_metric.py
Normal file
38
server/src/app/models/user_session_metric.py
Normal 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()
|
||||
)
|
||||
Reference in New Issue
Block a user