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 验证 画像注入与历史携带
135 lines
4.7 KiB
Python
Executable File
135 lines
4.7 KiB
Python
Executable File
"""``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()
|
||
)
|