feat(api): vertical slice POST /v1/agent/chat and the Celery ping path
One request exercises every layer: JWT auth, quota reservation, a versioned prompt through the LLM gateway, a cost row, quota commit or refund, and the unified envelope. The trivial ping task proves the queue routing and the tk_task transitions before any real long-running task is written. Routes are mounted at the /api/v1 base path of 架构文档 §9.1; /v1/* is rewritten to it at the ASGI layer so both spellings work from one route table.
This commit is contained in:
Executable
+64
@@ -0,0 +1,64 @@
|
||||
"""``POST /agent/chat`` — the vertical slice.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from apps.api.deps import AgentServiceDep, CurrentUserId
|
||||
from apps.api.schemas import ChatRequest, ChatResponse, ChatUsage
|
||||
from packages.llm.errors import (
|
||||
AllProvidersFailedError,
|
||||
CachePrefixViolationError,
|
||||
DailyBudgetExceededError,
|
||||
StructuredOutputError,
|
||||
)
|
||||
from packages.shared.errors import AppError, ErrorCode
|
||||
from packages.shared.response import ok
|
||||
|
||||
router = APIRouter(prefix="/agent", tags=["agent"])
|
||||
|
||||
|
||||
@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)
|
||||
except DailyBudgetExceededError as exc:
|
||||
raise AppError(ErrorCode.LLM_DAILY_BUDGET_EXCEEDED) 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(
|
||||
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()
|
||||
)
|
||||
Reference in New Issue
Block a user