"""Agent 对话编排 — the scaffold's thin vertical slice. It walks the full 架构文档 §7.2 spine but only the load-bearing parts: ① 输入审核 ← NOT implemented (compliance.moderate, CR-06) ② 意图识别 ← NOT implemented (FR-AGT-02) ③ 上下文组装 ← only the fixed system prefix + current turn ④ 混合检索 ← NOT implemented (retrieval-svc, FR-AGT-04) ⑤ LLM 生成 ← IMPLEMENTED (non-streaming) ⑥ 生成后校验 ← NOT implemented (FR-AGT-05) ⑦ 输出审核 ← NOT implemented (CR-06) ⑧ 落库 + 成本 ← cost only; ag_message is not in this scaffold ⑨ 异步偏好抽取 ← NOT implemented (FR-MEM-03) What *is* fully wired is the plumbing every future feature needs: JWT identity, the quota three-phase protocol, the LLM gateway, cost attribution, and the unified envelope. Transaction discipline: the reservation is committed **before** the LLM call. If the process dies mid-call, the reservation survives and can be reconciled; if the call fails, the refund is committed before the error propagates, so the API layer's rollback cannot undo it. """ from __future__ import annotations from dataclasses import dataclass from decimal import Decimal from sqlalchemy.ext.asyncio import AsyncSession from packages.billing.service import BillingService, Reservation from packages.llm.gateway import LLMGateway from packages.llm.types import Scene from packages.shared.logging import get_logger logger = get_logger(__name__) QUOTA_KEY_AGENT_CHAT = "agent_chat" REF_TYPE = "agent_chat" @dataclass(frozen=True, slots=True) class ChatResult: reply: str provider: str model: str prompt_id: str | None prompt_version: str | None call_id: int tokens_in: int tokens_out: int tokens_cached: int latency_ms: int cost_cents: Decimal quota_remaining: int reserve_tx_id: int class AgentService: def __init__( self, session: AsyncSession, *, gateway: LLMGateway, billing: BillingService, ) -> None: self._session = session self._gateway = gateway self._billing = billing async def chat(self, *, user_id: int, message: str, request_key: str) -> ChatResult: reservation = await self._billing.reserve( user_id=user_id, quota_key=QUOTA_KEY_AGENT_CHAT, request_key=request_key, amount=1, reason="agent chat turn", ref_type=REF_TYPE, ref_id=request_key, ) await self._session.commit() try: completion = await self._gateway.complete( scene=Scene.AGENT_CHAT, prompt_vars={"user_input": message}, user_id=user_id, ref_type=REF_TYPE, ref_id=request_key, ) except Exception as exc: await self._refund(reservation, reason=type(exc).__name__) raise await self._billing.commit(reservation) await self._session.commit() return ChatResult( reply=completion.text, provider=completion.provider, model=completion.model, prompt_id=completion.prompt_id, prompt_version=completion.prompt_version, call_id=completion.call_id, tokens_in=completion.usage.tokens_in, tokens_out=completion.usage.tokens_out, tokens_cached=completion.usage.tokens_cached, latency_ms=completion.latency_ms, cost_cents=completion.cost_cents, quota_remaining=reservation.remaining, reserve_tx_id=reservation.tx_id, ) async def _refund(self, reservation: Reservation, *, reason: str) -> None: try: await self._billing.refund(reservation, reason=reason) await self._session.commit() except Exception: await self._session.rollback() logger.exception("quota_refund_failed", tx_id=reservation.tx_id)