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
+4
View File
@@ -126,6 +126,10 @@ class Settings(BaseSettings):
# --- Auth: refresh sessions (FR-ACC-05) --------------------------------
refresh_token_ttl_seconds: int = 2_592_000 # 30 天
# --- Agent 对话(FR-AGT-09)--------------------------------------------
# 对话上下文携带的最近轮数(一轮 = user + assistant 两条),默认 10。
agent_history_turns: int = 10
# --- 任务奖励(FR-BILL-03/04,配置化)-----------------------------------
# amount 全部是占位值 1:正式数值待产品在产品方案 §3.2 定稿后改 env,不改代码。
# claim_period 决定幂等粒度(同一任务同周期只能领一次)。
+106 -3
View File
@@ -1,9 +1,9 @@
"""SQLAlchemy 2.0 ORM models.
Scope of this scaffold: only the tables needed to prove the vertical slice
(架构文档 §6.2 identity / billing / task / 观测与成本). All remaining domains
(pf_ / rp_ / kb_ / ag_ / tr_ / cp_) are intentionally absent — see
packages/*/README.md for what still has to be built.
(架构文档 §6.2 identity / billing / task / 观测与成本) plus the Sprint batches
that have landed since (kb_ / rp_ / ag_). Remaining domains (pf_ / tr_ / cp_)
are intentionally absent — see packages/*/README.md for what still has to be built.
Conventions enforced here (架构文档 §6.1):
1. 表名带域前缀 (`id_` / `bl_` / `tk_` / `ob_`),为将来拆库预留边界。
@@ -541,3 +541,106 @@ class RpShare(Base):
)
__table_args__ = (UniqueConstraint("share_token", name="uq_rp_share_token"),)
class RpFeedback(Base):
"""维度级结论反馈(T-P3-21 最小可用版):准/不准 + 可选修正文本。
同维度重复反馈是覆盖更新(upsert),由 uq_rp_feedback_report_dimension
兜底;认同率/修正率两项指标从本表 SQL 直接算出。
有意裁剪:不回写报告结论、不写 L1 记忆(记忆系统未建,见迁移 0008)。"""
__tablename__ = "rp_feedback"
feedback_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
report_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
dimension_key: Mapped[str] = mapped_column(String(64), nullable=False)
verdict: Mapped[str] = mapped_column(String(16), nullable=False) # accurate | inaccurate
correction: Mapped[str | None] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
)
__table_args__ = (
UniqueConstraint("report_id", "dimension_key", name="uq_rp_feedback_report_dimension"),
Index("ix_rp_feedback_user_created", "user_id", "created_at"),
)
class RpReservation(Base):
"""预约留资(T-P3-22):topic 预留通用性,首期只有 full_report。
uq_rp_reservation_user_topic 承载幂等:重复预约返回已有记录(created=false)。
指标口径:预约率 = topic='full_report' 行数 ÷ 埋点 full_report.card_view
去重曝光数;决策门槛 ≥8%(产品方案 §2.2.3)。"""
__tablename__ = "rp_reservation"
reservation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
topic: Mapped[str] = mapped_column(String(32), nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (UniqueConstraint("user_id", "topic", name="uq_rp_reservation_user_topic"),)
# ---------------------------------------------------------------------------
# agent 域(FR-AGT-09 / T-P4-01:会话与消息,含检索轨迹与规则命中)
# ---------------------------------------------------------------------------
class AgConversation(Base):
"""对话会话。title 取首条用户消息的截断,列表页展示用。
保留策略(合规方案 §6:对话原文 90 天)由后续清理任务按
``last_message_at`` 执行,表结构先把时间锚点留好。"""
__tablename__ = "ag_conversation"
conversation_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
title: Mapped[str] = mapped_column(String(64), nullable=False, default="")
message_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
last_message_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (Index("ix_ag_conversation_user_active", "user_id", "last_message_at"),)
class AgMessage(Base):
"""单条对话消息。``retrieval_trace`` / ``rule_hits`` 是 FR-AGT-09「每条回答
可复盘」的复盘锚点:检索与规则命中在对应步骤(架构 §7.2 ④)落地前留空。
``call_id`` 关联 ``ob_ai_call``,一条 assistant 消息的模型/token/成本可
一路追到账本;user 消息没有 LLM 调用,这些字段恒 NULL。"""
__tablename__ = "ag_message"
message_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=False)
conversation_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
user_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
role: Mapped[str] = mapped_column(String(16), nullable=False) # user / assistant
content: Mapped[str] = mapped_column(Text, nullable=False)
call_id: Mapped[int | None] = mapped_column(BigInteger)
model: Mapped[str | None] = mapped_column(String(64))
tokens_in: Mapped[int | None] = mapped_column(Integer)
tokens_out: Mapped[int | None] = mapped_column(Integer)
tokens_cached: Mapped[int | None] = mapped_column(Integer)
latency_ms: Mapped[int | None] = mapped_column(Integer)
cost_cents: Mapped[Decimal | None] = mapped_column(Numeric(14, 6))
retrieval_trace: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
rule_hits: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now()
)
__table_args__ = (
Index("ix_ag_message_conversation_created", "conversation_id", "created_at"),
Index("ix_ag_message_user_created", "user_id", "created_at"),
)