"""报告结论反馈服务(T-P3-21 最小可用版)。 有意裁剪(防误解):T-P3-21 原文要求「修正后置信度置 1.0 并即时写 L1 记忆」 ——L1 记忆系统未建,本迭代只落 rp_feedback 表,保证认同率/修正率两项指标 可从 SQL 直接算出,**不回写报告结论**。 归属口径:他人报告与不存在同口径 40420(不透出报告是否存在的额外信息)。 """ from __future__ import annotations from dataclasses import dataclass from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from packages.domain.ids import next_id from packages.report import repo from packages.report.schemas import FeedbackVerdict from packages.shared.errors import AppError, ErrorCode from packages.shared.models import RpFeedback, RpReport DIMENSION_STATUS_OK = "ok" @dataclass(frozen=True, slots=True) class FeedbackUpsert: feedback: RpFeedback updated: bool # true=同维度已有反馈被覆盖;false=新建 def ok_dimension_keys(report_result: dict[str, Any]) -> set[str]: """报告产物中可反馈的维度集合:存在于 diagnosis.dimensions 且 status=ok。 纯函数(不碰 I/O),路由层校验与单测共用同一口径。 """ diagnosis = report_result.get("diagnosis") if not isinstance(diagnosis, dict): return set() dimensions = diagnosis.get("dimensions") if not isinstance(dimensions, list): return set() keys: set[str] = set() for item in dimensions: if ( isinstance(item, dict) and item.get("status") == DIMENSION_STATUS_OK and isinstance(item.get("key"), str) ): keys.add(item["key"]) return keys class FeedbackService: def __init__(self, session: AsyncSession) -> None: self._session = session async def _get_owned_report(self, *, user_id: int, report_id: int) -> RpReport: report = await repo.get_report(self._session, report_id) # 归属校验沿用 get_owned 语义,但对外同口径 40420: # 不存在与他人报告不可区分,不透出报告是否真实存在。 if report is None or report.user_id != user_id: raise AppError(ErrorCode.REPORT_NOT_FOUND) return report async def upsert_feedback( self, *, user_id: int, report_id: int, dimension_key: str, verdict: FeedbackVerdict, correction: str | None, ) -> FeedbackUpsert: """同维度重复反馈 = 覆盖更新(uq_rp_feedback_report_dimension 兜底)。""" report = await self._get_owned_report(user_id=user_id, report_id=report_id) if dimension_key not in ok_dimension_keys(report.result or {}): raise AppError( ErrorCode.VALIDATION_FAILED, message="该维度没有可反馈的结论", hint="只能对报告中已生成的维度结论反馈", ) correction = correction.strip() if correction is not None else None if correction == "": correction = None existing = await repo.get_feedback( self._session, report_id=report_id, dimension_key=dimension_key ) if existing is not None: existing.verdict = verdict existing.correction = correction await self._session.flush() return FeedbackUpsert(feedback=existing, updated=True) row = RpFeedback( feedback_id=next_id(), report_id=report_id, user_id=user_id, dimension_key=dimension_key, verdict=verdict, correction=correction, ) self._session.add(row) await self._session.flush() return FeedbackUpsert(feedback=row, updated=False) async def list_feedback(self, *, user_id: int, report_id: int) -> list[RpFeedback]: await self._get_owned_report(user_id=user_id, report_id=report_id) return await repo.list_feedback(self._session, report_id=report_id)