""" Google Gemini client for Pip. Handles: Text generation (emotion analysis, conversation) and image generation. Uses gemini-flash-lite-latest for text, imagen-4.0-fast-generate-001 for images. """ import os import json from typing import Optional, AsyncGenerator import google.generativeai as genai from google.generativeai import types import base64 class GeminiClient: """ Gemini-powered client for Pip. Primary LLM for emotion analysis, conversation, and image generation. """ # Model configurations - using actual available model names TEXT_MODEL_FAST = "models/gemini-flash-lite-latest" TEXT_MODEL_PRO = "models/gemini-flash-lite-latest" IMAGE_MODEL = "models/imagen-4.0-fast-generate-001" def __init__(self, api_key: str = None): """Initialize with optional custom API key.""" self.api_key = api_key or os.getenv("GOOGLE_API_KEY") self.available = bool(self.api_key) if self.available: genai.configure(api_key=self.api_key) print(f"✅ Gemini: Configured with model {self.TEXT_MODEL_FAST}") else: print("⚠️ Gemini: No API key found - service disabled") # Model instances (lazy loaded) self._fast_model = None self._pro_model = None def is_available(self) -> bool: """Check if the client is available.""" return self.available def _get_fast_model(self): """Get fast model for quick responses.""" if self._fast_model is None: self._fast_model = genai.GenerativeModel(self.TEXT_MODEL_FAST) return self._fast_model def _get_pro_model(self): """Get pro model for complex reasoning.""" if self._pro_model is None: self._pro_model = genai.GenerativeModel(self.TEXT_MODEL_PRO) return self._pro_model async def analyze_emotion(self, user_input: str, system_prompt: str) -> dict: """ Analyze emotional content of user input. Returns structured emotion analysis. """ if not self.available: return { "primary_emotions": ["neutral"], "intensity": 5, "pip_expression": "neutral", "intervention_needed": False } try: model = self._get_pro_model() prompt = f"""{system_prompt} USER INPUT TO ANALYZE: {user_input} Remember: Respond with ONLY valid JSON, no markdown formatting.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.3, max_output_tokens=1024, ) ) result_text = response.text.strip() # Parse JSON response if result_text.startswith("```"): result_text = result_text.split("```")[1] if result_text.startswith("json"): result_text = result_text[4:] return json.loads(result_text) except json.JSONDecodeError as e: print(f"Gemini emotion analysis JSON error: {e}") return { "primary_emotions": ["neutral"], "secondary_emotions": [], "intensity": 5, "underlying_needs": ["connection"], "intervention_needed": False } except Exception as e: print(f"Gemini emotion analysis error: {e}") raise async def decide_action(self, emotion_state: dict, system_prompt: str) -> dict: """ Decide what action Pip should take based on emotion analysis. """ try: model = self._get_fast_model() prompt = f"""{system_prompt} EMOTION ANALYSIS: {json.dumps(emotion_state, indent=2)} Respond with ONLY valid JSON, no markdown.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.4, max_output_tokens=512, ) ) result_text = response.text.strip() if result_text.startswith("```"): result_text = result_text.split("```")[1] if result_text.startswith("json"): result_text = result_text[4:] return json.loads(result_text) except json.JSONDecodeError: return { "action": "reflect", "image_style": "warm", "suggested_response_tone": "empathetic" } except Exception as e: print(f"Gemini action decision error: {e}") raise async def quick_acknowledge(self, user_input: str, system_prompt: str) -> str: """ Generate a quick acknowledgment (< 500ms target). Uses the fastest available model. """ if not self.available: return "I hear you..." try: model = self._get_fast_model() prompt = f"""{system_prompt} USER SAID: {user_input} Respond with JUST the acknowledgment phrase, nothing else.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.7, max_output_tokens=50, ) ) return response.text.strip() except Exception as e: print(f"Gemini quick ack error: {e}") return "I hear you..." async def generate_response_stream( self, user_input: str, emotion_state: dict, action: dict, system_prompt: str, history: list = None ) -> AsyncGenerator[str, None]: """ Generate conversational response with streaming. """ try: model = self._get_pro_model() # Build context history_text = "" if history: history_text = "\n".join([ f"{m['role'].upper()}: {m['content']}" for m in history[-6:] ]) prompt = f"""{system_prompt} EMOTION ANALYSIS: {json.dumps(emotion_state, indent=2)} ACTION TO TAKE: {json.dumps(action, indent=2)} CONVERSATION HISTORY: {history_text} CURRENT USER MESSAGE: {user_input} Respond naturally and warmly. Be concise but meaningful.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.8, max_output_tokens=500, ), stream=True ) async for chunk in response: if chunk.text: yield chunk.text except Exception as e: print(f"Gemini response stream error: {e}") yield "I'm here with you. Tell me more about what's on your mind." async def generate_intervention_response( self, user_input: str, emotion_state: dict, system_prompt: str ) -> AsyncGenerator[str, None]: """ Generate careful intervention response for concerning situations. """ try: model = self._get_pro_model() prompt = f"""{system_prompt} USER INPUT: {user_input} EMOTION ANALYSIS: {json.dumps(emotion_state, indent=2)} Respond with care, warmth, and appropriate resources if needed.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.5, max_output_tokens=600, ), stream=True ) async for chunk in response: if chunk.text: yield chunk.text except Exception as e: print(f"Gemini intervention error: {e}") yield "I hear that you're going through something difficult. I'm here with you, and I care about how you're feeling." async def generate_text(self, prompt: str) -> Optional[str]: """ Generate text (for prompts, summaries, etc). """ if not self.available: return None try: model = self._get_pro_model() response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.7, max_output_tokens=1024, ) ) return response.text except Exception as e: print(f"Gemini text generation error: {e}") return None async def enhance_prompt( self, user_input: str, emotion_state: dict, mode: str, system_prompt: str ) -> str: """ Enhance a prompt for image generation. """ try: model = self._get_fast_model() prompt = f"""{system_prompt} USER INPUT: {user_input} EMOTIONS: {json.dumps(emotion_state.get('primary_emotions', []))} MODE: {mode} Generate the enhanced image prompt only, no explanation.""" response = await model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=0.9, max_output_tokens=200, ) ) return response.text.strip() except Exception as e: print(f"Gemini prompt enhancement error: {e}") return f"A peaceful scene reflecting {emotion_state.get('primary_emotions', ['calm'])[0]}" async def generate_image(self, prompt: str) -> Optional[str]: """ Generate an image using Imagen 4.0. Returns base64 encoded image or None. """ if not self.available: return None try: # Use GenerativeModel with Imagen 4.0 imagen_model = genai.GenerativeModel(self.IMAGE_MODEL) response = await imagen_model.generate_content_async( prompt, generation_config=types.GenerationConfig( temperature=1.0, ) ) # Check for image in response if response.candidates: for candidate in response.candidates: if hasattr(candidate, 'content') and candidate.content.parts: for part in candidate.content.parts: if hasattr(part, 'inline_data') and part.inline_data: # Return base64 encoded image return base64.b64encode(part.inline_data.data).decode('utf-8') print("Imagen: No image in response") return None except Exception as e: error_str = str(e) if "429" in error_str or "quota" in error_str.lower(): print(f"Imagen rate limit exceeded: {e}") elif "not found" in error_str.lower() or "not supported" in error_str.lower(): print(f"Imagen not available: {e}") return None else: print(f"Imagen generation error: {e}") return None