Spaces:
Running
Running
| """ | |
| 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 "" | |