"""风控 Agent 专用的模型工具调用客户端。""" from __future__ import annotations from typing import Any, Protocol import httpx from app.core.errors import ( DependencyUnavailableError, RecoverableAgentError, UpstreamTimeoutError, ) from app.service.model_gateway import ( DatabaseModelEndpointResolver, EnvironmentSecretResolver, ) class RiskAgentEndpointResolver(Protocol): async def resolve(self, *, agent_type: str, task_type: str) -> list[Any]: ... class RiskAgentSecretResolver(Protocol): def resolve(self, secret_ref: str) -> str: ... class RiskAgentModelClient: """调用 OpenAI 兼容接口,并原样返回模型消息供业务编排校验。""" def __init__( self, *, endpoint_resolver: RiskAgentEndpointResolver | None = None, secret_resolver: RiskAgentSecretResolver | None = None, client: httpx.AsyncClient | None = None, max_attempts: int = 2, ) -> None: self.endpoint_resolver = endpoint_resolver or DatabaseModelEndpointResolver() self.secret_resolver = secret_resolver or EnvironmentSecretResolver() self.client = client self.max_attempts = max(1, max_attempts) self._owns_client = client is None async def chat( self, messages: list[dict[str, Any]], *, tools: list[dict[str, Any]], ) -> dict[str, Any]: endpoints = await self.endpoint_resolver.resolve( agent_type="risk", task_type="risk_agent_chat", ) if not endpoints: raise RecoverableAgentError("没有可用的风控 Agent 模型端点") last_error: Exception | None = None client = self.client or httpx.AsyncClient() try: for endpoint in endpoints[: self.max_attempts]: try: return await self._chat_with_endpoint(client, endpoint, messages, tools) except Exception as exc: last_error = exc raise RecoverableAgentError("风控 Agent 模型端点调用失败") from last_error finally: if self._owns_client: await client.aclose() async def _chat_with_endpoint( self, client: httpx.AsyncClient, endpoint: Any, messages: list[dict[str, Any]], tools: list[dict[str, Any]], ) -> dict[str, Any]: token = self.secret_resolver.resolve(endpoint.secret_ref) url = endpoint.base_url.rstrip("/") + "/chat/completions" payload: dict[str, Any] = { "model": endpoint.model_name, "messages": messages, "temperature": 0.2, "tools": tools, "tool_choice": "auto", } try: response = await client.post( url, headers={ "Authorization": f"Bearer {token}", "Content-Type": "application/json", }, json=payload, timeout=httpx.Timeout(endpoint.timeout_ms / 1000), ) response.raise_for_status() body = response.json() except httpx.TimeoutException as exc: raise UpstreamTimeoutError("风控 Agent 模型调用超时") from exc except httpx.HTTPError as exc: raise DependencyUnavailableError("风控 Agent 模型调用失败") from exc except ValueError as exc: raise DependencyUnavailableError("风控 Agent 模型响应不是有效 JSON") from exc choices = body.get("choices") if isinstance(body, dict) else None if not isinstance(choices, list) or not choices: raise DependencyUnavailableError("风控 Agent 模型响应缺少 choices") message = choices[0].get("message") if isinstance(choices[0], dict) else None if not isinstance(message, dict): raise DependencyUnavailableError("风控 Agent 模型响应缺少 message") return message