Files
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
3.4 KiB
Python

"""ag_conversation / ag_message 的数据访问。**只有本模块可以触这两张表**。"""
from __future__ import annotations
from datetime import UTC, datetime
from decimal import Decimal
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from packages.shared.models import AgConversation, AgMessage
ROLE_USER = "user"
ROLE_ASSISTANT = "assistant"
async def create_conversation(
session: AsyncSession, *, conversation_id: int, user_id: int
) -> AgConversation:
conversation = AgConversation(conversation_id=conversation_id, user_id=user_id)
session.add(conversation)
await session.flush()
return conversation
async def get_conversation(session: AsyncSession, conversation_id: int) -> AgConversation | None:
return await session.get(AgConversation, conversation_id)
async def list_conversations(
session: AsyncSession, *, user_id: int, limit: int = 20
) -> list[AgConversation]:
"""会话列表:按最近活跃倒序;从未发言的会话(last_message_at 为 NULL)排最后。"""
result = await session.execute(
select(AgConversation)
.where(AgConversation.user_id == user_id)
.order_by(AgConversation.last_message_at.desc().nulls_last())
.limit(limit)
)
return list(result.scalars().all())
async def list_messages(
session: AsyncSession, *, conversation_id: int, limit: int = 100
) -> list[AgMessage]:
"""历史接口用:按时间正序返回。"""
result = await session.execute(
select(AgMessage)
.where(AgMessage.conversation_id == conversation_id)
.order_by(AgMessage.created_at.asc(), AgMessage.message_id.asc())
.limit(limit)
)
return list(result.scalars().all())
async def list_recent_messages(
session: AsyncSession, *, conversation_id: int, limit: int
) -> list[AgMessage]:
"""上下文组装用:取最近 ``limit`` 条,返回时按时间正序(旧→新)。"""
result = await session.execute(
select(AgMessage)
.where(AgMessage.conversation_id == conversation_id)
.order_by(AgMessage.created_at.desc(), AgMessage.message_id.desc())
.limit(limit)
)
return list(reversed(result.scalars().all()))
async def append_message(
session: AsyncSession,
*,
message_id: int,
conversation: AgConversation,
role: str,
content: str,
call_id: int | None = None,
model: str | None = None,
tokens_in: int | None = None,
tokens_out: int | None = None,
tokens_cached: int | None = None,
latency_ms: int | None = None,
cost_cents: Decimal | None = None,
) -> AgMessage:
"""追加消息并推进会话的 message_count / last_message_at / title。
title 只在首条用户消息时落定(截断前 20 字),之后不再改。
"""
message = AgMessage(
message_id=message_id,
conversation_id=conversation.conversation_id,
user_id=conversation.user_id,
role=role,
content=content,
call_id=call_id,
model=model,
tokens_in=tokens_in,
tokens_out=tokens_out,
tokens_cached=tokens_cached,
latency_ms=latency_ms,
cost_cents=cost_cents,
)
session.add(message)
conversation.message_count += 1
conversation.last_message_at = datetime.now(UTC)
if not conversation.title and role == ROLE_USER:
conversation.title = content[:20]
await session.flush()
return message