operations / agent /response_handler.py
jbbove's picture
Fix task output and test infrastructure
8bd860c
"""
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