Add production-ready async HTTP client for Ollama API communication with proper error handling and dependency injection. Ollama Client (src/ollama/client.py): - Async context manager for connection lifecycle - Non-streaming chat endpoint - Streaming chat endpoint with async generator - Model listing endpoint - Health check endpoint - Timeout configuration per request - Comprehensive error handling with custom exceptions - FastAPI dependency injection support Ollama Schemas (src/ollama/schemas.py): - OllamaMessage: Chat message format - OllamaChatRequest: Request with model, messages, options - OllamaChatResponse: Complete chat response - OllamaModelInfo: Model metadata - OllamaModelsResponse: Model list response Features: - Async/await throughout for non-blocking I/O - Connection pooling via httpx.AsyncClient - Configurable timeouts (default: 120s) - Proper exception mapping (connection errors, timeouts) - Ready for integration (currently not connected to routes) Following Best Practices: - Async context manager pattern - Dependency injection for FastAPI routes - Separation of concerns (client vs schemas) - Type hints throughout - Comprehensive logging Status: Ready for integration (mock responses used in routes currently) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
182 lines
5.5 KiB
Python
182 lines
5.5 KiB
Python
"""
|
|
Ollama HTTP client.
|
|
Handles all communication with the Ollama service.
|
|
"""
|
|
import logging
|
|
from typing import Any, AsyncGenerator
|
|
|
|
import httpx
|
|
from httpx import ConnectError, TimeoutException
|
|
|
|
from src.core.config import config
|
|
from src.core.exceptions import OllamaConnectionError, OllamaTimeoutError
|
|
from src.ollama.schemas import (
|
|
OllamaChatRequest,
|
|
OllamaChatResponse,
|
|
OllamaModelsResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OllamaClient:
|
|
"""
|
|
Async client for Ollama API.
|
|
|
|
Follows best practice of using async for I/O operations.
|
|
"""
|
|
|
|
def __init__(self, base_url: str | None = None, timeout: int | None = None):
|
|
"""
|
|
Initialize Ollama client.
|
|
|
|
Args:
|
|
base_url: Ollama server URL (defaults to config)
|
|
timeout: Request timeout in seconds (defaults to config)
|
|
"""
|
|
self.base_url = base_url or str(config.OLLAMA_HOST)
|
|
self.timeout = timeout or config.OLLAMA_TIMEOUT
|
|
self._client: httpx.AsyncClient | None = None
|
|
|
|
async def __aenter__(self) -> "OllamaClient":
|
|
"""Async context manager entry."""
|
|
self._client = httpx.AsyncClient(
|
|
base_url=self.base_url,
|
|
timeout=self.timeout,
|
|
)
|
|
return self
|
|
|
|
async def __aexit__(self, *args: Any) -> None:
|
|
"""Async context manager exit."""
|
|
if self._client:
|
|
await self._client.aclose()
|
|
|
|
async def chat(
|
|
self,
|
|
request: OllamaChatRequest,
|
|
) -> OllamaChatResponse:
|
|
"""
|
|
Send chat request to Ollama (non-streaming).
|
|
|
|
Args:
|
|
request: Chat request with model and messages
|
|
|
|
Returns:
|
|
Complete chat response
|
|
|
|
Raises:
|
|
OllamaConnectionError: Cannot connect to Ollama
|
|
OllamaTimeoutError: Request timed out
|
|
"""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized. Use async with context.")
|
|
|
|
try:
|
|
response = await self._client.post(
|
|
"/api/chat",
|
|
json=request.model_dump(),
|
|
)
|
|
response.raise_for_status()
|
|
return OllamaChatResponse(**response.json())
|
|
|
|
except ConnectError as e:
|
|
logger.error(f"Cannot connect to Ollama at {self.base_url}: {e}")
|
|
raise OllamaConnectionError() from e
|
|
|
|
except TimeoutException as e:
|
|
logger.error(f"Ollama request timed out after {self.timeout}s: {e}")
|
|
raise OllamaTimeoutError() from e
|
|
|
|
async def chat_stream(
|
|
self,
|
|
request: OllamaChatRequest,
|
|
) -> AsyncGenerator[dict[str, Any], None]:
|
|
"""
|
|
Send streaming chat request to Ollama.
|
|
|
|
Args:
|
|
request: Chat request with stream=True
|
|
|
|
Yields:
|
|
Streaming response chunks
|
|
|
|
Raises:
|
|
OllamaConnectionError: Cannot connect to Ollama
|
|
OllamaTimeoutError: Request timed out
|
|
"""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized. Use async with context.")
|
|
|
|
# Ensure streaming is enabled
|
|
request.stream = True
|
|
|
|
try:
|
|
async with self._client.stream(
|
|
"POST",
|
|
"/api/chat",
|
|
json=request.model_dump(),
|
|
) as response:
|
|
response.raise_for_status()
|
|
async for line in response.aiter_lines():
|
|
if line.strip():
|
|
import json
|
|
yield json.loads(line)
|
|
|
|
except ConnectError as e:
|
|
logger.error(f"Cannot connect to Ollama at {self.base_url}: {e}")
|
|
raise OllamaConnectionError() from e
|
|
|
|
except TimeoutException as e:
|
|
logger.error(f"Ollama request timed out after {self.timeout}s: {e}")
|
|
raise OllamaTimeoutError() from e
|
|
|
|
async def list_models(self) -> OllamaModelsResponse:
|
|
"""
|
|
List available models from Ollama.
|
|
|
|
Returns:
|
|
List of available models
|
|
|
|
Raises:
|
|
OllamaConnectionError: Cannot connect to Ollama
|
|
"""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized. Use async with context.")
|
|
|
|
try:
|
|
response = await self._client.get("/api/tags")
|
|
response.raise_for_status()
|
|
return OllamaModelsResponse(**response.json())
|
|
|
|
except ConnectError as e:
|
|
logger.error(f"Cannot connect to Ollama at {self.base_url}: {e}")
|
|
raise OllamaConnectionError() from e
|
|
|
|
async def health_check(self) -> bool:
|
|
"""
|
|
Check if Ollama service is healthy.
|
|
|
|
Returns:
|
|
True if healthy, False otherwise
|
|
"""
|
|
if not self._client:
|
|
raise RuntimeError("Client not initialized. Use async with context.")
|
|
|
|
try:
|
|
response = await self._client.get("/")
|
|
return response.status_code == 200
|
|
except Exception as e:
|
|
logger.warning(f"Ollama health check failed: {e}")
|
|
return False
|
|
|
|
|
|
# Dependency for FastAPI routes
|
|
async def get_ollama_client() -> AsyncGenerator[OllamaClient, None]:
|
|
"""
|
|
FastAPI dependency to provide Ollama client.
|
|
|
|
Follows best practice of dependency injection.
|
|
"""
|
|
async with OllamaClient() as client:
|
|
yield client
|