CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
- 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 验证 画像注入与历史携带
109 lines
4.0 KiB
Python
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)
|