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
+71 -5
View File
@@ -1,9 +1,12 @@
"""``POST /agent/chat`` — the vertical slice.
"""``POST /agent/chat`` + 会话持久化接口(FR-AGT-09 / T-P4-01)。
One request exercises: JWT auth → quota reserve → LLM gateway (routing, prompt
version, cache-prefix check, daily budget) → ``ob_ai_call`` cost row → quota
commit/refund → unified envelope. Streaming (SSE, FR-AGT-01) is intentionally
out of scope for the scaffold.
commit/refund → ``ag_message`` 双行落库 → unified envelope. Streaming (SSE,
FR-AGT-01) is intentionally out of scope for the scaffold.
会话接口:创建 / 列表 / 历史。chat 不带 conversation_id 时隐式建会话并在
响应里带回;带则做归属校验(他人会话与不存在同口径 404,防 ID 遍历)。
"""
from __future__ import annotations
@@ -13,7 +16,13 @@ import uuid
from fastapi import APIRouter
from apps.api.deps import AgentServiceDep, CurrentUserId
from apps.api.schemas import ChatRequest, ChatResponse, ChatUsage
from apps.api.schemas import (
ChatMessageItem,
ChatRequest,
ChatResponse,
ChatUsage,
ConversationResponse,
)
from packages.llm.errors import (
AllProvidersFailedError,
CachePrefixViolationError,
@@ -22,11 +31,62 @@ from packages.llm.errors import (
StructuredOutputError,
)
from packages.shared.errors import AppError, ErrorCode
from packages.shared.models import AgConversation
from packages.shared.response import ok
router = APIRouter(prefix="/agent", tags=["agent"])
def _conversation_payload(conversation: AgConversation) -> ConversationResponse:
return ConversationResponse(
conversation_id=str(conversation.conversation_id),
title=conversation.title,
message_count=conversation.message_count,
last_message_at=conversation.last_message_at,
created_at=conversation.created_at,
)
@router.post("/conversations", summary="创建空会话(FR-AGT-09)")
async def create_conversation(
user_id: CurrentUserId,
agent: AgentServiceDep,
) -> dict[str, object]:
conversation = await agent.create_conversation(user_id=user_id)
return ok(_conversation_payload(conversation).model_dump(mode="json"))
@router.get("/conversations", summary="会话列表(最近活跃倒序)")
async def list_conversations(
user_id: CurrentUserId,
agent: AgentServiceDep,
) -> dict[str, object]:
conversations = await agent.list_conversations(user_id=user_id)
return ok([_conversation_payload(c).model_dump(mode="json") for c in conversations])
@router.get("/conversations/{conversation_id}/messages", summary="会话历史(时间正序)")
async def list_messages(
conversation_id: int,
user_id: CurrentUserId,
agent: AgentServiceDep,
) -> dict[str, object]:
messages = await agent.list_messages(user_id=user_id, conversation_id=conversation_id)
return ok(
[
ChatMessageItem(
message_id=str(m.message_id),
role=m.role,
content=m.content,
model=m.model,
call_id=str(m.call_id) if m.call_id is not None else None,
created_at=m.created_at,
).model_dump(mode="json")
for m in messages
]
)
@router.post("/chat", summary="Single-turn styling chat (non-streaming)")
async def chat(
payload: ChatRequest,
@@ -35,7 +95,12 @@ async def chat(
) -> dict[str, object]:
request_key = payload.request_key or uuid.uuid4().hex
try:
result = await agent.chat(user_id=user_id, message=payload.message, request_key=request_key)
result = await agent.chat(
user_id=user_id,
message=payload.message,
request_key=request_key,
conversation_id=payload.conversation_id,
)
except DailyBudgetExceededError as exc:
raise AppError(ErrorCode.LLM_DAILY_BUDGET_EXCEEDED) from exc
except SceneDegradedError as exc:
@@ -50,6 +115,7 @@ async def chat(
return ok(
ChatResponse(
conversation_id=str(result.conversation_id),
reply=result.reply,
provider=result.provider,
model=result.model,