Spaces:
Runtime error
Runtime error
File size: 14,972 Bytes
8bd860c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 | """
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
|