""" Anthropic Claude client for Pip's emotional intelligence. Handles: Emotion analysis, action decisions, intervention logic. """ import os import json from typing import AsyncGenerator import anthropic class AnthropicClient: """Claude-powered emotional intelligence for Pip.""" def __init__(self, api_key: str = None): """Initialize with optional custom API key.""" self.api_key = api_key or os.getenv("ANTHROPIC_API_KEY") self.available = bool(self.api_key) if self.available: self.client = anthropic.Anthropic(api_key=self.api_key) self.async_client = anthropic.AsyncAnthropic(api_key=self.api_key) else: self.client = None self.async_client = None print("⚠️ Anthropic: No API key found - service disabled") self.model = "claude-sonnet-4-20250514" def is_available(self) -> bool: """Check if the client is available.""" return self.available async def analyze_emotion(self, user_input: str, system_prompt: str) -> dict: """ Analyze user's emotional state with nuance. Returns structured emotion data. """ if not self.available or not self.async_client: return { "primary_emotions": ["neutral"], "intensity": 5, "pip_expression": "neutral", "intervention_needed": False } response = await self.async_client.messages.create( model=self.model, max_tokens=1024, system=system_prompt, messages=[ {"role": "user", "content": user_input} ] ) # Parse JSON response try: content = response.content[0].text # Try to extract JSON from the response if "```json" in content: content = content.split("```json")[1].split("```")[0] elif "```" in content: content = content.split("```")[1].split("```")[0] return json.loads(content.strip()) except (json.JSONDecodeError, IndexError): # Fallback if JSON parsing fails return { "primary_emotions": ["neutral"], "intensity": 5, "concerning_flags": [], "underlying_needs": ["conversation"], "pip_expression": "neutral", "intervention_needed": False, "raw_response": response.content[0].text } async def decide_action(self, emotion_state: dict, system_prompt: str) -> dict: """ Decide what action Pip should take based on emotional state. """ if not self.available or not self.async_client: return { "action": "reflect", "image_style": "gentle", "voice_tone": "warm" } response = await self.async_client.messages.create( model=self.model, max_tokens=512, system=system_prompt, messages=[ {"role": "user", "content": f"Emotion state: {json.dumps(emotion_state)}"} ] ) try: content = response.content[0].text if "```json" in content: content = content.split("```json")[1].split("```")[0] elif "```" in content: content = content.split("```")[1].split("```")[0] return json.loads(content.strip()) except (json.JSONDecodeError, IndexError): return { "action": "reflect", "image_style": "gentle", "voice_tone": "warm", "raw_response": response.content[0].text } async def generate_response_stream( self, user_input: str, emotion_state: dict, action: dict, system_prompt: str, conversation_history: list = None ) -> AsyncGenerator[str, None]: """ Generate Pip's conversational response with streaming. """ if not self.available or not self.async_client: yield "I'm here with you. Let me think about what you shared..." return messages = conversation_history or [] # Add context about current emotional state context = f""" [Current emotional context] User's emotions: {emotion_state.get('primary_emotions', [])} Intensity: {emotion_state.get('intensity', 5)}/10 Action to take: {action.get('action', 'reflect')} Voice tone: {action.get('voice_tone', 'warm')} [User's message] {user_input} """ messages.append({"role": "user", "content": context}) async with self.async_client.messages.stream( model=self.model, max_tokens=1024, system=system_prompt, messages=messages ) as stream: async for text in stream.text_stream: yield text async def generate_intervention_response( self, user_input: str, emotion_state: dict, system_prompt: str ) -> AsyncGenerator[str, None]: """ Generate a gentle intervention response for concerning emotional states. """ if not self.available or not self.async_client: yield "I hear you, and I want you to know that what you're feeling matters. Take a moment to breathe..." return context = f""" [INTERVENTION NEEDED] User message: {user_input} Detected emotions: {emotion_state.get('primary_emotions', [])} Intensity: {emotion_state.get('intensity', 5)}/10 Concerning flags: {emotion_state.get('concerning_flags', [])} Remember: Acknowledge briefly, then gently introduce curiosity/wonder. Do NOT be preachy or clinical. """ async with self.async_client.messages.stream( model=self.model, max_tokens=1024, system=system_prompt, messages=[{"role": "user", "content": context}] ) as stream: async for text in stream.text_stream: yield text async def generate_text(self, prompt: str) -> str: """ Generate text response for a given prompt. Used for summaries and other text generation needs. """ if not self.available or not self.async_client: return "" try: response = await self.async_client.messages.create( model=self.model, max_tokens=500, messages=[{"role": "user", "content": prompt}] ) return response.content[0].text except Exception as e: print(f"Claude text generation error: {e}") return ""