"""LLM client abstraction for pluggable LLM providers.""" import logging from typing import Optional from app.config import settings logger = logging.getLogger(__name__) class LLMClient: """ Abstraction layer for LLM providers. Supports: OpenAI, Ollama, LM Studio, or offline/mock mode. Can be extended to support other providers. """ def __init__( self, provider: Optional[str] = None, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, ): """ Initialize LLM client. Args: provider: "openai", "ollama", "lm_studio", or None for mock api_key: API key for provider base_url: Base URL for API (for ollama/lm_studio) model: Model identifier """ self.provider = provider or settings.LLM_PROVIDER self.api_key = api_key or settings.LLM_API_KEY self.base_url = base_url or settings.LLM_BASE_URL self.model = model or settings.LLM_MODEL logger.info(f"LLMClient initialized with provider: {self.provider}") async def generate(self, prompt: str, max_tokens: int = 200) -> str: """ Generate text from a prompt. Args: prompt: Input prompt max_tokens: Maximum tokens to generate Returns: Generated text TODO: Implement OpenAI API integration TODO: Implement Ollama API integration TODO: Implement LM Studio API integration """ if self.provider == "openai": return await self._generate_openai(prompt, max_tokens) elif self.provider == "ollama": return await self._generate_ollama(prompt, max_tokens) elif self.provider == "lm_studio": return await self._generate_lm_studio(prompt, max_tokens) else: return self._generate_mock(prompt) async def _generate_openai(self, prompt: str, max_tokens: int) -> str: """ Generate using OpenAI API. TODO: Implement using openai library - Create client with api_key - Call ChatCompletion - Handle errors and retries """ logger.warning("OpenAI provider not yet implemented (stub)") return self._generate_mock(prompt) async def _generate_ollama(self, prompt: str, max_tokens: int) -> str: """ Generate using Ollama local API. TODO: Implement using httpx or requests - POST to base_url/api/generate - Stream response and accumulate - Handle model pulling if needed """ logger.warning("Ollama provider not yet implemented (stub)") return self._generate_mock(prompt) async def _generate_lm_studio(self, prompt: str, max_tokens: int) -> str: """ Generate using LM Studio local API. TODO: Implement OpenAI-compatible API calls - POST to base_url/v1/chat/completions - Use same logic as OpenAI but with local endpoint """ logger.warning("LM Studio provider not yet implemented (stub)") return self._generate_mock(prompt) def _generate_mock(self, prompt: str) -> str: """ Generate deterministic mock response (no API needed). Used when no provider is configured or for testing. """ logger.debug(f"Mock generation for prompt: {prompt[:50]}...") if "content angle:" in prompt.lower(): for line in prompt.splitlines(): if line.lower().startswith("content angle:"): angle = line.split(":", 1)[1].strip() return f"The quiet here keeps circling back to {angle}." # Simple deterministic responses for testing if "hello" in prompt.lower(): return "Greetings, traveler! Welcome to The Sanctum." elif "help" in prompt.lower(): return "I am here to guide your discourse through the streams." elif "topic" in prompt.lower(): return "The archives speak of many topics worthy of discussion." else: return "An interesting observation. Tell me more."