""" LLM Provider - Handles different LLM services for ChatCal Voice. Implements the same fallback chain as the original ChatCal: Groq (primary) -> Anthropic (fallback) -> Mock (development) """ import logging from typing import Optional from llama_index.core.llms import LLM from llama_index.llms.groq import Groq from llama_index.llms.anthropic import Anthropic from .config import config logger = logging.getLogger(__name__) class MockLLM: """Mock LLM for development and testing.""" async def achat(self, messages): """Mock async chat method.""" last_message = messages[-1].content if messages else "Hello" # Simple rule-based responses for development if any(word in last_message.lower() for word in ["book", "schedule", "appointment"]): response = "I'd be happy to help you book an appointment! Please provide your name, preferred date and time." elif any(word in last_message.lower() for word in ["cancel", "delete"]): response = "I can help you cancel an appointment. Could you tell me which meeting you'd like to cancel?" elif any(word in last_message.lower() for word in ["available", "availability", "free"]): response = "Let me check Peter's availability for you. What dates are you considering?" else: response = "Hello! I'm ChatCal, your voice-enabled scheduling assistant. I can help you book appointments with Peter. What would you like to schedule?" class MockResponse: def __init__(self, content): self.message = self self.content = content return MockResponse(response) async def acomplete(self, prompt): """Mock async completion method.""" class MockCompletion: def __init__(self, content): self.text = content # Mock JSON response for booking parsing if "Parse this booking request" in prompt: return MockCompletion('{"date_time": "2024-01-01 14:00", "duration": 30, "meeting_type": "google_meet", "topic": "Meeting", "needs_clarification": true, "clarification_message": "Could you please specify the exact date and time?"}') return MockCompletion("Mock response for development") def get_llm() -> LLM: """ Get the appropriate LLM based on available configuration. Implements fallback chain: Groq -> Anthropic -> Mock """ # Try Groq first (primary) if config.groq_api_key: try: logger.info("🚀 Using Groq LLM (primary)") return Groq( model="llama-3.1-8b-instant", api_key=config.groq_api_key, temperature=0.1 ) except Exception as e: logger.warning(f"❌ Groq LLM failed to initialize: {e}") # Fallback to Anthropic if config.anthropic_api_key: try: logger.info("🧠 Using Anthropic Claude (fallback)") return Anthropic( model="claude-3-sonnet-20240229", api_key=config.anthropic_api_key, temperature=0.1 ) except Exception as e: logger.warning(f"❌ Anthropic LLM failed to initialize: {e}") # Final fallback to Mock LLM logger.warning("⚠️ Using Mock LLM (development/fallback)") return MockLLM() class LLMService: """Service wrapper for LLM operations.""" def __init__(self): self.llm = get_llm() self.is_mock = isinstance(self.llm, MockLLM) async def chat(self, messages, temperature: float = 0.1): """Send chat messages to LLM.""" if self.is_mock: return await self.llm.achat(messages) # For real LLMs, set temperature if supported try: if hasattr(self.llm, 'temperature'): original_temp = self.llm.temperature self.llm.temperature = temperature result = await self.llm.achat(messages) self.llm.temperature = original_temp return result else: return await self.llm.achat(messages) except Exception as e: logger.error(f"LLM chat error: {e}") # Return a graceful error response class ErrorResponse: def __init__(self, content): self.message = self self.content = content return ErrorResponse("I apologize, but I'm having trouble processing your request right now. Please try again.") async def complete(self, prompt: str, temperature: float = 0.1): """Send completion prompt to LLM.""" if self.is_mock: return await self.llm.acomplete(prompt) try: if hasattr(self.llm, 'temperature'): original_temp = self.llm.temperature self.llm.temperature = temperature result = await self.llm.acomplete(prompt) self.llm.temperature = original_temp return result else: return await self.llm.acomplete(prompt) except Exception as e: logger.error(f"LLM completion error: {e}") class ErrorCompletion: def __init__(self, content): self.text = content return ErrorCompletion("Error processing request") # Global LLM service instance llm_service = LLMService()