feat: 迁移奶龙风控业务模块与演示文档
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
"""风控 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
|
||||
Reference in New Issue
Block a user