113 lines
3.9 KiB
Python
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
|