150 lines
5.4 KiB
Python
150 lines
5.4 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
from fastapi import Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.config import settings
|
|
from app.db.session import get_db
|
|
from app.schemas.ai import (
|
|
AIProviderStatusResponse,
|
|
SituationalAnalysisRequest,
|
|
SituationalAnalysisResponse,
|
|
)
|
|
|
|
|
|
class AIProviderClient:
|
|
def __init__(
|
|
self,
|
|
*,
|
|
service_url: str | None = None,
|
|
service_token: str | None = None,
|
|
timeout: int | None = None,
|
|
retry_attempts: int | None = None,
|
|
llm_config: dict | None = None,
|
|
) -> None:
|
|
self.service_url = (
|
|
service_url if service_url is not None else settings.AI_PROVIDER_SERVICE_URL
|
|
).rstrip("/")
|
|
self.service_token = (
|
|
service_token if service_token is not None else settings.AI_PROVIDER_SERVICE_TOKEN
|
|
)
|
|
self.timeout = timeout if timeout is not None else settings.AI_PROVIDER_TIMEOUT_SECONDS
|
|
self.retry_attempts = max(
|
|
retry_attempts if retry_attempts is not None else settings.AI_PROVIDER_RETRY_ATTEMPTS,
|
|
1,
|
|
)
|
|
self.llm_config = llm_config or {}
|
|
|
|
def _headers(self, request_id: str | None = None) -> dict[str, str]:
|
|
headers = {"Content-Type": "application/json"}
|
|
if self.service_token:
|
|
headers["X-Provider-Token"] = self.service_token
|
|
if request_id:
|
|
headers["X-Request-ID"] = request_id
|
|
llm_header_map = {
|
|
"provider": "X-AI-Provider",
|
|
"provider_api": "X-AI-Provider-API",
|
|
"base_url": "X-AI-Base-URL",
|
|
"api_key": "X-AI-API-Key",
|
|
"model": "X-AI-Model",
|
|
"max_tokens": "X-AI-Max-Tokens",
|
|
"anthropic_version": "X-AI-Anthropic-Version",
|
|
}
|
|
for key, header_name in llm_header_map.items():
|
|
value = self.llm_config.get(key)
|
|
if value not in (None, ""):
|
|
headers[header_name] = str(value)
|
|
return headers
|
|
|
|
async def get_status(self, request_id: str | None = None) -> AIProviderStatusResponse:
|
|
if not self.service_url:
|
|
return AIProviderStatusResponse(
|
|
provider="unconfigured",
|
|
enabled=False,
|
|
configured=False,
|
|
model=None,
|
|
base_url=None,
|
|
)
|
|
|
|
data = await self._request("GET", "/v1/provider/status", request_id=request_id)
|
|
return AIProviderStatusResponse.model_validate(data)
|
|
|
|
async def analyze(
|
|
self,
|
|
payload: SituationalAnalysisRequest,
|
|
request_id: str | None = None,
|
|
) -> SituationalAnalysisResponse:
|
|
if not self.service_url:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="AI provider service URL is not configured.",
|
|
)
|
|
|
|
data = await self._request(
|
|
"POST",
|
|
"/v1/analyze",
|
|
json=payload.model_dump(),
|
|
request_id=request_id,
|
|
)
|
|
return SituationalAnalysisResponse.model_validate(data)
|
|
|
|
async def _request(
|
|
self,
|
|
method: str,
|
|
path: str,
|
|
json: dict | None = None,
|
|
request_id: str | None = None,
|
|
) -> dict:
|
|
last_error: Exception | None = None
|
|
for attempt in range(1, self.retry_attempts + 1):
|
|
try:
|
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
response = await client.request(
|
|
method,
|
|
f"{self.service_url}{path}",
|
|
headers=self._headers(request_id),
|
|
json=json,
|
|
)
|
|
response.raise_for_status()
|
|
return response.json()
|
|
except httpx.HTTPStatusError as exc:
|
|
last_error = exc
|
|
if attempt < self.retry_attempts and exc.response.status_code >= 500:
|
|
await asyncio.sleep(0.3 * attempt)
|
|
continue
|
|
detail = exc.response.text or "AI provider service returned an error"
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"AI provider service request failed: {detail}",
|
|
) from exc
|
|
except httpx.HTTPError as exc:
|
|
last_error = exc
|
|
if attempt < self.retry_attempts:
|
|
await asyncio.sleep(0.3 * attempt)
|
|
continue
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"Failed to reach AI provider service: {exc}",
|
|
) from exc
|
|
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"AI provider service request failed: {last_error}",
|
|
)
|
|
|
|
|
|
async def get_ai_provider_client(db: AsyncSession = Depends(get_db)) -> AIProviderClient:
|
|
from app.api.v1.settings import get_runtime_ai_provider_config
|
|
|
|
runtime_config = await get_runtime_ai_provider_config(db)
|
|
return AIProviderClient(
|
|
service_url=runtime_config["service_url"],
|
|
service_token=runtime_config["service_token"],
|
|
timeout=runtime_config["timeout_seconds"],
|
|
retry_attempts=runtime_config["retry_attempts"],
|
|
llm_config=runtime_config.get("llm_config") or {},
|
|
)
|