feat(agent,report): 会话持久化与画像注入、报告反馈与预约留资——PM/开发双Agent迭代1-3
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 验证
  画像注入与历史携带
This commit is contained in:
xsl
2026-09-11 21:50:58 +08:00
parent 00ef56b08b
commit 7bcbb8d40b
25 changed files with 1860 additions and 25 deletions
+83
View File
@@ -0,0 +1,83 @@
"""report.profile.latest_profile_summary:repo 打桩,验证摘要选取纪律。"""
from __future__ import annotations
import json
from types import SimpleNamespace
from typing import Any
import pytest
from packages.report import repo
from packages.report.profile import latest_profile_summary
_DIAGNOSIS = {
"dimensions": [
{
"key": "body_shape",
"label": "体型",
"status": "ok",
"value": "梨形",
"confidence": 0.9,
"evidence": ["问卷自报肩窄胯宽"],
},
{"key": "style_impression", "status": "skipped", "value": None},
],
"rule_hits": [
{"rule_key": "pear_lower_dark", "dimension": "body_shape", "suggestion": "下装选深色"}
],
}
def _report(report_id: int, *, status: str, result: dict[str, Any]) -> Any:
return SimpleNamespace(report_id=report_id, status=status, result=result)
@pytest.fixture
def stub_list(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
state: dict[str, Any] = {"reports": []}
async def fake_list(session: Any, *, user_id: int, limit: int = 20) -> list[Any]:
state["limit"] = limit
return state["reports"]
monkeypatch.setattr(repo, "list_reports", fake_list)
return state
class TestLatestProfileSummary:
async def test_no_reports_returns_none(self, stub_list: dict[str, Any]) -> None:
assert await latest_profile_summary(None, user_id=1, max_chars=800) is None # type: ignore[arg-type]
async def test_succeeded_report_with_diagnosis(self, stub_list: dict[str, Any]) -> None:
stub_list["reports"] = [_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS})]
summary = await latest_profile_summary(None, user_id=1, max_chars=800) # type: ignore[arg-type]
assert summary is not None
payload = json.loads(summary)
values = {d["key"]: d["value"] for d in payload["dimensions"]}
assert values["body_shape"] == "梨形"
# skipped 维度不进素材(防幻觉第一道闸)
assert "style_impression" not in values
assert payload["rule_hits"][0]["rule_key"] == "pear_lower_dark"
async def test_latest_failed_falls_back_to_earlier_succeeded(
self, stub_list: dict[str, Any]
) -> None:
stub_list["reports"] = [
_report(2, status="failed", result={}),
_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS}),
]
summary = await latest_profile_summary(None, user_id=1, max_chars=800) # type: ignore[arg-type]
assert summary is not None and "梨形" in summary
async def test_succeeded_without_diagnosis_is_skipped(self, stub_list: dict[str, Any]) -> None:
stub_list["reports"] = [_report(1, status="succeeded", result={"vision": {}})]
assert await latest_profile_summary(None, user_id=1, max_chars=800) is None # type: ignore[arg-type]
async def test_budget_is_forwarded(self, stub_list: dict[str, Any]) -> None:
"""max_chars 收紧时低优先级维度被裁、硬结论保留(复用 copy 的裁剪纪律)。"""
stub_list["reports"] = [_report(1, status="succeeded", result={"diagnosis": _DIAGNOSIS})]
summary = await latest_profile_summary(None, user_id=1, max_chars=60) # type: ignore[arg-type]
assert summary is not None
assert len(summary) > 60 # 硬结论永不裁,载荷可超预算(copy.py 既定语义)
assert "梨形" in summary