Files
group_fqcd_jr/app/service/risk_agent_model_client.py
T

113 lines
3.9 KiB
Python

"""风控 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