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

135 lines
4.7 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""``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 → ``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
import uuid
from fastapi import APIRouter
from apps.api.deps import AgentServiceDep, CurrentUserId
from apps.api.schemas import (
ChatMessageItem,
ChatRequest,
ChatResponse,
ChatUsage,
ConversationResponse,
)
from packages.llm.errors import (
AllProvidersFailedError,
CachePrefixViolationError,
DailyBudgetExceededError,
SceneDegradedError,
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,
user_id: CurrentUserId,
agent: AgentServiceDep,
) -> 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,
conversation_id=payload.conversation_id,
)
except DailyBudgetExceededError as exc:
raise AppError(ErrorCode.LLM_DAILY_BUDGET_EXCEEDED) from exc
except SceneDegradedError as exc:
# 分级降级(§13.3 tier2/3):该场景今日被关停,但平台未爆预算
raise AppError(ErrorCode.LLM_SCENE_DEGRADED) from exc
except StructuredOutputError as exc:
raise AppError(ErrorCode.LLM_STRUCTURED_OUTPUT_FAILED) from exc
except AllProvidersFailedError as exc:
raise AppError(ErrorCode.LLM_ALL_PROVIDERS_FAILED) from exc
except CachePrefixViolationError as exc:
raise AppError(ErrorCode.LLM_PROMPT_CACHE_VIOLATION) from exc
return ok(
ChatResponse(
conversation_id=str(result.conversation_id),
reply=result.reply,
provider=result.provider,
model=result.model,
prompt_id=result.prompt_id,
prompt_version=result.prompt_version,
call_id=str(result.call_id),
usage=ChatUsage(
tokens_in=result.tokens_in,
tokens_out=result.tokens_out,
tokens_cached=result.tokens_cached,
latency_ms=result.latency_ms,
cost_cents=str(result.cost_cents),
),
quota_remaining=result.quota_remaining,
).model_dump()
)