"""LLM Integration Module (Phase 7). Supports: - OpenAI API (GPT-4, GPT-3.5) - Anthropic API (Claude) - Streaming responses - Response caching """ import asyncio import logging from typing import Optional, AsyncGenerator, Dict, Any from enum import Enum from abc import ABC, abstractmethod logger = logging.getLogger(__name__) class LLMProvider(str, Enum): """LLM providers""" OPENAI = "openai" ANTHROPIC = "anthropic" LOCAL = "local" # LM Studio, Ollama class LLMConfig: """LLM configuration""" def __init__( self, provider: LLMProvider = LLMProvider.OPENAI, api_key: Optional[str] = None, model: str = "gpt-4", temperature: float = 0.7, max_tokens: int = 500, base_url: Optional[str] = None, ): self.provider = provider self.api_key = api_key self.model = model self.temperature = temperature self.max_tokens = max_tokens self.base_url = base_url class BaseLLMClient(ABC): """Base LLM client interface""" def __init__(self, config: LLMConfig): self.config = config @abstractmethod async def generate( self, prompt: str, stream: bool = False, ) -> str: """Generate response from prompt""" pass @abstractmethod async def generate_stream( self, prompt: str, ) -> AsyncGenerator[str, None]: """Generate response as token stream""" pass class OpenAIClient(BaseLLMClient): """OpenAI API client""" def __init__(self, config: LLMConfig): super().__init__(config) try: import openai self.client = openai.AsyncOpenAI(api_key=config.api_key) except ImportError: raise ImportError("openai package required: pip install openai") async def generate( self, prompt: str, stream: bool = False, ) -> str: """Generate response from OpenAI""" try: response = await self.client.chat.completions.create( model=self.config.model, messages=[ { "role": "user", "content": prompt } ], temperature=self.config.temperature, max_tokens=self.config.max_tokens, stream=stream, ) if stream: # Collect streamed tokens full_response = "" async for chunk in response: if chunk.choices[0].delta.content: full_response += chunk.choices[0].delta.content return full_response else: return response.choices[0].message.content except Exception as e: logger.error(f"OpenAI generation failed: {e}") raise async def generate_stream( self, prompt: str, ) -> AsyncGenerator[str, None]: """Stream tokens from OpenAI""" try: response = await self.client.chat.completions.create( model=self.config.model, messages=[ { "role": "user", "content": prompt } ], temperature=self.config.temperature, max_tokens=self.config.max_tokens, stream=True, ) async for chunk in response: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content except Exception as e: logger.error(f"OpenAI streaming failed: {e}") raise class AnthropicClient(BaseLLMClient): """Anthropic API client (Claude)""" def __init__(self, config: LLMConfig): super().__init__(config) try: import anthropic self.client = anthropic.AsyncAnthropic(api_key=config.api_key) except ImportError: raise ImportError("anthropic package required: pip install anthropic") async def generate( self, prompt: str, stream: bool = False, ) -> str: """Generate response from Claude""" try: if stream: full_response = "" async with self.client.messages.stream( model=self.config.model, max_tokens=self.config.max_tokens, messages=[ { "role": "user", "content": prompt } ], ) as stream: async for text in stream.text_stream: full_response += text return full_response else: message = await self.client.messages.create( model=self.config.model, max_tokens=self.config.max_tokens, messages=[ { "role": "user", "content": prompt } ], ) return message.content[0].text except Exception as e: logger.error(f"Anthropic generation failed: {e}") raise async def generate_stream( self, prompt: str, ) -> AsyncGenerator[str, None]: """Stream tokens from Claude""" try: async with self.client.messages.stream( model=self.config.model, max_tokens=self.config.max_tokens, messages=[ { "role": "user", "content": prompt } ], ) as stream: async for text in stream.text_stream: yield text except Exception as e: logger.error(f"Anthropic streaming failed: {e}") raise class LocalLLMClient(BaseLLMClient): """Local LLM client (LM Studio, Ollama)""" def __init__(self, config: LLMConfig): super().__init__(config) try: import httpx self.client = httpx.AsyncClient(base_url=config.base_url) except ImportError: raise ImportError("httpx package required: pip install httpx") async def generate( self, prompt: str, stream: bool = False, ) -> str: """Generate response from local LLM""" try: response = await self.client.post( "/v1/completions", json={ "model": self.config.model, "prompt": prompt, "temperature": self.config.temperature, "max_tokens": self.config.max_tokens, "stream": stream, }, ) if stream: full_response = "" async for chunk in response.aiter_lines(): if chunk.startswith("data: "): import json try: data = json.loads(chunk[6:]) if "choices" in data: full_response += data["choices"][0].get("text", "") except: pass return full_response else: data = response.json() return data["choices"][0]["text"] except Exception as e: logger.error(f"Local LLM generation failed: {e}") raise async def generate_stream( self, prompt: str, ) -> AsyncGenerator[str, None]: """Stream tokens from local LLM""" try: async with self.client.stream( "POST", "/v1/completions", json={ "model": self.config.model, "prompt": prompt, "temperature": self.config.temperature, "max_tokens": self.config.max_tokens, "stream": True, }, ) as response: async for chunk in response.aiter_lines(): if chunk.startswith("data: "): import json try: data = json.loads(chunk[6:]) if "choices" in data: text = data["choices"][0].get("text", "") if text: yield text except: pass except Exception as e: logger.error(f"Local LLM streaming failed: {e}") raise class LLMManager: """LLM management and client selection""" def __init__(self, config: LLMConfig): self.config = config self.client = self._create_client(config) def _create_client(self, config: LLMConfig) -> BaseLLMClient: """Create appropriate LLM client""" if config.provider == LLMProvider.OPENAI: return OpenAIClient(config) elif config.provider == LLMProvider.ANTHROPIC: return AnthropicClient(config) elif config.provider == LLMProvider.LOCAL: return LocalLLMClient(config) else: raise ValueError(f"Unknown provider: {config.provider}") async def generate( self, prompt: str, stream: bool = False, ) -> str: """Generate response""" return await self.client.generate(prompt, stream=stream) async def generate_stream( self, prompt: str, ) -> AsyncGenerator[str, None]: """Generate streaming response""" async for token in self.client.generate_stream(prompt): yield token async def generate_with_metadata( self, prompt: str, stream: bool = False, ) -> Dict[str, Any]: """Generate response with metadata""" import time start_time = time.time() response = await self.generate(prompt, stream=stream) end_time = time.time() return { "response": response, "tokens": len(response.split()), "latency": end_time - start_time, "model": self.config.model, "provider": self.config.provider.value, }