"""DeepSeek LLM client with robust error handling.""" import logging from typing import Any import requests from requests.exceptions import HTTPError, RequestException, Timeout from ..config import settings from .exceptions import LLMAPIError, LLMConfigurationError logger = logging.getLogger(__name__) class DeepSeekClient: """Client for interacting with DeepSeek API.""" def __init__( self, api_key: str | None = None, base_url: str | None = None, model: str | None = None, timeout: int | None = None, ): """ Initialize DeepSeek client. Args: api_key: API key for authentication (defaults to settings) base_url: Base URL for API (defaults to settings) model: Model name to use (defaults to settings) timeout: Request timeout in seconds (defaults to settings) Raises: LLMConfigurationError: If API key is missing """ self.api_key = api_key or settings.deepseek_api_key self.base_url = base_url or settings.deepseek_base_url self.model = model or settings.model self.timeout = timeout or settings.request_timeout if not self.api_key: raise LLMConfigurationError( "DeepSeek API key is required. Set DEEPSEEK_API_KEY environment variable." ) if not self.base_url: raise LLMConfigurationError( "DeepSeek base URL is required. Set DEEPSEEK_BASE_URL environment variable." ) logger.info(f"DeepSeek client initialized with model: {self.model}") def complete(self, messages: list[dict[str, Any]]) -> str: """ Generate a completion from the LLM. Args: messages: List of message dicts with 'role' and 'content' keys Returns: Generated text response Raises: LLMAPIError: If API request fails ValueError: If messages format is invalid """ # Validate messages format if not messages: raise ValueError("Messages list cannot be empty") for msg in messages: if not isinstance(msg, dict): raise ValueError(f"Each message must be a dict, got {type(msg)}") if "role" not in msg or "content" not in msg: raise ValueError( f"Each message must have 'role' and 'content' keys, got {msg.keys()}" ) if msg["role"] not in ("system", "user", "assistant"): raise ValueError(f"Invalid role: {msg['role']}") url = f"{self.base_url}/v1/chat/completions" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } payload = { "model": self.model, "messages": messages, "temperature": settings.temperature, } try: logger.debug(f"Sending request to {url} with {len(messages)} messages") response = requests.post( url, headers=headers, json=payload, timeout=self.timeout ) response.raise_for_status() data = response.json() # Validate response structure if "choices" not in data or not data["choices"]: raise LLMAPIError("Invalid API response: missing 'choices'") if "message" not in data["choices"][0]: raise LLMAPIError("Invalid API response: missing 'message' in choice") if "content" not in data["choices"][0]["message"]: raise LLMAPIError("Invalid API response: missing 'content' in message") content = data["choices"][0]["message"]["content"] logger.debug(f"Received response with {len(content)} characters") return content except Timeout as e: logger.error(f"Request timeout after {self.timeout}s: {e}") raise LLMAPIError(f"Request timeout after {self.timeout} seconds") from e except HTTPError as e: logger.error(f"HTTP error from DeepSeek API: {e}") if e.response is not None: try: error_data = e.response.json() error_msg = error_data.get("error", {}).get("message", str(e)) except Exception: error_msg = str(e) raise LLMAPIError(f"DeepSeek API error: {error_msg}") from e raise LLMAPIError(f"HTTP error: {e}") from e except RequestException as e: logger.error(f"Request failed: {e}") raise LLMAPIError(f"Failed to connect to DeepSeek API: {e}") from e except (KeyError, IndexError, TypeError) as e: logger.error(f"Failed to parse API response: {e}") raise LLMAPIError(f"Invalid API response format: {e}") from e