Spaces:
Runtime error
Runtime error
| """ | |
| 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 | |
| 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 | |