from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.api.deps import get_db from app.schemas.common import ErrorResponse from app.schemas.risk_observation import ( RiskObservationDashboardRead, RiskObservationFeedbackCreate, RiskObservationFeedbackRead, RiskObservationListRead, RiskObservationRead, ) from app.services.risk_observations import RiskObservationService router = APIRouter(prefix="/risk-observations") DbSession = Annotated[Session, Depends(get_db)] @router.get( "", response_model=RiskObservationListRead, summary="查询风险观察列表", description="按单据、风险等级、风险信号、状态和来源筛选统一风险观察池。", ) def list_risk_observations( db: DbSession, claim_id: Annotated[str | None, Query(max_length=80)] = None, run_id: Annotated[str | None, Query(max_length=80)] = None, execution_log_id: Annotated[str | None, Query(max_length=80)] = None, risk_level: Annotated[str | None, Query(max_length=20)] = None, risk_signal: Annotated[str | None, Query(max_length=100)] = None, status_value: Annotated[ str | None, Query(alias="status", max_length=30), ] = None, source: Annotated[str | None, Query(max_length=60)] = None, limit: Annotated[int, Query(ge=1, le=200)] = 50, offset: Annotated[int, Query(ge=0)] = 0, ) -> RiskObservationListRead: items, total = RiskObservationService(db).list_observations( claim_id=claim_id, run_id=run_id, execution_log_id=execution_log_id, risk_level=risk_level, risk_signal=risk_signal, status=status_value, source=source, limit=limit, offset=offset, ) return RiskObservationListRead(items=items, total=total, limit=limit, offset=offset) @router.get( "/dashboard", response_model=RiskObservationDashboardRead, summary="查询风险看板聚合", description="返回风险观察池的总量、分布、算法效果和近期高风险记录。", ) def summarize_risk_observations( db: DbSession, window_days: Annotated[int, Query(ge=1, le=365)] = 30, limit: Annotated[int, Query(ge=1, le=2000)] = 500, ) -> RiskObservationDashboardRead: return RiskObservationService(db).summarize_dashboard( window_days=window_days, limit=limit, ) @router.get( "/claim/{claim_id}", response_model=list[RiskObservationRead], summary="查询单据风险观察", description="按报销单 ID 返回该单据关联的风险观察,供单据详情证据链使用。", ) def list_claim_risk_observations(claim_id: str, db: DbSession) -> list[RiskObservationRead]: return RiskObservationService(db).list_claim_observations(claim_id) @router.get( "/execution-log/{execution_log_id}", response_model=list[RiskObservationRead], summary="查询数字员工工作记录风险观察", description="按数字员工执行日志 ID 返回本次任务生成的风险观察。", ) def list_execution_log_risk_observations( execution_log_id: str, db: DbSession, ) -> list[RiskObservationRead]: return RiskObservationService(db).list_execution_log_observations(execution_log_id) @router.get( "/{observation_key_or_id}", response_model=RiskObservationRead, summary="读取风险观察详情", description="按观察 key 或 ID 返回风险评分、证据链、图谱节点、制度引用和决策追踪。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "风险观察不存在。", } }, ) def get_risk_observation( observation_key_or_id: str, db: DbSession, ) -> RiskObservationRead: observation = RiskObservationService(db).get_observation(observation_key_or_id) if observation is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Risk observation not found", ) return observation @router.post( "/{observation_key_or_id}/feedback", response_model=RiskObservationFeedbackRead, summary="写入风险观察反馈", description="记录人工确认、误报、忽略、已处理或备注反馈,并同步更新观察状态。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, "description": "风险观察不存在。", } }, ) def create_risk_observation_feedback( observation_key_or_id: str, payload: RiskObservationFeedbackCreate, db: DbSession, ) -> RiskObservationFeedbackRead: try: return RiskObservationService(db).create_feedback(observation_key_or_id, payload) except LookupError: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Risk observation not found", ) from None