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
+67
View File
@@ -0,0 +1,67 @@
"""上下文组装(架构 §7.2 步骤③)的纯逻辑测试:画像注入与历史截尾。"""
from __future__ import annotations
from packages.agent.context import (
PROFILE_SUMMARY_MAX_CHARS,
build_context_messages,
build_profile_message,
)
from packages.llm.types import Message
class TestBuildContextMessages:
def test_profile_first_then_history(self) -> None:
history = [
Message(role="user", content="第一句"),
Message(role="assistant", content="第一答"),
]
messages = build_context_messages(
profile_summary='{"dimensions":[]}', history=history, history_turns=10
)
assert len(messages) == 3
assert messages[0].role == "system"
assert '{"dimensions":[]}' in messages[0].content
assert [m.role for m in messages[1:]] == ["user", "assistant"]
assert [m.content for m in messages[1:]] == ["第一句", "第一答"]
def test_no_profile_degrades_to_history_only(self) -> None:
"""无画像用户(FR-AGT-10 降级):不注入画像消息,历史照常携带。"""
history = [Message(role="user", content="hi")]
messages = build_context_messages(profile_summary=None, history=history, history_turns=10)
assert len(messages) == 1
assert messages[0].role == "user"
def test_empty_profile_string_treated_as_absent(self) -> None:
messages = build_context_messages(profile_summary="", history=[], history_turns=10)
assert messages == []
def test_history_tail_kept_by_turns(self) -> None:
"""一轮 = user+assistant 两条;N=2 时只留最近 4 条,截头不截尾。"""
history: list[Message] = []
for i in range(5):
history.append(Message(role="user", content=f"u{i}"))
history.append(Message(role="assistant", content=f"a{i}"))
messages = build_context_messages(profile_summary=None, history=history, history_turns=2)
assert [m.content for m in messages] == ["u3", "a3", "u4", "a4"]
def test_zero_turns_drops_history(self) -> None:
history = [Message(role="user", content="u0")]
messages = build_context_messages(profile_summary=None, history=history, history_turns=0)
assert messages == []
def test_history_message_content_capped(self) -> None:
history = [Message(role="user", content="x" * 5000)]
messages = build_context_messages(profile_summary=None, history=history, history_turns=10)
assert len(messages[0].content) == 2000
class TestBuildProfileMessage:
def test_wraps_summary_as_system_message(self) -> None:
message = build_profile_message("{}")
assert message.role == "system"
assert message.content.endswith("{}")
def test_budget_constant_within_token_budget(self) -> None:
"""画像摘要预算 ≤ 500 token(README 预算表),按 ~1.6 字符/token 折算。"""
assert PROFILE_SUMMARY_MAX_CHARS <= 500 * 1.6