""" LLM (Large Language Model) Service - Direct OpenAI SDK implementation """ import os from typing import Optional from openai import AsyncOpenAI from loguru import logger class LLMService: """ LLM (Large Language Model) service Direct implementation using OpenAI SDK. No capability layer needed. Supports all OpenAI SDK compatible providers: - OpenAI (gpt-4o, gpt-4o-mini, gpt-3.5-turbo) - Alibaba Qwen (qwen-max, qwen-plus, qwen-turbo) - Anthropic Claude (claude-sonnet-4-5, claude-opus-4, claude-haiku-4) - DeepSeek (deepseek-chat) - Moonshot Kimi (moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k) - Ollama (llama3.2, qwen2.5, mistral, codellama) - FREE & LOCAL! - Any custom provider with OpenAI-compatible API Usage: # Direct call answer = await reelforge.llm("Explain atomic habits") # With parameters answer = await reelforge.llm( prompt="Explain atomic habits in 3 sentences", temperature=0.7, max_tokens=2000 ) """ def __init__(self, config: dict): """ Initialize LLM service Args: config: Full application config dict """ self.config = config.get("llm", {}) self._client: Optional[AsyncOpenAI] = None def _get_config_value(self, key: str, default=None): """ Get config value from config file Args: key: Config key name default: Default value if not found Returns: Config value """ return self.config.get(key, default) def _create_client( self, api_key: Optional[str] = None, base_url: Optional[str] = None, ) -> AsyncOpenAI: """ Create OpenAI client Args: api_key: API key (optional, uses config if not provided) base_url: Base URL (optional, uses config if not provided) Returns: AsyncOpenAI client instance """ # Get API key (priority: parameter > config) final_api_key = ( api_key or self._get_config_value("api_key") or "dummy-key" # Ollama doesn't need real key ) # Get base URL (priority: parameter > config) final_base_url = ( base_url or self._get_config_value("base_url") ) # Create client client_kwargs = {"api_key": final_api_key} if final_base_url: client_kwargs["base_url"] = final_base_url return AsyncOpenAI(**client_kwargs) async def __call__( self, prompt: str, api_key: Optional[str] = None, base_url: Optional[str] = None, model: Optional[str] = None, temperature: float = 0.7, max_tokens: int = 2000, **kwargs ) -> str: """ Generate text using LLM Args: prompt: The prompt to generate from api_key: API key (optional, uses config if not provided) base_url: Base URL (optional, uses config if not provided) model: Model name (optional, uses config if not provided) temperature: Sampling temperature (0.0-2.0). Lower is more deterministic. max_tokens: Maximum tokens to generate **kwargs: Additional provider-specific parameters Returns: Generated text Examples: # Use config from config.yaml answer = await reelforge.llm("Explain atomic habits") # Override with custom parameters answer = await reelforge.llm( prompt="Explain atomic habits in 3 sentences", api_key="sk-custom-key", base_url="https://api.custom.com/v1", model="custom-model", temperature=0.7, max_tokens=500 ) """ # Create client (new instance each time to support parameter overrides) client = self._create_client(api_key=api_key, base_url=base_url) # Get model (priority: parameter > config) final_model = ( model or self._get_config_value("model") or "gpt-3.5-turbo" # Default fallback ) logger.debug(f"LLM call: model={final_model}, base_url={client.base_url}") try: response = await client.chat.completions.create( model=final_model, messages=[{"role": "user", "content": prompt}], temperature=temperature, max_tokens=max_tokens, **kwargs ) result = response.choices[0].message.content logger.debug(f"LLM response length: {len(result)} chars") return result except Exception as e: logger.error(f"LLM call error (model={final_model}, base_url={client.base_url}): {e}") raise @property def active(self) -> str: """ Get active model name Returns: Active model name Example: print(f"Using model: {reelforge.llm.active}") """ return self._get_config_value("model", "gpt-3.5-turbo") def __repr__(self) -> str: """String representation""" model = self.active base_url = self._get_config_value("base_url", "default") return f""