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