"""Emotion extraction service for analyzing emotions from statements. This service extracts emotion information from user statements using LLM, including emotion type, intensity, keywords, subject classification, and target. Classes: EmotionExtractionService: Service for extracting emotions from statements """ import logging from typing import Optional from app.core.memory.llm_tools.llm_client import LLMClientException from app.core.memory.models.emotion_models import EmotionExtraction from app.core.memory.utils.llm.llm_utils import MemoryClientFactory from app.db import get_db_context from app.models.memory_config_model import MemoryConfig logger = logging.getLogger(__name__) class EmotionExtractionService: """Service for extracting emotion information from statements. This service uses LLM to analyze statements and extract structured emotion information including type, intensity, keywords, subject, and target. It respects configuration settings for enabling/disabling extraction and filtering by intensity threshold. Attributes: llm_client: LLM client for making structured output calls """ def __init__(self, llm_id: Optional[str] = None): """Initialize the emotion extraction service. Args: llm_id: Optional LLM model ID. If None, uses default from config. """ self.llm_client = None self.llm_id = llm_id logger.info(f"Initialized EmotionExtractionService with llm_id={llm_id}") def _get_llm_client(self, model_id: Optional[str] = None): """Get or create LLM client instance. Args: model_id: Optional model ID to use. If None, uses instance llm_id. Returns: LLM client instance """ if self.llm_client is None or model_id: effective_model_id = model_id or self.llm_id with get_db_context() as db: factory = MemoryClientFactory(db) self.llm_client = factory.get_llm_client(effective_model_id) return self.llm_client async def extract_emotion( self, statement: str, config: MemoryConfig ) -> Optional[EmotionExtraction]: """Extract emotion information from a statement. This method checks if emotion extraction is enabled in the config, builds an appropriate prompt, calls the LLM for structured output, and applies intensity threshold filtering. Args: statement: The statement text to analyze config: Data configuration object containing emotion settings Returns: EmotionExtraction object if extraction succeeds and passes threshold, None if extraction is disabled, fails, or doesn't meet threshold Raises: No exceptions are raised - failures are logged and return None """ # Check if emotion extraction is enabled if not config.emotion_enabled: logger.debug("Emotion extraction is disabled in config") return None # Validate statement if not statement or not statement.strip(): logger.warning("Empty statement provided for emotion extraction") return None try: # Build the emotion extraction prompt prompt = await self._build_emotion_prompt( statement=statement, extract_keywords=config.emotion_extract_keywords, enable_subject=config.emotion_enable_subject ) # Call LLM for structured output emotion = await self._call_llm_structured( prompt=prompt, model_id=config.emotion_model_id ) # Apply intensity threshold filtering if emotion.emotion_intensity < config.emotion_min_intensity: logger.debug( f"Emotion intensity {emotion.emotion_intensity} below threshold " f"{config.emotion_min_intensity}, skipping storage" ) return None logger.info( f"Successfully extracted emotion: type={emotion.emotion_type}, " f"intensity={emotion.emotion_intensity}, subject={emotion.emotion_subject}" ) return emotion except Exception as e: logger.error( f"Emotion extraction failed for statement: {statement[:50]}..., " f"error: {str(e)}", exc_info=True ) return None async def _build_emotion_prompt( self, statement: str, extract_keywords: bool, enable_subject: bool ) -> str: """Build the emotion extraction prompt based on configuration. This method constructs a detailed prompt for the LLM that includes instructions for emotion type classification, intensity assessment, and optionally keyword extraction and subject classification. Args: statement: The statement to analyze extract_keywords: Whether to extract emotion keywords enable_subject: Whether to enable subject classification Returns: Formatted prompt string for LLM """ from app.core.memory.utils.prompt.prompt_utils import ( render_emotion_extraction_prompt, ) prompt = await render_emotion_extraction_prompt( statement=statement, extract_keywords=extract_keywords, enable_subject=enable_subject ) return prompt async def _call_llm_structured( self, prompt: str, model_id: Optional[str] = None ) -> EmotionExtraction: """Call LLM for structured emotion extraction output. This method uses the LLM client's response_structured method to get a validated EmotionExtraction object from the LLM. Args: prompt: The formatted prompt for emotion extraction model_id: Optional model ID to use for this call Returns: EmotionExtraction object with validated emotion data Raises: LLMClientException: If LLM call fails or times out ValidationError: If LLM response doesn't match expected schema """ try: # Get LLM client llm_client = self._get_llm_client(model_id) # Prepare messages messages = [ {"role": "user", "content": prompt} ] # Call LLM with structured output emotion = await llm_client.response_structured( messages=messages, response_model=EmotionExtraction, temperature=0.3, max_tokens=500 ) return emotion except LLMClientException as e: logger.error(f"LLM call failed: {str(e)}") raise except Exception as e: logger.error(f"Unexpected error in LLM structured call: {str(e)}") raise LLMClientException(f"Emotion extraction LLM call failed: {str(e)}")