Files
meida_backend/packages/report/feedback_service.py
T
xsl 7bcbb8d40b
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
feat(agent,report): 会话持久化与画像注入、报告反馈与预约留资——PM/开发双Agent迭代1-3
- agent: ag_conversation/ag_message 表(迁移0007),上下文组装注入最近报告画像摘要
  (report.profile.latest_profile_summary 经 apps 层闭包注入,同层不互调),历史带最近
  AGENT_HISTORY_TURNS=10 轮;POST/GET /agent/conversations 与消息历史接口
- agent 修复:ChatRequest.conversation_id 改 str(雪花ID超2^53,JS Number丢精度);
  _build_context 挪进 reserve commit 后同一 try,异常走 refund 兜底
- report: rp_feedback(维度反馈 upsert,认同率/修正率 SQL 可算,T-P3-21 最小版——
  L1 记忆未建,不回写结论)+ rp_reservation(预约留资幂等,T-P3-22,预约率口径
  行数/full_report.card_view 去重曝光,门槛≥8%),迁移0008
- 测试:全量 606 passed;ruff/mypy/lint-imports 全绿;E2E 真实 deepseek-v4-flash 验证
  画像注入与历史携带
2026-09-11 21:50:58 +08:00

109 lines
4.0 KiB
Python

"""报告结论反馈服务(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)