"""DeepSeek LLM client with robust error handling.""" from typing import List, Dict, Any, Optional import logging import requests from requests.exceptions import RequestException, Timeout, HTTPError from ..config import settings logger = logging.getLogger(__name__) class LLMError(Exception): """Base exception for LLM-related errors.""" pass class LLMConfigurationError(LLMError): """Raised when LLM is not properly configured.""" pass class LLMAPIError(LLMError): """Raised when LLM API returns an error.""" pass class DeepSeekClient: """Client for interacting with DeepSeek API.""" def __init__( self, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, timeout: Optional[int] = 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