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