""" Response Template Handler This module provides a configurable response generation system that eliminates hard-coded response templates and makes the agent truly tool-agnostic. Purpose: - Load response templates from YAML configuration - Generate responses dynamically based on tool results - Support multiple languages and customizable templates - Eliminate hard-coded response strings from agent code Architecture: - ResponseTemplateHandler: Main class for template management - Template loading from agent/config/response_templates.yaml - Dynamic variable substitution and content generation - Tool-agnostic response formatting Used by: - nodes.py: Response generation node - agent.py: Error response formatting - Future modules requiring configurable responses """ import yaml import logging from pathlib import Path from typing import Dict, Any, List, Optional, Union from dataclasses import dataclass @dataclass class ResponseContext: """Context information for response generation""" tool_results: List[Any] processing_status: str errors: List[str] clarification_request: Optional[str] = None rejection_reason: Optional[str] = None language: str = "it" class ResponseTemplateHandler: """ Handles loading and applying response templates for tool-agnostic responses This class eliminates hard-coded response strings by loading configurable templates from YAML and applying them based on context and tool results. """ def __init__(self, config_path: Optional[Path] = None): """ Initialize the response template handler Args: config_path: Path to response templates YAML file """ self.config_path = config_path or Path(__file__).parent / "config" / "response_templates.yaml" self.templates = {} self.logger = logging.getLogger(__name__) self._load_templates() def _load_templates(self): """Load response templates from YAML configuration""" try: if not self.config_path.exists(): self.logger.warning(f"Response templates not found: {self.config_path}") self._use_fallback_templates() return with open(self.config_path, 'r', encoding='utf-8') as f: self.templates = yaml.safe_load(f) self.logger.info(f"Response templates loaded from {self.config_path}") except Exception as e: self.logger.error(f"Failed to load response templates: {e}") self._use_fallback_templates() def _use_fallback_templates(self): """Use minimal fallback templates if loading fails""" self.templates = { 'success_responses': { 'general_operation_completed': {'template': 'āœ… **Operazione Completata**\n'} }, 'error_responses': { 'processing_error': {'template': 'āš ļø **Errore nell\'Elaborazione**\n'} }, 'dynamic_content': { 'tool_specific_emojis': {'default': 'āœ…'}, 'operation_names': {'data_extraction': 'Estrazione Dati'} } } self.logger.warning("Using fallback response templates") def generate_success_response(self, context: ResponseContext) -> str: """ Generate success response based on tool results and context Args: context: Response generation context Returns: Formatted success response string """ response_parts = [] # Get successful results successful_results = [r for r in context.tool_results if r.success] if not successful_results: return self._generate_error_response(context) # Generate header based on tool type header = self._get_success_header(successful_results) response_parts.append(header) # Add tool result summaries for result in successful_results: response_parts.append(result.summary_text) # Add LLM insights if available llm_insights = result.metadata.get("llm_insights") if llm_insights and len(llm_insights.strip()) > 20: insights_header = self._get_template_text('result_formatting', 'insights_header', "🧠 **Analisi Intelligente:**") response_parts.append(f"\n{insights_header}") response_parts.append(llm_insights) # Add artifact information if result.artifacts: artifacts_section = self._format_artifacts(result.artifacts) response_parts.append(artifacts_section) # Add warnings if any if result.warnings: warnings_section = self._format_warnings(result.warnings) response_parts.append(warnings_section) # Add data sources all_sources = [] for result in successful_results: all_sources.extend(result.sources) if all_sources: sources_section = self._format_sources(list(set(all_sources))) response_parts.append(sources_section) return "\n".join(response_parts) def generate_help_response(self, context: ResponseContext) -> str: """Generate help/guidance response""" help_config = self.templates.get('help_responses', {}).get('general_help', {}) response_parts = [] # Header header = help_config.get('header', 'ā„¹ļø **Assistente Operazioni - Guida**\n') response_parts.append(header) # Capabilities intro capabilities_intro = help_config.get('capabilities_intro', 'Posso aiutarti con:') response_parts.append(capabilities_intro) # Add capabilities from configuration capabilities_by_tool = self.templates.get('help_responses', {}).get('capabilities_by_tool', {}) for tool_name, tool_caps in capabilities_by_tool.items(): capability_line = f"• **{tool_caps.get('category', tool_name)}**: {tool_caps.get('description', '')}" response_parts.append(capability_line) # Examples intro examples_intro = help_config.get('examples_intro', '\n**Esempi di richieste:**') response_parts.append(examples_intro) # Add examples from configuration for tool_name, tool_caps in capabilities_by_tool.items(): examples = tool_caps.get('examples', []) for example in examples[:2]: # Limit examples response_parts.append(f"• '{example}'") # Add clarification if available if context.clarification_request: clarification_config = self.templates.get('clarification_responses', {}).get('geographic_clarification', {}) if isinstance(clarification_config, dict): clarification_template = clarification_config.get('template', "ā“ **Specifica meglio:** {clarification_request}") else: clarification_template = "ā“ **Specifica meglio:** {clarification_request}" try: clarification_text = clarification_template.format(clarification_request=context.clarification_request) response_parts.append(f"\n{clarification_text}") except (KeyError, ValueError) as e: self.logger.warning(f"Clarification template error: {e}") response_parts.append(f"\nā“ **Specifica meglio:** {context.clarification_request}") return "\n".join(response_parts) def generate_error_response(self, context: ResponseContext) -> str: """Generate error response based on context""" return self._generate_error_response(context) def generate_rejection_response(self, context: ResponseContext) -> str: """Generate rejection response for unsupported requests""" if not context.rejection_reason: return self._generate_error_response(context) # Get geographic rejection template rejection_config = self.templates.get('error_responses', {}).get('geographic_rejection', {}) template = rejection_config.get('template', "āš ļø **Richiesta Non Supportata**\n{rejection_reason}\n**Nota:** Posso fornire dati solo per la regione {supported_region}.") # Default values - these could come from geographic config variables = { 'rejection_reason': context.rejection_reason, 'supported_region': 'Liguria', 'geographic_scope_label': 'Province supportate', 'supported_areas': 'Genova, Savona, Imperia, La Spezia' } try: return template.format(**variables) except KeyError as e: self.logger.warning(f"Template variable missing: {e}") return f"āš ļø **Richiesta Non Supportata**\n{context.rejection_reason}" def _get_success_header(self, successful_results: List[Any]) -> str: """Get appropriate success header based on tool results""" # Check if this is OMIRL-related if any("omirl" in r.tool_name.lower() for r in successful_results): template_config = self.templates.get('success_responses', {}).get('omirl_data_completed', {}) return template_config.get('template', '🌊 **Estrazione Dati OMIRL Completata**\n') # Default success header template_config = self.templates.get('success_responses', {}).get('general_operation_completed', {}) return template_config.get('template', 'āœ… **Operazione Completata**\n') def _generate_error_response(self, context: ResponseContext) -> str: """Generate error response""" response_parts = [] # Get error header template, handling both string and dict formats error_config = self.templates.get('error_responses', {}).get('processing_error', {}) if isinstance(error_config, dict): error_header = error_config.get('template', 'āš ļø **Errore nell\'Elaborazione**\n') else: error_header = str(error_config) response_parts.append(error_header) # Show tool errors failed_results = [r for r in context.tool_results if not r.success] for result in failed_results: response_parts.append(f"• {result.summary_text}") # Show general errors for error in context.errors: response_parts.append(f"• {error}") # Add helpful suggestion response_parts.append("\nā„¹ļø Prova a riformulare la richiesta o chiedi 'aiuto' per maggiori informazioni.") return "\n".join(response_parts) def _format_artifacts(self, artifacts: List[str]) -> str: """Format artifacts section using configuration""" artifact_config = self.templates.get('result_formatting', {}).get('artifacts', {}) if len(artifacts) == 1: template = artifact_config.get('single', '\nšŸ“„ **File generato:** {artifact_name}') return template.format(artifact_name=artifacts[0]) # Multiple artifacts max_displayed = artifact_config.get('max_displayed', 3) header = artifact_config.get('multiple', '\nšŸ“„ **File generati:** {count}\n{artifact_list}') list_item_template = artifact_config.get('list_item', ' • {artifact_name}') displayed_artifacts = artifacts[:max_displayed] artifact_list = '\n'.join([list_item_template.format(artifact_name=artifact) for artifact in displayed_artifacts]) if len(artifacts) > max_displayed: remaining_count = len(artifacts) - max_displayed overflow_template = artifact_config.get('overflow_message', ' • ... e altri {remaining_count} file') artifact_list += '\n' + overflow_template.format(remaining_count=remaining_count) return header.format(count=len(artifacts), artifact_list=artifact_list) def _format_warnings(self, warnings: List[str]) -> str: """Format warnings section using configuration""" warning_config = self.templates.get('result_formatting', {}).get('warnings', {}) max_displayed = warning_config.get('max_displayed', 2) intro_template = warning_config.get('intro', '\nāš ļø **Avvisi:** {count}') list_item_template = warning_config.get('list_item', ' • {warning_text}') intro = intro_template.format(count=len(warnings)) warning_list = '\n'.join([ list_item_template.format(warning_text=warning) for warning in warnings[:max_displayed] ]) return intro + '\n' + warning_list def _format_sources(self, sources: List[str]) -> str: """Format sources section using configuration""" source_config = self.templates.get('result_formatting', {}).get('sources', {}) intro = source_config.get('intro', '\nšŸ”— **Fonti dati:**') list_item_template = source_config.get('list_item', ' • {source_url}') source_list = '\n'.join([ list_item_template.format(source_url=source) for source in sources ]) return intro + '\n' + source_list def _get_template_text(self, section: str, key: str, default: str) -> str: """Get template text from configuration with fallback""" try: return self.templates.get(section, {}).get(key, default) except (KeyError, AttributeError): return default def get_tool_emoji(self, tool_name: str) -> str: """Get emoji for specific tool""" tool_emojis = self.templates.get('dynamic_content', {}).get('tool_specific_emojis', {}) return tool_emojis.get(tool_name, tool_emojis.get('default', 'āœ…')) def get_operation_name(self, operation_type: str) -> str: """Get localized operation name""" operation_names = self.templates.get('dynamic_content', {}).get('operation_names', {}) return operation_names.get(operation_type, operation_type.title()) # Global instance for easy import _response_handler = None def get_response_handler() -> ResponseTemplateHandler: """Get or create global response template handler instance""" global _response_handler if _response_handler is None: _response_handler = ResponseTemplateHandler() return _response_handler def reset_response_handler(): """Reset global response handler (useful for testing)""" global _response_handler _response_handler = None