Spaces:
Runtime error
Runtime error
Fix task output and test infrastructure
Browse files- Fixed artifact generation parameter error in tools/omirl/adapter.py
- Fixed metadata storage issue in tools/omirl/tables/massimi_precipitazione.py
- Added comprehensive full flow test in tests/agent/full/test_full_flow.py
- Added configuration viewer and navigator utilities
- Added documentation for config-driven architecture
- Created test infrastructure for agent refactoring validation
- All tests now show formatted task output correctly connecting to LLM processing
- agent/agent.py +32 -26
- agent/config/geography.yaml +2 -4
- agent/config/llm_router_config.yaml +1 -0
- agent/config/response_templates.yaml +127 -0
- agent/nodes.py +69 -117
- agent/response_handler.py +351 -0
- agent/tests/README.md +169 -0
- agent/tests/config_viewer.py +209 -0
- agent/tests/test_full_flow.py +612 -0
- docs/CONFIGURATION_REFACTORING.md +0 -89
- docs/DEPLOYMENT_GUIDE.md +0 -238
- docs/LEGACY_CODE_REMOVAL_PLAN.md +0 -163
- docs/LEGACY_REMOVAL_SUCCESS_REPORT.md +0 -162
- docs/LLM_ROUTER_PROGRESS.md +0 -295
- docs/OMIRL_PHASE_1_ADAPTER_COMPLETE.md +0 -76
- docs/OMIRL_WEB_MIGRATION_PLAN.md +0 -175
- docs/PHASE_1_LLM_SUMMARIZATION_COMPLETE.md +0 -428
- docs/SECURITY_SUMMARY.md +0 -166
- docs/STEP12_CONFIG_NAVIGATION_GUIDE.md +218 -0
- docs/TASK_4_OMIRL_INTEGRATION_COMPLETE.md +0 -402
- docs/TODO_NEXT_DEV_SESSION.md +0 -440
- scripts/config_navigator.py +261 -0
- test_integration_steps1to5.py +0 -170
- test_step1_task_config.py +0 -94
- test_step2_url_config.py +0 -124
- test_step3_timeperiods_config.py +0 -135
- test_step4_period_mappings.py +0 -169
- test_step5_dynamic_toolspec.py +0 -147
- tests/agent/full/README.md +127 -0
- tests/agent/full/config_viewer.py +209 -0
- tests/agent/full/test_full_flow.py +583 -0
- tests/agent/test_refactored_agent.py +237 -0
- tests/agent/test_refactored_nodes.py +349 -0
- tests/agent/test_response_handler.py +345 -0
- tests/agent/test_response_templates.py +219 -0
- tests/agent/test_simplified_agent.py +183 -0
- tools/omirl/config/tasks.yaml +11 -14
- tools/omirl/shared/validation.py +11 -3
- tools/omirl/tables/massimi_precipitazione.py +4 -3
agent/agent.py
CHANGED
|
@@ -37,6 +37,15 @@ from .graph import get_default_workflow, create_simple_workflow, print_workflow_
|
|
| 37 |
from .registry import get_tool_registry, print_registry_summary
|
| 38 |
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
class ColoredFormatter(logging.Formatter):
|
| 41 |
"""Custom formatter with colors and emojis for development"""
|
| 42 |
|
|
@@ -164,22 +173,22 @@ class OperationsAgent:
|
|
| 164 |
"""
|
| 165 |
|
| 166 |
if not user_message:
|
| 167 |
-
raise ValidationError("
|
| 168 |
|
| 169 |
if not isinstance(user_message, str):
|
| 170 |
-
raise ValidationError("
|
| 171 |
|
| 172 |
if not user_message.strip():
|
| 173 |
-
raise ValidationError("
|
| 174 |
|
| 175 |
if len(user_message) > 10000: # Reasonable limit
|
| 176 |
-
raise ValidationError("
|
| 177 |
|
| 178 |
if user_id is not None and not isinstance(user_id, str):
|
| 179 |
-
raise ValidationError("
|
| 180 |
|
| 181 |
if context is not None and not isinstance(context, dict):
|
| 182 |
-
raise ValidationError("
|
| 183 |
|
| 184 |
def _initialize_workflow(self):
|
| 185 |
"""Initialize the workflow and tools (called lazily if needed)"""
|
|
@@ -215,7 +224,7 @@ class OperationsAgent:
|
|
| 215 |
user_message: str,
|
| 216 |
user_id: str = None,
|
| 217 |
context: Dict[str, Any] = None,
|
| 218 |
-
timeout: float = 60.0 #
|
| 219 |
) -> Dict[str, Any]:
|
| 220 |
"""
|
| 221 |
Process a user request through the agent workflow
|
|
@@ -328,9 +337,9 @@ class OperationsAgent:
|
|
| 328 |
without going through natural language processing.
|
| 329 |
|
| 330 |
Args:
|
| 331 |
-
sensor_type: Type of sensor (
|
| 332 |
-
provincia: Province filter (
|
| 333 |
-
comune: Municipality filter (
|
| 334 |
**kwargs: Additional parameters
|
| 335 |
|
| 336 |
Returns:
|
|
@@ -342,14 +351,15 @@ class OperationsAgent:
|
|
| 342 |
|
| 343 |
try:
|
| 344 |
# Validate OMIRL-specific parameters
|
| 345 |
-
if provincia and provincia.upper() not in
|
| 346 |
-
|
|
|
|
| 347 |
|
| 348 |
if sensor_type and not isinstance(sensor_type, str):
|
| 349 |
-
raise ValidationError("
|
| 350 |
|
| 351 |
if comune and not isinstance(comune, str):
|
| 352 |
-
raise ValidationError("
|
| 353 |
|
| 354 |
# Build filters
|
| 355 |
filters = {}
|
|
@@ -413,20 +423,16 @@ class OperationsAgent:
|
|
| 413 |
"""
|
| 414 |
|
| 415 |
return {
|
| 416 |
-
"supported_sensors":
|
| 417 |
-
|
| 418 |
-
"Umidità dell'aria", "Eliofanie", "Radiazione solare", "Bagnatura Fogliare",
|
| 419 |
-
"Pressione Atmosferica", "Tensione Batteria", "Stato del Mare", "Neve"
|
| 420 |
-
],
|
| 421 |
-
"supported_provinces": ["GENOVA", "SAVONA", "IMPERIA", "LA SPEZIA"],
|
| 422 |
"data_source": "https://omirl.regione.liguria.it/#/sensorstable",
|
| 423 |
"description": "Osservatorio Meteorologico e Idropluviometrico Regionale Liguria",
|
| 424 |
"capabilities": [
|
| 425 |
-
"
|
| 426 |
-
"
|
| 427 |
-
"
|
| 428 |
-
"
|
| 429 |
-
"
|
| 430 |
]
|
| 431 |
}
|
| 432 |
|
|
@@ -531,7 +537,7 @@ class OperationsAgent:
|
|
| 531 |
Formatted error response
|
| 532 |
"""
|
| 533 |
|
| 534 |
-
#
|
| 535 |
if error_type == "ValidationError":
|
| 536 |
response_text = f"⚠️ **Errore di Validazione**\n\n{error_message}\n\nVerifica che la richiesta sia corretta e riprova."
|
| 537 |
elif error_type == "TimeoutError":
|
|
|
|
| 37 |
from .registry import get_tool_registry, print_registry_summary
|
| 38 |
|
| 39 |
|
| 40 |
+
# Simple constants from existing geography config (avoid complex config loading)
|
| 41 |
+
VALID_PROVINCES = ["GENOVA", "SAVONA", "IMPERIA", "LA SPEZIA"]
|
| 42 |
+
SUPPORTED_SENSORS = [
|
| 43 |
+
"Precipitazione", "Temperatura", "Livelli Idrometrici", "Vento",
|
| 44 |
+
"Umidità dell'aria", "Eliofanie", "Radiazione solare", "Bagnatura Fogliare",
|
| 45 |
+
"Pressione Atmosferica", "Tensione Batteria", "Stato del Mare", "Neve"
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
|
| 49 |
class ColoredFormatter(logging.Formatter):
|
| 50 |
"""Custom formatter with colors and emojis for development"""
|
| 51 |
|
|
|
|
| 173 |
"""
|
| 174 |
|
| 175 |
if not user_message:
|
| 176 |
+
raise ValidationError("Il messaggio utente è richiesto")
|
| 177 |
|
| 178 |
if not isinstance(user_message, str):
|
| 179 |
+
raise ValidationError("Il messaggio utente deve essere una stringa")
|
| 180 |
|
| 181 |
if not user_message.strip():
|
| 182 |
+
raise ValidationError("Il messaggio utente non può essere vuoto")
|
| 183 |
|
| 184 |
if len(user_message) > 10000: # Reasonable limit
|
| 185 |
+
raise ValidationError("Messaggio utente troppo lungo (massimo 10.000 caratteri)")
|
| 186 |
|
| 187 |
if user_id is not None and not isinstance(user_id, str):
|
| 188 |
+
raise ValidationError("L'ID utente deve essere una stringa")
|
| 189 |
|
| 190 |
if context is not None and not isinstance(context, dict):
|
| 191 |
+
raise ValidationError("Il contesto deve essere un dizionario")
|
| 192 |
|
| 193 |
def _initialize_workflow(self):
|
| 194 |
"""Initialize the workflow and tools (called lazily if needed)"""
|
|
|
|
| 224 |
user_message: str,
|
| 225 |
user_id: str = None,
|
| 226 |
context: Dict[str, Any] = None,
|
| 227 |
+
timeout: float = 60.0 # Default timeout in seconds
|
| 228 |
) -> Dict[str, Any]:
|
| 229 |
"""
|
| 230 |
Process a user request through the agent workflow
|
|
|
|
| 337 |
without going through natural language processing.
|
| 338 |
|
| 339 |
Args:
|
| 340 |
+
sensor_type: Type of sensor (configurable via sensor types config)
|
| 341 |
+
provincia: Province filter (configurable via geography config)
|
| 342 |
+
comune: Municipality filter (configurable via geography config)
|
| 343 |
**kwargs: Additional parameters
|
| 344 |
|
| 345 |
Returns:
|
|
|
|
| 351 |
|
| 352 |
try:
|
| 353 |
# Validate OMIRL-specific parameters
|
| 354 |
+
if provincia and provincia.upper() not in VALID_PROVINCES:
|
| 355 |
+
valid_provinces_str = ", ".join(VALID_PROVINCES)
|
| 356 |
+
raise ValidationError(f"Provincia non valida: {provincia}. Usa: {valid_provinces_str}")
|
| 357 |
|
| 358 |
if sensor_type and not isinstance(sensor_type, str):
|
| 359 |
+
raise ValidationError("Il tipo sensore deve essere una stringa")
|
| 360 |
|
| 361 |
if comune and not isinstance(comune, str):
|
| 362 |
+
raise ValidationError("Il comune deve essere una stringa")
|
| 363 |
|
| 364 |
# Build filters
|
| 365 |
filters = {}
|
|
|
|
| 423 |
"""
|
| 424 |
|
| 425 |
return {
|
| 426 |
+
"supported_sensors": SUPPORTED_SENSORS,
|
| 427 |
+
"supported_provinces": VALID_PROVINCES,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
"data_source": "https://omirl.regione.liguria.it/#/sensorstable",
|
| 429 |
"description": "Osservatorio Meteorologico e Idropluviometrico Regionale Liguria",
|
| 430 |
"capabilities": [
|
| 431 |
+
"Estrazione dati stazioni meteorologiche",
|
| 432 |
+
"Filtraggio per tipo sensore",
|
| 433 |
+
"Filtraggio geografico (provincia, comune)",
|
| 434 |
+
"Accesso dati in tempo reale",
|
| 435 |
+
"Generazione file JSON"
|
| 436 |
]
|
| 437 |
}
|
| 438 |
|
|
|
|
| 537 |
Formatted error response
|
| 538 |
"""
|
| 539 |
|
| 540 |
+
# Italian error message templates
|
| 541 |
if error_type == "ValidationError":
|
| 542 |
response_text = f"⚠️ **Errore di Validazione**\n\n{error_message}\n\nVerifica che la richiesta sia corretta e riprova."
|
| 543 |
elif error_type == "TimeoutError":
|
agent/config/geography.yaml
CHANGED
|
@@ -45,7 +45,6 @@ regions:
|
|
| 45 |
"C-": "Zona C - Zona M"
|
| 46 |
"C+": "Zona C + Zona M"
|
| 47 |
|
| 48 |
-
# TODO: To be populated from Excel station data
|
| 49 |
comuni_by_province:
|
| 50 |
"Genova":
|
| 51 |
- "Genova"
|
|
@@ -280,15 +279,14 @@ validation:
|
|
| 280 |
case_sensitive: false
|
| 281 |
allow_province_codes: true
|
| 282 |
allow_province_full_names: true
|
| 283 |
-
auto_convert_provinces_to_codes: true
|
| 284 |
fuzzy_matching_enabled: true
|
| 285 |
-
fuzzy_threshold: 0.
|
| 286 |
|
| 287 |
# Geographic resolution strategies
|
| 288 |
resolution_strategies:
|
| 289 |
ambiguous_locations:
|
| 290 |
# When user says "Genova" without specifying comune/provincia
|
| 291 |
-
default_to_comune_for_major_cities: ["
|
| 292 |
require_clarification_for_others: true
|
| 293 |
|
| 294 |
province_inference:
|
|
|
|
| 45 |
"C-": "Zona C - Zona M"
|
| 46 |
"C+": "Zona C + Zona M"
|
| 47 |
|
|
|
|
| 48 |
comuni_by_province:
|
| 49 |
"Genova":
|
| 50 |
- "Genova"
|
|
|
|
| 279 |
case_sensitive: false
|
| 280 |
allow_province_codes: true
|
| 281 |
allow_province_full_names: true
|
|
|
|
| 282 |
fuzzy_matching_enabled: true
|
| 283 |
+
fuzzy_threshold: 0.6
|
| 284 |
|
| 285 |
# Geographic resolution strategies
|
| 286 |
resolution_strategies:
|
| 287 |
ambiguous_locations:
|
| 288 |
# When user says "Genova" without specifying comune/provincia
|
| 289 |
+
default_to_comune_for_major_cities: ["Genova", "Savona", "Imperia", "La Spezia"]
|
| 290 |
require_clarification_for_others: true
|
| 291 |
|
| 292 |
province_inference:
|
agent/config/llm_router_config.yaml
CHANGED
|
@@ -7,6 +7,7 @@
|
|
| 7 |
llm:
|
| 8 |
# Primary provider (openai, anthropic, gemini, local, mock)
|
| 9 |
provider: "gemini"
|
|
|
|
| 10 |
|
| 11 |
# Fallback configuration
|
| 12 |
fallback:
|
|
|
|
| 7 |
llm:
|
| 8 |
# Primary provider (openai, anthropic, gemini, local, mock)
|
| 9 |
provider: "gemini"
|
| 10 |
+
model: "gemini-1.5-flash" # Default model for the provider
|
| 11 |
|
| 12 |
# Fallback configuration
|
| 13 |
fallback:
|
agent/config/response_templates.yaml
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Response Templates Configuration
|
| 2 |
+
# Configurable response templates to make the agent tool-agnostic
|
| 3 |
+
|
| 4 |
+
# Success response templates
|
| 5 |
+
success_responses:
|
| 6 |
+
data_extraction_completed:
|
| 7 |
+
template: "✅ **{operation_name} Completata**\n"
|
| 8 |
+
variables:
|
| 9 |
+
operation_name: "str" # Will be filled dynamically based on tool
|
| 10 |
+
|
| 11 |
+
omirl_data_completed:
|
| 12 |
+
template: "🌊 **Estrazione Dati OMIRL Completata**\n"
|
| 13 |
+
condition: "tool_name_contains_omirl"
|
| 14 |
+
|
| 15 |
+
general_operation_completed:
|
| 16 |
+
template: "✅ **Operazione Completata**\n"
|
| 17 |
+
condition: "default"
|
| 18 |
+
|
| 19 |
+
# Help and guidance templates
|
| 20 |
+
help_responses:
|
| 21 |
+
general_help:
|
| 22 |
+
header: "ℹ️ **Assistente Operazioni - Guida**\n"
|
| 23 |
+
capabilities_intro: "Posso aiutarti con:"
|
| 24 |
+
capabilities:
|
| 25 |
+
- "• **{capability_category}**: {capability_description}"
|
| 26 |
+
examples_intro: "\n**Esempi di richieste:**"
|
| 27 |
+
examples:
|
| 28 |
+
- "• '{example_text}'"
|
| 29 |
+
|
| 30 |
+
capabilities_by_tool:
|
| 31 |
+
omirl_tool:
|
| 32 |
+
category: "Dati meteorologici OMIRL"
|
| 33 |
+
description: "estrazione stazioni meteo della Liguria"
|
| 34 |
+
filters: "tipo sensore, provincia, comune"
|
| 35 |
+
examples:
|
| 36 |
+
- "Mostra precipitazioni a Genova"
|
| 37 |
+
- "Temperatura a La Spezia"
|
| 38 |
+
- "Stazioni meteo in provincia di Savona"
|
| 39 |
+
|
| 40 |
+
rag_pipeline:
|
| 41 |
+
category: "Procedure di emergenza"
|
| 42 |
+
description: "piani di protezione civile e protocolli"
|
| 43 |
+
filters: "tipo emergenza, provincia, livello gravità"
|
| 44 |
+
examples:
|
| 45 |
+
- "Procedura evacuazione frane"
|
| 46 |
+
- "Piano emergenza alluvioni"
|
| 47 |
+
- "Protocolli protezione civile"
|
| 48 |
+
|
| 49 |
+
# Error response templates
|
| 50 |
+
error_responses:
|
| 51 |
+
processing_error:
|
| 52 |
+
template: "⚠️ **Errore nell'Elaborazione**\n"
|
| 53 |
+
|
| 54 |
+
geographic_rejection:
|
| 55 |
+
template: "⚠️ **Richiesta Non Supportata**\n{rejection_reason}\n**Nota:** Posso fornire dati solo per la regione {supported_region}.\n**{geographic_scope_label}:** {supported_areas}"
|
| 56 |
+
variables:
|
| 57 |
+
rejection_reason: "str"
|
| 58 |
+
supported_region: "str"
|
| 59 |
+
geographic_scope_label: "str" # "Province supportate" or "Zone supportate"
|
| 60 |
+
supported_areas: "list"
|
| 61 |
+
|
| 62 |
+
unrecognized_request:
|
| 63 |
+
template: "❓ **Richiesta Non Riconosciuta**\n"
|
| 64 |
+
fallback_message: "Non ho capito cosa vuoi che faccia."
|
| 65 |
+
suggestions_intro: "Prova con:"
|
| 66 |
+
fallback_examples:
|
| 67 |
+
- "'{example}' per {description}"
|
| 68 |
+
help_hint: "'aiuto' per vedere tutte le funzioni"
|
| 69 |
+
|
| 70 |
+
# Clarification templates
|
| 71 |
+
clarification_responses:
|
| 72 |
+
geographic_clarification:
|
| 73 |
+
template: "❓ **Specifica meglio:** {clarification_request}"
|
| 74 |
+
|
| 75 |
+
parameter_clarification:
|
| 76 |
+
template: "❓ **Informazioni aggiuntive necessarie:** {missing_parameters}"
|
| 77 |
+
|
| 78 |
+
# Artifact and source formatting
|
| 79 |
+
result_formatting:
|
| 80 |
+
artifacts:
|
| 81 |
+
single: "\n📄 **File generato:** {artifact_name}"
|
| 82 |
+
multiple: "\n📄 **File generati:** {count}\n{artifact_list}"
|
| 83 |
+
list_item: " • {artifact_name}"
|
| 84 |
+
max_displayed: 3
|
| 85 |
+
overflow_message: " • ... e altri {remaining_count} file"
|
| 86 |
+
|
| 87 |
+
sources:
|
| 88 |
+
intro: "\n🔗 **Fonti dati:**"
|
| 89 |
+
list_item: " • {source_url}"
|
| 90 |
+
|
| 91 |
+
warnings:
|
| 92 |
+
intro: "\n⚠️ **Avvisi:** {count}"
|
| 93 |
+
list_item: " • {warning_text}"
|
| 94 |
+
max_displayed: 2
|
| 95 |
+
|
| 96 |
+
# Multi-language support (future)
|
| 97 |
+
languages:
|
| 98 |
+
default: "it"
|
| 99 |
+
supported: ["it", "en"]
|
| 100 |
+
|
| 101 |
+
translations:
|
| 102 |
+
en:
|
| 103 |
+
success_responses:
|
| 104 |
+
general_operation_completed:
|
| 105 |
+
template: "✅ **Operation Completed**\n"
|
| 106 |
+
help_responses:
|
| 107 |
+
general_help:
|
| 108 |
+
header: "ℹ️ **Operations Assistant - Help**\n"
|
| 109 |
+
capabilities_intro: "I can help you with:"
|
| 110 |
+
|
| 111 |
+
# Dynamic content insertion rules
|
| 112 |
+
dynamic_content:
|
| 113 |
+
tool_specific_emojis:
|
| 114 |
+
omirl_tool: "🌊"
|
| 115 |
+
rag_pipeline: "📋"
|
| 116 |
+
default: "✅"
|
| 117 |
+
|
| 118 |
+
operation_names:
|
| 119 |
+
data_extraction: "Estrazione Dati"
|
| 120 |
+
emergency_procedures: "Procedure di Emergenza"
|
| 121 |
+
monitoring: "Monitoraggio"
|
| 122 |
+
analysis: "Analisi"
|
| 123 |
+
|
| 124 |
+
geographic_labels:
|
| 125 |
+
provinces: "Province supportate"
|
| 126 |
+
zones: "Zone supportate"
|
| 127 |
+
municipalities: "Comuni supportati"
|
agent/nodes.py
CHANGED
|
@@ -10,13 +10,14 @@ Purpose:
|
|
| 10 |
- Define workflow nodes for the LangGraph agent
|
| 11 |
- Integrate LLM router for intelligent request routing
|
| 12 |
- Execute tools and process results
|
| 13 |
-
- Generate user-facing responses
|
| 14 |
- Manage error handling and recovery
|
| 15 |
|
| 16 |
Dependencies:
|
| 17 |
- state.py: Agent state definitions
|
| 18 |
- registry.py: Tool registry and discovery
|
| 19 |
- llm_router_node.py: LLM-based routing logic
|
|
|
|
| 20 |
- asyncio: For async tool execution
|
| 21 |
|
| 22 |
Used by:
|
|
@@ -32,6 +33,7 @@ from .state import AgentState, ToolCall, ToolResult, update_processing_status, a
|
|
| 32 |
from .registry import get_tool_registry, get_tool_by_name, validate_tool_parameters
|
| 33 |
from .llm_router_node import llm_router_node
|
| 34 |
from .llm_client import LLMClient
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
async def llm_routing_node(state: AgentState) -> AgentState:
|
|
@@ -318,8 +320,8 @@ async def response_generation_node(state: AgentState) -> AgentState:
|
|
| 318 |
"""
|
| 319 |
Generate final response based on tool results and LLM router status
|
| 320 |
|
| 321 |
-
This node creates the final user-facing response
|
| 322 |
-
|
| 323 |
|
| 324 |
Args:
|
| 325 |
state: Current agent state with tool results
|
|
@@ -330,111 +332,68 @@ async def response_generation_node(state: AgentState) -> AgentState:
|
|
| 330 |
|
| 331 |
print(f"📝 Generating user response...")
|
| 332 |
|
|
|
|
|
|
|
|
|
|
| 333 |
state = update_processing_status(state, "generating_response", "Creating user response")
|
| 334 |
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
processing_status = state.get("processing_status", "unknown")
|
| 338 |
|
| 339 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
-
#
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
# Generate response header based on tool type
|
| 347 |
-
if any("omirl" in r.tool_name.lower() for r in successful_results):
|
| 348 |
-
response_parts.append("🌊 **Estrazione Dati OMIRL Completata**\n")
|
| 349 |
-
else:
|
| 350 |
-
response_parts.append("✅ **Operazione Completata**\n")
|
| 351 |
-
|
| 352 |
-
# Add tool result summaries
|
| 353 |
-
for result in successful_results:
|
| 354 |
-
response_parts.append(result.summary_text)
|
| 355 |
-
|
| 356 |
-
# Add LLM insights if available (Phase 4 enhancement)
|
| 357 |
-
llm_insights = result.metadata.get("llm_insights")
|
| 358 |
-
if llm_insights and len(llm_insights.strip()) > 20:
|
| 359 |
-
response_parts.append(f"\n🧠 **Analisi Intelligente:**")
|
| 360 |
-
response_parts.append(llm_insights)
|
| 361 |
-
|
| 362 |
-
# Add artifact information
|
| 363 |
-
if result.artifacts:
|
| 364 |
-
response_parts.append(f"\n📄 **File generati:** {len(result.artifacts)}")
|
| 365 |
-
for artifact in result.artifacts[:3]: # Show max 3 artifacts
|
| 366 |
-
response_parts.append(f" • {artifact}")
|
| 367 |
-
if len(result.artifacts) > 3:
|
| 368 |
-
response_parts.append(f" • ... e altri {len(result.artifacts) - 3} file")
|
| 369 |
-
|
| 370 |
-
# Add warnings if any
|
| 371 |
-
if result.warnings:
|
| 372 |
-
response_parts.append(f"\n⚠️ **Avvisi:** {len(result.warnings)}")
|
| 373 |
-
for warning in result.warnings[:2]: # Show max 2 warnings
|
| 374 |
-
response_parts.append(f" • {warning}")
|
| 375 |
-
|
| 376 |
-
# Add data sources
|
| 377 |
-
all_sources = []
|
| 378 |
-
for result in successful_results:
|
| 379 |
-
all_sources.extend(result.sources)
|
| 380 |
-
|
| 381 |
-
if all_sources:
|
| 382 |
-
unique_sources = list(set(all_sources))
|
| 383 |
-
response_parts.append(f"\n🔗 **Fonti dati:**")
|
| 384 |
-
for source in unique_sources:
|
| 385 |
-
response_parts.append(f" • {source}")
|
| 386 |
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
| 388 |
elif processing_status in ["needs_clarification", "help_requested"]:
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
response_parts.append("• **Dati meteorologici OMIRL**: estrazione stazioni meteo della Liguria")
|
| 392 |
-
response_parts.append("• **Filtri disponibili**: tipo sensore, provincia, comune")
|
| 393 |
-
response_parts.append("• **Sensori supportati**: Precipitazione, Temperatura, Vento, ecc.")
|
| 394 |
-
response_parts.append("\n**Esempi di richieste:**")
|
| 395 |
-
response_parts.append("• 'Mostra precipitazioni a Genova'")
|
| 396 |
-
response_parts.append("• 'Temperatura a La Spezia'")
|
| 397 |
-
response_parts.append("• 'Stazioni meteo in provincia di Savona'")
|
| 398 |
|
| 399 |
-
# Add clarification request if available
|
| 400 |
-
clarification_request = state.get("clarification_request")
|
| 401 |
-
if clarification_request:
|
| 402 |
-
response_parts.append(f"\n❓ **Specifica meglio:** {clarification_request}")
|
| 403 |
-
|
| 404 |
-
# Handle geographic rejections
|
| 405 |
elif processing_status == "rejected":
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
response_parts.append(f"{rejection_reason}")
|
| 409 |
-
response_parts.append("\n**Nota:** Posso fornire dati solo per la regione Liguria.")
|
| 410 |
-
response_parts.append("**Province supportate:** Genova, Savona, Imperia, La Spezia")
|
| 411 |
-
|
| 412 |
-
# Handle errors
|
| 413 |
-
elif errors or any(not result.success for result in tool_results):
|
| 414 |
-
response_parts.append("⚠️ **Errore nell'Elaborazione**\n")
|
| 415 |
-
|
| 416 |
-
# Show tool errors
|
| 417 |
-
failed_results = [r for r in tool_results if not r.success]
|
| 418 |
-
for result in failed_results:
|
| 419 |
-
response_parts.append(f"• {result.summary_text}")
|
| 420 |
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
|
| 425 |
-
response_parts.append("\nℹ️ Prova a riformulare la richiesta o chiedi 'aiuto' per maggiori informazioni.")
|
| 426 |
-
|
| 427 |
-
# Fallback for unrecognized requests
|
| 428 |
else:
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
state["agent_response"] = final_response
|
| 439 |
|
| 440 |
state = update_processing_status(state, "completed", "Response generated successfully")
|
|
@@ -449,7 +408,7 @@ async def error_handling_node(state: AgentState) -> AgentState:
|
|
| 449 |
Handle errors and generate appropriate error responses
|
| 450 |
|
| 451 |
This node is called when errors occur in the workflow
|
| 452 |
-
and generates user-friendly error messages.
|
| 453 |
|
| 454 |
Args:
|
| 455 |
state: Current agent state with errors
|
|
@@ -468,25 +427,18 @@ async def error_handling_node(state: AgentState) -> AgentState:
|
|
| 468 |
# No errors to handle
|
| 469 |
return state
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
"• Controlla che la richiesta sia formulata correttamente",
|
| 484 |
-
"• Prova con parametri diversi (es. altra provincia o tipo sensore)",
|
| 485 |
-
"• Scrivi 'aiuto' per vedere esempi di richieste valide",
|
| 486 |
-
"• Riprova più tardi se il problema persiste"
|
| 487 |
-
])
|
| 488 |
-
|
| 489 |
-
state["agent_response"] = "\n".join(error_response_parts)
|
| 490 |
state = update_processing_status(state, "completed_with_errors", "Error response generated")
|
| 491 |
|
| 492 |
print(f"✅ Error response generated")
|
|
|
|
| 10 |
- Define workflow nodes for the LangGraph agent
|
| 11 |
- Integrate LLM router for intelligent request routing
|
| 12 |
- Execute tools and process results
|
| 13 |
+
- Generate user-facing responses using configurable templates
|
| 14 |
- Manage error handling and recovery
|
| 15 |
|
| 16 |
Dependencies:
|
| 17 |
- state.py: Agent state definitions
|
| 18 |
- registry.py: Tool registry and discovery
|
| 19 |
- llm_router_node.py: LLM-based routing logic
|
| 20 |
+
- response_handler.py: Configurable response generation
|
| 21 |
- asyncio: For async tool execution
|
| 22 |
|
| 23 |
Used by:
|
|
|
|
| 33 |
from .registry import get_tool_registry, get_tool_by_name, validate_tool_parameters
|
| 34 |
from .llm_router_node import llm_router_node
|
| 35 |
from .llm_client import LLMClient
|
| 36 |
+
from .response_handler import get_response_handler, ResponseContext
|
| 37 |
|
| 38 |
|
| 39 |
async def llm_routing_node(state: AgentState) -> AgentState:
|
|
|
|
| 320 |
"""
|
| 321 |
Generate final response based on tool results and LLM router status
|
| 322 |
|
| 323 |
+
This node creates the final user-facing response using configurable
|
| 324 |
+
templates to eliminate hard-coded response strings.
|
| 325 |
|
| 326 |
Args:
|
| 327 |
state: Current agent state with tool results
|
|
|
|
| 332 |
|
| 333 |
print(f"📝 Generating user response...")
|
| 334 |
|
| 335 |
+
# Capture processing status BEFORE updating it
|
| 336 |
+
original_processing_status = state.get("processing_status", "unknown")
|
| 337 |
+
|
| 338 |
state = update_processing_status(state, "generating_response", "Creating user response")
|
| 339 |
|
| 340 |
+
# Get response handler
|
| 341 |
+
response_handler = get_response_handler()
|
|
|
|
| 342 |
|
| 343 |
+
# Create response context
|
| 344 |
+
context = ResponseContext(
|
| 345 |
+
tool_results=state["tool_results"],
|
| 346 |
+
processing_status=original_processing_status, # Use original status
|
| 347 |
+
errors=state["errors"],
|
| 348 |
+
clarification_request=state.get("clarification_request"),
|
| 349 |
+
rejection_reason=state.get("rejection_reason")
|
| 350 |
+
)
|
| 351 |
|
| 352 |
+
# Generate appropriate response based on ORIGINAL processing status
|
| 353 |
+
processing_status = original_processing_status
|
| 354 |
+
|
| 355 |
+
# Check for successful tool execution first
|
| 356 |
+
has_successful_results = bool(state["tool_results"]) and any(result.success for result in state["tool_results"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
+
if has_successful_results:
|
| 359 |
+
# Successful tool execution
|
| 360 |
+
final_response = response_handler.generate_success_response(context)
|
| 361 |
+
|
| 362 |
elif processing_status in ["needs_clarification", "help_requested"]:
|
| 363 |
+
# Help or clarification requests
|
| 364 |
+
final_response = response_handler.generate_help_response(context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
elif processing_status == "rejected":
|
| 367 |
+
# Request rejected with explanation
|
| 368 |
+
final_response = response_handler.generate_rejection_response(context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
|
| 370 |
+
elif state["errors"] or (state["tool_results"] and any(not result.success for result in state["tool_results"])):
|
| 371 |
+
# Errors occurred
|
| 372 |
+
final_response = response_handler.generate_error_response(context)
|
| 373 |
|
|
|
|
|
|
|
|
|
|
| 374 |
else:
|
| 375 |
+
# Fallback for unrecognized requests - use template if available
|
| 376 |
+
unrecognized_config = response_handler.templates.get('error_responses', {}).get('unrecognized_request', {})
|
| 377 |
+
if isinstance(unrecognized_config, dict) and 'template' in unrecognized_config:
|
| 378 |
+
template = unrecognized_config['template']
|
| 379 |
+
fallback_message = unrecognized_config.get('fallback_message', 'Non ho capito cosa vuoi che faccia.')
|
| 380 |
+
suggestions_intro = unrecognized_config.get('suggestions_intro', 'Prova con:')
|
| 381 |
+
help_hint = unrecognized_config.get('help_hint', "'aiuto' per vedere tutte le funzioni")
|
| 382 |
+
|
| 383 |
+
final_response = template + fallback_message + "\n" + suggestions_intro + "\n"
|
| 384 |
+
final_response += "• 'Mostra precipitazioni a Genova'\n"
|
| 385 |
+
final_response += "• 'Temperatura in provincia di Savona'\n"
|
| 386 |
+
final_response += "• " + help_hint
|
| 387 |
+
else:
|
| 388 |
+
# Fallback when templates not available
|
| 389 |
+
final_response = "❓ **Richiesta Non Riconosciuta**\n"
|
| 390 |
+
final_response += "Non ho capito cosa vuoi che faccia.\n"
|
| 391 |
+
final_response += "Prova con:\n"
|
| 392 |
+
final_response += "• 'Mostra precipitazioni a Genova'\n"
|
| 393 |
+
final_response += "• 'Temperatura in provincia di Savona'\n"
|
| 394 |
+
final_response += "• 'aiuto' per vedere tutte le funzioni"
|
| 395 |
+
|
| 396 |
+
# Set final response
|
| 397 |
state["agent_response"] = final_response
|
| 398 |
|
| 399 |
state = update_processing_status(state, "completed", "Response generated successfully")
|
|
|
|
| 408 |
Handle errors and generate appropriate error responses
|
| 409 |
|
| 410 |
This node is called when errors occur in the workflow
|
| 411 |
+
and generates user-friendly error messages using configurable templates.
|
| 412 |
|
| 413 |
Args:
|
| 414 |
state: Current agent state with errors
|
|
|
|
| 427 |
# No errors to handle
|
| 428 |
return state
|
| 429 |
|
| 430 |
+
# Use response handler for error response generation
|
| 431 |
+
response_handler = get_response_handler()
|
| 432 |
+
|
| 433 |
+
context = ResponseContext(
|
| 434 |
+
tool_results=state["tool_results"],
|
| 435 |
+
processing_status="error",
|
| 436 |
+
errors=errors
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
error_response = response_handler.generate_error_response(context)
|
| 440 |
+
|
| 441 |
+
state["agent_response"] = error_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
state = update_processing_status(state, "completed_with_errors", "Error response generated")
|
| 443 |
|
| 444 |
print(f"✅ Error response generated")
|
agent/response_handler.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Response Template Handler
|
| 3 |
+
|
| 4 |
+
This module provides a configurable response generation system that eliminates
|
| 5 |
+
hard-coded response templates and makes the agent truly tool-agnostic.
|
| 6 |
+
|
| 7 |
+
Purpose:
|
| 8 |
+
- Load response templates from YAML configuration
|
| 9 |
+
- Generate responses dynamically based on tool results
|
| 10 |
+
- Support multiple languages and customizable templates
|
| 11 |
+
- Eliminate hard-coded response strings from agent code
|
| 12 |
+
|
| 13 |
+
Architecture:
|
| 14 |
+
- ResponseTemplateHandler: Main class for template management
|
| 15 |
+
- Template loading from agent/config/response_templates.yaml
|
| 16 |
+
- Dynamic variable substitution and content generation
|
| 17 |
+
- Tool-agnostic response formatting
|
| 18 |
+
|
| 19 |
+
Used by:
|
| 20 |
+
- nodes.py: Response generation node
|
| 21 |
+
- agent.py: Error response formatting
|
| 22 |
+
- Future modules requiring configurable responses
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import yaml
|
| 26 |
+
import logging
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Dict, Any, List, Optional, Union
|
| 29 |
+
from dataclasses import dataclass
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@dataclass
|
| 33 |
+
class ResponseContext:
|
| 34 |
+
"""Context information for response generation"""
|
| 35 |
+
tool_results: List[Any]
|
| 36 |
+
processing_status: str
|
| 37 |
+
errors: List[str]
|
| 38 |
+
clarification_request: Optional[str] = None
|
| 39 |
+
rejection_reason: Optional[str] = None
|
| 40 |
+
language: str = "it"
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class ResponseTemplateHandler:
|
| 44 |
+
"""
|
| 45 |
+
Handles loading and applying response templates for tool-agnostic responses
|
| 46 |
+
|
| 47 |
+
This class eliminates hard-coded response strings by loading configurable
|
| 48 |
+
templates from YAML and applying them based on context and tool results.
|
| 49 |
+
"""
|
| 50 |
+
|
| 51 |
+
def __init__(self, config_path: Optional[Path] = None):
|
| 52 |
+
"""
|
| 53 |
+
Initialize the response template handler
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
config_path: Path to response templates YAML file
|
| 57 |
+
"""
|
| 58 |
+
self.config_path = config_path or Path(__file__).parent / "config" / "response_templates.yaml"
|
| 59 |
+
self.templates = {}
|
| 60 |
+
self.logger = logging.getLogger(__name__)
|
| 61 |
+
|
| 62 |
+
self._load_templates()
|
| 63 |
+
|
| 64 |
+
def _load_templates(self):
|
| 65 |
+
"""Load response templates from YAML configuration"""
|
| 66 |
+
try:
|
| 67 |
+
if not self.config_path.exists():
|
| 68 |
+
self.logger.warning(f"Response templates not found: {self.config_path}")
|
| 69 |
+
self._use_fallback_templates()
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
| 73 |
+
self.templates = yaml.safe_load(f)
|
| 74 |
+
|
| 75 |
+
self.logger.info(f"Response templates loaded from {self.config_path}")
|
| 76 |
+
|
| 77 |
+
except Exception as e:
|
| 78 |
+
self.logger.error(f"Failed to load response templates: {e}")
|
| 79 |
+
self._use_fallback_templates()
|
| 80 |
+
|
| 81 |
+
def _use_fallback_templates(self):
|
| 82 |
+
"""Use minimal fallback templates if loading fails"""
|
| 83 |
+
self.templates = {
|
| 84 |
+
'success_responses': {
|
| 85 |
+
'general_operation_completed': {'template': '✅ **Operazione Completata**\n'}
|
| 86 |
+
},
|
| 87 |
+
'error_responses': {
|
| 88 |
+
'processing_error': {'template': '⚠️ **Errore nell\'Elaborazione**\n'}
|
| 89 |
+
},
|
| 90 |
+
'dynamic_content': {
|
| 91 |
+
'tool_specific_emojis': {'default': '✅'},
|
| 92 |
+
'operation_names': {'data_extraction': 'Estrazione Dati'}
|
| 93 |
+
}
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
self.logger.warning("Using fallback response templates")
|
| 97 |
+
|
| 98 |
+
def generate_success_response(self, context: ResponseContext) -> str:
|
| 99 |
+
"""
|
| 100 |
+
Generate success response based on tool results and context
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
context: Response generation context
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
Formatted success response string
|
| 107 |
+
"""
|
| 108 |
+
response_parts = []
|
| 109 |
+
|
| 110 |
+
# Get successful results
|
| 111 |
+
successful_results = [r for r in context.tool_results if r.success]
|
| 112 |
+
|
| 113 |
+
if not successful_results:
|
| 114 |
+
return self._generate_error_response(context)
|
| 115 |
+
|
| 116 |
+
# Generate header based on tool type
|
| 117 |
+
header = self._get_success_header(successful_results)
|
| 118 |
+
response_parts.append(header)
|
| 119 |
+
|
| 120 |
+
# Add tool result summaries
|
| 121 |
+
for result in successful_results:
|
| 122 |
+
response_parts.append(result.summary_text)
|
| 123 |
+
|
| 124 |
+
# Add LLM insights if available
|
| 125 |
+
llm_insights = result.metadata.get("llm_insights")
|
| 126 |
+
if llm_insights and len(llm_insights.strip()) > 20:
|
| 127 |
+
insights_header = self._get_template_text('result_formatting', 'insights_header', "🧠 **Analisi Intelligente:**")
|
| 128 |
+
response_parts.append(f"\n{insights_header}")
|
| 129 |
+
response_parts.append(llm_insights)
|
| 130 |
+
|
| 131 |
+
# Add artifact information
|
| 132 |
+
if result.artifacts:
|
| 133 |
+
artifacts_section = self._format_artifacts(result.artifacts)
|
| 134 |
+
response_parts.append(artifacts_section)
|
| 135 |
+
|
| 136 |
+
# Add warnings if any
|
| 137 |
+
if result.warnings:
|
| 138 |
+
warnings_section = self._format_warnings(result.warnings)
|
| 139 |
+
response_parts.append(warnings_section)
|
| 140 |
+
|
| 141 |
+
# Add data sources
|
| 142 |
+
all_sources = []
|
| 143 |
+
for result in successful_results:
|
| 144 |
+
all_sources.extend(result.sources)
|
| 145 |
+
|
| 146 |
+
if all_sources:
|
| 147 |
+
sources_section = self._format_sources(list(set(all_sources)))
|
| 148 |
+
response_parts.append(sources_section)
|
| 149 |
+
|
| 150 |
+
return "\n".join(response_parts)
|
| 151 |
+
|
| 152 |
+
def generate_help_response(self, context: ResponseContext) -> str:
|
| 153 |
+
"""Generate help/guidance response"""
|
| 154 |
+
help_config = self.templates.get('help_responses', {}).get('general_help', {})
|
| 155 |
+
|
| 156 |
+
response_parts = []
|
| 157 |
+
|
| 158 |
+
# Header
|
| 159 |
+
header = help_config.get('header', 'ℹ️ **Assistente Operazioni - Guida**\n')
|
| 160 |
+
response_parts.append(header)
|
| 161 |
+
|
| 162 |
+
# Capabilities intro
|
| 163 |
+
capabilities_intro = help_config.get('capabilities_intro', 'Posso aiutarti con:')
|
| 164 |
+
response_parts.append(capabilities_intro)
|
| 165 |
+
|
| 166 |
+
# Add capabilities from configuration
|
| 167 |
+
capabilities_by_tool = self.templates.get('help_responses', {}).get('capabilities_by_tool', {})
|
| 168 |
+
for tool_name, tool_caps in capabilities_by_tool.items():
|
| 169 |
+
capability_line = f"• **{tool_caps.get('category', tool_name)}**: {tool_caps.get('description', '')}"
|
| 170 |
+
response_parts.append(capability_line)
|
| 171 |
+
|
| 172 |
+
# Examples intro
|
| 173 |
+
examples_intro = help_config.get('examples_intro', '\n**Esempi di richieste:**')
|
| 174 |
+
response_parts.append(examples_intro)
|
| 175 |
+
|
| 176 |
+
# Add examples from configuration
|
| 177 |
+
for tool_name, tool_caps in capabilities_by_tool.items():
|
| 178 |
+
examples = tool_caps.get('examples', [])
|
| 179 |
+
for example in examples[:2]: # Limit examples
|
| 180 |
+
response_parts.append(f"• '{example}'")
|
| 181 |
+
|
| 182 |
+
# Add clarification if available
|
| 183 |
+
if context.clarification_request:
|
| 184 |
+
clarification_config = self.templates.get('clarification_responses', {}).get('geographic_clarification', {})
|
| 185 |
+
if isinstance(clarification_config, dict):
|
| 186 |
+
clarification_template = clarification_config.get('template', "❓ **Specifica meglio:** {clarification_request}")
|
| 187 |
+
else:
|
| 188 |
+
clarification_template = "❓ **Specifica meglio:** {clarification_request}"
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
clarification_text = clarification_template.format(clarification_request=context.clarification_request)
|
| 192 |
+
response_parts.append(f"\n{clarification_text}")
|
| 193 |
+
except (KeyError, ValueError) as e:
|
| 194 |
+
self.logger.warning(f"Clarification template error: {e}")
|
| 195 |
+
response_parts.append(f"\n❓ **Specifica meglio:** {context.clarification_request}")
|
| 196 |
+
|
| 197 |
+
return "\n".join(response_parts)
|
| 198 |
+
|
| 199 |
+
def generate_error_response(self, context: ResponseContext) -> str:
|
| 200 |
+
"""Generate error response based on context"""
|
| 201 |
+
return self._generate_error_response(context)
|
| 202 |
+
|
| 203 |
+
def generate_rejection_response(self, context: ResponseContext) -> str:
|
| 204 |
+
"""Generate rejection response for unsupported requests"""
|
| 205 |
+
if not context.rejection_reason:
|
| 206 |
+
return self._generate_error_response(context)
|
| 207 |
+
|
| 208 |
+
# Get geographic rejection template
|
| 209 |
+
rejection_config = self.templates.get('error_responses', {}).get('geographic_rejection', {})
|
| 210 |
+
template = rejection_config.get('template',
|
| 211 |
+
"⚠️ **Richiesta Non Supportata**\n{rejection_reason}\n**Nota:** Posso fornire dati solo per la regione {supported_region}.")
|
| 212 |
+
|
| 213 |
+
# Default values - these could come from geographic config
|
| 214 |
+
variables = {
|
| 215 |
+
'rejection_reason': context.rejection_reason,
|
| 216 |
+
'supported_region': 'Liguria',
|
| 217 |
+
'geographic_scope_label': 'Province supportate',
|
| 218 |
+
'supported_areas': 'Genova, Savona, Imperia, La Spezia'
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
return template.format(**variables)
|
| 223 |
+
except KeyError as e:
|
| 224 |
+
self.logger.warning(f"Template variable missing: {e}")
|
| 225 |
+
return f"⚠️ **Richiesta Non Supportata**\n{context.rejection_reason}"
|
| 226 |
+
|
| 227 |
+
def _get_success_header(self, successful_results: List[Any]) -> str:
|
| 228 |
+
"""Get appropriate success header based on tool results"""
|
| 229 |
+
# Check if this is OMIRL-related
|
| 230 |
+
if any("omirl" in r.tool_name.lower() for r in successful_results):
|
| 231 |
+
template_config = self.templates.get('success_responses', {}).get('omirl_data_completed', {})
|
| 232 |
+
return template_config.get('template', '🌊 **Estrazione Dati OMIRL Completata**\n')
|
| 233 |
+
|
| 234 |
+
# Default success header
|
| 235 |
+
template_config = self.templates.get('success_responses', {}).get('general_operation_completed', {})
|
| 236 |
+
return template_config.get('template', '✅ **Operazione Completata**\n')
|
| 237 |
+
|
| 238 |
+
def _generate_error_response(self, context: ResponseContext) -> str:
|
| 239 |
+
"""Generate error response"""
|
| 240 |
+
response_parts = []
|
| 241 |
+
|
| 242 |
+
# Get error header template, handling both string and dict formats
|
| 243 |
+
error_config = self.templates.get('error_responses', {}).get('processing_error', {})
|
| 244 |
+
if isinstance(error_config, dict):
|
| 245 |
+
error_header = error_config.get('template', '⚠️ **Errore nell\'Elaborazione**\n')
|
| 246 |
+
else:
|
| 247 |
+
error_header = str(error_config)
|
| 248 |
+
|
| 249 |
+
response_parts.append(error_header)
|
| 250 |
+
|
| 251 |
+
# Show tool errors
|
| 252 |
+
failed_results = [r for r in context.tool_results if not r.success]
|
| 253 |
+
for result in failed_results:
|
| 254 |
+
response_parts.append(f"• {result.summary_text}")
|
| 255 |
+
|
| 256 |
+
# Show general errors
|
| 257 |
+
for error in context.errors:
|
| 258 |
+
response_parts.append(f"• {error}")
|
| 259 |
+
|
| 260 |
+
# Add helpful suggestion
|
| 261 |
+
response_parts.append("\nℹ️ Prova a riformulare la richiesta o chiedi 'aiuto' per maggiori informazioni.")
|
| 262 |
+
|
| 263 |
+
return "\n".join(response_parts)
|
| 264 |
+
|
| 265 |
+
def _format_artifacts(self, artifacts: List[str]) -> str:
|
| 266 |
+
"""Format artifacts section using configuration"""
|
| 267 |
+
artifact_config = self.templates.get('result_formatting', {}).get('artifacts', {})
|
| 268 |
+
|
| 269 |
+
if len(artifacts) == 1:
|
| 270 |
+
template = artifact_config.get('single', '\n📄 **File generato:** {artifact_name}')
|
| 271 |
+
return template.format(artifact_name=artifacts[0])
|
| 272 |
+
|
| 273 |
+
# Multiple artifacts
|
| 274 |
+
max_displayed = artifact_config.get('max_displayed', 3)
|
| 275 |
+
header = artifact_config.get('multiple', '\n📄 **File generati:** {count}\n{artifact_list}')
|
| 276 |
+
list_item_template = artifact_config.get('list_item', ' • {artifact_name}')
|
| 277 |
+
|
| 278 |
+
displayed_artifacts = artifacts[:max_displayed]
|
| 279 |
+
artifact_list = '\n'.join([list_item_template.format(artifact_name=artifact) for artifact in displayed_artifacts])
|
| 280 |
+
|
| 281 |
+
if len(artifacts) > max_displayed:
|
| 282 |
+
remaining_count = len(artifacts) - max_displayed
|
| 283 |
+
overflow_template = artifact_config.get('overflow_message', ' • ... e altri {remaining_count} file')
|
| 284 |
+
artifact_list += '\n' + overflow_template.format(remaining_count=remaining_count)
|
| 285 |
+
|
| 286 |
+
return header.format(count=len(artifacts), artifact_list=artifact_list)
|
| 287 |
+
|
| 288 |
+
def _format_warnings(self, warnings: List[str]) -> str:
|
| 289 |
+
"""Format warnings section using configuration"""
|
| 290 |
+
warning_config = self.templates.get('result_formatting', {}).get('warnings', {})
|
| 291 |
+
max_displayed = warning_config.get('max_displayed', 2)
|
| 292 |
+
|
| 293 |
+
intro_template = warning_config.get('intro', '\n⚠️ **Avvisi:** {count}')
|
| 294 |
+
list_item_template = warning_config.get('list_item', ' • {warning_text}')
|
| 295 |
+
|
| 296 |
+
intro = intro_template.format(count=len(warnings))
|
| 297 |
+
warning_list = '\n'.join([
|
| 298 |
+
list_item_template.format(warning_text=warning)
|
| 299 |
+
for warning in warnings[:max_displayed]
|
| 300 |
+
])
|
| 301 |
+
|
| 302 |
+
return intro + '\n' + warning_list
|
| 303 |
+
|
| 304 |
+
def _format_sources(self, sources: List[str]) -> str:
|
| 305 |
+
"""Format sources section using configuration"""
|
| 306 |
+
source_config = self.templates.get('result_formatting', {}).get('sources', {})
|
| 307 |
+
|
| 308 |
+
intro = source_config.get('intro', '\n🔗 **Fonti dati:**')
|
| 309 |
+
list_item_template = source_config.get('list_item', ' • {source_url}')
|
| 310 |
+
|
| 311 |
+
source_list = '\n'.join([
|
| 312 |
+
list_item_template.format(source_url=source)
|
| 313 |
+
for source in sources
|
| 314 |
+
])
|
| 315 |
+
|
| 316 |
+
return intro + '\n' + source_list
|
| 317 |
+
|
| 318 |
+
def _get_template_text(self, section: str, key: str, default: str) -> str:
|
| 319 |
+
"""Get template text from configuration with fallback"""
|
| 320 |
+
try:
|
| 321 |
+
return self.templates.get(section, {}).get(key, default)
|
| 322 |
+
except (KeyError, AttributeError):
|
| 323 |
+
return default
|
| 324 |
+
|
| 325 |
+
def get_tool_emoji(self, tool_name: str) -> str:
|
| 326 |
+
"""Get emoji for specific tool"""
|
| 327 |
+
tool_emojis = self.templates.get('dynamic_content', {}).get('tool_specific_emojis', {})
|
| 328 |
+
return tool_emojis.get(tool_name, tool_emojis.get('default', '✅'))
|
| 329 |
+
|
| 330 |
+
def get_operation_name(self, operation_type: str) -> str:
|
| 331 |
+
"""Get localized operation name"""
|
| 332 |
+
operation_names = self.templates.get('dynamic_content', {}).get('operation_names', {})
|
| 333 |
+
return operation_names.get(operation_type, operation_type.title())
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
# Global instance for easy import
|
| 337 |
+
_response_handler = None
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def get_response_handler() -> ResponseTemplateHandler:
|
| 341 |
+
"""Get or create global response template handler instance"""
|
| 342 |
+
global _response_handler
|
| 343 |
+
if _response_handler is None:
|
| 344 |
+
_response_handler = ResponseTemplateHandler()
|
| 345 |
+
return _response_handler
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def reset_response_handler():
|
| 349 |
+
"""Reset global response handler (useful for testing)"""
|
| 350 |
+
global _response_handler
|
| 351 |
+
_response_handler = None
|
agent/tests/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Full Flow Testing Suite
|
| 2 |
+
|
| 3 |
+
This directory contains utilities for testing the complete application flow from user query to output generation. These tools are designed to help you understand how configuration changes affect the agent's behavior.
|
| 4 |
+
|
| 5 |
+
## Test Files
|
| 6 |
+
|
| 7 |
+
### 1. `test_full_flow.py` - Interactive Full Flow Test
|
| 8 |
+
|
| 9 |
+
The main interactive test that executes the complete application flow step by step.
|
| 10 |
+
|
| 11 |
+
**Usage:**
|
| 12 |
+
```bash
|
| 13 |
+
cd /home/jeanbaptistebove/projects/operations
|
| 14 |
+
python agent/tests/test_full_flow.py
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
**What it does:**
|
| 18 |
+
- Asks you for a user query (avoiding the need to run Streamlit)
|
| 19 |
+
- Executes the complete flow with detailed logging at each step
|
| 20 |
+
- Shows configuration impact on agent behavior
|
| 21 |
+
- **Analyzes failures and recommends configuration changes**
|
| 22 |
+
|
| 23 |
+
**Steps displayed:**
|
| 24 |
+
1. **Configuration Status** - Current LLM provider, model, tools available
|
| 25 |
+
2. **Query Validation** - Intent detection, parameter extraction, validation results
|
| 26 |
+
3. **Tool Selection** - Which tool was selected and why
|
| 27 |
+
4. **Task Execution** - Task execution with parameters, results, and artifacts
|
| 28 |
+
5. **LLM Response Generation** - Response generation process and preview
|
| 29 |
+
6. **Final Output** - Complete application output as user would see it
|
| 30 |
+
7. **Configuration Recommendations** - Smart analysis of failures with specific config fixes
|
| 31 |
+
|
| 32 |
+
### 2. `config_viewer.py` - Configuration State Viewer
|
| 33 |
+
|
| 34 |
+
Quick utility to view current configuration state before testing.
|
| 35 |
+
|
| 36 |
+
**Usage:**
|
| 37 |
+
```bash
|
| 38 |
+
cd /home/jeanbaptistebove/projects/operations
|
| 39 |
+
python agent/tests/config_viewer.py
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
**What it shows:**
|
| 43 |
+
- LLM Router configuration (provider, model, fallback settings)
|
| 44 |
+
- Tool Registry (available tools and their status)
|
| 45 |
+
- Geography configuration (supported regions and provinces)
|
| 46 |
+
- OMIRL task configuration (valid tasks and requirements)
|
| 47 |
+
- Validation rules (validation settings and rules)
|
| 48 |
+
|
| 49 |
+
## Typical Testing Workflow
|
| 50 |
+
|
| 51 |
+
1. **Before making configuration changes:**
|
| 52 |
+
```bash
|
| 53 |
+
python agent/tests/config_viewer.py
|
| 54 |
+
```
|
| 55 |
+
This shows your current configuration state.
|
| 56 |
+
|
| 57 |
+
2. **Make your configuration changes** in the relevant YAML files.
|
| 58 |
+
|
| 59 |
+
3. **Test the impact:**
|
| 60 |
+
```bash
|
| 61 |
+
python agent/tests/test_full_flow.py
|
| 62 |
+
```
|
| 63 |
+
Enter test queries to see how the changes affect behavior.
|
| 64 |
+
|
| 65 |
+
4. **Compare results** with previous behavior to validate your changes.
|
| 66 |
+
|
| 67 |
+
## Smart Configuration Recommendations
|
| 68 |
+
|
| 69 |
+
The test suite now includes intelligent analysis that recommends specific configuration changes based on failures:
|
| 70 |
+
|
| 71 |
+
### Types of Recommendations
|
| 72 |
+
|
| 73 |
+
**High Priority (🔥):**
|
| 74 |
+
- Geographic validation failures → Update `geography.yaml`
|
| 75 |
+
- Missing sensor types → Update `validation_rules.yaml`
|
| 76 |
+
- Tool selection failures → Update `tool_registry.yaml`
|
| 77 |
+
- API/LLM quota issues → Update `llm_router_config.yaml`
|
| 78 |
+
|
| 79 |
+
**Medium Priority:**
|
| 80 |
+
- Performance issues → Caching and timeout configurations
|
| 81 |
+
- LLM parameter tuning → Temperature and token settings
|
| 82 |
+
|
| 83 |
+
**Low Priority:**
|
| 84 |
+
- Optimization suggestions → Enable additional features
|
| 85 |
+
|
| 86 |
+
### Example Recommendation Output
|
| 87 |
+
|
| 88 |
+
```
|
| 89 |
+
📋 CONFIGURATION RECOMMENDATIONS
|
| 90 |
+
============================================================
|
| 91 |
+
Found 2 potential improvements:
|
| 92 |
+
|
| 93 |
+
🔥 HIGH PRIORITY
|
| 94 |
+
----------------------------------------
|
| 95 |
+
|
| 96 |
+
1. **Geographic validation failed**
|
| 97 |
+
📁 File: agent/config/geography.yaml
|
| 98 |
+
💡 Fix: Add missing provinces or regions to the supported_regions section
|
| 99 |
+
📝 Example: Add the requested province to the provinces list under the appropriate region
|
| 100 |
+
|
| 101 |
+
2. **LLM quota or rate limiting**
|
| 102 |
+
📁 File: agent/config/llm_router_config.yaml
|
| 103 |
+
💡 Fix: Enable fallback provider or change primary provider
|
| 104 |
+
📝 Example: Set fallback.enabled: true or change llm.provider to a different option
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
## Example Test Queries
|
| 108 |
+
|
| 109 |
+
Here are some example queries you can use to test different aspects:
|
| 110 |
+
|
| 111 |
+
### OMIRL Data Extraction
|
| 112 |
+
- `"Mostra precipitazioni a Genova"`
|
| 113 |
+
- `"Temperatura a La Spezia"`
|
| 114 |
+
- `"Stazioni meteo in provincia di Savona"`
|
| 115 |
+
- `"Livelli idrometrici zona A"`
|
| 116 |
+
|
| 117 |
+
### Geographic Validation
|
| 118 |
+
- `"Dati meteo a Milano"` (should be rejected - not in Liguria)
|
| 119 |
+
- `"Precipitazioni in Toscana"` (should be rejected)
|
| 120 |
+
|
| 121 |
+
### Parameter Validation
|
| 122 |
+
- `"Precipitazioni"` (missing location - should ask for clarification)
|
| 123 |
+
- `"Sensore XYZ a Genova"` (invalid sensor type)
|
| 124 |
+
|
| 125 |
+
### Error Scenarios
|
| 126 |
+
- Malformed queries
|
| 127 |
+
- Missing required parameters
|
| 128 |
+
- Invalid parameter values
|
| 129 |
+
|
| 130 |
+
## Configuration Files Tested
|
| 131 |
+
|
| 132 |
+
The test suite monitors these configuration files for changes:
|
| 133 |
+
|
| 134 |
+
- `agent/config/llm_router_config.yaml` - LLM settings
|
| 135 |
+
- `agent/config/tool_registry.yaml` - Available tools
|
| 136 |
+
- `agent/config/geography.yaml` - Geographic data
|
| 137 |
+
- `agent/config/response_templates.yaml` - Response formatting
|
| 138 |
+
- `tools/omirl/config/tasks.yaml` - OMIRL task definitions
|
| 139 |
+
- `tools/omirl/config/validation_rules.yaml` - Validation behavior
|
| 140 |
+
|
| 141 |
+
## Understanding the Output
|
| 142 |
+
|
| 143 |
+
Each step in the flow shows:
|
| 144 |
+
|
| 145 |
+
- ✅ Success indicators
|
| 146 |
+
- ❌ Failure indicators
|
| 147 |
+
- 🔧 Configuration information
|
| 148 |
+
- ��� User input
|
| 149 |
+
- 🛠️ Tool selection
|
| 150 |
+
- 📊 Data and parameters
|
| 151 |
+
- 🤖 LLM processing
|
| 152 |
+
- 📋 Final output
|
| 153 |
+
|
| 154 |
+
This detailed logging helps you understand exactly how configuration changes propagate through the system and affect the final user experience.
|
| 155 |
+
|
| 156 |
+
## Troubleshooting
|
| 157 |
+
|
| 158 |
+
If tests fail to run:
|
| 159 |
+
|
| 160 |
+
1. **Check Python environment** - Make sure you're in the correct conda environment
|
| 161 |
+
2. **Check dependencies** - Ensure all required packages are installed
|
| 162 |
+
3. **Check file paths** - Verify configuration files exist and are readable
|
| 163 |
+
4. **Check imports** - Make sure the project structure allows imports
|
| 164 |
+
|
| 165 |
+
Common issues:
|
| 166 |
+
- Missing configuration files
|
| 167 |
+
- Invalid YAML syntax in config files
|
| 168 |
+
- Missing environment variables (API keys, etc.)
|
| 169 |
+
- Python path issues
|
agent/tests/config_viewer.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Configuration Viewer for Full Flow Testing
|
| 4 |
+
==========================================
|
| 5 |
+
|
| 6 |
+
Quick utility to display current configuration state before running tests.
|
| 7 |
+
Useful for understanding how configuration changes will affect the agent.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python agent/tests/config_viewer.py
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import yaml
|
| 17 |
+
import json
|
| 18 |
+
from typing import Dict, Any
|
| 19 |
+
|
| 20 |
+
# Add project root to path
|
| 21 |
+
project_root = Path(__file__).parent.parent.parent
|
| 22 |
+
sys.path.insert(0, str(project_root))
|
| 23 |
+
|
| 24 |
+
def load_yaml_config(config_path: Path) -> Dict[str, Any]:
|
| 25 |
+
"""Load a YAML configuration file."""
|
| 26 |
+
try:
|
| 27 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 28 |
+
return yaml.safe_load(f) or {}
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return {'error': f"Failed to load {config_path}: {e}"}
|
| 31 |
+
|
| 32 |
+
def display_llm_config():
|
| 33 |
+
"""Display LLM router configuration."""
|
| 34 |
+
print("🤖 LLM ROUTER CONFIGURATION")
|
| 35 |
+
print("=" * 50)
|
| 36 |
+
|
| 37 |
+
config_path = project_root / "agent" / "config" / "llm_router_config.yaml"
|
| 38 |
+
config = load_yaml_config(config_path)
|
| 39 |
+
|
| 40 |
+
if 'error' in config:
|
| 41 |
+
print(f"❌ {config['error']}")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
# Main LLM settings
|
| 45 |
+
llm_config = config.get('llm', {})
|
| 46 |
+
print(f"Provider: {llm_config.get('provider', 'Not set')}")
|
| 47 |
+
print(f"Model: {llm_config.get('model', 'Not set')}")
|
| 48 |
+
|
| 49 |
+
# Fallback settings
|
| 50 |
+
fallback = llm_config.get('fallback', {})
|
| 51 |
+
if fallback.get('enabled'):
|
| 52 |
+
print(f"Fallback: {fallback.get('provider')} - {fallback.get('model')}")
|
| 53 |
+
else:
|
| 54 |
+
print("Fallback: Disabled")
|
| 55 |
+
|
| 56 |
+
# Available models
|
| 57 |
+
models = llm_config.get('models', {})
|
| 58 |
+
print(f"\nAvailable Models:")
|
| 59 |
+
for provider, provider_models in models.items():
|
| 60 |
+
print(f" {provider}:")
|
| 61 |
+
for model_type, model_name in provider_models.items():
|
| 62 |
+
print(f" {model_type}: {model_name}")
|
| 63 |
+
|
| 64 |
+
# Routing settings
|
| 65 |
+
routing = config.get('routing', {})
|
| 66 |
+
confidence = routing.get('confidence', {})
|
| 67 |
+
print(f"\nRouting Configuration:")
|
| 68 |
+
print(f" Execution threshold: {confidence.get('minimum_execution', 'Not set')}")
|
| 69 |
+
print(f" Clarification threshold: {confidence.get('clarification_threshold', 'Not set')}")
|
| 70 |
+
print(f" Primary language: {routing.get('language', {}).get('primary', 'Not set')}")
|
| 71 |
+
|
| 72 |
+
def display_tool_registry():
|
| 73 |
+
"""Display tool registry configuration."""
|
| 74 |
+
print("\n🛠️ TOOL REGISTRY CONFIGURATION")
|
| 75 |
+
print("=" * 50)
|
| 76 |
+
|
| 77 |
+
config_path = project_root / "agent" / "config" / "tool_registry.yaml"
|
| 78 |
+
config = load_yaml_config(config_path)
|
| 79 |
+
|
| 80 |
+
if 'error' in config:
|
| 81 |
+
print(f"❌ {config['error']}")
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
tools = config.get('tools', {})
|
| 85 |
+
print(f"Registered Tools: {len(tools)}")
|
| 86 |
+
|
| 87 |
+
for tool_name, tool_config in tools.items():
|
| 88 |
+
status = "✅ Enabled" if tool_config.get('enabled', False) else "❌ Disabled"
|
| 89 |
+
print(f"\n {tool_name} ({status})")
|
| 90 |
+
print(f" Description: {tool_config.get('description', 'No description')}")
|
| 91 |
+
print(f" Module: {tool_config.get('module_path', 'Not specified')}")
|
| 92 |
+
|
| 93 |
+
# Show supported tasks
|
| 94 |
+
tasks = tool_config.get('supported_tasks', [])
|
| 95 |
+
if tasks:
|
| 96 |
+
print(f" Supported tasks: {', '.join(tasks)}")
|
| 97 |
+
|
| 98 |
+
def display_geography_config():
|
| 99 |
+
"""Display geography configuration."""
|
| 100 |
+
print("\n🗺️ GEOGRAPHY CONFIGURATION")
|
| 101 |
+
print("=" * 50)
|
| 102 |
+
|
| 103 |
+
config_path = project_root / "agent" / "config" / "geography.yaml"
|
| 104 |
+
config = load_yaml_config(config_path)
|
| 105 |
+
|
| 106 |
+
if 'error' in config:
|
| 107 |
+
print(f"❌ {config['error']}")
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
# Supported regions
|
| 111 |
+
regions = config.get('supported_regions', {})
|
| 112 |
+
print(f"Supported Regions:")
|
| 113 |
+
for region_name, region_config in regions.items():
|
| 114 |
+
print(f" {region_name}:")
|
| 115 |
+
|
| 116 |
+
provinces = region_config.get('provinces', {})
|
| 117 |
+
if provinces:
|
| 118 |
+
print(f" Provinces: {len(provinces)}")
|
| 119 |
+
for prov_code, prov_name in list(provinces.items())[:3]:
|
| 120 |
+
print(f" {prov_code}: {prov_name}")
|
| 121 |
+
if len(provinces) > 3:
|
| 122 |
+
print(f" ... and {len(provinces) - 3} more")
|
| 123 |
+
|
| 124 |
+
def display_omirl_tasks():
|
| 125 |
+
"""Display OMIRL task configuration."""
|
| 126 |
+
print("\n🌊 OMIRL TASK CONFIGURATION")
|
| 127 |
+
print("=" * 50)
|
| 128 |
+
|
| 129 |
+
config_path = project_root / "tools" / "omirl" / "config" / "tasks.yaml"
|
| 130 |
+
config = load_yaml_config(config_path)
|
| 131 |
+
|
| 132 |
+
if 'error' in config:
|
| 133 |
+
print(f"❌ {config['error']}")
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
# Valid tasks
|
| 137 |
+
valid_tasks = config.get('valid_tasks', [])
|
| 138 |
+
print(f"Valid Tasks: {len(valid_tasks)}")
|
| 139 |
+
for task in valid_tasks:
|
| 140 |
+
print(f" • {task}")
|
| 141 |
+
|
| 142 |
+
# Task requirements
|
| 143 |
+
task_reqs = config.get('task_requirements', {})
|
| 144 |
+
print(f"\nTask Requirements:")
|
| 145 |
+
for task_name, requirements in task_reqs.items():
|
| 146 |
+
required_filters = requirements.get('required_filters', [])
|
| 147 |
+
optional_filters = requirements.get('optional_filters', [])
|
| 148 |
+
|
| 149 |
+
print(f" {task_name}:")
|
| 150 |
+
if required_filters:
|
| 151 |
+
print(f" Required: {', '.join(required_filters)}")
|
| 152 |
+
if optional_filters:
|
| 153 |
+
print(f" Optional: {', '.join(optional_filters)}")
|
| 154 |
+
print(f" Output: {requirements.get('primary_output', 'Not specified')}")
|
| 155 |
+
|
| 156 |
+
def display_validation_rules():
|
| 157 |
+
"""Display validation rules configuration."""
|
| 158 |
+
print("\n✅ VALIDATION RULES CONFIGURATION")
|
| 159 |
+
print("=" * 50)
|
| 160 |
+
|
| 161 |
+
config_path = project_root / "tools" / "omirl" / "config" / "validation_rules.yaml"
|
| 162 |
+
config = load_yaml_config(config_path)
|
| 163 |
+
|
| 164 |
+
if 'error' in config:
|
| 165 |
+
print(f"❌ {config['error']}")
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
# Validation settings
|
| 169 |
+
settings = config.get('validation_settings', {})
|
| 170 |
+
print(f"Validation Settings:")
|
| 171 |
+
print(f" Strict mode: {settings.get('strict_mode', 'Not set')}")
|
| 172 |
+
print(f" Auto-correct: {settings.get('auto_correct', 'Not set')}")
|
| 173 |
+
print(f" Provide suggestions: {settings.get('provide_suggestions', 'Not set')}")
|
| 174 |
+
|
| 175 |
+
# Rules summary
|
| 176 |
+
rules = config.get('rules', {})
|
| 177 |
+
print(f"\nValidation Rules: {len(rules)} configured")
|
| 178 |
+
for rule_name, rule_config in rules.items():
|
| 179 |
+
case_sensitive = rule_config.get('case_sensitive', True)
|
| 180 |
+
sensitivity = "Case sensitive" if case_sensitive else "Case insensitive"
|
| 181 |
+
print(f" {rule_name}: {sensitivity}")
|
| 182 |
+
|
| 183 |
+
def main():
|
| 184 |
+
"""Main entry point."""
|
| 185 |
+
print("📋 CONFIGURATION VIEWER")
|
| 186 |
+
print("=" * 60)
|
| 187 |
+
print("Current configuration state for full flow testing")
|
| 188 |
+
print("=" * 60)
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
display_llm_config()
|
| 192 |
+
display_tool_registry()
|
| 193 |
+
display_geography_config()
|
| 194 |
+
display_omirl_tasks()
|
| 195 |
+
display_validation_rules()
|
| 196 |
+
|
| 197 |
+
print("\n" + "=" * 60)
|
| 198 |
+
print("✅ Configuration review complete!")
|
| 199 |
+
print("💡 Use this information to understand how your config changes")
|
| 200 |
+
print(" will affect the agent behavior in the full flow test.")
|
| 201 |
+
print("=" * 60)
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print(f"❌ Error displaying configuration: {e}")
|
| 205 |
+
import traceback
|
| 206 |
+
traceback.print_exc()
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
agent/tests/test_full_flow.py
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Full Flow Integration Test
|
| 4 |
+
=========================
|
| 5 |
+
|
| 6 |
+
Interactive test to validate the complete application flow from user query to final output.
|
| 7 |
+
Useful for testing configuration changes and observing agent behavior.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python agent/tests/test_full_flow.py
|
| 11 |
+
|
| 12 |
+
The test will:
|
| 13 |
+
1. Ask for a user query
|
| 14 |
+
2. Execute the complete flow
|
| 15 |
+
3. Display detailed information at each step
|
| 16 |
+
4. Show how configuration changes affect behavior
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import sys
|
| 20 |
+
import os
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Dict, Any, Optional, List
|
| 23 |
+
import json
|
| 24 |
+
from datetime import datetime
|
| 25 |
+
|
| 26 |
+
# Add project root to path
|
| 27 |
+
project_root = Path(__file__).parent.parent.parent.parent
|
| 28 |
+
sys.path.insert(0, str(project_root))
|
| 29 |
+
|
| 30 |
+
from agent.agent import OperationsAgent
|
| 31 |
+
from agent.state import AgentState
|
| 32 |
+
from agent.llm_client import LLMClient
|
| 33 |
+
from agent.validator import RequestValidator
|
| 34 |
+
from agent.registry import ToolRegistry
|
| 35 |
+
|
| 36 |
+
class ConfigRecommender:
|
| 37 |
+
"""Analyzes test results and recommends configuration changes."""
|
| 38 |
+
|
| 39 |
+
def __init__(self):
|
| 40 |
+
self.recommendations = []
|
| 41 |
+
|
| 42 |
+
def analyze_flow_results(self, flow_data: Dict[str, Any]) -> List[str]:
|
| 43 |
+
"""Analyze complete flow results and generate recommendations."""
|
| 44 |
+
recommendations = []
|
| 45 |
+
|
| 46 |
+
# Analyze validation results
|
| 47 |
+
validation_result = flow_data.get('validation_result', {})
|
| 48 |
+
if not validation_result.get('valid', False):
|
| 49 |
+
recommendations.extend(self._analyze_validation_failure(validation_result))
|
| 50 |
+
|
| 51 |
+
# Analyze tool selection
|
| 52 |
+
tool_selection = flow_data.get('tool_selection', {})
|
| 53 |
+
if not tool_selection.get('success', False):
|
| 54 |
+
recommendations.extend(self._analyze_tool_selection_failure(tool_selection))
|
| 55 |
+
|
| 56 |
+
# Analyze task execution
|
| 57 |
+
task_execution = flow_data.get('task_execution', {})
|
| 58 |
+
if not task_execution.get('success', False):
|
| 59 |
+
recommendations.extend(self._analyze_task_execution_failure(task_execution))
|
| 60 |
+
|
| 61 |
+
# Analyze LLM generation
|
| 62 |
+
llm_generation = flow_data.get('llm_generation', {})
|
| 63 |
+
if not llm_generation.get('success', False):
|
| 64 |
+
recommendations.extend(self._analyze_llm_failure(llm_generation))
|
| 65 |
+
|
| 66 |
+
# Analyze performance issues
|
| 67 |
+
recommendations.extend(self._analyze_performance_issues(flow_data))
|
| 68 |
+
|
| 69 |
+
return recommendations
|
| 70 |
+
|
| 71 |
+
def _analyze_validation_failure(self, validation_result: Dict[str, Any]) -> List[str]:
|
| 72 |
+
"""Analyze validation failures and recommend fixes."""
|
| 73 |
+
recommendations = []
|
| 74 |
+
error = validation_result.get('error', '').lower()
|
| 75 |
+
|
| 76 |
+
if 'provincia' in error or 'region' in error:
|
| 77 |
+
recommendations.append({
|
| 78 |
+
'type': 'validation',
|
| 79 |
+
'priority': 'high',
|
| 80 |
+
'issue': 'Geographic validation failed',
|
| 81 |
+
'config_file': 'agent/config/geography.yaml',
|
| 82 |
+
'recommendation': 'Add missing provinces or regions to the supported_regions section',
|
| 83 |
+
'example': 'Add the requested province to the provinces list under the appropriate region'
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
if 'sensor' in error or 'tipo' in error:
|
| 87 |
+
recommendations.append({
|
| 88 |
+
'type': 'validation',
|
| 89 |
+
'priority': 'high',
|
| 90 |
+
'issue': 'Sensor type validation failed',
|
| 91 |
+
'config_file': 'tools/omirl/config/validation_rules.yaml',
|
| 92 |
+
'recommendation': 'Add valid sensor types or enable auto-correction for sensor names',
|
| 93 |
+
'example': 'Set auto_correct: true or add sensor type aliases'
|
| 94 |
+
})
|
| 95 |
+
|
| 96 |
+
if 'task' in error:
|
| 97 |
+
recommendations.append({
|
| 98 |
+
'type': 'validation',
|
| 99 |
+
'priority': 'high',
|
| 100 |
+
'issue': 'Task validation failed',
|
| 101 |
+
'config_file': 'tools/omirl/config/tasks.yaml',
|
| 102 |
+
'recommendation': 'Check task requirements and add missing required filters',
|
| 103 |
+
'example': 'Review task_requirements section for the specified task'
|
| 104 |
+
})
|
| 105 |
+
|
| 106 |
+
return recommendations
|
| 107 |
+
|
| 108 |
+
def _analyze_tool_selection_failure(self, tool_selection: Dict[str, Any]) -> List[str]:
|
| 109 |
+
"""Analyze tool selection failures."""
|
| 110 |
+
recommendations = []
|
| 111 |
+
|
| 112 |
+
if 'no suitable tool' in str(tool_selection.get('error', '')).lower():
|
| 113 |
+
recommendations.append({
|
| 114 |
+
'type': 'tool_selection',
|
| 115 |
+
'priority': 'high',
|
| 116 |
+
'issue': 'No tool found for user intent',
|
| 117 |
+
'config_file': 'agent/config/tool_registry.yaml',
|
| 118 |
+
'recommendation': 'Enable additional tools or review tool intent mappings',
|
| 119 |
+
'example': 'Set enabled: true for relevant tools or update intent_keywords'
|
| 120 |
+
})
|
| 121 |
+
|
| 122 |
+
return recommendations
|
| 123 |
+
|
| 124 |
+
def _analyze_task_execution_failure(self, task_execution: Dict[str, Any]) -> List[str]:
|
| 125 |
+
"""Analyze task execution failures."""
|
| 126 |
+
recommendations = []
|
| 127 |
+
error = str(task_execution.get('error', '')).lower()
|
| 128 |
+
|
| 129 |
+
if 'timeout' in error:
|
| 130 |
+
recommendations.append({
|
| 131 |
+
'type': 'performance',
|
| 132 |
+
'priority': 'medium',
|
| 133 |
+
'issue': 'Task execution timeout',
|
| 134 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 135 |
+
'recommendation': 'Increase timeout values in security or api sections',
|
| 136 |
+
'example': 'Increase timeout_seconds in security section or timeout in api section'
|
| 137 |
+
})
|
| 138 |
+
|
| 139 |
+
if 'api' in error or 'connection' in error:
|
| 140 |
+
recommendations.append({
|
| 141 |
+
'type': 'reliability',
|
| 142 |
+
'priority': 'high',
|
| 143 |
+
'issue': 'API connection issues',
|
| 144 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 145 |
+
'recommendation': 'Enable fallback provider or increase retry settings',
|
| 146 |
+
'example': 'Set fallback.enabled: true or increase api.max_retries'
|
| 147 |
+
})
|
| 148 |
+
|
| 149 |
+
return recommendations
|
| 150 |
+
|
| 151 |
+
def _analyze_llm_failure(self, llm_generation: Dict[str, Any]) -> List[str]:
|
| 152 |
+
"""Analyze LLM generation failures."""
|
| 153 |
+
recommendations = []
|
| 154 |
+
error = str(llm_generation.get('error', '')).lower()
|
| 155 |
+
|
| 156 |
+
if 'quota' in error or 'rate limit' in error:
|
| 157 |
+
recommendations.append({
|
| 158 |
+
'type': 'llm',
|
| 159 |
+
'priority': 'high',
|
| 160 |
+
'issue': 'LLM quota or rate limiting',
|
| 161 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 162 |
+
'recommendation': 'Enable fallback provider or change primary provider',
|
| 163 |
+
'example': 'Set fallback.enabled: true or change llm.provider to a different option'
|
| 164 |
+
})
|
| 165 |
+
|
| 166 |
+
if 'temperature' in error or 'token' in error:
|
| 167 |
+
recommendations.append({
|
| 168 |
+
'type': 'llm',
|
| 169 |
+
'priority': 'medium',
|
| 170 |
+
'issue': 'LLM parameter issues',
|
| 171 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 172 |
+
'recommendation': 'Adjust LLM parameters (temperature, max_tokens)',
|
| 173 |
+
'example': 'Modify api.temperature or api.max_tokens values'
|
| 174 |
+
})
|
| 175 |
+
|
| 176 |
+
return recommendations
|
| 177 |
+
|
| 178 |
+
def _analyze_performance_issues(self, flow_data: Dict[str, Any]) -> List[str]:
|
| 179 |
+
"""Analyze performance issues across the flow."""
|
| 180 |
+
recommendations = []
|
| 181 |
+
|
| 182 |
+
# Check if routing took too long (if timing data available)
|
| 183 |
+
routing_time = flow_data.get('timing', {}).get('routing_time', 0)
|
| 184 |
+
if routing_time > 5000: # 5 seconds
|
| 185 |
+
recommendations.append({
|
| 186 |
+
'type': 'performance',
|
| 187 |
+
'priority': 'medium',
|
| 188 |
+
'issue': 'Slow routing performance',
|
| 189 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 190 |
+
'recommendation': 'Enable caching or reduce routing complexity',
|
| 191 |
+
'example': 'Set routing.performance.cache_enabled: true'
|
| 192 |
+
})
|
| 193 |
+
|
| 194 |
+
return recommendations
|
| 195 |
+
|
| 196 |
+
def display_recommendations(self, recommendations: List[Dict[str, Any]]):
|
| 197 |
+
"""Display configuration recommendations in a user-friendly format."""
|
| 198 |
+
if not recommendations:
|
| 199 |
+
print("\n✅ No configuration issues detected!")
|
| 200 |
+
return
|
| 201 |
+
|
| 202 |
+
print(f"\n📋 CONFIGURATION RECOMMENDATIONS")
|
| 203 |
+
print("=" * 60)
|
| 204 |
+
print(f"Found {len(recommendations)} potential improvements:")
|
| 205 |
+
|
| 206 |
+
# Group by priority
|
| 207 |
+
high_priority = [r for r in recommendations if r.get('priority') == 'high']
|
| 208 |
+
medium_priority = [r for r in recommendations if r.get('priority') == 'medium']
|
| 209 |
+
low_priority = [r for r in recommendations if r.get('priority') == 'low']
|
| 210 |
+
|
| 211 |
+
for priority_group, priority_name in [(high_priority, 'HIGH PRIORITY'),
|
| 212 |
+
(medium_priority, 'MEDIUM PRIORITY'),
|
| 213 |
+
(low_priority, 'LOW PRIORITY')]:
|
| 214 |
+
if not priority_group:
|
| 215 |
+
continue
|
| 216 |
+
|
| 217 |
+
print(f"\n🔥 {priority_name}")
|
| 218 |
+
print("-" * 40)
|
| 219 |
+
|
| 220 |
+
for i, rec in enumerate(priority_group, 1):
|
| 221 |
+
print(f"\n{i}. **{rec['issue']}**")
|
| 222 |
+
print(f" 📁 File: {rec['config_file']}")
|
| 223 |
+
print(f" 💡 Fix: {rec['recommendation']}")
|
| 224 |
+
print(f" 📝 Example: {rec['example']}")
|
| 225 |
+
|
| 226 |
+
print(f"\n💡 **Quick Fix Guide:**")
|
| 227 |
+
print(f"1. Open the mentioned config files")
|
| 228 |
+
print(f"2. Apply the recommended changes")
|
| 229 |
+
print(f"3. Re-run this test to verify fixes")
|
| 230 |
+
print(f"4. Check the config_viewer.py to see current state")
|
| 231 |
+
|
| 232 |
+
class FullFlowTester:
|
| 233 |
+
"""Interactive tester for the complete application flow."""
|
| 234 |
+
|
| 235 |
+
def __init__(self):
|
| 236 |
+
"""Initialize the tester with all required components."""
|
| 237 |
+
self.agent = None
|
| 238 |
+
self.llm_client = None
|
| 239 |
+
self.validator = None
|
| 240 |
+
self.tool_registry = None
|
| 241 |
+
self.recommender = ConfigRecommender()
|
| 242 |
+
self.test_session_id = f"test_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
| 243 |
+
self.flow_data = {} # Store data for recommendations
|
| 244 |
+
|
| 245 |
+
def setup_components(self) -> bool:
|
| 246 |
+
"""Initialize all application components."""
|
| 247 |
+
try:
|
| 248 |
+
print("🔧 Initializing application components...")
|
| 249 |
+
|
| 250 |
+
# Initialize LLM client
|
| 251 |
+
self.llm_client = LLMClient()
|
| 252 |
+
print(f" ✅ LLM Client initialized (Provider: {self.llm_client.get_provider()})")
|
| 253 |
+
|
| 254 |
+
# Initialize validator
|
| 255 |
+
self.validator = RequestValidator()
|
| 256 |
+
print(" ✅ Request Validator initialized")
|
| 257 |
+
|
| 258 |
+
# Initialize tool registry
|
| 259 |
+
self.tool_registry = ToolRegistry()
|
| 260 |
+
available_tools = self.tool_registry.get_available_tools()
|
| 261 |
+
print(f" ✅ Tool Registry initialized ({len(available_tools)} tools available)")
|
| 262 |
+
|
| 263 |
+
# Initialize agent
|
| 264 |
+
self.agent = OperationsAgent()
|
| 265 |
+
print(" ✅ Operations Agent initialized")
|
| 266 |
+
|
| 267 |
+
return True
|
| 268 |
+
|
| 269 |
+
except Exception as e:
|
| 270 |
+
print(f" ❌ Failed to initialize components: {e}")
|
| 271 |
+
return False
|
| 272 |
+
|
| 273 |
+
def get_user_query(self) -> Optional[str]:
|
| 274 |
+
"""Get user query from input."""
|
| 275 |
+
print("\n" + "="*60)
|
| 276 |
+
print("📝 USER QUERY INPUT")
|
| 277 |
+
print("="*60)
|
| 278 |
+
|
| 279 |
+
query = input("\nEnter your query (or 'quit' to exit): ").strip()
|
| 280 |
+
|
| 281 |
+
if query.lower() in ['quit', 'exit', 'q']:
|
| 282 |
+
return None
|
| 283 |
+
|
| 284 |
+
if not query:
|
| 285 |
+
print("⚠️ Empty query, please try again.")
|
| 286 |
+
return self.get_user_query()
|
| 287 |
+
|
| 288 |
+
return query
|
| 289 |
+
|
| 290 |
+
def display_step_header(self, step_number: int, step_name: str):
|
| 291 |
+
"""Display a formatted step header."""
|
| 292 |
+
print(f"\n{'='*60}")
|
| 293 |
+
print(f"STEP {step_number}: {step_name.upper()}")
|
| 294 |
+
print("="*60)
|
| 295 |
+
|
| 296 |
+
def display_config_info(self):
|
| 297 |
+
"""Display current configuration information."""
|
| 298 |
+
self.display_step_header(1, "Configuration Status")
|
| 299 |
+
|
| 300 |
+
try:
|
| 301 |
+
# LLM Configuration
|
| 302 |
+
provider = self.llm_client.get_provider() if self.llm_client else "Unknown"
|
| 303 |
+
model = getattr(self.llm_client, 'current_model', 'Unknown') if self.llm_client else "Unknown"
|
| 304 |
+
print(f"🤖 LLM Provider: {provider}")
|
| 305 |
+
print(f"🧠 Model: {model}")
|
| 306 |
+
|
| 307 |
+
# Tool Registry
|
| 308 |
+
if self.tool_registry:
|
| 309 |
+
tools = self.tool_registry.get_available_tools()
|
| 310 |
+
print(f"🛠️ Available Tools: {', '.join(tools)}")
|
| 311 |
+
|
| 312 |
+
# Validation Settings
|
| 313 |
+
if self.validator:
|
| 314 |
+
print(f"✅ Validator: Active")
|
| 315 |
+
|
| 316 |
+
except Exception as e:
|
| 317 |
+
print(f"⚠️ Could not retrieve configuration info: {e}")
|
| 318 |
+
|
| 319 |
+
def execute_validation(self, query: str) -> tuple[bool, Dict[str, Any]]:
|
| 320 |
+
"""Execute query validation step."""
|
| 321 |
+
self.display_step_header(2, "Query Validation")
|
| 322 |
+
|
| 323 |
+
print(f"📝 User Query: '{query}'")
|
| 324 |
+
|
| 325 |
+
try:
|
| 326 |
+
# Parse and validate the query
|
| 327 |
+
validation_result = self.validator.validate_request(query)
|
| 328 |
+
|
| 329 |
+
print(f"✅ Validation Status: {'PASSED' if validation_result.get('valid', False) else 'FAILED'}")
|
| 330 |
+
|
| 331 |
+
if validation_result.get('valid', False):
|
| 332 |
+
print(f"🎯 Detected Intent: {validation_result.get('intent', 'Unknown')}")
|
| 333 |
+
if 'extracted_params' in validation_result:
|
| 334 |
+
print(f"📊 Extracted Parameters:")
|
| 335 |
+
for key, value in validation_result['extracted_params'].items():
|
| 336 |
+
print(f" • {key}: {value}")
|
| 337 |
+
else:
|
| 338 |
+
print(f"❌ Validation Error: {validation_result.get('error', 'Unknown error')}")
|
| 339 |
+
if 'suggestions' in validation_result:
|
| 340 |
+
print(f"💡 Suggestions: {validation_result['suggestions']}")
|
| 341 |
+
|
| 342 |
+
# Store validation data for recommendations
|
| 343 |
+
self.flow_data['validation_result'] = validation_result
|
| 344 |
+
|
| 345 |
+
return validation_result.get('valid', False), validation_result
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
print(f"❌ Validation failed with exception: {e}")
|
| 349 |
+
# Store validation failure data
|
| 350 |
+
self.flow_data['validation_result'] = {'valid': False, 'error': str(e)}
|
| 351 |
+
return False, {'error': str(e)}
|
| 352 |
+
|
| 353 |
+
def execute_tool_selection(self, validation_result: Dict[str, Any]) -> tuple[Optional[str], Dict[str, Any]]:
|
| 354 |
+
"""Execute tool selection step."""
|
| 355 |
+
self.display_step_header(3, "Tool Selection")
|
| 356 |
+
|
| 357 |
+
try:
|
| 358 |
+
# Get the intent from validation
|
| 359 |
+
intent = validation_result.get('intent', '')
|
| 360 |
+
params = validation_result.get('extracted_params', {})
|
| 361 |
+
|
| 362 |
+
print(f"🔍 Analyzing intent: '{intent}'")
|
| 363 |
+
print(f"📋 Available parameters: {list(params.keys())}")
|
| 364 |
+
|
| 365 |
+
# Use agent's tool selection logic
|
| 366 |
+
selected_tool = self.agent.select_tool(intent, params)
|
| 367 |
+
|
| 368 |
+
if selected_tool:
|
| 369 |
+
print(f"🛠️ Selected Tool: {selected_tool}")
|
| 370 |
+
|
| 371 |
+
# Get tool configuration
|
| 372 |
+
tool_config = self.tool_registry.get_tool_config(selected_tool)
|
| 373 |
+
if tool_config:
|
| 374 |
+
print(f"⚙️ Tool Configuration:")
|
| 375 |
+
print(f" • Description: {tool_config.get('description', 'N/A')}")
|
| 376 |
+
print(f" • Supported Tasks: {tool_config.get('supported_tasks', [])}")
|
| 377 |
+
|
| 378 |
+
# Store tool selection data
|
| 379 |
+
self.flow_data['tool_selection'] = {'success': True, 'selected_tool': selected_tool, 'config': tool_config}
|
| 380 |
+
|
| 381 |
+
return selected_tool, tool_config or {}
|
| 382 |
+
else:
|
| 383 |
+
print("❌ No suitable tool found for this query")
|
| 384 |
+
# Store tool selection failure
|
| 385 |
+
self.flow_data['tool_selection'] = {'success': False, 'error': 'No suitable tool found'}
|
| 386 |
+
return None, {}
|
| 387 |
+
|
| 388 |
+
except Exception as e:
|
| 389 |
+
print(f"❌ Tool selection failed: {e}")
|
| 390 |
+
# Store tool selection failure
|
| 391 |
+
self.flow_data['tool_selection'] = {'success': False, 'error': str(e)}
|
| 392 |
+
return None, {'error': str(e)}
|
| 393 |
+
|
| 394 |
+
def execute_task_execution(self, tool_name: str, validation_result: Dict[str, Any]) -> tuple[bool, Dict[str, Any]]:
|
| 395 |
+
"""Execute the selected task."""
|
| 396 |
+
self.display_step_header(4, "Task Execution")
|
| 397 |
+
|
| 398 |
+
try:
|
| 399 |
+
# Create agent state
|
| 400 |
+
state = AgentState(
|
| 401 |
+
user_query=validation_result.get('original_query', ''),
|
| 402 |
+
session_id=self.test_session_id
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
# Update state with validation results
|
| 406 |
+
state.intent = validation_result.get('intent', '')
|
| 407 |
+
state.extracted_params = validation_result.get('extracted_params', {})
|
| 408 |
+
state.selected_tool = tool_name
|
| 409 |
+
|
| 410 |
+
print(f"🚀 Executing tool: {tool_name}")
|
| 411 |
+
print(f"📊 Parameters:")
|
| 412 |
+
for key, value in state.extracted_params.items():
|
| 413 |
+
print(f" • {key}: {value}")
|
| 414 |
+
|
| 415 |
+
# Execute the task
|
| 416 |
+
execution_result = self.agent.execute_task(state)
|
| 417 |
+
|
| 418 |
+
if execution_result.get('success', False):
|
| 419 |
+
print(f"✅ Task execution: SUCCESS")
|
| 420 |
+
|
| 421 |
+
# Display results
|
| 422 |
+
if 'data' in execution_result:
|
| 423 |
+
data = execution_result['data']
|
| 424 |
+
if isinstance(data, list):
|
| 425 |
+
print(f"📊 Data records: {len(data)}")
|
| 426 |
+
elif isinstance(data, dict):
|
| 427 |
+
print(f"📊 Data keys: {list(data.keys())}")
|
| 428 |
+
else:
|
| 429 |
+
print(f"📊 Data type: {type(data).__name__}")
|
| 430 |
+
|
| 431 |
+
if 'artifacts' in execution_result:
|
| 432 |
+
artifacts = execution_result['artifacts']
|
| 433 |
+
print(f"📄 Generated artifacts: {len(artifacts)}")
|
| 434 |
+
for artifact in artifacts[:3]: # Show first 3
|
| 435 |
+
print(f" • {artifact}")
|
| 436 |
+
if len(artifacts) > 3:
|
| 437 |
+
print(f" • ... and {len(artifacts) - 3} more")
|
| 438 |
+
|
| 439 |
+
if 'metadata' in execution_result:
|
| 440 |
+
metadata = execution_result['metadata']
|
| 441 |
+
print(f"📋 Metadata: {list(metadata.keys())}")
|
| 442 |
+
|
| 443 |
+
else:
|
| 444 |
+
print(f"❌ Task execution: FAILED")
|
| 445 |
+
print(f" Error: {execution_result.get('error', 'Unknown error')}")
|
| 446 |
+
|
| 447 |
+
# Store task execution data
|
| 448 |
+
self.flow_data['task_execution'] = execution_result
|
| 449 |
+
|
| 450 |
+
return execution_result.get('success', False), execution_result
|
| 451 |
+
|
| 452 |
+
except Exception as e:
|
| 453 |
+
print(f"❌ Task execution failed with exception: {e}")
|
| 454 |
+
# Store task execution failure
|
| 455 |
+
self.flow_data['task_execution'] = {'success': False, 'error': str(e)}
|
| 456 |
+
return False, {'error': str(e)}
|
| 457 |
+
|
| 458 |
+
def execute_llm_generation(self, execution_result: Dict[str, Any], original_query: str) -> tuple[bool, str]:
|
| 459 |
+
"""Execute LLM response generation."""
|
| 460 |
+
self.display_step_header(5, "LLM Response Generation")
|
| 461 |
+
|
| 462 |
+
try:
|
| 463 |
+
print(f"🤖 Generating response using {self.llm_client.get_provider()}")
|
| 464 |
+
|
| 465 |
+
# Prepare context for LLM
|
| 466 |
+
context = {
|
| 467 |
+
'user_query': original_query,
|
| 468 |
+
'execution_success': execution_result.get('success', False),
|
| 469 |
+
'data': execution_result.get('data'),
|
| 470 |
+
'artifacts': execution_result.get('artifacts', []),
|
| 471 |
+
'metadata': execution_result.get('metadata', {}),
|
| 472 |
+
'error': execution_result.get('error')
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
# Generate response
|
| 476 |
+
llm_response = self.agent.generate_response(context)
|
| 477 |
+
|
| 478 |
+
if llm_response:
|
| 479 |
+
print(f"✅ LLM response generated ({len(llm_response)} characters)")
|
| 480 |
+
print(f"📝 Response preview: {llm_response[:150]}{'...' if len(llm_response) > 150 else ''}")
|
| 481 |
+
# Store LLM generation success
|
| 482 |
+
self.flow_data['llm_generation'] = {'success': True, 'response_length': len(llm_response)}
|
| 483 |
+
return True, llm_response
|
| 484 |
+
else:
|
| 485 |
+
print("❌ Failed to generate LLM response")
|
| 486 |
+
# Store LLM generation failure
|
| 487 |
+
self.flow_data['llm_generation'] = {'success': False, 'error': 'No response generated'}
|
| 488 |
+
return False, ""
|
| 489 |
+
|
| 490 |
+
except Exception as e:
|
| 491 |
+
print(f"❌ LLM generation failed: {e}")
|
| 492 |
+
# Store LLM generation failure
|
| 493 |
+
self.flow_data['llm_generation'] = {'success': False, 'error': str(e)}
|
| 494 |
+
return False, ""
|
| 495 |
+
|
| 496 |
+
def display_final_output(self, llm_response: str, execution_result: Dict[str, Any]):
|
| 497 |
+
"""Display the final application output."""
|
| 498 |
+
self.display_step_header(6, "Final Output")
|
| 499 |
+
|
| 500 |
+
print("📋 COMPLETE APPLICATION OUTPUT:")
|
| 501 |
+
print("-" * 40)
|
| 502 |
+
print(llm_response)
|
| 503 |
+
print("-" * 40)
|
| 504 |
+
|
| 505 |
+
# Additional information
|
| 506 |
+
if execution_result.get('artifacts'):
|
| 507 |
+
print(f"\n📄 Generated Files: {len(execution_result['artifacts'])}")
|
| 508 |
+
for artifact in execution_result['artifacts']:
|
| 509 |
+
print(f" • {artifact}")
|
| 510 |
+
|
| 511 |
+
if execution_result.get('metadata'):
|
| 512 |
+
print(f"\n📊 Execution Metadata:")
|
| 513 |
+
for key, value in execution_result['metadata'].items():
|
| 514 |
+
print(f" • {key}: {value}")
|
| 515 |
+
|
| 516 |
+
def run_test_flow(self):
|
| 517 |
+
"""Run the complete test flow."""
|
| 518 |
+
print("🧪 FULL FLOW INTEGRATION TEST")
|
| 519 |
+
print("=" * 60)
|
| 520 |
+
print("This test will execute the complete application flow and show")
|
| 521 |
+
print("detailed information at each step to help you understand how")
|
| 522 |
+
print("configuration changes affect the agent's behavior.")
|
| 523 |
+
|
| 524 |
+
# Setup components
|
| 525 |
+
if not self.setup_components():
|
| 526 |
+
print("❌ Failed to setup components. Exiting.")
|
| 527 |
+
return
|
| 528 |
+
|
| 529 |
+
while True:
|
| 530 |
+
# Get user query
|
| 531 |
+
query = self.get_user_query()
|
| 532 |
+
if query is None:
|
| 533 |
+
print("👋 Goodbye!")
|
| 534 |
+
break
|
| 535 |
+
|
| 536 |
+
print(f"\n🚀 Starting flow for query: '{query}'")
|
| 537 |
+
|
| 538 |
+
# Step 1: Show configuration
|
| 539 |
+
self.display_config_info()
|
| 540 |
+
|
| 541 |
+
# Step 2: Validation
|
| 542 |
+
validation_success, validation_result = self.execute_validation(query)
|
| 543 |
+
if not validation_success:
|
| 544 |
+
print("\n❌ Flow stopped due to validation failure.")
|
| 545 |
+
# Show recommendations for validation failure
|
| 546 |
+
self.display_step_header(7, "Configuration Recommendations")
|
| 547 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 548 |
+
self.recommender.display_recommendations(recommendations)
|
| 549 |
+
self.flow_data = {} # Reset for next iteration
|
| 550 |
+
continue
|
| 551 |
+
|
| 552 |
+
# Step 3: Tool selection
|
| 553 |
+
selected_tool, tool_config = self.execute_tool_selection(validation_result)
|
| 554 |
+
if not selected_tool:
|
| 555 |
+
print("\n❌ Flow stopped due to tool selection failure.")
|
| 556 |
+
# Show recommendations for tool selection failure
|
| 557 |
+
self.display_step_header(7, "Configuration Recommendations")
|
| 558 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 559 |
+
self.recommender.display_recommendations(recommendations)
|
| 560 |
+
self.flow_data = {} # Reset for next iteration
|
| 561 |
+
continue
|
| 562 |
+
|
| 563 |
+
# Step 4: Task execution
|
| 564 |
+
task_success, execution_result = self.execute_task_execution(selected_tool, validation_result)
|
| 565 |
+
if not task_success:
|
| 566 |
+
print("\n❌ Flow stopped due to task execution failure.")
|
| 567 |
+
# Show recommendations for task execution failure
|
| 568 |
+
self.display_step_header(7, "Configuration Recommendations")
|
| 569 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 570 |
+
self.recommender.display_recommendations(recommendations)
|
| 571 |
+
self.flow_data = {} # Reset for next iteration
|
| 572 |
+
continue
|
| 573 |
+
|
| 574 |
+
# Step 5: LLM response generation
|
| 575 |
+
llm_success, llm_response = self.execute_llm_generation(execution_result, query)
|
| 576 |
+
if not llm_success:
|
| 577 |
+
print("\n❌ Flow stopped due to LLM generation failure.")
|
| 578 |
+
# Show recommendations for LLM failure
|
| 579 |
+
self.display_step_header(7, "Configuration Recommendations")
|
| 580 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 581 |
+
self.recommender.display_recommendations(recommendations)
|
| 582 |
+
self.flow_data = {} # Reset for next iteration
|
| 583 |
+
continue
|
| 584 |
+
|
| 585 |
+
# Step 6: Final output
|
| 586 |
+
self.display_final_output(llm_response, execution_result)
|
| 587 |
+
|
| 588 |
+
# Step 7: Configuration recommendations
|
| 589 |
+
self.display_step_header(7, "Configuration Recommendations")
|
| 590 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 591 |
+
self.recommender.display_recommendations(recommendations)
|
| 592 |
+
|
| 593 |
+
print(f"\n✅ Flow completed successfully!")
|
| 594 |
+
print(f"🔄 Ready for next query...")
|
| 595 |
+
|
| 596 |
+
# Reset flow data for next iteration
|
| 597 |
+
self.flow_data = {}
|
| 598 |
+
|
| 599 |
+
def main():
|
| 600 |
+
"""Main entry point."""
|
| 601 |
+
tester = FullFlowTester()
|
| 602 |
+
try:
|
| 603 |
+
tester.run_test_flow()
|
| 604 |
+
except KeyboardInterrupt:
|
| 605 |
+
print("\n\n👋 Test interrupted by user. Goodbye!")
|
| 606 |
+
except Exception as e:
|
| 607 |
+
print(f"\n❌ Unexpected error: {e}")
|
| 608 |
+
import traceback
|
| 609 |
+
traceback.print_exc()
|
| 610 |
+
|
| 611 |
+
if __name__ == "__main__":
|
| 612 |
+
main()
|
docs/CONFIGURATION_REFACTORING.md
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
# Configuration Refactoring: Global Geography Integration
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
This document outlines the refactoring done to eliminate redundancy between global geography configuration and OMIRL tool-specific configuration, establishing a single source of truth for geographic data.
|
| 6 |
-
|
| 7 |
-
## Changes Made
|
| 8 |
-
|
| 9 |
-
### 1. Global Geography Configuration (`/agent/config/geography.yaml`)
|
| 10 |
-
- **Status**: ✅ Centralized source of truth for all geographic data
|
| 11 |
-
- **Contains**:
|
| 12 |
-
- Province names, codes, and mappings
|
| 13 |
-
- Alert zones and descriptions
|
| 14 |
-
- Comuni by province (to be populated)
|
| 15 |
-
- Regional information for Liguria
|
| 16 |
-
|
| 17 |
-
### 2. OMIRL Local Configuration (`/tools/omirl/config/parameters.yaml`)
|
| 18 |
-
- **Status**: ✅ Refactored to remove redundancies
|
| 19 |
-
- **Removed**:
|
| 20 |
-
- `provinces` section (now loaded from global config)
|
| 21 |
-
- `alert_zones` section (now loaded from global config)
|
| 22 |
-
- **Preserved**:
|
| 23 |
-
- OMIRL-specific sensor types
|
| 24 |
-
- Time periods
|
| 25 |
-
- Climate variables
|
| 26 |
-
- Satellite areas
|
| 27 |
-
- Task URL mappings
|
| 28 |
-
|
| 29 |
-
### 3. OMIRL Validator (`/tools/omirl/shared/validation.py`)
|
| 30 |
-
- **Status**: ✅ Updated to use global geography configuration
|
| 31 |
-
- **Changes**:
|
| 32 |
-
- Added `_load_geography_config()` method
|
| 33 |
-
- Loads geographic data from `/agent/config/geography.yaml`
|
| 34 |
-
- Maintains backward compatibility with existing validation API
|
| 35 |
-
- Preserves task-specific province format conversion (codes vs names)
|
| 36 |
-
|
| 37 |
-
## Architecture Benefits
|
| 38 |
-
|
| 39 |
-
### ✅ Single Source of Truth
|
| 40 |
-
- All geographic data centralized in `/agent/config/geography.yaml`
|
| 41 |
-
- Eliminates duplicate maintenance of province and alert zone data
|
| 42 |
-
- Consistent data across all tools and components
|
| 43 |
-
|
| 44 |
-
### ✅ Separation of Concerns
|
| 45 |
-
- **Global Config**: Geographic data used by routing and multiple tools
|
| 46 |
-
- **Tool Config**: Tool-specific parameters and settings
|
| 47 |
-
- **Validation**: Task-specific format requirements and rules
|
| 48 |
-
|
| 49 |
-
### ✅ Backward Compatibility
|
| 50 |
-
- OMIRL validator API remains unchanged
|
| 51 |
-
- All existing validation functionality preserved
|
| 52 |
-
- Province format conversion still works correctly:
|
| 53 |
-
- `valori_stazioni`: "Genova" → "GE" (codes)
|
| 54 |
-
- `massimi_precipitazione`: "genova" → "Genova" (proper names)
|
| 55 |
-
|
| 56 |
-
## Configuration Flow
|
| 57 |
-
|
| 58 |
-
```
|
| 59 |
-
Global Geography Config
|
| 60 |
-
↓
|
| 61 |
-
Agent Router System
|
| 62 |
-
↓
|
| 63 |
-
Tool Registry
|
| 64 |
-
↓
|
| 65 |
-
OMIRL Validator
|
| 66 |
-
↓
|
| 67 |
-
Task-Specific Validation
|
| 68 |
-
```
|
| 69 |
-
|
| 70 |
-
## Testing Verification
|
| 71 |
-
|
| 72 |
-
All validation functionality tested and confirmed working:
|
| 73 |
-
|
| 74 |
-
- ✅ Province validation with case-insensitive matching
|
| 75 |
-
- ✅ Task-specific format conversion (codes vs names)
|
| 76 |
-
- ✅ Alert zone validation
|
| 77 |
-
- ✅ Complete agent routing flow
|
| 78 |
-
- ✅ LLM prompt building with dynamic geography data
|
| 79 |
-
|
| 80 |
-
## Future Considerations
|
| 81 |
-
|
| 82 |
-
1. **Additional Tools**: New tools can easily leverage the global geography configuration
|
| 83 |
-
2. **Data Updates**: Geographic data updates only need to be made in one place
|
| 84 |
-
3. **Extensibility**: Easy to add new geographic entities (regions, zones, etc.)
|
| 85 |
-
4. **Validation**: Centralized validation rules can be applied consistently
|
| 86 |
-
|
| 87 |
-
## Summary
|
| 88 |
-
|
| 89 |
-
The refactoring successfully eliminates configuration redundancy while maintaining full functionality. The system now has a clean separation between global geographic data and tool-specific parameters, making the codebase more maintainable and extensible.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/DEPLOYMENT_GUIDE.md
DELETED
|
@@ -1,238 +0,0 @@
|
|
| 1 |
-
# LLM Router Deployment Guide
|
| 2 |
-
|
| 3 |
-
This guide covers how to deploy the LLM router across different environments with proper API key management.
|
| 4 |
-
|
| 5 |
-
## 🔑 API Key Management
|
| 6 |
-
|
| 7 |
-
### 1. **Local Development** (.env file)
|
| 8 |
-
```bash
|
| 9 |
-
# Create .env file in project root
|
| 10 |
-
echo "GEMINI_API_KEY=your_gemini_key_here" >> .env
|
| 11 |
-
echo "OPENAI_API_KEY=your_openai_key_here" >> .env
|
| 12 |
-
```
|
| 13 |
-
|
| 14 |
-
**Python Usage:**
|
| 15 |
-
```python
|
| 16 |
-
from agent.config.env_config import load_environment, get_api_key
|
| 17 |
-
|
| 18 |
-
# Auto-load environment
|
| 19 |
-
load_environment()
|
| 20 |
-
|
| 21 |
-
# Get API keys
|
| 22 |
-
gemini_key = get_api_key('GEMINI_API_KEY')
|
| 23 |
-
```
|
| 24 |
-
|
| 25 |
-
### 2. **Streamlit Apps** (secrets.toml)
|
| 26 |
-
```toml
|
| 27 |
-
# .streamlit/secrets.toml
|
| 28 |
-
GEMINI_API_KEY = "your_gemini_key_here"
|
| 29 |
-
OPENAI_API_KEY = "your_openai_key_here"
|
| 30 |
-
```
|
| 31 |
-
|
| 32 |
-
**Python Usage:**
|
| 33 |
-
```python
|
| 34 |
-
import streamlit as st
|
| 35 |
-
|
| 36 |
-
# Automatic loading in Streamlit
|
| 37 |
-
api_key = st.secrets["GEMINI_API_KEY"]
|
| 38 |
-
|
| 39 |
-
# Or use unified config
|
| 40 |
-
from agent.config.env_config import get_api_key
|
| 41 |
-
api_key = get_api_key('GEMINI_API_KEY') # Auto-detects Streamlit
|
| 42 |
-
```
|
| 43 |
-
|
| 44 |
-
### 3. **Hugging Face Spaces** 🤗 (Repository Secrets)
|
| 45 |
-
|
| 46 |
-
**Step 1: Add Secrets to Your Space**
|
| 47 |
-
1. Go to your HF Space: `https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE/settings`
|
| 48 |
-
2. Scroll to "Repository secrets" section
|
| 49 |
-
3. Add each secret:
|
| 50 |
-
- **Name**: `GEMINI_API_KEY`
|
| 51 |
-
- **Value**: `AI...your_actual_key...` (your actual Gemini API key)
|
| 52 |
-
- Click "Add secret"
|
| 53 |
-
- Repeat for `OPENAI_API_KEY`
|
| 54 |
-
|
| 55 |
-
**Step 2: Use in Code**
|
| 56 |
-
```python
|
| 57 |
-
import os
|
| 58 |
-
from agent.config.env_config import get_api_key
|
| 59 |
-
|
| 60 |
-
# HF Spaces automatically loads repository secrets as environment variables
|
| 61 |
-
api_key = get_api_key('GEMINI_API_KEY') # Works automatically
|
| 62 |
-
|
| 63 |
-
# Or direct access
|
| 64 |
-
api_key = os.getenv('GEMINI_API_KEY')
|
| 65 |
-
```
|
| 66 |
-
|
| 67 |
-
**Step 3: Verify in Space**
|
| 68 |
-
```python
|
| 69 |
-
# Add this to your Streamlit app for debugging
|
| 70 |
-
import streamlit as st
|
| 71 |
-
import os
|
| 72 |
-
|
| 73 |
-
st.write("🔑 API Keys Status:")
|
| 74 |
-
st.write(f"Gemini: {'✅' if os.getenv('GEMINI_API_KEY') else '❌'}")
|
| 75 |
-
st.write(f"OpenAI: {'✅' if os.getenv('OPENAI_API_KEY') else '❌'}")
|
| 76 |
-
```
|
| 77 |
-
|
| 78 |
-
### 4. **Docker Deployment**
|
| 79 |
-
```dockerfile
|
| 80 |
-
# Pass as environment variables
|
| 81 |
-
docker run -e GEMINI_API_KEY=your_key your_image
|
| 82 |
-
```
|
| 83 |
-
|
| 84 |
-
```yaml
|
| 85 |
-
# docker-compose.yml
|
| 86 |
-
environment:
|
| 87 |
-
- GEMINI_API_KEY=your_key
|
| 88 |
-
- OPENAI_API_KEY=your_key
|
| 89 |
-
```
|
| 90 |
-
|
| 91 |
-
**Python Usage:**
|
| 92 |
-
```python
|
| 93 |
-
# Same as other environments
|
| 94 |
-
from agent.config.env_config import get_api_key
|
| 95 |
-
api_key = get_api_key('GEMINI_API_KEY')
|
| 96 |
-
```
|
| 97 |
-
|
| 98 |
-
## 🚀 **LLM Router Usage in Different Environments**
|
| 99 |
-
|
| 100 |
-
### Unified Initialization
|
| 101 |
-
```python
|
| 102 |
-
from agent.llm_router_node import RouterOrchestrator
|
| 103 |
-
from agent.config.env_config import get_llm_config
|
| 104 |
-
|
| 105 |
-
# Auto-configure based on available API keys
|
| 106 |
-
config = get_llm_config()
|
| 107 |
-
router = RouterOrchestrator(
|
| 108 |
-
llm_provider=config['provider'], # 'gemini', 'openai', or 'anthropic'
|
| 109 |
-
llm_model=config['model'] # Optimized model for provider
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
# Route Italian queries
|
| 113 |
-
result = await router.route_query("Mostra precipitazioni a Genova")
|
| 114 |
-
```
|
| 115 |
-
|
| 116 |
-
### Manual Configuration
|
| 117 |
-
```python
|
| 118 |
-
# Force specific provider
|
| 119 |
-
router = RouterOrchestrator(
|
| 120 |
-
llm_provider="gemini",
|
| 121 |
-
llm_model="gemini-1.5-flash"
|
| 122 |
-
)
|
| 123 |
-
```
|
| 124 |
-
|
| 125 |
-
## 🌐 **Environment Detection**
|
| 126 |
-
|
| 127 |
-
The system automatically detects your deployment environment:
|
| 128 |
-
|
| 129 |
-
```python
|
| 130 |
-
from agent.config.env_config import setup_for_deployment
|
| 131 |
-
|
| 132 |
-
# Auto-detect and configure
|
| 133 |
-
success = setup_for_deployment('auto')
|
| 134 |
-
|
| 135 |
-
# Or specify environment
|
| 136 |
-
setup_for_deployment('huggingface') # 🤗 HF Spaces
|
| 137 |
-
setup_for_deployment('streamlit') # 🚀 Streamlit Cloud
|
| 138 |
-
setup_for_deployment('local') # 💻 Local dev
|
| 139 |
-
setup_for_deployment('docker') # 🐳 Docker
|
| 140 |
-
```
|
| 141 |
-
|
| 142 |
-
## 📋 **Deployment Checklist**
|
| 143 |
-
|
| 144 |
-
### For Hugging Face Spaces:
|
| 145 |
-
- [ ] Create HF Space repository
|
| 146 |
-
- [ ] Add `GEMINI_API_KEY` to repository secrets
|
| 147 |
-
- [ ] Add `OPENAI_API_KEY` to repository secrets (optional)
|
| 148 |
-
- [ ] Deploy app code
|
| 149 |
-
- [ ] Verify secrets loaded in app logs
|
| 150 |
-
|
| 151 |
-
### For Streamlit Cloud:
|
| 152 |
-
- [ ] Create `.streamlit/secrets.toml`
|
| 153 |
-
- [ ] Add API keys to secrets.toml
|
| 154 |
-
- [ ] Deploy to Streamlit Cloud
|
| 155 |
-
- [ ] Add secrets.toml content to Streamlit Cloud secrets
|
| 156 |
-
|
| 157 |
-
### For Local Development:
|
| 158 |
-
- [ ] Create `.env` file
|
| 159 |
-
- [ ] Add API keys to .env
|
| 160 |
-
- [ ] Install `python-dotenv`
|
| 161 |
-
- [ ] Test with `load_environment()`
|
| 162 |
-
|
| 163 |
-
## 🔒 **Security Best Practices**
|
| 164 |
-
|
| 165 |
-
1. **Never commit API keys to git**
|
| 166 |
-
```bash
|
| 167 |
-
# Add to .gitignore
|
| 168 |
-
echo ".env" >> .gitignore
|
| 169 |
-
echo ".streamlit/secrets.toml" >> .gitignore
|
| 170 |
-
```
|
| 171 |
-
|
| 172 |
-
2. **Use environment-specific patterns**
|
| 173 |
-
- **Development**: `.env` file
|
| 174 |
-
- **Streamlit**: `secrets.toml`
|
| 175 |
-
- **HF Spaces**: Repository secrets
|
| 176 |
-
- **Production**: Environment variables
|
| 177 |
-
|
| 178 |
-
3. **Validate keys on startup**
|
| 179 |
-
```python
|
| 180 |
-
from agent.config.env_config import get_api_key
|
| 181 |
-
|
| 182 |
-
# This validates key format and logs issues
|
| 183 |
-
key = get_api_key('GEMINI_API_KEY')
|
| 184 |
-
if not key:
|
| 185 |
-
st.error("❌ Gemini API key not configured")
|
| 186 |
-
```
|
| 187 |
-
|
| 188 |
-
## 🧪 **Testing API Keys**
|
| 189 |
-
|
| 190 |
-
```python
|
| 191 |
-
# Quick test script
|
| 192 |
-
from agent.config.env_config import load_environment, get_api_key
|
| 193 |
-
|
| 194 |
-
load_environment()
|
| 195 |
-
|
| 196 |
-
providers = {
|
| 197 |
-
'GEMINI_API_KEY': 'Gemini',
|
| 198 |
-
'OPENAI_API_KEY': 'OpenAI',
|
| 199 |
-
'ANTHROPIC_API_KEY': 'Anthropic'
|
| 200 |
-
}
|
| 201 |
-
|
| 202 |
-
for key_name, provider_name in providers.items():
|
| 203 |
-
key = get_api_key(key_name)
|
| 204 |
-
status = "✅" if key else "❌"
|
| 205 |
-
print(f"{status} {provider_name}: {key[:10]}..." if key else f"{status} {provider_name}: Not found")
|
| 206 |
-
```
|
| 207 |
-
|
| 208 |
-
## 💰 **Cost Optimization**
|
| 209 |
-
|
| 210 |
-
**Recommended Provider Priority:**
|
| 211 |
-
1. **Gemini Flash**: $0.075/1M tokens (cheapest)
|
| 212 |
-
2. **GPT-4o-mini**: $0.15/1M tokens
|
| 213 |
-
3. **Claude Haiku**: $0.25/1M tokens
|
| 214 |
-
|
| 215 |
-
**Auto-selection:**
|
| 216 |
-
```python
|
| 217 |
-
# System automatically chooses cheapest available provider
|
| 218 |
-
config = get_llm_config()
|
| 219 |
-
print(f"Using: {config['provider']}/{config['model']}")
|
| 220 |
-
```
|
| 221 |
-
|
| 222 |
-
## 🤗 **Hugging Face Specific Notes**
|
| 223 |
-
|
| 224 |
-
- Repository secrets are automatically loaded as environment variables
|
| 225 |
-
- No additional configuration needed in Python code
|
| 226 |
-
- Secrets are private and not visible in Space code
|
| 227 |
-
- Changes to secrets require Space restart
|
| 228 |
-
- Available in both public and private Spaces
|
| 229 |
-
|
| 230 |
-
**HF Space Environment Variables:**
|
| 231 |
-
```python
|
| 232 |
-
import os
|
| 233 |
-
print("Space ID:", os.getenv('SPACE_ID')) # HF Space identifier
|
| 234 |
-
print("Space URL:", os.getenv('SPACE_HOST')) # Space URL
|
| 235 |
-
print("HF Token:", os.getenv('HF_TOKEN')) # HF access token (if set)
|
| 236 |
-
```
|
| 237 |
-
|
| 238 |
-
This unified approach ensures your LLM router works seamlessly across all deployment environments!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/LEGACY_CODE_REMOVAL_PLAN.md
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
# Legacy Code Removal Plan for OMIRL Web Services
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
After successful migration to the new web services architecture, we can now safely remove legacy OMIRL-specific code that has been replaced by the generic architecture.
|
| 5 |
-
|
| 6 |
-
## Migration Status ✅
|
| 7 |
-
- ✅ OMIRL adapter migrated to use new architecture via compatibility layer
|
| 8 |
-
- ✅ All integration tests passing
|
| 9 |
-
- ✅ All compatibility tests passing
|
| 10 |
-
- ✅ Performance validated
|
| 11 |
-
- ✅ Zero breaking changes confirmed
|
| 12 |
-
|
| 13 |
-
## Files Safe to Remove
|
| 14 |
-
|
| 15 |
-
### 1. Core Legacy Files (OMIRL-Specific)
|
| 16 |
-
**services/web/browser.py** (468 lines)
|
| 17 |
-
- ✅ **SAFE TO REMOVE**
|
| 18 |
-
- Replaced by: `services/web/generic_browser.py` + `services/web/configs/omirl_config.py`
|
| 19 |
-
- All OMIRL-specific browser knowledge moved to configuration
|
| 20 |
-
- No longer used by OMIRL adapter (uses compat layer)
|
| 21 |
-
|
| 22 |
-
**services/web/table_scraper.py** (465 lines)
|
| 23 |
-
- ✅ **SAFE TO REMOVE**
|
| 24 |
-
- Replaced by: `services/web/generic_table.py` + `services/web/adapters/omirl_adapter.py`
|
| 25 |
-
- All OMIRL table scraping logic preserved in new architecture
|
| 26 |
-
- No longer used by OMIRL adapter (uses compat layer)
|
| 27 |
-
|
| 28 |
-
### 2. Files That Need to Stay
|
| 29 |
-
|
| 30 |
-
**services/web/compat.py**
|
| 31 |
-
- ❌ **KEEP** - Provides backward compatibility layer
|
| 32 |
-
- Used by OMIRL adapter for seamless migration
|
| 33 |
-
- Wraps new architecture with old API
|
| 34 |
-
|
| 35 |
-
**services/web/configs/omirl_config.py**
|
| 36 |
-
- ❌ **KEEP** - Contains all preserved OMIRL knowledge
|
| 37 |
-
- Essential for new architecture functionality
|
| 38 |
-
- Centralizes all OMIRL-specific configuration
|
| 39 |
-
|
| 40 |
-
**services/web/adapters/omirl_adapter.py**
|
| 41 |
-
- ❌ **KEEP** - New OMIRL implementation using generic components
|
| 42 |
-
- Core of the new architecture
|
| 43 |
-
- Provides same functionality as old files but more maintainable
|
| 44 |
-
|
| 45 |
-
**services/web/generic_*.py and base.py**
|
| 46 |
-
- ❌ **KEEP** - Core new architecture components
|
| 47 |
-
- Generic, reusable for future websites
|
| 48 |
-
- Foundation of the new system
|
| 49 |
-
|
| 50 |
-
## Files That Reference Legacy Code
|
| 51 |
-
|
| 52 |
-
### Files Still Using Legacy Imports (Need Updates)
|
| 53 |
-
|
| 54 |
-
1. **scripts/discovery/test_massimi_precipitazioni.py**
|
| 55 |
-
- Current: `from services.web.table_scraper import fetch_omirl_massimi_precipitazioni`
|
| 56 |
-
- Change to: `from services.web.compat import fetch_omirl_massimi_precipitazioni`
|
| 57 |
-
|
| 58 |
-
2. **scripts/discovery/test_valori_stazioni_after_changes.py**
|
| 59 |
-
- Current: `from services.web.table_scraper import fetch_omirl_stations`
|
| 60 |
-
- Change to: `from services.web.compat import fetch_omirl_stations`
|
| 61 |
-
|
| 62 |
-
3. **services/media/__init__.py** (Line 52-53)
|
| 63 |
-
- Current: `from services.web.browser import get_browser_context`
|
| 64 |
-
- Current: `from services.web.table_scraper import extract_table_data`
|
| 65 |
-
- Note: This needs investigation - media services might need different solution
|
| 66 |
-
|
| 67 |
-
### Test Files Using Legacy Imports
|
| 68 |
-
|
| 69 |
-
4. **tests/omirl/test_fast.py**
|
| 70 |
-
- Uses `@patch('services.web.table_scraper.fetch_omirl_stations')`
|
| 71 |
-
- Change to: `@patch('services.web.compat.fetch_omirl_stations')`
|
| 72 |
-
|
| 73 |
-
5. **tests/test_omirl_implementation.py**
|
| 74 |
-
- Uses `from services.web.browser import _browser_manager`
|
| 75 |
-
- Change to: Use new architecture browser management
|
| 76 |
-
|
| 77 |
-
6. **tests/omirl/performance_analysis.py**
|
| 78 |
-
- Uses `from services.web.browser import close_all_browser_sessions`
|
| 79 |
-
- Change to: `from services.web.compat import close_all_browser_sessions`
|
| 80 |
-
|
| 81 |
-
## Removal Steps (Safe Order)
|
| 82 |
-
|
| 83 |
-
### Phase 1: Update Remaining References
|
| 84 |
-
1. Update discovery scripts to use compat layer
|
| 85 |
-
2. Update test files to use compat layer
|
| 86 |
-
3. Investigate and fix media services imports
|
| 87 |
-
4. Run full test suite to validate changes
|
| 88 |
-
|
| 89 |
-
### Phase 2: Remove Legacy Files
|
| 90 |
-
1. Move `browser.py` to `browser_legacy.py` (backup)
|
| 91 |
-
2. Move `table_scraper.py` to `table_scraper_legacy.py` (backup)
|
| 92 |
-
3. Run full test suite
|
| 93 |
-
4. If all tests pass, delete backup files
|
| 94 |
-
|
| 95 |
-
### Phase 3: Clean Up (Optional)
|
| 96 |
-
1. Remove any remaining references to old files
|
| 97 |
-
2. Update documentation
|
| 98 |
-
3. Remove backup files after confidence period
|
| 99 |
-
|
| 100 |
-
## Risk Assessment
|
| 101 |
-
|
| 102 |
-
### Low Risk ✅
|
| 103 |
-
- Legacy files are no longer used by production OMIRL adapter
|
| 104 |
-
- Compatibility layer provides identical API
|
| 105 |
-
- All functionality preserved in new architecture
|
| 106 |
-
- Easy rollback by renaming files back
|
| 107 |
-
|
| 108 |
-
### Medium Risk ⚠️
|
| 109 |
-
- Some test files need updates
|
| 110 |
-
- Discovery scripts need updates
|
| 111 |
-
- Media services import needs investigation
|
| 112 |
-
|
| 113 |
-
### High Risk ❌
|
| 114 |
-
- None identified - migration was successful
|
| 115 |
-
|
| 116 |
-
## Validation Plan
|
| 117 |
-
|
| 118 |
-
### Before Removal
|
| 119 |
-
```bash
|
| 120 |
-
# Ensure all current tests pass
|
| 121 |
-
python tests/test_omirl_integration.py
|
| 122 |
-
python tests/omirl/test_adapter_integration.py
|
| 123 |
-
python tests/services/test_compatibility.py
|
| 124 |
-
```
|
| 125 |
-
|
| 126 |
-
### After Each Step
|
| 127 |
-
```bash
|
| 128 |
-
# Validate specific functionality
|
| 129 |
-
python -c "from services.web.compat import fetch_omirl_stations; print('✅ Import works')"
|
| 130 |
-
python tests/test_omirl_integration.py
|
| 131 |
-
```
|
| 132 |
-
|
| 133 |
-
### Final Validation
|
| 134 |
-
```bash
|
| 135 |
-
# Complete integration test
|
| 136 |
-
python tests/services/test_integration.py
|
| 137 |
-
```
|
| 138 |
-
|
| 139 |
-
## Benefits of Removal
|
| 140 |
-
|
| 141 |
-
1. **Reduced Codebase**: ~933 lines of legacy code removed
|
| 142 |
-
2. **No Confusion**: Only one way to do OMIRL scraping
|
| 143 |
-
3. **Easier Maintenance**: Generic architecture easier to extend
|
| 144 |
-
4. **Better Testing**: New architecture has comprehensive test coverage
|
| 145 |
-
5. **Future-Proof**: Ready for additional websites
|
| 146 |
-
|
| 147 |
-
## Rollback Plan
|
| 148 |
-
|
| 149 |
-
If issues discovered after removal:
|
| 150 |
-
1. Restore files from git: `git checkout HEAD -- services/web/browser.py services/web/table_scraper.py`
|
| 151 |
-
2. Update OMIRL adapter imports back to legacy files
|
| 152 |
-
3. Investigate and fix new architecture issues
|
| 153 |
-
4. Re-attempt removal after fixes
|
| 154 |
-
|
| 155 |
-
## Success Criteria
|
| 156 |
-
|
| 157 |
-
Removal is successful when:
|
| 158 |
-
1. All OMIRL functionality works unchanged
|
| 159 |
-
2. All tests pass
|
| 160 |
-
3. No import errors
|
| 161 |
-
4. No references to removed files
|
| 162 |
-
5. New architecture handles all use cases
|
| 163 |
-
6. Performance maintained or improved
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/LEGACY_REMOVAL_SUCCESS_REPORT.md
DELETED
|
@@ -1,162 +0,0 @@
|
|
| 1 |
-
# Legacy Code Removal Complete - Success Report
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
Successfully completed the removal of legacy OMIRL-specific web scraping code after migrating to the new generic web services architecture.
|
| 5 |
-
|
| 6 |
-
## What Was Removed ✅
|
| 7 |
-
|
| 8 |
-
### Files Completely Removed (933 lines total)
|
| 9 |
-
1. **services/web/browser.py** (468 lines)
|
| 10 |
-
- Legacy OMIRL-specific browser management
|
| 11 |
-
- Replaced by: `services/web/generic_browser.py` + `services/web/configs/omirl_config.py`
|
| 12 |
-
|
| 13 |
-
2. **services/web/table_scraper.py** (465 lines)
|
| 14 |
-
- Legacy OMIRL-specific table scraping
|
| 15 |
-
- Replaced by: `services/web/generic_table.py` + `services/web/adapters/omirl_adapter.py`
|
| 16 |
-
|
| 17 |
-
### Files Updated to Use New Architecture
|
| 18 |
-
1. **tools/omirl/tables/valori_stazioni.py** - Updated import to use compatibility layer
|
| 19 |
-
2. **tools/omirl/tables/massimi_precipitazione.py** - Updated import to use compatibility layer
|
| 20 |
-
3. **scripts/discovery/test_massimi_precipitazioni.py** - Updated import
|
| 21 |
-
4. **scripts/discovery/test_valori_stazioni_after_changes.py** - Updated import
|
| 22 |
-
5. **tests/omirl/test_fast.py** - Updated mock patches (4 locations)
|
| 23 |
-
6. **tests/omirl/performance_analysis.py** - Updated import
|
| 24 |
-
7. **services/media/__init__.py** - Updated example code in comments
|
| 25 |
-
|
| 26 |
-
## Migration Results ✅
|
| 27 |
-
|
| 28 |
-
### Zero Breaking Changes Confirmed
|
| 29 |
-
- ✅ All OMIRL adapter functionality preserved
|
| 30 |
-
- ✅ All function signatures unchanged
|
| 31 |
-
- ✅ All return data structures identical
|
| 32 |
-
- ✅ All sensor filters working (Precipitazione, Temperatura, etc.)
|
| 33 |
-
- ✅ All geographic filters working (provincia, comune)
|
| 34 |
-
- ✅ All error handling maintained
|
| 35 |
-
- ✅ All artifact generation working
|
| 36 |
-
|
| 37 |
-
### Test Results (All Passing)
|
| 38 |
-
1. **OMIRL Integration Tests**: ✅ 4/4 tests passed
|
| 39 |
-
- Basic extraction: 199 stations
|
| 40 |
-
- Filtered extraction: Precipitazione sensor
|
| 41 |
-
- Error handling: Invalid sensors properly rejected
|
| 42 |
-
- Multiple filters: Temperatura + GENOVA
|
| 43 |
-
|
| 44 |
-
2. **OMIRL Adapter Integration Tests**: ✅ 5/5 tests passed
|
| 45 |
-
- Basic functionality with Temperatura: 184 stations
|
| 46 |
-
- Precipitation functionality with GENOVA filtering
|
| 47 |
-
- Invalid mode, sensor, and subtask error handling
|
| 48 |
-
|
| 49 |
-
3. **Compatibility Tests**: ✅ All tests passed
|
| 50 |
-
- Backward compatibility maintained
|
| 51 |
-
- Existing code patterns work unchanged
|
| 52 |
-
- No import errors detected
|
| 53 |
-
|
| 54 |
-
### Performance Validation ✅
|
| 55 |
-
- **Data Quality**: Same number of stations extracted (199 precipitation, 184 temperature)
|
| 56 |
-
- **Response Time**: Comparable performance within normal variance
|
| 57 |
-
- **Browser Automation**: All OMIRL-specific behaviors preserved
|
| 58 |
-
- **Error Recovery**: Enhanced error messages and handling
|
| 59 |
-
|
| 60 |
-
## Current Architecture State
|
| 61 |
-
|
| 62 |
-
### New Architecture (In Use)
|
| 63 |
-
```
|
| 64 |
-
OMIRL Adapter → Compatibility Layer → New Generic Architecture
|
| 65 |
-
↓
|
| 66 |
-
services/web/compat.py → services/web/adapters/omirl_adapter.py
|
| 67 |
-
↓
|
| 68 |
-
Generic Components:
|
| 69 |
-
- services/web/generic_browser.py
|
| 70 |
-
- services/web/generic_table.py
|
| 71 |
-
- services/web/configs/omirl_config.py
|
| 72 |
-
```
|
| 73 |
-
|
| 74 |
-
### Files Retained (Essential)
|
| 75 |
-
- **services/web/compat.py** - Backward compatibility layer
|
| 76 |
-
- **services/web/base.py** - Abstract interfaces
|
| 77 |
-
- **services/web/generic_browser.py** - Generic browser manager
|
| 78 |
-
- **services/web/generic_table.py** - Generic table scraper
|
| 79 |
-
- **services/web/configs/omirl_config.py** - OMIRL knowledge preservation
|
| 80 |
-
- **services/web/adapters/omirl_adapter.py** - New OMIRL implementation
|
| 81 |
-
|
| 82 |
-
## Benefits Achieved
|
| 83 |
-
|
| 84 |
-
### 1. Codebase Cleanup
|
| 85 |
-
- **Removed**: 933 lines of legacy, OMIRL-specific code
|
| 86 |
-
- **Eliminated**: Code duplication and maintenance overhead
|
| 87 |
-
- **Simplified**: Only one way to do OMIRL scraping (via compatibility layer)
|
| 88 |
-
|
| 89 |
-
### 2. Architecture Improvements
|
| 90 |
-
- **Separation of Concerns**: Generic components vs site-specific configuration
|
| 91 |
-
- **Extensibility**: Easy to add new websites beyond OMIRL
|
| 92 |
-
- **Maintainability**: Centralized OMIRL knowledge in configuration
|
| 93 |
-
- **Testing**: Comprehensive test coverage for new architecture
|
| 94 |
-
|
| 95 |
-
### 3. Future-Proofing
|
| 96 |
-
- **Generic Framework**: Ready for additional websites
|
| 97 |
-
- **Configuration-Driven**: Easy to modify OMIRL behavior without code changes
|
| 98 |
-
- **Modular Design**: Independent components for browser, table extraction, etc.
|
| 99 |
-
- **Backward Compatibility**: Existing tools continue working unchanged
|
| 100 |
-
|
| 101 |
-
## Risk Assessment (Post-Removal)
|
| 102 |
-
|
| 103 |
-
### Zero Risk ✅
|
| 104 |
-
- All functionality preserved and tested
|
| 105 |
-
- Compatibility layer provides identical API
|
| 106 |
-
- Easy to extend or modify in the future
|
| 107 |
-
- No performance degradation
|
| 108 |
-
|
| 109 |
-
### Low Risk ⚠️
|
| 110 |
-
- Some tests had to be updated (already completed)
|
| 111 |
-
- Future development should use new architecture directly
|
| 112 |
-
|
| 113 |
-
### No High Risk Items ❌
|
| 114 |
-
|
| 115 |
-
## Migration Strategy Success
|
| 116 |
-
|
| 117 |
-
The migration followed a careful, low-risk strategy:
|
| 118 |
-
|
| 119 |
-
1. **Phase 1**: Built new generic architecture while preserving legacy code
|
| 120 |
-
2. **Phase 2**: Created compatibility layer maintaining exact APIs
|
| 121 |
-
3. **Phase 3**: Migrated OMIRL adapter to use compatibility layer
|
| 122 |
-
4. **Phase 4**: Updated remaining references to use compatibility layer
|
| 123 |
-
5. **Phase 5**: Removed legacy files after comprehensive testing
|
| 124 |
-
|
| 125 |
-
This approach ensured zero downtime and zero breaking changes throughout the process.
|
| 126 |
-
|
| 127 |
-
## Ready for Production ✅
|
| 128 |
-
|
| 129 |
-
The OMIRL adapter is now running on the modern web services architecture:
|
| 130 |
-
|
| 131 |
-
- ✅ **Fully Functional**: All OMIRL features working as before
|
| 132 |
-
- ✅ **Well Tested**: Comprehensive integration and compatibility testing
|
| 133 |
-
- ✅ **Future-Ready**: Generic architecture ready for extension
|
| 134 |
-
- ✅ **Maintainable**: Clean separation of concerns and centralized configuration
|
| 135 |
-
- ✅ **Backward Compatible**: Existing integrations continue working unchanged
|
| 136 |
-
|
| 137 |
-
## Next Steps (Optional)
|
| 138 |
-
|
| 139 |
-
### Immediate (Ready Now)
|
| 140 |
-
- Start using the OMIRL adapter in production with confidence
|
| 141 |
-
- All existing tools will continue working without changes
|
| 142 |
-
|
| 143 |
-
### Future Enhancements (When Needed)
|
| 144 |
-
- Gradually migrate to direct use of new architecture APIs
|
| 145 |
-
- Add additional websites using the generic framework
|
| 146 |
-
- Implement advanced features like caching, rate limiting presets, etc.
|
| 147 |
-
|
| 148 |
-
### Long-term (Optional)
|
| 149 |
-
- Eventually deprecate compatibility layer (when all consumers migrated)
|
| 150 |
-
- Add more sophisticated configuration options
|
| 151 |
-
- Implement additional website adapters
|
| 152 |
-
|
| 153 |
-
## Success Metrics Achieved
|
| 154 |
-
|
| 155 |
-
✅ **Zero Breaking Changes**: All existing functionality preserved
|
| 156 |
-
✅ **Code Reduction**: 933 lines of legacy code removed
|
| 157 |
-
✅ **Test Coverage**: 100% of tests passing after migration
|
| 158 |
-
✅ **Performance**: Maintained or improved response times
|
| 159 |
-
✅ **Architecture**: Modern, extensible, maintainable design
|
| 160 |
-
✅ **Documentation**: Comprehensive migration and usage documentation
|
| 161 |
-
|
| 162 |
-
**The legacy code removal is complete and successful!**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/LLM_ROUTER_PROGRESS.md
DELETED
|
@@ -1,295 +0,0 @@
|
|
| 1 |
-
# LLM Router Implementation Progress Summary
|
| 2 |
-
|
| 3 |
-
## 🎯 Objective
|
| 4 |
-
Pivot from complex 6-component router architecture to simple "LLM router + deterministic validator" approach for civil protection decision support in Liguria region.
|
| 5 |
-
|
| 6 |
-
## ✅ Completed Tasks (Tasks 1-5 of 5) - ALL COMPLETE!
|
| 7 |
-
|
| 8 |
-
### Task 1: LLM Infrastructure ✅
|
| 9 |
-
**File:** `agent/llm_client.py`
|
| 10 |
-
- **Status:** Complete and tested with fallback system
|
| 11 |
-
- **Features:**
|
| 12 |
-
- Multi-provider LLM client (Gemini, OpenAI, Anthropic, local models)
|
| 13 |
-
- **NEW:** Automatic fallback from Gemini to OpenAI when quota exceeded
|
| 14 |
-
- Structured JSON parsing for routing decisions
|
| 15 |
-
- Mock client for development and testing
|
| 16 |
-
- Comprehensive error handling and logging
|
| 17 |
-
- Tool proposal standardization with confidence scoring
|
| 18 |
-
|
| 19 |
-
### Task 2: Deterministic Validator ✅
|
| 20 |
-
**Files:** `agent/validator.py`, `agent/prompts.py`
|
| 21 |
-
- **Status:** Complete and tested with configuration integration
|
| 22 |
-
- **Features:**
|
| 23 |
-
- Geographic validation for Liguria boundaries
|
| 24 |
-
- Province name normalization (GE→GENOVA, SP→LA SPEZIA, etc.)
|
| 25 |
-
- **NEW:** Dynamic tool validation from configuration (no hardcoded requirements)
|
| 26 |
-
- **NEW:** Flexible parameter validation (no mandatory province requirement)
|
| 27 |
-
- Security validation (input sanitization)
|
| 28 |
-
- Italian language prompts with emergency keyword detection
|
| 29 |
-
- Confidence-based decision making
|
| 30 |
-
|
| 31 |
-
### Task 3: LLM Router Node ✅
|
| 32 |
-
**File:** `agent/llm_router_node.py`
|
| 33 |
-
- **Status:** Complete and tested with fallback configuration
|
| 34 |
-
- **Features:**
|
| 35 |
-
- Router orchestrator integrating LLM + validator
|
| 36 |
-
- **NEW:** Fallback provider configuration (Gemini → OpenAI)
|
| 37 |
-
- LangGraph node function for workflow integration
|
| 38 |
-
- Performance tracking and timing metrics
|
| 39 |
-
- Result standardization with execution readiness flags
|
| 40 |
-
- State management integration
|
| 41 |
-
|
| 42 |
-
### Task 4: OMIRL Adapter Integration ✅
|
| 43 |
-
**Target Files:** `tools/omirl/*.py`, `services/geographic/resolver.py`
|
| 44 |
-
- **Status:** Complete and tested
|
| 45 |
-
- **Features:**
|
| 46 |
-
- **NEW:** Geographic Resolver service for intelligent municipality-to-province mapping
|
| 47 |
-
- **NEW:** Dynamic geographic resolution with fuzzy matching
|
| 48 |
-
- **NEW:** Comprehensive Liguria municipality database
|
| 49 |
-
- Updated OMIRL tool to use Geographic Resolver
|
| 50 |
-
- Removed hardcoded municipality mappings
|
| 51 |
-
- End-to-end data extraction workflow validated
|
| 52 |
-
- Screenshot and filtering functionality confirmed
|
| 53 |
-
|
| 54 |
-
### Task 5: Graph Integration & Cleanup ✅
|
| 55 |
-
**Target Files:** `agent/graph.py`, `agent/nodes.py`, `agent/config/llm_router_config.yaml`
|
| 56 |
-
- **Status:** Complete and tested
|
| 57 |
-
- **Features:**
|
| 58 |
-
- **NEW:** Updated configuration to use Gemini primary with OpenAI fallback
|
| 59 |
-
- **NEW:** Flexible confidence thresholds (0.5 execution, 0.3 clarification)
|
| 60 |
-
- **NEW:** No mandatory tool parameters (intelligent resolution)
|
| 61 |
-
- Main workflow successfully uses LLM router node
|
| 62 |
-
- Deprecated complex intent parsing components
|
| 63 |
-
- Clean state flow for new routing structure
|
| 64 |
-
|
| 65 |
-
## 🧪 Testing Status
|
| 66 |
-
|
| 67 |
-
### Validation Tests ✅
|
| 68 |
-
- ✅ Component imports and instantiation
|
| 69 |
-
- ✅ Italian query processing ("Mostra precipitazioni a Genova")
|
| 70 |
-
- ✅ Geographic validation (Liguria province boundaries)
|
| 71 |
-
- ✅ LangGraph node integration
|
| 72 |
-
- ✅ **NEW:** OpenAI LLM client functionality with real API calls
|
| 73 |
-
- ✅ **NEW:** Gemini to OpenAI fallback system
|
| 74 |
-
- ✅ **NEW:** Geographic Resolver with municipality mapping
|
| 75 |
-
- ✅ **NEW:** End-to-end OMIRL data extraction (37 stations found for Savona)
|
| 76 |
-
- ✅ State structure compatibility
|
| 77 |
-
|
| 78 |
-
### Test Results - Updated August 29, 2025
|
| 79 |
-
```
|
| 80 |
-
🧪 Testing LLM Router with Savona query...
|
| 81 |
-
Query: Precipitazione nella provincia di Savona (SV)
|
| 82 |
-
✅ Routing completed successfully
|
| 83 |
-
Status: approved
|
| 84 |
-
Execution ready: True
|
| 85 |
-
Routing confidence: 0.9
|
| 86 |
-
Validation confidence: 1.0
|
| 87 |
-
Routing time: 2930ms (OpenAI fallback)
|
| 88 |
-
Tool calls: 1
|
| 89 |
-
1. omirl_tool: {'provincia': 'SAVONA', 'tipo_sensore': 'Precipitazione'}
|
| 90 |
-
|
| 91 |
-
OMIRL Extraction Results:
|
| 92 |
-
✅ Successfully extracted 37 weather stations for Savona province
|
| 93 |
-
✅ Geographic resolution: SAVONA → SV code conversion
|
| 94 |
-
✅ Data saved to JSON artifact
|
| 95 |
-
|
| 96 |
-
🎯 Test result: PASSED - Full pipeline working!
|
| 97 |
-
```
|
| 98 |
-
|
| 99 |
-
### Geographic Resolution Tests ✅
|
| 100 |
-
```
|
| 101 |
-
Testing Geographic Resolver:
|
| 102 |
-
✅ Rapallo → GENOVA (confidence: 1.00, method: static)
|
| 103 |
-
✅ Sanremo → IMPERIA (confidence: 1.00, method: static)
|
| 104 |
-
✅ Genoa → GENOVA (confidence: 0.90, method: variation)
|
| 105 |
-
✅ VENTIMIGLIA → IMPERIA (confidence: 1.00, method: static)
|
| 106 |
-
❌ typo_test → Could not resolve (expected behavior)
|
| 107 |
-
|
| 108 |
-
Geographic Database Coverage:
|
| 109 |
-
✅ 50+ municipalities mapped across all 4 Liguria provinces
|
| 110 |
-
✅ Fuzzy matching for common typos and variations
|
| 111 |
-
✅ Caching for performance optimization
|
| 112 |
-
```
|
| 113 |
-
|
| 114 |
-
## 📋 Remaining Tasks - NONE! ✅
|
| 115 |
-
|
| 116 |
-
### ~~Task 4: OMIRL Adapter Integration~~ ✅ COMPLETED
|
| 117 |
-
**Target Files:** `tools/omirl/*.py`, `services/geographic/resolver.py`
|
| 118 |
-
- **Status:** ✅ Complete and tested
|
| 119 |
-
- **Achievements:**
|
| 120 |
-
- ✅ Created intelligent Geographic Resolver service
|
| 121 |
-
- ✅ OMIRL tool successfully uses dynamic geographic resolution
|
| 122 |
-
- ✅ End-to-end data extraction confirmed (37 Savona stations)
|
| 123 |
-
- ✅ Removed hardcoded geographic mappings
|
| 124 |
-
|
| 125 |
-
### ~~Task 5: Graph Integration & Cleanup~~ ✅ COMPLETED
|
| 126 |
-
**Target Files:** `agent/graph.py`, `agent/nodes.py`, `agent/config/llm_router_config.yaml`
|
| 127 |
-
- **Status:** ✅ Complete and tested
|
| 128 |
-
- **Achievements:**
|
| 129 |
-
- ✅ Full LLM router integration in workflow
|
| 130 |
-
- ✅ Gemini-first with OpenAI fallback configuration
|
| 131 |
-
- ✅ Flexible validation (no mandatory parameters)
|
| 132 |
-
- ✅ Updated confidence thresholds for better user experience
|
| 133 |
-
|
| 134 |
-
## 🎯 Future Enhancement Opportunities
|
| 135 |
-
|
| 136 |
-
### Performance Optimizations
|
| 137 |
-
- **Response Caching:** Cache LLM responses for identical weather queries
|
| 138 |
-
- **Geographic API Integration:** Connect to official Liguria municipality APIs
|
| 139 |
-
- **Batch Processing:** Support multiple location queries in single request
|
| 140 |
-
|
| 141 |
-
### User Experience Enhancements
|
| 142 |
-
- **Temporal Queries:** Support "ieri" (yesterday), "ultima settimana" (last week)
|
| 143 |
-
- **Multi-Sensor Analysis:** "condizioni meteo complete" (complete weather conditions)
|
| 144 |
-
- **Italian Error Messages:** More user-friendly responses in Italian
|
| 145 |
-
|
| 146 |
-
## 🏗️ Architecture Overview
|
| 147 |
-
|
| 148 |
-
### Current Implementation - UPDATED ARCHITECTURE
|
| 149 |
-
```
|
| 150 |
-
User Query (Italian)
|
| 151 |
-
↓
|
| 152 |
-
LLM Router (agent/llm_router_node.py)
|
| 153 |
-
├── LLM Client (agent/llm_client.py) [Gemini → OpenAI fallback]
|
| 154 |
-
│ ├── Structured prompt (agent/prompts.py)
|
| 155 |
-
│ └── JSON parsing & confidence scoring
|
| 156 |
-
└── Validator (agent/validator.py) [Dynamic configuration]
|
| 157 |
-
├── Geographic validation (Liguria boundaries)
|
| 158 |
-
├── Geographic Resolver (services/geographic/resolver.py) [NEW]
|
| 159 |
-
├── Tool parameter validation (flexible)
|
| 160 |
-
└── Security validation
|
| 161 |
-
↓
|
| 162 |
-
Approved Tool Calls → OMIRL Tool → Response → Gemini Summarization
|
| 163 |
-
```
|
| 164 |
-
|
| 165 |
-
### Key Improvements - ENHANCED
|
| 166 |
-
1. **Simplified Architecture:** 2 components vs. 6 components ✅
|
| 167 |
-
2. **Natural Language Understanding:** LLM-first approach ✅
|
| 168 |
-
3. **Geographic Safety:** Strict Liguria boundary enforcement ✅
|
| 169 |
-
4. **Italian Language Support:** Native query processing ✅
|
| 170 |
-
5. **Performance:** ~100ms-3s routing time (depending on provider) ✅
|
| 171 |
-
6. **Confidence Scoring:** Dual validation confidence metrics ✅
|
| 172 |
-
7. ****NEW:** Smart Fallback System:** Gemini → OpenAI automatic fallback ✅
|
| 173 |
-
8. ****NEW:** Intelligent Geographic Resolution:** 50+ municipalities with fuzzy matching ✅
|
| 174 |
-
9. ****NEW:** Flexible Validation:** No mandatory parameters, intelligent resolution ✅
|
| 175 |
-
10. ****NEW:** Real-World Validation:** 37 weather stations extracted successfully ✅
|
| 176 |
-
|
| 177 |
-
## 🔧 Configuration
|
| 178 |
-
|
| 179 |
-
### LLM Router Config (`agent/config/llm_router_config.yaml`) - UPDATED
|
| 180 |
-
- **Provider:** Gemini primary with OpenAI fallback (smart quota management)
|
| 181 |
-
- **Validation:** Geographic, tool, security validation (dynamic configuration)
|
| 182 |
-
- **Performance:** Flexible timeout, caching, lowered confidence thresholds (0.5/0.3)
|
| 183 |
-
- **Language:** Italian primary, English fallback
|
| 184 |
-
- **Emergency:** Keyword detection and priority handling
|
| 185 |
-
- **Geographic:** Auto-resolution enabled, strict mode disabled for flexibility
|
| 186 |
-
|
| 187 |
-
### State Management Updates (`agent/state.py`)
|
| 188 |
-
- **Added:** `routing_result` field for LLM router output
|
| 189 |
-
- **Deprecated:** `parsed_intent` (maintained for compatibility)
|
| 190 |
-
- **Enhanced:** Tool execution tracking and metadata
|
| 191 |
-
|
| 192 |
-
### Geographic Service (`services/geographic/resolver.py`) - NEW
|
| 193 |
-
- **Features:** Intelligent municipality-to-province mapping
|
| 194 |
-
- **Coverage:** 50+ Liguria municipalities across 4 provinces
|
| 195 |
-
- **Capabilities:** Fuzzy matching, variations handling, caching
|
| 196 |
-
- **Performance:** Sub-millisecond resolution with fallback strategies
|
| 197 |
-
|
| 198 |
-
## 🎯 Next Steps
|
| 199 |
-
|
| 200 |
-
### Immediate (Task 4)
|
| 201 |
-
1. **OMIRL Integration Test**
|
| 202 |
-
```bash
|
| 203 |
-
# Test OMIRL tool with LLM router output
|
| 204 |
-
python -c "from tools.omirl import run_omirl; # Test with router params"
|
| 205 |
-
```
|
| 206 |
-
|
| 207 |
-
2. **Parameter Mapping Validation**
|
| 208 |
-
- Ensure province normalization compatibility
|
| 209 |
-
- Test sensor_type parameter handling
|
| 210 |
-
- Validate date range processing
|
| 211 |
-
|
| 212 |
-
### Short-term (Task 5)
|
| 213 |
-
1. **Graph Workflow Update**
|
| 214 |
-
- Replace intent parsing with LLM routing
|
| 215 |
-
- Update node sequence and state flow
|
| 216 |
-
- Remove deprecated components
|
| 217 |
-
|
| 218 |
-
2. **Integration Testing**
|
| 219 |
-
- End-to-end workflow testing
|
| 220 |
-
- Performance benchmarking
|
| 221 |
-
- Error scenario validation
|
| 222 |
-
|
| 223 |
-
### Future Enhancements
|
| 224 |
-
- **RAG Integration:** Weather knowledge base
|
| 225 |
-
- **Multi-tool Coordination:** Complex query handling
|
| 226 |
-
- **Conversation Context:** Multi-turn interactions
|
| 227 |
-
- **Learning Feedback:** Performance optimization
|
| 228 |
-
|
| 229 |
-
## 📊 Performance Metrics
|
| 230 |
-
|
| 231 |
-
### Current Performance
|
| 232 |
-
- **Routing Time:** ~100ms average
|
| 233 |
-
- **Validation:** Near-instantaneous (<10ms)
|
| 234 |
-
- **Memory Usage:** Minimal (stateless design)
|
| 235 |
-
- **Accuracy:** High confidence routing (0.9+)
|
| 236 |
-
|
| 237 |
-
### Success Metrics
|
| 238 |
-
- ✅ Italian query understanding
|
| 239 |
-
- ✅ Geographic boundary enforcement
|
| 240 |
-
- ✅ Tool parameter extraction
|
| 241 |
-
- ✅ State integration
|
| 242 |
-
- ✅ Error handling
|
| 243 |
-
|
| 244 |
-
## 🔍 Code Quality
|
| 245 |
-
|
| 246 |
-
### Documentation
|
| 247 |
-
- ✅ Comprehensive docstrings
|
| 248 |
-
- ✅ Type annotations
|
| 249 |
-
- ✅ Configuration examples
|
| 250 |
-
- ✅ Future extension TODOs
|
| 251 |
-
|
| 252 |
-
### Error Handling
|
| 253 |
-
- ✅ LLM API failures
|
| 254 |
-
- ✅ JSON parsing errors
|
| 255 |
-
- ✅ Validation failures
|
| 256 |
-
- ✅ Geographic boundary violations
|
| 257 |
-
|
| 258 |
-
### Logging
|
| 259 |
-
- ✅ Performance tracking
|
| 260 |
-
- ✅ Decision reasoning
|
| 261 |
-
- ✅ Validation outcomes
|
| 262 |
-
- ✅ Error details
|
| 263 |
-
|
| 264 |
-
## 🎉 Summary - MISSION ACCOMPLISHED!
|
| 265 |
-
|
| 266 |
-
The LLM router implementation has been **COMPLETED SUCCESSFULLY** with all 5 tasks finished and tested. The system has evolved beyond the original scope with significant enhancements:
|
| 267 |
-
|
| 268 |
-
### Core Achievements ✅
|
| 269 |
-
- **Natural Language Understanding** for Italian weather queries ✅
|
| 270 |
-
- **Geographic Safety** with Liguria boundary enforcement ✅
|
| 271 |
-
- **Fast Performance** with smart provider fallback ✅
|
| 272 |
-
- **High Reliability** with comprehensive validation ✅
|
| 273 |
-
- **Clean Architecture** with clear separation of concerns ✅
|
| 274 |
-
|
| 275 |
-
### Advanced Features ✅
|
| 276 |
-
- **Smart Fallback System:** Gemini (50 free calls) → OpenAI seamless transition
|
| 277 |
-
- **Intelligent Geographic Resolution:** 50+ municipalities with fuzzy matching
|
| 278 |
-
- **Flexible Validation:** No mandatory parameters, intelligent auto-resolution
|
| 279 |
-
- **Real-World Validation:** Successfully extracted 37 weather stations from Savona
|
| 280 |
-
- **Production Ready:** Full configuration management and error handling
|
| 281 |
-
|
| 282 |
-
### Performance Metrics - FINAL
|
| 283 |
-
- **Routing Time:** 100ms (Gemini) / 3s (OpenAI fallback)
|
| 284 |
-
- **Geographic Resolution:** <1ms with caching
|
| 285 |
-
- **Memory Usage:** Minimal (stateless design)
|
| 286 |
-
- **Accuracy:** High confidence routing (0.9+)
|
| 287 |
-
- **Success Rate:** 100% for tested Italian weather queries
|
| 288 |
-
|
| 289 |
-
### Supported Query Examples ✅
|
| 290 |
-
- ✅ "temperatura a Genova" → Extracts 13 temperature stations
|
| 291 |
-
- ✅ "Precipitazione nella provincia di Savona (SV)" → Extracts 37 precipitation stations
|
| 292 |
-
- ✅ "sensori meteo Imperia" → Province-wide sensor queries
|
| 293 |
-
- ✅ Municipality variations: "Genoa", "San Remo", "Rapallo", etc.
|
| 294 |
-
|
| 295 |
-
The LLM router is now **PRODUCTION READY** and fully integrated with the OMIRL weather monitoring system! 🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/OMIRL_PHASE_1_ADAPTER_COMPLETE.md
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
# OMIRL Phase 1 Refactor - Adapter Update Complete
|
| 2 |
-
|
| 3 |
-
## Summary
|
| 4 |
-
|
| 5 |
-
Successfully updated the OMIRL adapter.py to use the new YAML-based validation architecture and the refactored valori_stazioni implementation. All tests are passing and the system maintains backward compatibility.
|
| 6 |
-
|
| 7 |
-
## Changes Made
|
| 8 |
-
|
| 9 |
-
### 1. Updated adapter.py Imports
|
| 10 |
-
- Replaced old `services_tables` imports with new modular components:
|
| 11 |
-
- `OMIRLFilterSet`, `OMIRLResult` from `tools.omirl.shared`
|
| 12 |
-
- `get_validator`, `get_valid_sensor_types`, `validate_sensor_type` from validation system
|
| 13 |
-
- `fetch_valori_stazioni_async` from `tools.omirl.tables.valori_stazioni`
|
| 14 |
-
|
| 15 |
-
### 2. Updated Main Tool Function
|
| 16 |
-
- Modified `omirl_tool()` to use the new `OMIRLFilterSet` class for filter management
|
| 17 |
-
- Integrated YAML-based validation system for sensor types
|
| 18 |
-
- Updated to call `fetch_valori_stazioni_async()` instead of old `fetch_station_data()`
|
| 19 |
-
- Improved error handling and provincia name-to-code conversion
|
| 20 |
-
- Maintained the same tool interface contract for agent compatibility
|
| 21 |
-
|
| 22 |
-
### 3. Enhanced Error Handling
|
| 23 |
-
- Better graceful handling of missing dependencies (geographic resolver)
|
| 24 |
-
- Maintained all expected output fields (summary_text, artifacts, sources, metadata, warnings)
|
| 25 |
-
- Improved logging and debugging information
|
| 26 |
-
|
| 27 |
-
## Test Results
|
| 28 |
-
|
| 29 |
-
Created comprehensive test suites that validate:
|
| 30 |
-
|
| 31 |
-
### test_adapter_integration.py
|
| 32 |
-
- ✅ Basic adapter functionality
|
| 33 |
-
- ✅ Invalid mode error handling
|
| 34 |
-
- ✅ Invalid sensor error handling
|
| 35 |
-
- **Result: 3/3 tests passed**
|
| 36 |
-
|
| 37 |
-
### test_omirl_new_architecture.py
|
| 38 |
-
- ✅ YAML validation system
|
| 39 |
-
- ✅ OMIRLFilterSet functionality
|
| 40 |
-
- ✅ Adapter output format compliance
|
| 41 |
-
- ✅ Provincia name-to-code conversion
|
| 42 |
-
- **Result: 4/4 tests passed**
|
| 43 |
-
|
| 44 |
-
### Backward Compatibility
|
| 45 |
-
- ✅ Existing test_omirl_implementation.py still passes
|
| 46 |
-
- ✅ Tool interface contract maintained
|
| 47 |
-
- ✅ Agent registry compatibility preserved
|
| 48 |
-
|
| 49 |
-
## Data Extraction Results
|
| 50 |
-
|
| 51 |
-
The updated system successfully extracts:
|
| 52 |
-
- **199 stations** for Precipitazione sensor
|
| 53 |
-
- **184 stations** for Temperatura sensor
|
| 54 |
-
- **69 stations** for Temperatura in Genova province
|
| 55 |
-
- Artifacts saved to `/tmp/omirl_data/` with proper naming
|
| 56 |
-
- LLM-based summaries generated correctly
|
| 57 |
-
|
| 58 |
-
## Key Features Working
|
| 59 |
-
|
| 60 |
-
1. **YAML-based Validation**: Sensor types validated against configuration files
|
| 61 |
-
2. **Filter Management**: Clean separation of geographic, measurement, and temporal filters
|
| 62 |
-
3. **Province Conversion**: Automatic conversion from full names ("GENOVA") to codes ("GE")
|
| 63 |
-
4. **Error Recovery**: Graceful handling of validation errors with helpful suggestions
|
| 64 |
-
5. **Artifact Generation**: JSON files with proper metadata and timestamps
|
| 65 |
-
6. **Smart Summarization**: LLM-powered summaries of extracted data
|
| 66 |
-
|
| 67 |
-
## Next Steps
|
| 68 |
-
|
| 69 |
-
The OMIRL Phase 1 refactor is complete. The adapter now:
|
| 70 |
-
- Uses the new modular architecture
|
| 71 |
-
- Implements YAML-based validation
|
| 72 |
-
- Maintains full backward compatibility
|
| 73 |
-
- Passes all integration tests
|
| 74 |
-
- Ready for agent integration
|
| 75 |
-
|
| 76 |
-
The system is ready for Phase 2 implementation of additional modes/tasks.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/OMIRL_WEB_MIGRATION_PLAN.md
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
# OMIRL Web Services Migration Plan
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
This document outlines the migration plan to update the OMIRL adapter to use the new web services architecture while maintaining full backward compatibility.
|
| 5 |
-
|
| 6 |
-
## Current State Analysis
|
| 7 |
-
|
| 8 |
-
### Files That Need Migration
|
| 9 |
-
1. **tools/omirl/tables/valori_stazioni.py**
|
| 10 |
-
- Currently imports: `from services.web.table_scraper import fetch_omirl_stations`
|
| 11 |
-
- Currently calls: `station_data = await fetch_omirl_stations(sensor_type)`
|
| 12 |
-
|
| 13 |
-
2. **tools/omirl/tables/massimi_precipitazione.py**
|
| 14 |
-
- Currently imports: `from services.web.table_scraper import fetch_omirl_massimi_precipitazioni`
|
| 15 |
-
- Currently calls: `precipitation_data = await fetch_omirl_massimi_precipitazioni()`
|
| 16 |
-
|
| 17 |
-
### Current Architecture Flow
|
| 18 |
-
```
|
| 19 |
-
OMIRL Adapter → OMIRL Tasks → OLD services.web.table_scraper → OLD browser.py + table_scraper.py
|
| 20 |
-
```
|
| 21 |
-
|
| 22 |
-
### Target Architecture Flow
|
| 23 |
-
```
|
| 24 |
-
OMIRL Adapter → OMIRL Tasks → NEW services.web.compat OR services.web.adapters.omirl_adapter
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
## Migration Strategy
|
| 28 |
-
|
| 29 |
-
### Option 1: Use Compatibility Layer (Recommended - Minimal Risk)
|
| 30 |
-
**Pros:**
|
| 31 |
-
- Zero code changes in OMIRL tasks
|
| 32 |
-
- Identical function signatures
|
| 33 |
-
- Immediate migration with no testing overhead
|
| 34 |
-
- Full backward compatibility guaranteed
|
| 35 |
-
|
| 36 |
-
**Cons:**
|
| 37 |
-
- Still using legacy API (but with new implementation underneath)
|
| 38 |
-
- Doesn't take advantage of new architecture features
|
| 39 |
-
|
| 40 |
-
**Implementation:**
|
| 41 |
-
```python
|
| 42 |
-
# In tools/omirl/tables/valori_stazioni.py
|
| 43 |
-
# CHANGE THIS:
|
| 44 |
-
from services.web.table_scraper import fetch_omirl_stations
|
| 45 |
-
|
| 46 |
-
# TO THIS:
|
| 47 |
-
from services.web.compat import fetch_omirl_stations
|
| 48 |
-
```
|
| 49 |
-
|
| 50 |
-
### Option 2: Use New Architecture Directly (Future-Proof)
|
| 51 |
-
**Pros:**
|
| 52 |
-
- Uses new architecture directly
|
| 53 |
-
- Access to advanced configuration options
|
| 54 |
-
- Better error handling and logging
|
| 55 |
-
- More flexible and extensible
|
| 56 |
-
|
| 57 |
-
**Cons:**
|
| 58 |
-
- Requires code changes in OMIRL tasks
|
| 59 |
-
- Need to update function calls and parameters
|
| 60 |
-
- More testing required
|
| 61 |
-
|
| 62 |
-
**Implementation:**
|
| 63 |
-
```python
|
| 64 |
-
# In tools/omirl/tables/valori_stazioni.py
|
| 65 |
-
from services.web.adapters.omirl_adapter import OMIRLAdapter
|
| 66 |
-
|
| 67 |
-
# Replace old function calls with new adapter
|
| 68 |
-
adapter = OMIRLAdapter()
|
| 69 |
-
result = await adapter.fetch_valori_stazioni_data(sensor_filter=sensor_type)
|
| 70 |
-
station_data = result.data
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
## Recommended Migration Path
|
| 74 |
-
|
| 75 |
-
### Phase 1: Immediate Migration (Compatibility Layer)
|
| 76 |
-
1. Update imports in OMIRL task files
|
| 77 |
-
2. No other code changes needed
|
| 78 |
-
3. Run existing tests to validate
|
| 79 |
-
4. Deploy with confidence
|
| 80 |
-
|
| 81 |
-
### Phase 2: Future Enhancement (New Architecture)
|
| 82 |
-
1. Gradually migrate to new architecture when adding new features
|
| 83 |
-
2. Take advantage of advanced configuration options
|
| 84 |
-
3. Improve error handling and monitoring
|
| 85 |
-
|
| 86 |
-
## Implementation Steps
|
| 87 |
-
|
| 88 |
-
### Step 1: Update Valori Stazioni Import
|
| 89 |
-
File: `tools/omirl/tables/valori_stazioni.py`
|
| 90 |
-
```python
|
| 91 |
-
# Line 19: Change import
|
| 92 |
-
from services.web.compat import fetch_omirl_stations
|
| 93 |
-
```
|
| 94 |
-
|
| 95 |
-
### Step 2: Update Massimi Precipitazione Import
|
| 96 |
-
File: `tools/omirl/tables/massimi_precipitazione.py`
|
| 97 |
-
```python
|
| 98 |
-
# Line 30: Change import
|
| 99 |
-
from services.web.compat import fetch_omirl_massimi_precipitazioni
|
| 100 |
-
```
|
| 101 |
-
|
| 102 |
-
### Step 3: Test Migration
|
| 103 |
-
```bash
|
| 104 |
-
cd /home/jeanbaptistebove/projects/operations
|
| 105 |
-
python tests/test_omirl_integration.py
|
| 106 |
-
python tests/omirl/test_adapter_integration.py
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
### Step 4: Validate Integration
|
| 110 |
-
```bash
|
| 111 |
-
cd /home/jeanbaptistebove/projects/operations
|
| 112 |
-
python tests/services/test_compatibility.py
|
| 113 |
-
```
|
| 114 |
-
|
| 115 |
-
## Risk Assessment
|
| 116 |
-
|
| 117 |
-
### Low Risk Items ✅
|
| 118 |
-
- Compatibility layer maintains exact same API
|
| 119 |
-
- Existing tests will continue to pass
|
| 120 |
-
- Zero breaking changes
|
| 121 |
-
- Can rollback by changing imports back
|
| 122 |
-
|
| 123 |
-
### Medium Risk Items ⚠️
|
| 124 |
-
- New web architecture has different internal implementation
|
| 125 |
-
- Network timing might be slightly different
|
| 126 |
-
- Need to validate all OMIRL-specific configurations preserved
|
| 127 |
-
|
| 128 |
-
### High Risk Items ❌
|
| 129 |
-
- None for compatibility layer approach
|
| 130 |
-
|
| 131 |
-
## Validation Checklist
|
| 132 |
-
|
| 133 |
-
### Pre-Migration
|
| 134 |
-
- [ ] All existing OMIRL tests passing
|
| 135 |
-
- [ ] New web services architecture tests passing
|
| 136 |
-
- [ ] Compatibility layer tests passing
|
| 137 |
-
|
| 138 |
-
### Post-Migration
|
| 139 |
-
- [ ] OMIRL valori_stazioni extraction working
|
| 140 |
-
- [ ] OMIRL massimi_precipitazioni extraction working
|
| 141 |
-
- [ ] All sensor type filters working
|
| 142 |
-
- [ ] Geographic filters (provincia, comune) working
|
| 143 |
-
- [ ] Data structure consistency maintained
|
| 144 |
-
- [ ] Performance within acceptable range
|
| 145 |
-
- [ ] Error handling working correctly
|
| 146 |
-
|
| 147 |
-
## Rollback Plan
|
| 148 |
-
|
| 149 |
-
If issues are discovered:
|
| 150 |
-
1. Change imports back to original:
|
| 151 |
-
```python
|
| 152 |
-
from services.web.table_scraper import fetch_omirl_stations
|
| 153 |
-
from services.web.table_scraper import fetch_omirl_massimi_precipitazioni
|
| 154 |
-
```
|
| 155 |
-
2. Restart services
|
| 156 |
-
3. Investigate issues in new architecture
|
| 157 |
-
4. Fix and re-migrate
|
| 158 |
-
|
| 159 |
-
## Success Criteria
|
| 160 |
-
|
| 161 |
-
Migration is successful when:
|
| 162 |
-
1. All existing OMIRL functionality works unchanged
|
| 163 |
-
2. All existing tests pass
|
| 164 |
-
3. Performance is comparable or better
|
| 165 |
-
4. No new errors or warnings
|
| 166 |
-
5. Data quality and structure unchanged
|
| 167 |
-
|
| 168 |
-
## Benefits After Migration
|
| 169 |
-
|
| 170 |
-
1. **Future-Proof**: Using modern, maintainable architecture
|
| 171 |
-
2. **Separation of Concerns**: Generic vs OMIRL-specific logic separated
|
| 172 |
-
3. **Extensibility**: Easy to add new websites beyond OMIRL
|
| 173 |
-
4. **Testing**: Better test coverage and validation
|
| 174 |
-
5. **Configuration**: All OMIRL knowledge centralized and configurable
|
| 175 |
-
6. **Error Handling**: Improved error messages and recovery
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/PHASE_1_LLM_SUMMARIZATION_COMPLETE.md
DELETED
|
@@ -1,428 +0,0 @@
|
|
| 1 |
-
# Phase 1: LLM-Based Weather Data Summarization - COMPLETED ✅
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
This document provides comprehensive documentation for **Phase 1: LLM-Based Weather Data Summarization**, which replaces basic "X stations found" messages with intelligent, context-aware weather analysis using the Gemini API.
|
| 6 |
-
|
| 7 |
-
## 🎯 Implementation Summary
|
| 8 |
-
|
| 9 |
-
### What Was Built
|
| 10 |
-
|
| 11 |
-
**Intelligent Weather Summarization Service** (`services/text/summarization.py`)
|
| 12 |
-
- LLM-powered data analysis using Gemini Flash API
|
| 13 |
-
- Context-aware summary generation in Italian
|
| 14 |
-
- Fallback mechanisms for API unavailability
|
| 15 |
-
- Statistical analysis of weather station data
|
| 16 |
-
- Professional operational language formatting
|
| 17 |
-
|
| 18 |
-
### Key Improvements
|
| 19 |
-
|
| 20 |
-
**Before (Basic Summary):**
|
| 21 |
-
```
|
| 22 |
-
🌊 OMIRL - Estratte 69 stazioni meteo
|
| 23 |
-
📋 Sensore: Temperatura
|
| 24 |
-
🗺️ Provincia: GE
|
| 25 |
-
⏰ 16:39:22
|
| 26 |
-
```
|
| 27 |
-
|
| 28 |
-
**After (Intelligent Analysis):**
|
| 29 |
-
```
|
| 30 |
-
🌡️ **Temperatura Genova**: Nessun dato disponibile dalle 69 stazioni meteo OMIRL
|
| 31 |
-
in provincia di Genova. Impossibile determinare valori minimi, massimi o medi.
|
| 32 |
-
Si consiglia di verificare l'operatività delle stazioni.
|
| 33 |
-
```
|
| 34 |
-
|
| 35 |
-
## 🔧 Technical Implementation
|
| 36 |
-
|
| 37 |
-
### 1. New Summarization Service
|
| 38 |
-
|
| 39 |
-
**File:** `services/text/summarization.py`
|
| 40 |
-
|
| 41 |
-
**Core Components:**
|
| 42 |
-
- `WeatherDataSummarizer` class: Main service orchestrator
|
| 43 |
-
- `summarize_weather_data()` function: Public API interface
|
| 44 |
-
- Statistical analysis engine for weather data
|
| 45 |
-
- Gemini API integration with fallback mechanisms
|
| 46 |
-
|
| 47 |
-
**Key Features:**
|
| 48 |
-
```python
|
| 49 |
-
class WeatherDataSummarizer:
|
| 50 |
-
"""Intelligent weather data summarization using Gemini API"""
|
| 51 |
-
|
| 52 |
-
async def summarize_weather_data(
|
| 53 |
-
self,
|
| 54 |
-
station_data: List[Dict[str, Any]],
|
| 55 |
-
query_context: str = "",
|
| 56 |
-
sensor_type: str = "",
|
| 57 |
-
filters: Dict[str, Any] = None,
|
| 58 |
-
language: str = "it"
|
| 59 |
-
) -> str:
|
| 60 |
-
"""Generate intelligent summary of weather station data"""
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
**LLM Prompt Engineering:**
|
| 64 |
-
```python
|
| 65 |
-
def _build_summarization_prompt(self, data_analysis, query_context, sensor_type, filters, language):
|
| 66 |
-
"""Build context-aware prompt for LLM summarization"""
|
| 67 |
-
|
| 68 |
-
prompt = f"""
|
| 69 |
-
Sei un esperto meteorologo che analizza dati delle stazioni meteo OMIRL della Liguria.
|
| 70 |
-
|
| 71 |
-
CONTESTO RICHIESTA: "{query_context}"
|
| 72 |
-
DATI ANALIZZATI: {json.dumps(data_summary, indent=2)}
|
| 73 |
-
|
| 74 |
-
COMPITO:
|
| 75 |
-
Genera un riassunto operativo in italiano (max 4 righe) che includa:
|
| 76 |
-
1. Emoji appropriata per il tipo di sensore
|
| 77 |
-
2. Condizioni attuali principali con valori specifici
|
| 78 |
-
3. Range di valori e eventualmente stazioni significative
|
| 79 |
-
4. Osservazione utile o pattern geografico se evidente
|
| 80 |
-
"""
|
| 81 |
-
```
|
| 82 |
-
|
| 83 |
-
### 2. OMIRL Adapter Integration
|
| 84 |
-
|
| 85 |
-
**File:** `tools/omirl/adapter.py`
|
| 86 |
-
|
| 87 |
-
**Changes Made:**
|
| 88 |
-
- Replaced `_generate_basic_summary_temp()` with intelligent summarization
|
| 89 |
-
- Added conditional import to avoid circular dependencies
|
| 90 |
-
- Integrated fallback mechanism for service unavailability
|
| 91 |
-
|
| 92 |
-
**Updated Code:**
|
| 93 |
-
```python
|
| 94 |
-
# Generate intelligent summary using LLM-based summarization service
|
| 95 |
-
try:
|
| 96 |
-
from services.text.summarization import summarize_weather_data
|
| 97 |
-
summary_text = await summarize_weather_data(
|
| 98 |
-
station_data=result.data,
|
| 99 |
-
query_context=f"{mode} {subtask} {filters}",
|
| 100 |
-
sensor_type=filters.get("tipo_sensore", ""),
|
| 101 |
-
filters=filters,
|
| 102 |
-
language=language
|
| 103 |
-
)
|
| 104 |
-
except ImportError as e:
|
| 105 |
-
# Fallback to basic summary
|
| 106 |
-
summary_text = _generate_fallback_summary()
|
| 107 |
-
```
|
| 108 |
-
|
| 109 |
-
### 3. Data Analysis Engine
|
| 110 |
-
|
| 111 |
-
**Statistical Analysis Features:**
|
| 112 |
-
- Automatic numeric value extraction from multiple field formats
|
| 113 |
-
- Min/max/average calculations
|
| 114 |
-
- Notable station identification (highest/lowest values)
|
| 115 |
-
- Geographic pattern detection
|
| 116 |
-
- Data quality assessment
|
| 117 |
-
|
| 118 |
-
**Example Analysis Output:**
|
| 119 |
-
```python
|
| 120 |
-
analysis = {
|
| 121 |
-
'total_stations': 69,
|
| 122 |
-
'stations_with_data': 13,
|
| 123 |
-
'has_values': True,
|
| 124 |
-
'min_value': 22.1,
|
| 125 |
-
'max_value': 25.6,
|
| 126 |
-
'avg_value': 23.8,
|
| 127 |
-
'value_range': 3.5,
|
| 128 |
-
'unit': '°C',
|
| 129 |
-
'highest_station': {'nome': 'Genova Centro', 'valore': 25.6},
|
| 130 |
-
'lowest_station': {'nome': 'Genova Voltri', 'valore': 22.1}
|
| 131 |
-
}
|
| 132 |
-
```
|
| 133 |
-
|
| 134 |
-
## 🚀 API Integration
|
| 135 |
-
|
| 136 |
-
### Gemini API Configuration
|
| 137 |
-
|
| 138 |
-
**Environment Setup:**
|
| 139 |
-
```bash
|
| 140 |
-
# .env file
|
| 141 |
-
GEMINI_API_KEY=your_actual_gemini_api_key_here
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
**API Client Configuration:**
|
| 145 |
-
```python
|
| 146 |
-
import google.generativeai as genai
|
| 147 |
-
from agent.config.env_config import get_api_key
|
| 148 |
-
|
| 149 |
-
def __init__(self):
|
| 150 |
-
self.api_key = get_api_key('GEMINI_API_KEY')
|
| 151 |
-
if self.api_key:
|
| 152 |
-
genai.configure(api_key=self.api_key)
|
| 153 |
-
self.model = genai.GenerativeModel('gemini-1.5-flash')
|
| 154 |
-
```
|
| 155 |
-
|
| 156 |
-
**Response Processing:**
|
| 157 |
-
```python
|
| 158 |
-
async def _generate_llm_summary(self, data_analysis, query_context, sensor_type, filters, language):
|
| 159 |
-
prompt = self._build_summarization_prompt(data_analysis, ...)
|
| 160 |
-
response = self.model.generate_content(prompt)
|
| 161 |
-
summary = response.text.strip()
|
| 162 |
-
return summary
|
| 163 |
-
```
|
| 164 |
-
|
| 165 |
-
## 📊 Real-World Test Results
|
| 166 |
-
|
| 167 |
-
### Test Case 1: Temperature Data with Values
|
| 168 |
-
```
|
| 169 |
-
Input: 3 Genova temperature stations (20.8°C - 22.1°C)
|
| 170 |
-
Output: "🌡️ **Temperatura Genova**: 20.8°C-22.1°C con una media di 21.5°C
|
| 171 |
-
nelle 3 stazioni attive. Temperatura massima registrata a Genova Voltri
|
| 172 |
-
(22.1°C), minima a Genova Quarto (20.8°C). Lieve escursione termica
|
| 173 |
-
tra le aree costiere."
|
| 174 |
-
```
|
| 175 |
-
|
| 176 |
-
### Test Case 2: No Data Available
|
| 177 |
-
```
|
| 178 |
-
Input: 69 Genova stations but no current readings
|
| 179 |
-
Output: "🌡️ **Temperatura Genova**: Nessun dato disponibile dalle 69 stazioni
|
| 180 |
-
meteo OMIRL in provincia di Genova. Impossibile determinare valori
|
| 181 |
-
minimi, massimi o medi. Si consiglia di verificare l'operatività
|
| 182 |
-
delle stazioni."
|
| 183 |
-
```
|
| 184 |
-
|
| 185 |
-
### Test Case 3: Comprehensive Regional Data
|
| 186 |
-
```
|
| 187 |
-
Input: 184 stations across all Liguria provinces
|
| 188 |
-
Output: "🌡️ **Temperatura Liguria**: 22.1°C-29.7°C in 184 stazioni regionali.
|
| 189 |
-
Temperature più elevate nell'entroterra (Airole 29.7°C), più fresche
|
| 190 |
-
sulla costa (Genova 22.1°C). Gradiente termico costa-entroterra evidente."
|
| 191 |
-
```
|
| 192 |
-
|
| 193 |
-
## 🔧 Error Handling & Fallbacks
|
| 194 |
-
|
| 195 |
-
### Robust Error Management
|
| 196 |
-
|
| 197 |
-
**API Unavailability:**
|
| 198 |
-
```python
|
| 199 |
-
if self.model and self.api_key:
|
| 200 |
-
return await self._generate_llm_summary(...)
|
| 201 |
-
else:
|
| 202 |
-
return self._generate_enhanced_fallback_summary(...)
|
| 203 |
-
```
|
| 204 |
-
|
| 205 |
-
**Circular Import Protection:**
|
| 206 |
-
```python
|
| 207 |
-
try:
|
| 208 |
-
from services.text.summarization import summarize_weather_data
|
| 209 |
-
summary_text = await summarize_weather_data(...)
|
| 210 |
-
except ImportError as e:
|
| 211 |
-
summary_text = _generate_fallback_summary()
|
| 212 |
-
```
|
| 213 |
-
|
| 214 |
-
**Data Quality Issues:**
|
| 215 |
-
```python
|
| 216 |
-
if not data_analysis.get('has_values', False):
|
| 217 |
-
return self._generate_fallback_summary()
|
| 218 |
-
```
|
| 219 |
-
|
| 220 |
-
### Fallback Hierarchy
|
| 221 |
-
|
| 222 |
-
1. **Primary:** LLM-generated intelligent summary
|
| 223 |
-
2. **Secondary:** Enhanced template-based summary with statistics
|
| 224 |
-
3. **Tertiary:** Basic fallback with metadata only
|
| 225 |
-
|
| 226 |
-
## 🎯 Business Impact
|
| 227 |
-
|
| 228 |
-
### User Experience Transformation
|
| 229 |
-
|
| 230 |
-
**Information Quality:**
|
| 231 |
-
- From metadata → actionable weather insights
|
| 232 |
-
- From technical → operational language
|
| 233 |
-
- From static → context-aware responses
|
| 234 |
-
|
| 235 |
-
**Professional Communication:**
|
| 236 |
-
- Meteorological expertise embedded in responses
|
| 237 |
-
- Appropriate technical vocabulary
|
| 238 |
-
- Geographic and temporal context included
|
| 239 |
-
|
| 240 |
-
**Operational Value:**
|
| 241 |
-
- Immediate understanding of weather conditions
|
| 242 |
-
- Identification of problematic stations/areas
|
| 243 |
-
- Guidance for further investigation
|
| 244 |
-
|
| 245 |
-
## 📋 Technical Dependencies
|
| 246 |
-
|
| 247 |
-
### Required Packages
|
| 248 |
-
```
|
| 249 |
-
google-generativeai>=0.3.0 # Gemini API client
|
| 250 |
-
python-dotenv>=1.0.0 # Environment variable loading
|
| 251 |
-
```
|
| 252 |
-
|
| 253 |
-
### System Integration Points
|
| 254 |
-
```
|
| 255 |
-
services/text/summarization.py → New summarization service
|
| 256 |
-
tools/omirl/adapter.py → Updated OMIRL tool integration
|
| 257 |
-
agent/config/env_config.py → Existing API key management
|
| 258 |
-
```
|
| 259 |
-
|
| 260 |
-
### File Structure
|
| 261 |
-
```
|
| 262 |
-
services/
|
| 263 |
-
text/
|
| 264 |
-
__init__.py
|
| 265 |
-
formatters.py # Existing text utilities
|
| 266 |
-
summarization.py # New LLM summarization service
|
| 267 |
-
```
|
| 268 |
-
|
| 269 |
-
## 🔍 Code Quality Features
|
| 270 |
-
|
| 271 |
-
### Documentation Standards
|
| 272 |
-
- Comprehensive docstrings for all functions
|
| 273 |
-
- Type hints throughout the codebase
|
| 274 |
-
- Usage examples in docstrings
|
| 275 |
-
- Clear parameter descriptions
|
| 276 |
-
|
| 277 |
-
### Testing Capabilities
|
| 278 |
-
```python
|
| 279 |
-
# Direct service testing
|
| 280 |
-
summary = await summarize_weather_data(
|
| 281 |
-
station_data=test_stations,
|
| 282 |
-
query_context='temperatura genova',
|
| 283 |
-
sensor_type='Temperatura'
|
| 284 |
-
)
|
| 285 |
-
|
| 286 |
-
# Integration testing
|
| 287 |
-
result = await omirl_tool(
|
| 288 |
-
filters={'tipo_sensore': 'Temperatura', 'provincia': 'GE'}
|
| 289 |
-
)
|
| 290 |
-
```
|
| 291 |
-
|
| 292 |
-
### Performance Considerations
|
| 293 |
-
- Async/await throughout for non-blocking operations
|
| 294 |
-
- Efficient data analysis algorithms
|
| 295 |
-
- Minimal memory footprint
|
| 296 |
-
- Response caching opportunities identified
|
| 297 |
-
|
| 298 |
-
## �️ Geographic Parameter Resolution System
|
| 299 |
-
|
| 300 |
-
### **Critical Challenge Identified & Solved**
|
| 301 |
-
|
| 302 |
-
During Phase 1 implementation, we discovered a fundamental mismatch between user natural language queries and OMIRL table structure:
|
| 303 |
-
|
| 304 |
-
**The Problem:**
|
| 305 |
-
- Users ask: `"sensori temperatura in Genova"`
|
| 306 |
-
- LLM Router extracts: `comune="Genova"`
|
| 307 |
-
- OMIRL table requires: `provincia="GE"` (2-letter province code)
|
| 308 |
-
- Result: Geographic filters failed, causing incorrect data retrieval
|
| 309 |
-
|
| 310 |
-
**The Solution:**
|
| 311 |
-
|
| 312 |
-
Implemented intelligent geographic parameter resolution in `tools/omirl/adapter.py`:
|
| 313 |
-
|
| 314 |
-
```python
|
| 315 |
-
# 1. Auto-detect province from municipality
|
| 316 |
-
if comune and not provincia:
|
| 317 |
-
comune_upper = comune.upper().strip()
|
| 318 |
-
if comune_upper in MUNICIPALITY_TO_PROVINCE:
|
| 319 |
-
provincia = MUNICIPALITY_TO_PROVINCE[comune_upper]
|
| 320 |
-
print(f"🗺️ Auto-determined province '{provincia}' for municipality '{comune}'")
|
| 321 |
-
|
| 322 |
-
# 2. Convert full province names to OMIRL 2-letter codes
|
| 323 |
-
if provincia and provincia.upper() in PROVINCE_NAME_TO_CODE:
|
| 324 |
-
provincia_code = PROVINCE_NAME_TO_CODE[provincia.upper()]
|
| 325 |
-
print(f"🗺️ Converting province '{provincia}' → '{provincia_code}' for OMIRL table filtering")
|
| 326 |
-
provincia = provincia_code
|
| 327 |
-
```
|
| 328 |
-
|
| 329 |
-
### **Mapping Dictionaries:**
|
| 330 |
-
|
| 331 |
-
```python
|
| 332 |
-
MUNICIPALITY_TO_PROVINCE = {
|
| 333 |
-
'GENOVA': 'GENOVA',
|
| 334 |
-
'SAVONA': 'SAVONA',
|
| 335 |
-
'IMPERIA': 'IMPERIA',
|
| 336 |
-
'LA SPEZIA': 'LA SPEZIA',
|
| 337 |
-
'SANREMO': 'IMPERIA',
|
| 338 |
-
# ... comprehensive municipality coverage
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
PROVINCE_NAME_TO_CODE = {
|
| 342 |
-
'GENOVA': 'GE',
|
| 343 |
-
'SAVONA': 'SV',
|
| 344 |
-
'IMPERIA': 'IM',
|
| 345 |
-
'LA SPEZIA': 'SP'
|
| 346 |
-
}
|
| 347 |
-
```
|
| 348 |
-
|
| 349 |
-
### **Geographic Ambiguity Resolution**
|
| 350 |
-
|
| 351 |
-
Updated `agent/prompts.py` with intelligent geographic preference rules:
|
| 352 |
-
|
| 353 |
-
```python
|
| 354 |
-
### Geographic Ambiguity Resolution Rules
|
| 355 |
-
- For ambiguous names like "Genova": prefer city-specific results (comune="Genova") over province-wide (provincia="GENOVA")
|
| 356 |
-
- Users asking about "Genova" typically want the city itself, not the entire province
|
| 357 |
-
- Only use provincia when user explicitly mentions "provincia di Genova" or similar broad terms
|
| 358 |
-
|
| 359 |
-
Examples:
|
| 360 |
-
- "sensori in Genova" → comune="Genova" (city-specific: ~13 stations)
|
| 361 |
-
- "sensori nella provincia di Genova" → provincia="GENOVA" (province-wide: ~69 stations)
|
| 362 |
-
```
|
| 363 |
-
|
| 364 |
-
### **Validated Results:**
|
| 365 |
-
- ✅ **Query**: `"sensori temperatura in Genova"`
|
| 366 |
-
- ✅ **Auto-detection**: `comune="Genova"` → `provincia="GENOVA"`
|
| 367 |
-
- ✅ **Conversion**: `"GENOVA"` → `"GE"` for OMIRL compatibility
|
| 368 |
-
- ✅ **Data retrieval**: 13 temperature stations found in Genova city
|
| 369 |
-
- ✅ **LLM Summary**: Professional Italian meteorological analysis generated
|
| 370 |
-
|
| 371 |
-
This system ensures seamless conversion from natural language geographic references to the technical parameters required by OMIRL data sources.
|
| 372 |
-
|
| 373 |
-
## �🚀 Future Enhancement Opportunities
|
| 374 |
-
|
| 375 |
-
### Immediate Improvements
|
| 376 |
-
1. **Response Caching:** Cache LLM responses for identical data patterns
|
| 377 |
-
2. **Template Optimization:** Refine prompts based on real usage patterns
|
| 378 |
-
3. **Multi-language Support:** Extend beyond Italian
|
| 379 |
-
4. **Error Analytics:** Track and optimize fallback usage patterns
|
| 380 |
-
|
| 381 |
-
### Advanced Features
|
| 382 |
-
1. **Trend Analysis:** Historical data comparison
|
| 383 |
-
2. **Anomaly Detection:** Identify unusual weather patterns
|
| 384 |
-
3. **Predictive Insights:** Weather trend forecasting
|
| 385 |
-
4. **Visual Summarization:** Charts and graphs generation
|
| 386 |
-
|
| 387 |
-
## 🎉 Implementation Status
|
| 388 |
-
|
| 389 |
-
### ✅ Completed Features
|
| 390 |
-
- [x] LLM-based summarization service
|
| 391 |
-
- [x] Gemini API integration with authentication
|
| 392 |
-
- [x] Statistical data analysis engine
|
| 393 |
-
- [x] Context-aware prompt engineering
|
| 394 |
-
- [x] Fallback mechanisms for reliability
|
| 395 |
-
- [x] OMIRL adapter integration
|
| 396 |
-
- [x] Comprehensive error handling
|
| 397 |
-
- [x] Professional Italian language output
|
| 398 |
-
- [x] **Geographic parameter resolution system**
|
| 399 |
-
- [x] **Municipality to province auto-detection**
|
| 400 |
-
- [x] **Province name to code conversion (GENOVA→GE)**
|
| 401 |
-
- [x] **Geographic ambiguity handling in prompts**
|
| 402 |
-
- [x] Real-world testing and validation
|
| 403 |
-
|
| 404 |
-
### 📊 Success Metrics
|
| 405 |
-
- **Response Quality:** Transformed from basic metadata to professional analysis
|
| 406 |
-
- **Information Density:** 5x more useful information per response
|
| 407 |
-
- **User Experience:** Professional meteorological language
|
| 408 |
-
- **System Reliability:** Robust fallback mechanisms ensure 100% uptime
|
| 409 |
-
- **API Integration:** Successful Gemini Flash API integration with <2s response times
|
| 410 |
-
|
| 411 |
-
## 🏆 Conclusion
|
| 412 |
-
|
| 413 |
-
**Phase 1: LLM-Based Weather Data Summarization is SUCCESSFULLY COMPLETED** ✅
|
| 414 |
-
|
| 415 |
-
This implementation provides:
|
| 416 |
-
- **Intelligent Data Analysis:** Real weather insights instead of basic counts
|
| 417 |
-
- **Professional Communication:** Meteorological expertise in every response
|
| 418 |
-
- **Robust Architecture:** Fallback mechanisms ensure reliable operation
|
| 419 |
-
- **Scalable Foundation:** Easy extension to additional data types and languages
|
| 420 |
-
|
| 421 |
-
The system now transforms raw weather station data into actionable, professional summaries that provide genuine operational value to users.
|
| 422 |
-
|
| 423 |
-
---
|
| 424 |
-
|
| 425 |
-
**Document Version:** 1.0
|
| 426 |
-
**Implementation Date:** December 19, 2024
|
| 427 |
-
**Status:** Phase 1 Complete ✅
|
| 428 |
-
**Next Phase:** Prompt Optimization & Enhanced Query Recognition
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/SECURITY_SUMMARY.md
DELETED
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
# 🔒 API Key Security Summary
|
| 2 |
-
|
| 3 |
-
## ✅ Security Measures Implemented
|
| 4 |
-
|
| 5 |
-
### 1. **Enhanced .gitignore Protection**
|
| 6 |
-
```gitignore
|
| 7 |
-
# API Keys and Secrets
|
| 8 |
-
.streamlit/secrets.toml
|
| 9 |
-
.env
|
| 10 |
-
*.env
|
| 11 |
-
.env.*
|
| 12 |
-
secrets.json
|
| 13 |
-
config/secrets.*
|
| 14 |
-
*.key
|
| 15 |
-
*.pem
|
| 16 |
-
|
| 17 |
-
# Environment-specific config files
|
| 18 |
-
.env.local
|
| 19 |
-
.env.development
|
| 20 |
-
.env.staging
|
| 21 |
-
.env.production
|
| 22 |
-
|
| 23 |
-
# Hugging Face tokens
|
| 24 |
-
.huggingface_token
|
| 25 |
-
hf_token.txt
|
| 26 |
-
|
| 27 |
-
# OpenAI and LLM API keys
|
| 28 |
-
openai_key.txt
|
| 29 |
-
anthropic_key.txt
|
| 30 |
-
gemini_key.txt
|
| 31 |
-
api_keys.json
|
| 32 |
-
```
|
| 33 |
-
|
| 34 |
-
### 2. **Automated Security Check Script** (`check_secrets.sh`)
|
| 35 |
-
- **API Key Pattern Detection**: Detects Gemini, OpenAI, Anthropic, HF, GitHub tokens
|
| 36 |
-
- **Staged Changes Scan**: Prevents committing secrets
|
| 37 |
-
- **File Protection Verification**: Ensures secrets are properly ignored
|
| 38 |
-
- **Environment Variable Check**: Validates current API key setup
|
| 39 |
-
|
| 40 |
-
### 3. **Git Pre-commit Hook** (`.githooks/pre-commit`)
|
| 41 |
-
- **Automatic Execution**: Runs security check before every commit
|
| 42 |
-
- **Commit Blocking**: Prevents commits containing API keys
|
| 43 |
-
- **Zero-configuration**: Works automatically after setup
|
| 44 |
-
|
| 45 |
-
### 4. **Multi-Environment API Key Management**
|
| 46 |
-
```python
|
| 47 |
-
from agent.config.env_config import get_api_key, load_environment
|
| 48 |
-
|
| 49 |
-
# Secure loading from multiple sources:
|
| 50 |
-
# 1. Environment variables (Docker, HF Spaces)
|
| 51 |
-
# 2. .env file (local development)
|
| 52 |
-
# 3. Streamlit secrets (Streamlit apps)
|
| 53 |
-
load_environment()
|
| 54 |
-
api_key = get_api_key('GEMINI_API_KEY') # Validated and logged
|
| 55 |
-
```
|
| 56 |
-
|
| 57 |
-
## 🎯 **Current Security Status**
|
| 58 |
-
|
| 59 |
-
### ✅ **Protected Files:**
|
| 60 |
-
- `.env` - Local development API keys
|
| 61 |
-
- `.streamlit/secrets.toml` - Streamlit app secrets
|
| 62 |
-
- `*.key`, `secrets.*` - Any secret files
|
| 63 |
-
- Environment-specific configs (`.env.local`, `.env.production`, etc.)
|
| 64 |
-
|
| 65 |
-
### ✅ **Detection Patterns:**
|
| 66 |
-
- **Gemini**: `AI + za + Sy...` (35 chars)
|
| 67 |
-
- **OpenAI**: `sk-...` (48+ chars)
|
| 68 |
-
- **Anthropic**: `sk-ant-...` (95+ chars)
|
| 69 |
-
- **Hugging Face**: `hf_...` (37 chars)
|
| 70 |
-
- **GitHub**: `ghp_...` (36 chars)
|
| 71 |
-
|
| 72 |
-
### ✅ **Deployment-Ready:**
|
| 73 |
-
- **Local**: Uses `.env` file
|
| 74 |
-
- **Streamlit**: Uses `secrets.toml`
|
| 75 |
-
- **Hugging Face**: Uses repository secrets
|
| 76 |
-
- **Docker**: Uses environment variables
|
| 77 |
-
|
| 78 |
-
## 🚀 **Usage for Each Platform**
|
| 79 |
-
|
| 80 |
-
### Local Development:
|
| 81 |
-
```bash
|
| 82 |
-
# API keys in .env file (protected by .gitignore)
|
| 83 |
-
export GEMINI_API_KEY="your_key_here" # Or use .env file
|
| 84 |
-
./check_secrets.sh # Manual security check
|
| 85 |
-
```
|
| 86 |
-
|
| 87 |
-
### Hugging Face Spaces:
|
| 88 |
-
```bash
|
| 89 |
-
# Add in HF Space settings > Repository secrets:
|
| 90 |
-
# GEMINI_API_KEY = your_actual_key
|
| 91 |
-
# OPENAI_API_KEY = your_actual_key
|
| 92 |
-
```
|
| 93 |
-
|
| 94 |
-
### Streamlit:
|
| 95 |
-
```toml
|
| 96 |
-
# .streamlit/secrets.toml (protected by .gitignore)
|
| 97 |
-
GEMINI_API_KEY = "your_key_here"
|
| 98 |
-
OPENAI_API_KEY = "your_key_here"
|
| 99 |
-
```
|
| 100 |
-
|
| 101 |
-
## 🛡️ **Security Verification**
|
| 102 |
-
|
| 103 |
-
Run the security check anytime:
|
| 104 |
-
```bash
|
| 105 |
-
./check_secrets.sh
|
| 106 |
-
```
|
| 107 |
-
|
| 108 |
-
**Expected output:**
|
| 109 |
-
```
|
| 110 |
-
🔒 Checking for potential API key leaks...
|
| 111 |
-
📋 Checking staged changes...
|
| 112 |
-
📂 Checking working directory...
|
| 113 |
-
✅ Secret file properly ignored: ./.env
|
| 114 |
-
✅ Secret file properly ignored: ./.streamlit/secrets.toml
|
| 115 |
-
🛡️ Checking .gitignore coverage...
|
| 116 |
-
✅ Protected: .env
|
| 117 |
-
✅ Protected: *.env
|
| 118 |
-
✅ Protected: .streamlit/secrets.toml
|
| 119 |
-
✅ Protected: *.key
|
| 120 |
-
✅ Protected: secrets.*
|
| 121 |
-
🔑 Checking current environment...
|
| 122 |
-
✅ GEMINI_API_KEY is set (AI***...)
|
| 123 |
-
✅ OPENAI_API_KEY is set (sk-pro...)
|
| 124 |
-
🎯 Security check complete!
|
| 125 |
-
```
|
| 126 |
-
|
| 127 |
-
## ⚠️ **If Security Check Fails**
|
| 128 |
-
|
| 129 |
-
### API Key in Staged Changes:
|
| 130 |
-
```bash
|
| 131 |
-
git reset HEAD <file> # Unstage the file
|
| 132 |
-
# Remove the API key from the file
|
| 133 |
-
# Add the file pattern to .gitignore
|
| 134 |
-
```
|
| 135 |
-
|
| 136 |
-
### Secret File Tracked by Git:
|
| 137 |
-
```bash
|
| 138 |
-
git rm --cached <file> # Remove from git tracking
|
| 139 |
-
echo "<file>" >> .gitignore # Add to .gitignore
|
| 140 |
-
```
|
| 141 |
-
|
| 142 |
-
## 🔄 **Auto-Protection Setup**
|
| 143 |
-
|
| 144 |
-
The git pre-commit hook is already configured and will:
|
| 145 |
-
1. **Run automatically** before each commit
|
| 146 |
-
2. **Block commits** containing API keys
|
| 147 |
-
3. **Show clear instructions** for fixing issues
|
| 148 |
-
|
| 149 |
-
**Manual setup if needed:**
|
| 150 |
-
```bash
|
| 151 |
-
chmod +x .githooks/pre-commit
|
| 152 |
-
git config core.hooksPath .githooks
|
| 153 |
-
```
|
| 154 |
-
|
| 155 |
-
## 📋 **Security Checklist**
|
| 156 |
-
|
| 157 |
-
- [x] API keys stored in environment-specific files
|
| 158 |
-
- [x] All secret files in .gitignore
|
| 159 |
-
- [x] Automated security scanning
|
| 160 |
-
- [x] Pre-commit hooks configured
|
| 161 |
-
- [x] Multi-platform deployment support
|
| 162 |
-
- [x] Pattern detection for all major API providers
|
| 163 |
-
- [x] Validation and error handling
|
| 164 |
-
- [x] Documentation and usage guides
|
| 165 |
-
|
| 166 |
-
Your API keys are now fully protected across all deployment scenarios! 🔐
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/STEP12_CONFIG_NAVIGATION_GUIDE.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🗺️ CONFIG-DRIVEN ARCHITECTURE NAVIGATION GUIDE
|
| 2 |
+
|
| 3 |
+
## **📋 OVERVIEW: CONFIGURATION HIERARCHY**
|
| 4 |
+
|
| 5 |
+
Your operations system has a clean **2-level configuration architecture**:
|
| 6 |
+
|
| 7 |
+
### **🌐 GLOBAL LEVEL** (`/agent/config/`)
|
| 8 |
+
Controls **agent-wide behavior** and **cross-tool functionality**
|
| 9 |
+
|
| 10 |
+
### **🔧 TOOL LEVEL** (`/tools/{tool_name}/config/`)
|
| 11 |
+
Controls **tool-specific behavior** and **parameters**
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## **🌐 GLOBAL CONFIGURATION LAYER**
|
| 16 |
+
|
| 17 |
+
### **📍 Location**: `/agent/config/`
|
| 18 |
+
|
| 19 |
+
| Config File | Purpose | What You Control |
|
| 20 |
+
|-------------|---------|------------------|
|
| 21 |
+
| **`response_templates.yaml`** | Agent response behavior | All user-facing messages, templates, Italian text |
|
| 22 |
+
| **`tool_registry.yaml`** | Tool discovery & routing | Which tools are available, routing keywords |
|
| 23 |
+
| **`llm_router_config.yaml`** | LLM routing logic | How requests get routed to tools |
|
| 24 |
+
| **`geography.yaml`** | Geographic constants | Provinces, regions, municipalities |
|
| 25 |
+
|
| 26 |
+
---
|
| 27 |
+
|
| 28 |
+
## **🔧 TOOL-LEVEL CONFIGURATION**
|
| 29 |
+
|
| 30 |
+
### **📍 Location**: `/tools/{tool_name}/config/`
|
| 31 |
+
|
| 32 |
+
Currently implemented: **OMIRL Tool** (`/tools/omirl/config/`)
|
| 33 |
+
|
| 34 |
+
| Config File | Purpose | What You Control |
|
| 35 |
+
|-------------|---------|------------------|
|
| 36 |
+
| **`tasks.yaml`** | Available operations | What tasks the tool can perform |
|
| 37 |
+
| **`parameters.yaml`** | Parameter definitions | Valid parameters, types, defaults |
|
| 38 |
+
| **`validation_rules.yaml`** | Input validation | Validation rules, error messages |
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## **🎯 PRACTICAL NAVIGATION GUIDE**
|
| 43 |
+
|
| 44 |
+
### **🔄 Common Control Scenarios**
|
| 45 |
+
|
| 46 |
+
#### **1. Change Agent Response Behavior**
|
| 47 |
+
```yaml
|
| 48 |
+
# Edit: /agent/config/response_templates.yaml
|
| 49 |
+
success_responses:
|
| 50 |
+
data_extraction_completed:
|
| 51 |
+
template: "✅ **{operation_name} Completata**\n" # Modify this
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
#### **2. Add New Tool to System**
|
| 55 |
+
```yaml
|
| 56 |
+
# Edit: /agent/config/tool_registry.yaml
|
| 57 |
+
tools:
|
| 58 |
+
new_tool:
|
| 59 |
+
name: "new_tool"
|
| 60 |
+
display_name: "New Tool Name"
|
| 61 |
+
enabled: true
|
| 62 |
+
# Add tool configuration
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
#### **3. Change Geographic Coverage**
|
| 66 |
+
```yaml
|
| 67 |
+
# Edit: /agent/config/geography.yaml
|
| 68 |
+
regions:
|
| 69 |
+
liguria:
|
| 70 |
+
provinces:
|
| 71 |
+
full_names:
|
| 72 |
+
- "Genova" # Modify existing
|
| 73 |
+
- "New Province" # Add new
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
#### **4. Modify Tool Capabilities**
|
| 77 |
+
```yaml
|
| 78 |
+
# Edit: /tools/omirl/config/tasks.yaml
|
| 79 |
+
tasks:
|
| 80 |
+
new_task:
|
| 81 |
+
description: "New task description"
|
| 82 |
+
route_keywords: ["keyword1", "keyword2"]
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
#### **5. Change Validation Rules**
|
| 86 |
+
```yaml
|
| 87 |
+
# Edit: /tools/omirl/config/validation_rules.yaml
|
| 88 |
+
validation_settings:
|
| 89 |
+
strict_mode: false # Change to true for stricter validation
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## **📊 CONFIGURATION IMPACT MAP**
|
| 95 |
+
|
| 96 |
+
### **When You Change...**
|
| 97 |
+
|
| 98 |
+
| Configuration | Affects | Restart Required? |
|
| 99 |
+
|---------------|---------|-------------------|
|
| 100 |
+
| `response_templates.yaml` | User-facing messages | ❌ No (hot reload) |
|
| 101 |
+
| `tool_registry.yaml` | Tool availability | ✅ Yes (registry reload) |
|
| 102 |
+
| `llm_router_config.yaml` | Request routing | ✅ Yes (router reload) |
|
| 103 |
+
| `geography.yaml` | Geographic validation | ❌ No (constants) |
|
| 104 |
+
| Tool `tasks.yaml` | Tool capabilities | ✅ Yes (tool reload) |
|
| 105 |
+
| Tool `parameters.yaml` | Parameter validation | ✅ Yes (tool reload) |
|
| 106 |
+
| Tool `validation_rules.yaml` | Input validation | ❌ No (validation reload) |
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## **🔍 DEBUGGING & TROUBLESHOOTING**
|
| 111 |
+
|
| 112 |
+
### **Configuration Loading Order**
|
| 113 |
+
1. **Agent Config** → Global settings loaded first
|
| 114 |
+
2. **Tool Registry** → Tools discovered and loaded
|
| 115 |
+
3. **Tool Configs** → Individual tool configurations loaded
|
| 116 |
+
4. **Runtime** → Templates and rules applied dynamically
|
| 117 |
+
|
| 118 |
+
### **Common Issues & Solutions**
|
| 119 |
+
|
| 120 |
+
#### **❌ Tool Not Found**
|
| 121 |
+
- Check: `/agent/config/tool_registry.yaml` - is tool enabled?
|
| 122 |
+
- Check: Tool directory exists at `/tools/{tool_name}/`
|
| 123 |
+
|
| 124 |
+
#### **❌ Validation Errors**
|
| 125 |
+
- Check: `/tools/{tool_name}/config/validation_rules.yaml`
|
| 126 |
+
- Check: Parameter definitions in `parameters.yaml`
|
| 127 |
+
|
| 128 |
+
#### **❌ Wrong Response Language**
|
| 129 |
+
- Check: `/agent/config/response_templates.yaml`
|
| 130 |
+
- Ensure all templates use Italian text
|
| 131 |
+
|
| 132 |
+
#### **❌ Geographic Validation Fails**
|
| 133 |
+
- Check: `/agent/config/geography.yaml`
|
| 134 |
+
- Verify province/region names match expected format
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## **🚀 BEST PRACTICES**
|
| 139 |
+
|
| 140 |
+
### **1. Configuration Changes**
|
| 141 |
+
- Always test in development first
|
| 142 |
+
- Use version control for config files
|
| 143 |
+
- Document changes in commit messages
|
| 144 |
+
|
| 145 |
+
### **2. Tool Development**
|
| 146 |
+
- Follow the OMIRL pattern for new tools
|
| 147 |
+
- Keep tool configs self-contained
|
| 148 |
+
- Use consistent naming conventions
|
| 149 |
+
|
| 150 |
+
### **3. Validation Strategy**
|
| 151 |
+
- Use strict validation in production
|
| 152 |
+
- Provide clear Italian error messages
|
| 153 |
+
- Test edge cases with invalid inputs
|
| 154 |
+
|
| 155 |
+
### **4. Performance Optimization**
|
| 156 |
+
- Cache frequently accessed configs
|
| 157 |
+
- Avoid complex nested structures
|
| 158 |
+
- Use simple YAML structures
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## **🛠️ PRACTICAL EXAMPLES**
|
| 163 |
+
|
| 164 |
+
### **Example 1: Add New Sensor Type**
|
| 165 |
+
```yaml
|
| 166 |
+
# /tools/omirl/config/parameters.yaml
|
| 167 |
+
sensor_types:
|
| 168 |
+
available_types:
|
| 169 |
+
- "Precipitazione"
|
| 170 |
+
- "Temperatura"
|
| 171 |
+
- "New Sensor Type" # Add here
|
| 172 |
+
```
|
| 173 |
+
|
| 174 |
+
### **Example 2: Change Success Message**
|
| 175 |
+
```yaml
|
| 176 |
+
# /agent/config/response_templates.yaml
|
| 177 |
+
success_responses:
|
| 178 |
+
omirl_data_completed:
|
| 179 |
+
template: "🎉 **Dati OMIRL Estratti con Successo!**\n" # New message
|
| 180 |
+
```
|
| 181 |
+
|
| 182 |
+
### **Example 3: Add Route Keywords**
|
| 183 |
+
```yaml
|
| 184 |
+
# /tools/omirl/config/tasks.yaml
|
| 185 |
+
tasks:
|
| 186 |
+
valori_stazioni:
|
| 187 |
+
route_keywords:
|
| 188 |
+
- "valori"
|
| 189 |
+
- "dati"
|
| 190 |
+
- "new_keyword" # Add routing keyword
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
## **📚 QUICK REFERENCE**
|
| 196 |
+
|
| 197 |
+
### **File Locations Cheat Sheet**
|
| 198 |
+
```
|
| 199 |
+
📁 /agent/config/
|
| 200 |
+
├── 📄 response_templates.yaml # User messages
|
| 201 |
+
├── 📄 tool_registry.yaml # Tool discovery
|
| 202 |
+
├── 📄 llm_router_config.yaml # Request routing
|
| 203 |
+
└── 📄 geography.yaml # Geographic data
|
| 204 |
+
|
| 205 |
+
📁 /tools/omirl/config/
|
| 206 |
+
├── 📄 tasks.yaml # OMIRL operations
|
| 207 |
+
├── 📄 parameters.yaml # OMIRL parameters
|
| 208 |
+
└── 📄 validation_rules.yaml # OMIRL validation
|
| 209 |
+
```
|
| 210 |
+
|
| 211 |
+
### **Control Points Summary**
|
| 212 |
+
- **🗣️ User Experience**: `response_templates.yaml`
|
| 213 |
+
- **🧠 Intelligence**: `llm_router_config.yaml`
|
| 214 |
+
- **🔧 Tools**: `tool_registry.yaml`
|
| 215 |
+
- **🌍 Geography**: `geography.yaml`
|
| 216 |
+
- **⚙️ Tool Behavior**: Individual tool configs
|
| 217 |
+
|
| 218 |
+
This architecture gives you **granular control** without complexity!
|
docs/TASK_4_OMIRL_INTEGRATION_COMPLETE.md
DELETED
|
@@ -1,402 +0,0 @@
|
|
| 1 |
-
# Task 4: OMIRL Integration - COMPLETED ✅
|
| 2 |
-
|
| 3 |
-
## Overview
|
| 4 |
-
|
| 5 |
-
This document provides a comprehensive summary of Task 4: **OMIRL Integration with LLM Router**, which has been successfully completed. The integration replaces the complex intent parsing system with a modern LLM-based routing approach using LangGraph workflows.
|
| 6 |
-
|
| 7 |
-
## 🎯 Objectives Achieved
|
| 8 |
-
|
| 9 |
-
### Primary Goals ✅
|
| 10 |
-
1. **LLM Router Integration**: Replaced rule-based intent parsing with intelligent LLM routing
|
| 11 |
-
2. **LangGraph Workflow**: Updated workflow to use LLM router as primary entry point
|
| 12 |
-
3. **Parameter Validation**: Enhanced validators to handle new LLM output formats
|
| 13 |
-
4. **Real API Integration**: Confirmed Gemini Flash API working with proper authentication
|
| 14 |
-
5. **End-to-End Testing**: Verified complete workflow from user query to response
|
| 15 |
-
6. **🆕 LLM-Based Weather Summarization**: Intelligent weather data analysis using Gemini API
|
| 16 |
-
7. **🆕 Geographic Parameter Resolution**: Seamless conversion from natural language to OMIRL technical parameters
|
| 17 |
-
|
| 18 |
-
### Secondary Goals ✅
|
| 19 |
-
1. **Backward Compatibility**: Maintained support for legacy parameter formats
|
| 20 |
-
2. **Error Handling**: Robust error propagation throughout the pipeline
|
| 21 |
-
3. **State Management**: Proper LangGraph state transitions and updates
|
| 22 |
-
4. **Documentation**: Comprehensive documentation of integration approach
|
| 23 |
-
5. **🆕 Professional Weather Analysis**: Transformed basic metadata into meteorological insights
|
| 24 |
-
6. **🆕 Geographic Ambiguity Handling**: City vs province preference resolution
|
| 25 |
-
|
| 26 |
-
## 🏗️ Architecture Overview
|
| 27 |
-
|
| 28 |
-
### LangGraph Workflow Integration
|
| 29 |
-
|
| 30 |
-
```
|
| 31 |
-
User Query → LLM Routing Node → Conditional Router → Response/Tool Execution
|
| 32 |
-
↓ ↓ ↓ ↓
|
| 33 |
-
user_message → llm_routing → _llm_routing_router → response_node
|
| 34 |
-
↓ ↓ ↓
|
| 35 |
-
routing_result → status check → tool_planning_node
|
| 36 |
-
```
|
| 37 |
-
|
| 38 |
-
### Key Components Updated
|
| 39 |
-
|
| 40 |
-
1. **agent/nodes.py**:
|
| 41 |
-
- Added `llm_routing_node()` as workflow entry point
|
| 42 |
-
- Simplified tool planning validation
|
| 43 |
-
- Removed complex parameter mapping logic
|
| 44 |
-
|
| 45 |
-
2. **agent/graph.py**:
|
| 46 |
-
- Updated `create_operations_workflow()` to use LLM router
|
| 47 |
-
- Implemented conditional routing based on processing status
|
| 48 |
-
- Clean separation between routing and tool execution
|
| 49 |
-
|
| 50 |
-
3. **agent/validator.py**:
|
| 51 |
-
- Enhanced `GeographicValidator` for dual parameter format support
|
| 52 |
-
- Handles both `filters.provincia` (new) and `province` (legacy)
|
| 53 |
-
- Proper parameter normalization and validation
|
| 54 |
-
|
| 55 |
-
4. **agent/llm_client.py**:
|
| 56 |
-
- Confirmed real Gemini API integration working
|
| 57 |
-
- Proper mock fallback system for development
|
| 58 |
-
- Response time ~1.4s for real API calls
|
| 59 |
-
|
| 60 |
-
5. **🆕 services/text/summarization.py**:
|
| 61 |
-
- Complete LLM-based weather data summarization service
|
| 62 |
-
- Gemini Flash API integration for professional meteorological analysis
|
| 63 |
-
- Statistical analysis engine with min/max/avg calculations
|
| 64 |
-
- Context-aware prompts and robust fallback mechanisms
|
| 65 |
-
|
| 66 |
-
6. **🆕 tools/omirl/adapter.py**:
|
| 67 |
-
- Geographic parameter resolution system implementation
|
| 68 |
-
- Municipality → Province auto-detection (Genova → GENOVA)
|
| 69 |
-
- Province name → Code conversion (GENOVA → GE)
|
| 70 |
-
- Complete mapping dictionaries for Liguria region
|
| 71 |
-
|
| 72 |
-
7. **🆕 agent/prompts.py**:
|
| 73 |
-
- Enhanced with geographic ambiguity resolution rules
|
| 74 |
-
- City-specific vs province-wide query interpretation guidelines
|
| 75 |
-
- Updated examples showing correct parameter usage patterns
|
| 76 |
-
|
| 77 |
-
## 🔧 Technical Implementation
|
| 78 |
-
|
| 79 |
-
### LLM Router Integration
|
| 80 |
-
|
| 81 |
-
**Before (Intent Parsing):**
|
| 82 |
-
```python
|
| 83 |
-
# Complex rule-based intent detection
|
| 84 |
-
def intent_parsing_node(state):
|
| 85 |
-
user_message = state["user_message"].lower()
|
| 86 |
-
|
| 87 |
-
# Multiple keyword checks
|
| 88 |
-
has_weather = any(keyword in user_message for keyword in weather_keywords)
|
| 89 |
-
has_location = any(keyword in user_message for keyword in location_keywords)
|
| 90 |
-
|
| 91 |
-
# Province extraction with regex
|
| 92 |
-
province_match = re.search(province_patterns, user_message)
|
| 93 |
-
|
| 94 |
-
# Complex mapping logic
|
| 95 |
-
return map_to_tool_parameters(intent, extracted_data)
|
| 96 |
-
```
|
| 97 |
-
|
| 98 |
-
**After (LLM Router):**
|
| 99 |
-
```python
|
| 100 |
-
# Intelligent LLM-based routing
|
| 101 |
-
async def llm_routing_node(state):
|
| 102 |
-
try:
|
| 103 |
-
# Route through LLM router
|
| 104 |
-
routed_state = await llm_router_node(state)
|
| 105 |
-
|
| 106 |
-
# Simple status-based routing
|
| 107 |
-
if routed_state["processing_status"] == "llm_routing":
|
| 108 |
-
# Success - continue to tool planning
|
| 109 |
-
return routed_state
|
| 110 |
-
else:
|
| 111 |
-
# LLM decided on direct response
|
| 112 |
-
return routed_state
|
| 113 |
-
|
| 114 |
-
except Exception as e:
|
| 115 |
-
# Clean error handling
|
| 116 |
-
return handle_routing_error(state, e)
|
| 117 |
-
```
|
| 118 |
-
|
| 119 |
-
### Parameter Format Handling
|
| 120 |
-
|
| 121 |
-
**Enhanced Geographic Validator:**
|
| 122 |
-
```python
|
| 123 |
-
def validate(self, proposal: ToolProposal) -> ValidationOutcome:
|
| 124 |
-
params = proposal.params
|
| 125 |
-
|
| 126 |
-
# Support both new and legacy formats
|
| 127 |
-
province = None
|
| 128 |
-
if "filters" in params and "provincia" in params["filters"]:
|
| 129 |
-
# New format: params.filters.provincia
|
| 130 |
-
province = params["filters"]["provincia"]
|
| 131 |
-
elif "province" in params:
|
| 132 |
-
# Legacy format: params.province
|
| 133 |
-
province = params["province"]
|
| 134 |
-
|
| 135 |
-
# Unified validation logic
|
| 136 |
-
normalized_province = self._normalize_province(province)
|
| 137 |
-
|
| 138 |
-
# Return normalized parameters in correct format
|
| 139 |
-
if "filters" in params:
|
| 140 |
-
modified_params["filters"]["provincia"] = normalized_province
|
| 141 |
-
else:
|
| 142 |
-
modified_params["province"] = normalized_province
|
| 143 |
-
```
|
| 144 |
-
|
| 145 |
-
### Workflow Routing Logic
|
| 146 |
-
|
| 147 |
-
**Conditional Router:**
|
| 148 |
-
```python
|
| 149 |
-
def _llm_routing_router(state):
|
| 150 |
-
"""Route based on LLM processing status"""
|
| 151 |
-
status = state.get("processing_status", "unknown")
|
| 152 |
-
|
| 153 |
-
if status == "approved":
|
| 154 |
-
return "tool_planning"
|
| 155 |
-
elif status in ["needs_clarification", "rejected"]:
|
| 156 |
-
return "response_generation"
|
| 157 |
-
else:
|
| 158 |
-
return "response_generation" # Safe fallback
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
## 🧪 Testing & Validation
|
| 162 |
-
|
| 163 |
-
### Integration Tests Performed
|
| 164 |
-
|
| 165 |
-
1. **End-to-End Workflow Test**:
|
| 166 |
-
```bash
|
| 167 |
-
Query: "Vorrei sapere le temperature di oggi a Genova"
|
| 168 |
-
Result: ✅ Complete workflow execution
|
| 169 |
-
Status: needs_clarification (conservative LLM behavior)
|
| 170 |
-
Response: Generated appropriate clarification message
|
| 171 |
-
```
|
| 172 |
-
|
| 173 |
-
2. **Direct LLM API Test**:
|
| 174 |
-
```bash
|
| 175 |
-
Gemini API: ✅ Working (~1.4s response time)
|
| 176 |
-
Authentication: ✅ Valid API key configured
|
| 177 |
-
Response Format: ✅ Proper JSON structure
|
| 178 |
-
```
|
| 179 |
-
|
| 180 |
-
3. **Parameter Validation Test**:
|
| 181 |
-
```bash
|
| 182 |
-
New Format: {"filters": {"provincia": "GENOVA"}} ✅
|
| 183 |
-
Legacy Format: {"province": "GENOVA"} ✅
|
| 184 |
-
Geographic Validation: ✅ Both formats normalized
|
| 185 |
-
```
|
| 186 |
-
|
| 187 |
-
4. **Workflow State Management**:
|
| 188 |
-
```bash
|
| 189 |
-
State Transitions: ✅ Proper LangGraph flow
|
| 190 |
-
Error Handling: ✅ Graceful failure recovery
|
| 191 |
-
Response Generation: ✅ Complete 193-char responses
|
| 192 |
-
```
|
| 193 |
-
|
| 194 |
-
5. **🆕 Geographic Parameter Resolution Test**:
|
| 195 |
-
```bash
|
| 196 |
-
Query: "sensori temperatura in Genova"
|
| 197 |
-
Auto-detection: comune="Genova" → provincia="GENOVA" ✅
|
| 198 |
-
Conversion: "GENOVA" → "GE" for OMIRL compatibility ✅
|
| 199 |
-
Data retrieval: 13 temperature stations found ✅
|
| 200 |
-
Geographic mapping: Complete Liguria coverage ✅
|
| 201 |
-
```
|
| 202 |
-
|
| 203 |
-
6. **🆕 LLM Summarization Test**:
|
| 204 |
-
```bash
|
| 205 |
-
Input: 13 temperature stations in Genova
|
| 206 |
-
Processing: Statistical analysis + Gemini API ✅
|
| 207 |
-
Output: Professional Italian meteorological summary ✅
|
| 208 |
-
Response time: <2 seconds for real-world data ✅
|
| 209 |
-
Fallback: Robust error handling ensuring 100% uptime ✅
|
| 210 |
-
```
|
| 211 |
-
|
| 212 |
-
### Performance Metrics
|
| 213 |
-
|
| 214 |
-
- **LLM Response Time**: ~1.4 seconds (Gemini Flash)
|
| 215 |
-
- **Workflow Execution**: < 3 seconds end-to-end
|
| 216 |
-
- **Memory Usage**: Minimal state overhead
|
| 217 |
-
- **Error Rate**: 0% (robust error handling)
|
| 218 |
-
- **🆕 Weather Summarization**: <2 seconds for intelligent analysis
|
| 219 |
-
- **🆕 Geographic Resolution**: Instant parameter conversion
|
| 220 |
-
- **🆕 Data Quality**: Professional meteorological insights vs basic counts
|
| 221 |
-
|
| 222 |
-
## 🔍 Key Findings & Insights
|
| 223 |
-
|
| 224 |
-
### Why Mock Responses Were Appearing
|
| 225 |
-
|
| 226 |
-
**Initial Problem**: Mock responses being used instead of real Gemini API
|
| 227 |
-
**Root Cause**: Parameter format mismatches between components
|
| 228 |
-
**Resolution**:
|
| 229 |
-
1. Fixed prompt template inconsistencies
|
| 230 |
-
2. Updated validator parameter extraction
|
| 231 |
-
3. Aligned tool specifications with LLM output
|
| 232 |
-
|
| 233 |
-
### LLM Behavior Analysis
|
| 234 |
-
|
| 235 |
-
**Conservative Routing**: The LLM is being appropriately conservative, requesting clarification for ambiguous queries. This is actually **good behavior** for a production system.
|
| 236 |
-
|
| 237 |
-
**Example Responses**:
|
| 238 |
-
- Query: "Vorrei sapere le temperature di oggi a Genova"
|
| 239 |
-
- LLM Decision: `needs_clarification`
|
| 240 |
-
- Reason: Conservative interpretation (good for production)
|
| 241 |
-
|
| 242 |
-
### Parameter Format Evolution
|
| 243 |
-
|
| 244 |
-
**Old Format**: `{"province": "GENOVA", "comune": "Genova"}`
|
| 245 |
-
**New Format**: `{"filters": {"provincia": "GENOVA", "comune": "Genova"}}`
|
| 246 |
-
**Validator**: Supports both formats seamlessly
|
| 247 |
-
|
| 248 |
-
### 🆕 Geographic Parameter Resolution Insights
|
| 249 |
-
|
| 250 |
-
**Critical Discovery**: Natural language queries don't match OMIRL technical parameters:
|
| 251 |
-
- **User says**: "sensori temperatura in Genova"
|
| 252 |
-
- **LLM extracts**: `comune="Genova"`
|
| 253 |
-
- **OMIRL needs**: `provincia="GE"` (2-letter code)
|
| 254 |
-
|
| 255 |
-
**Solution Implemented**: Intelligent parameter conversion system:
|
| 256 |
-
1. **Auto-detection**: `comune="Genova"` → `provincia="GENOVA"`
|
| 257 |
-
2. **Code conversion**: `"GENOVA"` → `"GE"` for OMIRL compatibility
|
| 258 |
-
3. **Geographic ambiguity**: City-specific preference over province-wide
|
| 259 |
-
|
| 260 |
-
### 🆕 Weather Data Quality Transformation
|
| 261 |
-
|
| 262 |
-
**Before Implementation**:
|
| 263 |
-
```
|
| 264 |
-
Output: "Trovate 13 stazioni meteo"
|
| 265 |
-
Information Value: Minimal (just counts)
|
| 266 |
-
User Experience: Basic metadata
|
| 267 |
-
```
|
| 268 |
-
|
| 269 |
-
**After Implementation**:
|
| 270 |
-
```
|
| 271 |
-
Output: "🌡️ Temperatura Genova: Nessun dato disponibile dalle 13 stazioni meteo OMIRL.
|
| 272 |
-
Impossibile fornire valori minimi, massimi o medi. È necessario verificare
|
| 273 |
-
l'operatività delle stazioni."
|
| 274 |
-
Information Value: High (professional analysis)
|
| 275 |
-
User Experience: Meteorological expertise
|
| 276 |
-
```
|
| 277 |
-
|
| 278 |
-
## 📊 Architecture Benefits
|
| 279 |
-
|
| 280 |
-
### Before (Intent Parsing)
|
| 281 |
-
- ❌ Complex rule-based logic
|
| 282 |
-
- ❌ Brittle keyword matching
|
| 283 |
-
- ❌ Hard to extend or modify
|
| 284 |
-
- ❌ Limited language understanding
|
| 285 |
-
- ❌ Manual parameter mapping
|
| 286 |
-
- ❌ Basic metadata responses ("X stations found")
|
| 287 |
-
|
| 288 |
-
### After (LLM Router + Intelligent Summarization)
|
| 289 |
-
- ✅ Intelligent natural language understanding
|
| 290 |
-
- ✅ Easy to extend with new tools
|
| 291 |
-
- ✅ Robust error handling
|
| 292 |
-
- ✅ Consistent parameter formats
|
| 293 |
-
- ✅ Scalable architecture
|
| 294 |
-
- ✅ **Professional meteorological analysis**
|
| 295 |
-
- ✅ **Geographic parameter auto-resolution**
|
| 296 |
-
- ✅ **Context-aware weather insights**
|
| 297 |
-
|
| 298 |
-
## 🚀 Production Readiness
|
| 299 |
-
|
| 300 |
-
### Deployment Checklist ✅
|
| 301 |
-
|
| 302 |
-
1. **API Integration**: Real Gemini API confirmed working
|
| 303 |
-
2. **Error Handling**: Comprehensive exception management
|
| 304 |
-
3. **Parameter Validation**: Geographic and security validation active
|
| 305 |
-
4. **State Management**: Proper LangGraph state transitions
|
| 306 |
-
5. **Logging**: Detailed logging throughout pipeline
|
| 307 |
-
6. **Documentation**: Complete technical documentation
|
| 308 |
-
|
| 309 |
-
### Security Features ✅
|
| 310 |
-
|
| 311 |
-
1. **Geographic Boundaries**: Validated to Liguria region only
|
| 312 |
-
2. **Parameter Sanitization**: Security validator active
|
| 313 |
-
3. **API Key Management**: Secure environment variable loading
|
| 314 |
-
4. **Error Information**: No sensitive data in error messages
|
| 315 |
-
|
| 316 |
-
## 📝 Next Steps & Recommendations
|
| 317 |
-
|
| 318 |
-
### Immediate Actions
|
| 319 |
-
1. **Prompt Optimization**: Fine-tune LLM prompts for better Italian weather vocabulary
|
| 320 |
-
2. **Tool Expansion**: Add additional OMIRL tools for different data types
|
| 321 |
-
3. **Performance Monitoring**: Implement response time tracking
|
| 322 |
-
4. **User Testing**: Collect feedback on LLM routing decisions
|
| 323 |
-
|
| 324 |
-
### Future Enhancements
|
| 325 |
-
1. **Multi-language Support**: Extend beyond Italian
|
| 326 |
-
2. **Context Awareness**: Leverage conversation history
|
| 327 |
-
3. **Advanced Routing**: More sophisticated tool selection logic
|
| 328 |
-
4. **Caching**: Implement response caching for common queries
|
| 329 |
-
|
| 330 |
-
## 🔗 File References
|
| 331 |
-
|
| 332 |
-
### Core Integration Files
|
| 333 |
-
- `agent/nodes.py`: LLM routing node implementation
|
| 334 |
-
- `agent/graph.py`: LangGraph workflow definition
|
| 335 |
-
- `agent/validator.py`: Enhanced parameter validation
|
| 336 |
-
- `agent/llm_client.py`: Gemini API integration
|
| 337 |
-
- **🆕 `services/text/summarization.py`**: LLM-based weather data analysis
|
| 338 |
-
- **🆕 `tools/omirl/adapter.py`**: Geographic parameter resolution system
|
| 339 |
-
|
| 340 |
-
### Configuration Files
|
| 341 |
-
- `agent/config/env_config.py`: Environment variable management
|
| 342 |
-
- `agent/prompts.py`: LLM routing prompts + geographic ambiguity rules
|
| 343 |
-
- `agent/registry.py`: Tool registry and specifications
|
| 344 |
-
|
| 345 |
-
### Documentation Files
|
| 346 |
-
- `docs/agent_spec.md`: Agent specification
|
| 347 |
-
- `docs/TASK_4_OMIRL_INTEGRATION_COMPLETE.md`: This document
|
| 348 |
-
- **🆕 `docs/PHASE_1_LLM_SUMMARIZATION_COMPLETE.md`**: Detailed Phase 1 implementation
|
| 349 |
-
|
| 350 |
-
## 📈 Success Metrics
|
| 351 |
-
|
| 352 |
-
| Metric | Target | Achieved | Status |
|
| 353 |
-
|--------|--------|----------|---------|
|
| 354 |
-
| LLM Integration | Working API | ✅ Gemini Flash | ✅ |
|
| 355 |
-
| Workflow Update | LangGraph | ✅ Complete | ✅ |
|
| 356 |
-
| Parameter Validation | Dual Format | ✅ New + Legacy | ✅ |
|
| 357 |
-
| End-to-End Flow | Functional | ✅ Complete Pipeline | ✅ |
|
| 358 |
-
| Error Handling | Robust | ✅ Comprehensive | ✅ |
|
| 359 |
-
| Documentation | Complete | ✅ This Document | ✅ |
|
| 360 |
-
| **🆕 Weather Summarization** | **LLM Analysis** | **✅ Gemini Integration** | **✅** |
|
| 361 |
-
| **🆕 Geographic Resolution** | **Auto-mapping** | **✅ Municipality→Province→Code** | **✅** |
|
| 362 |
-
| **🆕 Professional Output** | **Meteorological** | **✅ Italian Expert Analysis** | **✅** |
|
| 363 |
-
|
| 364 |
-
## 🎉 Conclusion
|
| 365 |
-
|
| 366 |
-
**Task 4: OMIRL Integration is SUCCESSFULLY COMPLETED** ✅
|
| 367 |
-
|
| 368 |
-
The integration of LLM-based routing with LangGraph workflow represents a significant architectural improvement over the previous rule-based intent parsing system. The implementation provides:
|
| 369 |
-
|
| 370 |
-
- **Intelligent Query Understanding**: Natural language processing via Gemini API
|
| 371 |
-
- **Scalable Architecture**: Easy to extend with new tools and capabilities
|
| 372 |
-
- **Robust Validation**: Geographic and security constraints properly enforced
|
| 373 |
-
- **Production Ready**: Complete error handling and state management
|
| 374 |
-
- **Backward Compatible**: Supports legacy parameter formats
|
| 375 |
-
- **🆕 Professional Weather Analysis**: LLM-based meteorological insights instead of basic metadata
|
| 376 |
-
- **🆕 Geographic Intelligence**: Seamless conversion from natural language to technical parameters
|
| 377 |
-
- **🆕 Complete Data Pipeline**: End-to-end intelligent weather information system
|
| 378 |
-
|
| 379 |
-
### 🌟 Major Achievement: Phase 1 LLM Summarization
|
| 380 |
-
|
| 381 |
-
Beyond the core LLM router integration, we successfully implemented **Phase 1: LLM-Based Weather Data Summarization**, which includes:
|
| 382 |
-
|
| 383 |
-
1. **Intelligent Weather Analysis**: Gemini API integration generating professional meteorological summaries
|
| 384 |
-
2. **Geographic Parameter Resolution**: Municipality→Province→Code conversion system solving the natural language to technical parameter gap
|
| 385 |
-
3. **Statistical Data Processing**: Min/max/avg calculations with context-aware analysis
|
| 386 |
-
4. **Geographic Ambiguity Handling**: City vs province preference resolution in prompts
|
| 387 |
-
|
| 388 |
-
### 🔄 System Transformation
|
| 389 |
-
|
| 390 |
-
**Before**: `"Trovate 13 stazioni meteo"` (basic count)
|
| 391 |
-
**After**: `"🌡️ Temperatura Genova: Nessun dato disponibile dalle 13 stazioni meteo OMIRL. Impossibile fornire valori minimi, massimi o medi."` (professional analysis)
|
| 392 |
-
|
| 393 |
-
**Query Processing**: `"sensori temperatura in Genova"` → Auto-detects `provincia="GENOVA"` → Converts to `"GE"` → Retrieves 13 stations → Generates intelligent summary
|
| 394 |
-
|
| 395 |
-
The system is now ready for production deployment with comprehensive testing validation, proper documentation, and a complete intelligent weather data analysis pipeline. The conservative LLM behavior (requesting clarification) is actually a feature, not a bug, ensuring reliable operation in production environments.
|
| 396 |
-
|
| 397 |
-
---
|
| 398 |
-
|
| 399 |
-
**Document Version**: 2.0
|
| 400 |
-
**Last Updated**: August 29, 2025
|
| 401 |
-
**Status**: Task Complete ✅ + Phase 1 LLM Summarization Complete ✅
|
| 402 |
-
**Next Phase**: Enhanced context understanding and temporal analysis
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/TODO_NEXT_DEV_SESSION.md
DELETED
|
@@ -1,440 +0,0 @@
|
|
| 1 |
-
# TODO: Next Development Session
|
| 2 |
-
|
| 3 |
-
## 🎯 Current Status (September 6, 2025)
|
| 4 |
-
|
| 5 |
-
✅ **COMPLETED: OMIRL Phase 1 Refactor**
|
| 6 |
-
- Modular YAML-based validation architecture implemented
|
| 7 |
-
- adapter.py successfully updated to use new components
|
| 8 |
-
- All tests passing (7/7 comprehensive test suites)
|
| 9 |
-
- Real data extraction validated with 199 precipitation stations
|
| 10 |
-
- Geographic filtering behavior confirmed (AND logic working)
|
| 11 |
-
- Full backward compatibility maintained
|
| 12 |
-
- Comprehensive commit: `75478fa` on `omirl_refactor` branch
|
| 13 |
-
|
| 14 |
-
## 🚀 Next Phase Priorities
|
| 15 |
-
|
| 16 |
-
### 1. Test Cleanup and Organization (Priority: HIGH)
|
| 17 |
-
- **Goal**: Clean up and improve the test structure
|
| 18 |
-
- **Tasks**:
|
| 19 |
-
- Consolidate test files and remove redundancy
|
| 20 |
-
- Organize tests into logical categories (unit, integration, E2E)
|
| 21 |
-
- Create a unified test runner with proper fixtures
|
| 22 |
-
- Add performance benchmarks for data extraction
|
| 23 |
-
- Implement test data mocking to reduce external dependencies
|
| 24 |
-
|
| 25 |
-
### 2. Agent State and Node Updates (Priority: HIGH)
|
| 26 |
-
- **Goal**: Update the agent architecture to leverage OMIRL refactor
|
| 27 |
-
- **Tasks**:
|
| 28 |
-
- Update agent state nodes to use new OMIRLFilterSet interface
|
| 29 |
-
- Integrate YAML-based validation into LLM router
|
| 30 |
-
- Update tool registry to use new OMIRL adapter interface
|
| 31 |
-
- Ensure agent can handle new error formats and suggestions
|
| 32 |
-
- Test agent end-to-end with new OMIRL implementation
|
| 33 |
-
|
| 34 |
-
### 3. New OMIRL Tasks Implementation (Priority: MEDIUM)
|
| 35 |
-
- **Goal**: Implement additional OMIRL modes beyond valori_stazioni
|
| 36 |
-
- **Tasks**:
|
| 37 |
-
- Implement `massimi_precipitazione` task in tables mode
|
| 38 |
-
- Implement `livelli_idrometrici` task in tables mode
|
| 39 |
-
- Create foundation for maps mode (stazioni, mappe tasks)
|
| 40 |
-
- Create foundation for sensori mode (radar, satellite tasks)
|
| 41 |
-
- Use established pattern from valori_stazioni for consistency
|
| 42 |
-
|
| 43 |
-
### 4. Enhanced Services (Priority: MEDIUM)
|
| 44 |
-
- **Goal**: Improve supporting services for OMIRL integration
|
| 45 |
-
- **Tasks**:
|
| 46 |
-
- Enhance geographic resolver with more municipalities
|
| 47 |
-
- Improve LLM-based summarization for different data types
|
| 48 |
-
- Add image capture capabilities for map/radar tasks
|
| 49 |
-
- Implement caching layer for frequently accessed data
|
| 50 |
-
- Add rate limiting and politeness delays
|
| 51 |
-
|
| 52 |
-
## 📁 Key Files for Next Session
|
| 53 |
-
|
| 54 |
-
### Test Files to Review/Cleanup:
|
| 55 |
-
- `tests/test_adapter_integration.py` - Core adapter functionality
|
| 56 |
-
- `tests/test_omirl_new_architecture.py` - Architecture validation
|
| 57 |
-
- `tests/test_genova_filtering.py` - Geographic filtering behavior
|
| 58 |
-
- `tests/test_yaml_validator.py` - YAML validation system
|
| 59 |
-
- `tests/test_valori_stazioni.py` - Task-specific testing
|
| 60 |
-
|
| 61 |
-
### Agent Files to Update:
|
| 62 |
-
- `agent/nodes.py` - Update nodes to use OMIRLFilterSet
|
| 63 |
-
- `agent/state.py` - Integrate new OMIRL result types
|
| 64 |
-
- `agent/registry.py` - Update tool registration for new interface
|
| 65 |
-
- `agent/llm_router_node.py` - Integrate YAML validation
|
| 66 |
-
|
| 67 |
-
### OMIRL Implementation:
|
| 68 |
-
- `tools/omirl/shared/` - Core architecture (complete)
|
| 69 |
-
- `tools/omirl/tables/valori_stazioni.py` - Reference implementation
|
| 70 |
-
- `tools/omirl/adapter.py` - Tool interface (updated)
|
| 71 |
-
|
| 72 |
-
## 🔧 Development Environment
|
| 73 |
-
|
| 74 |
-
**Branch**: `omirl_refactor`
|
| 75 |
-
**Python Environment**: `promptaid_ops` (conda)
|
| 76 |
-
**Key Dependencies**: PyYAML, Playwright, asyncio
|
| 77 |
-
|
| 78 |
-
## 📋 Testing Strategy
|
| 79 |
-
|
| 80 |
-
**Current Coverage**:
|
| 81 |
-
- ✅ YAML validation system
|
| 82 |
-
- ✅ OMIRLFilterSet functionality
|
| 83 |
-
- ✅ Adapter output format compliance
|
| 84 |
-
- ✅ Provincia name-to-code conversion
|
| 85 |
-
- ✅ Geographic filtering (AND logic)
|
| 86 |
-
- ✅ Real data extraction
|
| 87 |
-
|
| 88 |
-
**Needed Coverage**:
|
| 89 |
-
- Agent integration testing
|
| 90 |
-
- Performance benchmarks
|
| 91 |
-
- Error handling edge cases
|
| 92 |
-
- Multiple task combinations
|
| 93 |
-
- Browser automation reliability
|
| 94 |
-
|
| 95 |
-
## 💡 Technical Notes
|
| 96 |
-
|
| 97 |
-
1. **Architecture Pattern**: The valori_stazioni implementation provides the template for all future OMIRL tasks
|
| 98 |
-
2. **Validation System**: YAML files in `tools/omirl/config/` control all parameter validation behavior
|
| 99 |
-
3. **Filter Logic**: AND logic confirmed for provincia+comune combinations (69→13 stations in Genova test)
|
| 100 |
-
4. **Backward Compatibility**: Legacy functions maintained for smooth migration
|
| 101 |
-
5. **Error Handling**: Graceful degradation with helpful suggestions implemented
|
| 102 |
-
|
| 103 |
-
## 🎯 Success Metrics for Next Session
|
| 104 |
-
|
| 105 |
-
- [ ] Test suite runs in <30 seconds with <5 test files
|
| 106 |
-
- [ ] Agent can process OMIRL queries end-to-end using new architecture
|
| 107 |
-
- [ ] At least 2 additional OMIRL tasks implemented and tested
|
| 108 |
-
- [ ] Performance benchmarks established for data extraction speed
|
| 109 |
-
- [ ] Documentation updated to reflect new capabilities
|
| 110 |
-
|
| 111 |
-
---
|
| 112 |
-
|
| 113 |
-
**Last Updated**: September 6, 2025
|
| 114 |
-
**Status**: Ready for next development session
|
| 115 |
-
**Confidence**: High (solid foundation established)
|
| 116 |
-
|
| 117 |
-
#### ~~3. Sensor Type Normalization~~ ✅ **COMPLETED**
|
| 118 |
-
**Status**: ✅ **LLM HANDLES AUTOMATICALLY**
|
| 119 |
-
**Achievements**:
|
| 120 |
-
- ✅ LLM router intelligently normalizes sensor types
|
| 121 |
-
- ✅ Case-insensitive processing ("temperatura" → "Temperatura")
|
| 122 |
-
- ✅ Italian language support for all sensor types
|
| 123 |
-
- ✅ **BONUS:** No manual mapping required, LLM understands context
|
| 124 |
-
|
| 125 |
-
### 🟡 **MEDIUM PRIORITY** - Enhancement & Optimization (FUTURE SESSIONS)
|
| 126 |
-
|
| 127 |
-
#### 4. Intelligent Temporal Handling
|
| 128 |
-
**Current Status**: No temporal analysis
|
| 129 |
-
**Enhancement Opportunity**:
|
| 130 |
-
```python
|
| 131 |
-
# TODO: Add temporal intelligence to summarization service
|
| 132 |
-
# services/text/summarization.py
|
| 133 |
-
|
| 134 |
-
async def analyze_temporal_patterns(station_data, time_range=None):
|
| 135 |
-
"""
|
| 136 |
-
Analyze weather patterns over time
|
| 137 |
-
- Compare current vs historical data
|
| 138 |
-
- Identify trends and anomalies
|
| 139 |
-
- Generate temporal insights
|
| 140 |
-
"""
|
| 141 |
-
# Implementation needed
|
| 142 |
-
```
|
| 143 |
-
|
| 144 |
-
**User Query Examples**:
|
| 145 |
-
- "temperature ieri a Genova" (yesterday's data)
|
| 146 |
-
- "andamento precipitazioni ultima settimana" (weekly trends)
|
| 147 |
-
- "confronto temperature mese scorso" (monthly comparison)
|
| 148 |
-
|
| 149 |
-
#### 5. Multi-Sensor Analysis
|
| 150 |
-
**Current Status**: Single sensor type queries only
|
| 151 |
-
**Enhancement**:
|
| 152 |
-
```python
|
| 153 |
-
# TODO: Support queries like "condizioni meteo complete a Genova"
|
| 154 |
-
# Should return temperature + precipitation + humidity analysis
|
| 155 |
-
|
| 156 |
-
def analyze_complete_weather_conditions(location, sensors=['Temperatura', 'Precipitazione', 'Umidità dell\'aria']):
|
| 157 |
-
"""Generate comprehensive weather report for location"""
|
| 158 |
-
# Implementation needed
|
| 159 |
-
```
|
| 160 |
-
|
| 161 |
-
#### 6. **PRIORITY: Scraped Table Caching System** 🚀
|
| 162 |
-
**Current Status**: No caching - re-scrapes table for each query
|
| 163 |
-
**Performance Impact**: Significant - repeated scraping for follow-up questions
|
| 164 |
-
**Target Enhancement**:
|
| 165 |
-
```python
|
| 166 |
-
# TODO: Implement table caching in tools/omirl/adapter.py
|
| 167 |
-
import hashlib
|
| 168 |
-
from datetime import datetime, timedelta
|
| 169 |
-
|
| 170 |
-
class OMIRLTableCache:
|
| 171 |
-
"""Cache scraped weather station tables to avoid re-scraping for follow-up queries"""
|
| 172 |
-
|
| 173 |
-
def __init__(self, ttl_minutes=60): # 1-hour cache for weather data
|
| 174 |
-
self.cache = {}
|
| 175 |
-
self.ttl = timedelta(minutes=ttl_minutes)
|
| 176 |
-
|
| 177 |
-
def get_cache_key(self, location, sensor_type, table_url):
|
| 178 |
-
"""Generate cache key from location + sensor + URL fingerprint"""
|
| 179 |
-
key_data = f"{location}_{sensor_type}_{table_url}"
|
| 180 |
-
return hashlib.md5(key_data.encode()).hexdigest()
|
| 181 |
-
|
| 182 |
-
def cache_table_data(self, cache_key, table_data):
|
| 183 |
-
"""Store scraped table with timestamp"""
|
| 184 |
-
self.cache[cache_key] = {
|
| 185 |
-
'data': table_data,
|
| 186 |
-
'timestamp': datetime.now(),
|
| 187 |
-
'stations_count': len(table_data)
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
def get_cached_table(self, cache_key):
|
| 191 |
-
"""Retrieve cached table if still valid"""
|
| 192 |
-
if cache_key in self.cache:
|
| 193 |
-
cached_entry = self.cache[cache_key]
|
| 194 |
-
if datetime.now() - cached_entry['timestamp'] < self.ttl:
|
| 195 |
-
return cached_entry['data']
|
| 196 |
-
else:
|
| 197 |
-
# Remove expired entry
|
| 198 |
-
del self.cache[cache_key]
|
| 199 |
-
return None
|
| 200 |
-
|
| 201 |
-
# Integration in OMIRL adapter:
|
| 202 |
-
# 1. Check cache before scraping
|
| 203 |
-
# 2. Store successful scrapes
|
| 204 |
-
# 3. Filter cached data for follow-up queries
|
| 205 |
-
```
|
| 206 |
-
|
| 207 |
-
**Use Case Scenarios**:
|
| 208 |
-
- Initial query: "temperatura a Genova" → scrapes full table, caches it
|
| 209 |
-
- Follow-up: "umidità a Genova" → uses cached Genova table, filters for humidity
|
| 210 |
-
- Follow-up: "precipitazioni Rapallo" → uses cached Genova table (Rapallo is in Genova province)
|
| 211 |
-
|
| 212 |
-
#### 7. Response Caching System
|
| 213 |
-
**Current Status**: No LLM response caching
|
| 214 |
-
**Performance Optimization**:
|
| 215 |
-
```python
|
| 216 |
-
# TODO: Implement LLM response caching in services/text/summarization.py
|
| 217 |
-
class WeatherSummaryCache:
|
| 218 |
-
"""Cache LLM responses for identical data patterns"""
|
| 219 |
-
|
| 220 |
-
def __init__(self, ttl_minutes=30):
|
| 221 |
-
self.cache = {}
|
| 222 |
-
self.ttl = timedelta(minutes=ttl_minutes)
|
| 223 |
-
|
| 224 |
-
def get_cache_key(self, station_data, query_context):
|
| 225 |
-
"""Generate cache key from data fingerprint"""
|
| 226 |
-
# Implementation needed
|
| 227 |
-
|
| 228 |
-
def get_cached_summary(self, cache_key):
|
| 229 |
-
"""Retrieve cached summary if still valid"""
|
| 230 |
-
# Implementation needed
|
| 231 |
-
```
|
| 232 |
-
|
| 233 |
-
---
|
| 234 |
-
|
| 235 |
-
### 🟢 **LOW PRIORITY** - Polish & Extended Features
|
| 236 |
-
|
| 237 |
-
#### 7. Enhanced Error Messages
|
| 238 |
-
**Current Status**: Basic error handling
|
| 239 |
-
**Improvement**:
|
| 240 |
-
```python
|
| 241 |
-
# TODO: More user-friendly error messages in Italian
|
| 242 |
-
ERROR_MESSAGES_IT = {
|
| 243 |
-
'no_stations_found': "❌ Nessuna stazione meteorologica trovata per {location}. Verificare il nome del comune o provincia.",
|
| 244 |
-
'api_timeout': "⏱️ Timeout nella raccolta dati meteo. Riprovare tra qualche minuto.",
|
| 245 |
-
'invalid_sensor': "🌡️ Tipo sensore '{sensor}' non riconosciuto. Tipi disponibili: temperatura, precipitazione, umidità, vento."
|
| 246 |
-
}
|
| 247 |
-
```
|
| 248 |
-
|
| 249 |
-
#### 8. Visual Data Enhancement
|
| 250 |
-
**Current Status**: Text-only responses
|
| 251 |
-
**Future Enhancement**:
|
| 252 |
-
```python
|
| 253 |
-
# TODO: Consider adding weather emoji and formatting
|
| 254 |
-
def format_weather_summary_with_visuals(summary_text, weather_data):
|
| 255 |
-
"""Add weather emojis and visual formatting"""
|
| 256 |
-
# 🌡️ for temperature
|
| 257 |
-
# 🌧️ for precipitation
|
| 258 |
-
# 💨 for wind
|
| 259 |
-
# ☁️ for humidity
|
| 260 |
-
```
|
| 261 |
-
|
| 262 |
-
#### 9. API Rate Limiting
|
| 263 |
-
**Current Status**: No rate limiting
|
| 264 |
-
**Production Consideration**:
|
| 265 |
-
```python
|
| 266 |
-
# TODO: Implement rate limiting for Gemini API calls
|
| 267 |
-
from asyncio import Semaphore
|
| 268 |
-
|
| 269 |
-
class GeminiRateLimiter:
|
| 270 |
-
def __init__(self, max_concurrent=5, requests_per_minute=60):
|
| 271 |
-
self.semaphore = Semaphore(max_concurrent)
|
| 272 |
-
# Implementation needed
|
| 273 |
-
```
|
| 274 |
-
|
| 275 |
-
---
|
| 276 |
-
|
| 277 |
-
## 🧪 Testing Tasks
|
| 278 |
-
|
| 279 |
-
### Integration Testing
|
| 280 |
-
1. **End-to-End Flow Testing**:
|
| 281 |
-
```bash
|
| 282 |
-
# TODO: Create comprehensive test script
|
| 283 |
-
cd /home/jeanbaptistebove/projects/operations
|
| 284 |
-
python -m pytest tests/test_complete_pipeline.py -v
|
| 285 |
-
```
|
| 286 |
-
|
| 287 |
-
2. **Geographic Parameter Edge Cases**:
|
| 288 |
-
```python
|
| 289 |
-
# TODO: Test edge cases in tests/test_geographic_resolution.py
|
| 290 |
-
test_cases = [
|
| 291 |
-
"sensori a Genova centro", # Sub-city specificity
|
| 292 |
-
"stazioni provincia Genova", # Province-wide explicit
|
| 293 |
-
"temperatura costa ligure", # Regional query
|
| 294 |
-
"pioggia entroterra genovese", # Geographic description
|
| 295 |
-
]
|
| 296 |
-
```
|
| 297 |
-
|
| 298 |
-
3. **Stress Testing**:
|
| 299 |
-
```python
|
| 300 |
-
# TODO: Test with multiple concurrent requests
|
| 301 |
-
# Test Gemini API rate limits
|
| 302 |
-
# Test OMIRL website scraping reliability
|
| 303 |
-
```
|
| 304 |
-
|
| 305 |
-
---
|
| 306 |
-
|
| 307 |
-
## 📋 Documentation Tasks
|
| 308 |
-
|
| 309 |
-
### 1. User Guide Creation
|
| 310 |
-
**File**: `docs/USER_GUIDE.md`
|
| 311 |
-
```markdown
|
| 312 |
-
# TODO: Create user-friendly guide with examples
|
| 313 |
-
## Supported Query Types
|
| 314 |
-
- "temperatura a Genova"
|
| 315 |
-
- "precipitazioni ieri a Savona"
|
| 316 |
-
- "condizioni meteo Imperia"
|
| 317 |
-
- "stazioni umidità provincia La Spezia"
|
| 318 |
-
```
|
| 319 |
-
|
| 320 |
-
### 2. API Reference
|
| 321 |
-
**File**: `docs/API_REFERENCE.md`
|
| 322 |
-
```markdown
|
| 323 |
-
# TODO: Document all tool interfaces
|
| 324 |
-
## OMIRL Tool API
|
| 325 |
-
### Parameters
|
| 326 |
-
### Response Format
|
| 327 |
-
### Error Codes
|
| 328 |
-
```
|
| 329 |
-
|
| 330 |
-
### 3. Development Setup Guide
|
| 331 |
-
**File**: `docs/DEVELOPMENT_SETUP.md`
|
| 332 |
-
```markdown
|
| 333 |
-
# TODO: Document environment setup
|
| 334 |
-
## Environment Variables Required
|
| 335 |
-
- GEMINI_API_KEY
|
| 336 |
-
- Other configuration
|
| 337 |
-
## Testing Setup
|
| 338 |
-
## Common Issues & Solutions
|
| 339 |
-
```
|
| 340 |
-
|
| 341 |
-
---
|
| 342 |
-
|
| 343 |
-
## 🔧 Configuration & Deployment
|
| 344 |
-
|
| 345 |
-
### 1. Environment Configuration
|
| 346 |
-
```bash
|
| 347 |
-
# TODO: Verify all environment variables are documented
|
| 348 |
-
# Check .env.example file exists
|
| 349 |
-
# Validate configuration loading
|
| 350 |
-
```
|
| 351 |
-
|
| 352 |
-
### 2. Production Readiness Checklist
|
| 353 |
-
- [ ] Error logging configured
|
| 354 |
-
- [ ] Performance monitoring setup
|
| 355 |
-
- [ ] API key security validation
|
| 356 |
-
- [ ] Geographic boundary enforcement
|
| 357 |
-
- [ ] Rate limiting implementation
|
| 358 |
-
|
| 359 |
-
---
|
| 360 |
-
|
| 361 |
-
## 🎯 Success Criteria for Next Session - ALREADY ACHIEVED!
|
| 362 |
-
|
| 363 |
-
✅ **ALL ORIGINAL GOALS COMPLETED AND EXCEEDED:**
|
| 364 |
-
|
| 365 |
-
1. ✅ ✅ **Complete Geographic Coverage**: 50+ municipalities with intelligent resolution
|
| 366 |
-
2. ✅ ✅ **Enhanced Prompts**: LLM-powered Italian weather query processing
|
| 367 |
-
3. ✅ ✅ **Case-Insensitive Sensors**: Automatic normalization via LLM
|
| 368 |
-
4. ✅ ✅ **Temporal Intelligence**: Ready for "ieri" queries (LLM can understand)
|
| 369 |
-
5. ✅ ✅ **Comprehensive Testing**: Real-world validation with 37 Savona stations
|
| 370 |
-
6. ✅ ✅ **User Documentation**: Updated progress documentation
|
| 371 |
-
|
| 372 |
-
**BONUS ACHIEVEMENTS:**
|
| 373 |
-
- ✅ **Smart Fallback System**: Gemini → OpenAI quota management
|
| 374 |
-
- ✅ **Production-Ready Pipeline**: Full configuration-driven validation
|
| 375 |
-
- ✅ **Real Data Extraction**: Successfully extracted weather station data
|
| 376 |
-
|
| 377 |
-
---
|
| 378 |
-
|
| 379 |
-
## 📂 Key Files Completed
|
| 380 |
-
|
| 381 |
-
### ✅ All Primary Development Files Completed:
|
| 382 |
-
1. ✅ `services/geographic/resolver.py` - **NEW** Intelligent geographic resolution
|
| 383 |
-
2. ✅ `agent/llm_client.py` - **ENHANCED** with OpenAI + fallback system
|
| 384 |
-
3. ✅ `agent/validator.py` - **ENHANCED** with dynamic configuration
|
| 385 |
-
4. ✅ `agent/llm_router_node.py` - **ENHANCED** with fallback support
|
| 386 |
-
5. ✅ `agent/config/llm_router_config.yaml` - **UPDATED** flexible configuration
|
| 387 |
-
6. ✅ `tools/omirl/adapter.py` - **UPDATED** to use Geographic Resolver
|
| 388 |
-
|
| 389 |
-
### ✅ Documentation Files Updated:
|
| 390 |
-
1. ✅ `docs/LLM_ROUTER_PROGRESS.md` - Complete implementation status
|
| 391 |
-
2. ✅ `docs/TODO_NEXT_DEV_SESSION.md` - Updated achievement status
|
| 392 |
-
|
| 393 |
-
---
|
| 394 |
-
|
| 395 |
-
## 🚀 Quick Start Commands - SYSTEM READY TO USE!
|
| 396 |
-
|
| 397 |
-
```bash
|
| 398 |
-
# Environment setup (already working)
|
| 399 |
-
cd /home/jeanbaptistebove/projects/operations
|
| 400 |
-
conda activate promptaid_ops
|
| 401 |
-
|
| 402 |
-
# Test the complete system
|
| 403 |
-
python -c "
|
| 404 |
-
import asyncio
|
| 405 |
-
from agent.agent import OperationsAgent
|
| 406 |
-
|
| 407 |
-
async def test():
|
| 408 |
-
agent = OperationsAgent()
|
| 409 |
-
|
| 410 |
-
# Test Italian weather queries
|
| 411 |
-
queries = [
|
| 412 |
-
'temperatura a Genova',
|
| 413 |
-
'Precipitazione nella provincia di Savona (SV)',
|
| 414 |
-
'sensori meteo Rapallo',
|
| 415 |
-
'umidità Imperia'
|
| 416 |
-
]
|
| 417 |
-
|
| 418 |
-
for query in queries:
|
| 419 |
-
response = await agent.process_request(query)
|
| 420 |
-
print(f'✅ {query} → Processed successfully')
|
| 421 |
-
|
| 422 |
-
asyncio.run(test())
|
| 423 |
-
"
|
| 424 |
-
|
| 425 |
-
# Start Streamlit app (if available)
|
| 426 |
-
streamlit run app/main.py --server.port 8501
|
| 427 |
-
```
|
| 428 |
-
|
| 429 |
-
---
|
| 430 |
-
|
| 431 |
-
## 🎉 **SYSTEM STATUS: PRODUCTION READY!**
|
| 432 |
-
|
| 433 |
-
**The intelligent weather analysis system is now fully operational with:**
|
| 434 |
-
- 🧠 **Smart LLM routing** with Gemini→OpenAI fallback
|
| 435 |
-
- 🗺️ **Intelligent geographic resolution** for 50+ municipalities
|
| 436 |
-
- 🌊 **Real weather data extraction** from OMIRL stations
|
| 437 |
-
- 🇮🇹 **Native Italian language support**
|
| 438 |
-
- ⚡ **High performance** with smart caching and fallbacks
|
| 439 |
-
|
| 440 |
-
**Next development session can focus on advanced features or new capabilities rather than core functionality!** 🚀
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/config_navigator.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Configuration Navigator for Operations System
|
| 4 |
+
|
| 5 |
+
This script helps you navigate and understand your config-driven architecture.
|
| 6 |
+
Use it to explore, validate, and modify configurations at both global and tool levels.
|
| 7 |
+
|
| 8 |
+
Usage:
|
| 9 |
+
python scripts/config_navigator.py --help
|
| 10 |
+
python scripts/config_navigator.py --list-all
|
| 11 |
+
python scripts/config_navigator.py --show agent.response_templates
|
| 12 |
+
python scripts/config_navigator.py --show omirl.tasks
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import yaml
|
| 18 |
+
import argparse
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Dict, Any, List
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ConfigNavigator:
|
| 24 |
+
"""Navigate the config-driven architecture"""
|
| 25 |
+
|
| 26 |
+
def __init__(self, project_root: Path = None):
|
| 27 |
+
if project_root is None:
|
| 28 |
+
project_root = Path(__file__).parent.parent
|
| 29 |
+
self.project_root = project_root
|
| 30 |
+
self.agent_config_dir = project_root / "agent" / "config"
|
| 31 |
+
self.tools_dir = project_root / "tools"
|
| 32 |
+
|
| 33 |
+
def list_all_configs(self) -> Dict[str, List[str]]:
|
| 34 |
+
"""List all available configuration files"""
|
| 35 |
+
configs = {
|
| 36 |
+
"🌐 Global Agent Configs": [],
|
| 37 |
+
"🔧 Tool-Specific Configs": []
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Global configs
|
| 41 |
+
if self.agent_config_dir.exists():
|
| 42 |
+
for config_file in self.agent_config_dir.glob("*.yaml"):
|
| 43 |
+
configs["🌐 Global Agent Configs"].append(f"agent.{config_file.stem}")
|
| 44 |
+
|
| 45 |
+
# Tool configs
|
| 46 |
+
if self.tools_dir.exists():
|
| 47 |
+
for tool_dir in self.tools_dir.iterdir():
|
| 48 |
+
if tool_dir.is_dir():
|
| 49 |
+
tool_config_dir = tool_dir / "config"
|
| 50 |
+
if tool_config_dir.exists():
|
| 51 |
+
for config_file in tool_config_dir.glob("*.yaml"):
|
| 52 |
+
configs["🔧 Tool-Specific Configs"].append(f"{tool_dir.name}.{config_file.stem}")
|
| 53 |
+
|
| 54 |
+
return configs
|
| 55 |
+
|
| 56 |
+
def load_config(self, config_path: str) -> Dict[str, Any]:
|
| 57 |
+
"""Load a specific configuration file"""
|
| 58 |
+
try:
|
| 59 |
+
if "." not in config_path:
|
| 60 |
+
raise ValueError("Config path must be in format 'namespace.config_name'")
|
| 61 |
+
|
| 62 |
+
namespace, config_name = config_path.split(".", 1)
|
| 63 |
+
|
| 64 |
+
if namespace == "agent":
|
| 65 |
+
file_path = self.agent_config_dir / f"{config_name}.yaml"
|
| 66 |
+
else:
|
| 67 |
+
# Assume it's a tool name
|
| 68 |
+
file_path = self.tools_dir / namespace / "config" / f"{config_name}.yaml"
|
| 69 |
+
|
| 70 |
+
if not file_path.exists():
|
| 71 |
+
raise FileNotFoundError(f"Config file not found: {file_path}")
|
| 72 |
+
|
| 73 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
| 74 |
+
return yaml.safe_load(f)
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"❌ Error loading config '{config_path}': {e}")
|
| 78 |
+
return {}
|
| 79 |
+
|
| 80 |
+
def show_config_structure(self, config_path: str, max_depth: int = 3):
|
| 81 |
+
"""Show the structure of a configuration file"""
|
| 82 |
+
config = self.load_config(config_path)
|
| 83 |
+
if not config:
|
| 84 |
+
return
|
| 85 |
+
|
| 86 |
+
print(f"\n📋 **Configuration: {config_path}**")
|
| 87 |
+
print("=" * 60)
|
| 88 |
+
self._print_dict_structure(config, max_depth=max_depth)
|
| 89 |
+
|
| 90 |
+
def _print_dict_structure(self, obj: Any, indent: int = 0, max_depth: int = 3, current_depth: int = 0):
|
| 91 |
+
"""Recursively print dictionary structure"""
|
| 92 |
+
if current_depth >= max_depth:
|
| 93 |
+
print(" " * indent + "...")
|
| 94 |
+
return
|
| 95 |
+
|
| 96 |
+
if isinstance(obj, dict):
|
| 97 |
+
for key, value in obj.items():
|
| 98 |
+
if isinstance(value, dict):
|
| 99 |
+
print(" " * indent + f"📁 {key}:")
|
| 100 |
+
self._print_dict_structure(value, indent + 1, max_depth, current_depth + 1)
|
| 101 |
+
elif isinstance(value, list):
|
| 102 |
+
print(" " * indent + f"📝 {key}: [{len(value)} items]")
|
| 103 |
+
if value and current_depth < max_depth - 1:
|
| 104 |
+
if isinstance(value[0], str):
|
| 105 |
+
# Show first few string items
|
| 106 |
+
sample = value[:3]
|
| 107 |
+
print(" " * (indent + 1) + f"↳ {sample}{'...' if len(value) > 3 else ''}")
|
| 108 |
+
else:
|
| 109 |
+
self._print_dict_structure(value[0], indent + 1, max_depth, current_depth + 1)
|
| 110 |
+
else:
|
| 111 |
+
value_str = str(value)
|
| 112 |
+
if len(value_str) > 50:
|
| 113 |
+
value_str = value_str[:47] + "..."
|
| 114 |
+
print(" " * indent + f"📄 {key}: {value_str}")
|
| 115 |
+
elif isinstance(obj, list) and obj:
|
| 116 |
+
for i, item in enumerate(obj[:3]): # Show first 3 items
|
| 117 |
+
print(" " * indent + f"[{i}]:")
|
| 118 |
+
self._print_dict_structure(item, indent + 1, max_depth, current_depth + 1)
|
| 119 |
+
if len(obj) > 3:
|
| 120 |
+
print(" " * indent + f"... and {len(obj) - 3} more items")
|
| 121 |
+
|
| 122 |
+
def find_in_configs(self, search_term: str) -> List[tuple]:
|
| 123 |
+
"""Search for a term across all configurations"""
|
| 124 |
+
results = []
|
| 125 |
+
all_configs = self.list_all_configs()
|
| 126 |
+
|
| 127 |
+
for category, configs in all_configs.items():
|
| 128 |
+
for config_path in configs:
|
| 129 |
+
config = self.load_config(config_path)
|
| 130 |
+
if self._search_in_dict(config, search_term.lower()):
|
| 131 |
+
results.append((config_path, category))
|
| 132 |
+
|
| 133 |
+
return results
|
| 134 |
+
|
| 135 |
+
def _search_in_dict(self, obj: Any, search_term: str) -> bool:
|
| 136 |
+
"""Recursively search for a term in a dictionary"""
|
| 137 |
+
if isinstance(obj, dict):
|
| 138 |
+
for key, value in obj.items():
|
| 139 |
+
if search_term in key.lower() or self._search_in_dict(value, search_term):
|
| 140 |
+
return True
|
| 141 |
+
elif isinstance(obj, list):
|
| 142 |
+
for item in obj:
|
| 143 |
+
if self._search_in_dict(item, search_term):
|
| 144 |
+
return True
|
| 145 |
+
elif isinstance(obj, str):
|
| 146 |
+
return search_term in obj.lower()
|
| 147 |
+
|
| 148 |
+
return False
|
| 149 |
+
|
| 150 |
+
def validate_configs(self) -> Dict[str, List[str]]:
|
| 151 |
+
"""Validate all configuration files"""
|
| 152 |
+
results = {"✅ Valid": [], "❌ Invalid": []}
|
| 153 |
+
all_configs = self.list_all_configs()
|
| 154 |
+
|
| 155 |
+
for category, configs in all_configs.items():
|
| 156 |
+
for config_path in configs:
|
| 157 |
+
try:
|
| 158 |
+
config = self.load_config(config_path)
|
| 159 |
+
if config:
|
| 160 |
+
results["✅ Valid"].append(config_path)
|
| 161 |
+
else:
|
| 162 |
+
results["❌ Invalid"].append(f"{config_path} (empty)")
|
| 163 |
+
except Exception as e:
|
| 164 |
+
results["❌ Invalid"].append(f"{config_path} ({str(e)})")
|
| 165 |
+
|
| 166 |
+
return results
|
| 167 |
+
|
| 168 |
+
def show_config_relationships(self):
|
| 169 |
+
"""Show how configurations relate to each other"""
|
| 170 |
+
print("\n🔗 **Configuration Relationships**")
|
| 171 |
+
print("=" * 60)
|
| 172 |
+
|
| 173 |
+
print("\n📊 **Control Flow:**")
|
| 174 |
+
print("1. 🗣️ User Request")
|
| 175 |
+
print("2. 🧠 llm_router_config.yaml → Route to tool")
|
| 176 |
+
print("3. 🔧 tool_registry.yaml → Find tool capabilities")
|
| 177 |
+
print("4. ⚙️ tool/config/*.yaml → Execute with parameters")
|
| 178 |
+
print("5. 📝 response_templates.yaml → Format response")
|
| 179 |
+
|
| 180 |
+
print("\n🌍 **Shared Data:**")
|
| 181 |
+
print("• geography.yaml → Used by all tools for validation")
|
| 182 |
+
print("• response_templates.yaml → Used by all tools for responses")
|
| 183 |
+
|
| 184 |
+
print("\n🔧 **Tool-Specific:**")
|
| 185 |
+
print("• tools/{tool}/config/tasks.yaml → What the tool can do")
|
| 186 |
+
print("• tools/{tool}/config/parameters.yaml → Tool parameters")
|
| 187 |
+
print("• tools/{tool}/config/validation_rules.yaml → Input validation")
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def main():
|
| 191 |
+
parser = argparse.ArgumentParser(
|
| 192 |
+
description="Navigate the config-driven architecture",
|
| 193 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 194 |
+
epilog="""
|
| 195 |
+
Examples:
|
| 196 |
+
%(prog)s --list-all # List all configs
|
| 197 |
+
%(prog)s --show agent.response_templates # Show agent response templates
|
| 198 |
+
%(prog)s --show omirl.tasks # Show OMIRL tasks config
|
| 199 |
+
%(prog)s --search "precipitazione" # Search for term in all configs
|
| 200 |
+
%(prog)s --validate # Validate all configs
|
| 201 |
+
%(prog)s --relationships # Show config relationships
|
| 202 |
+
"""
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
parser.add_argument("--list-all", action="store_true",
|
| 206 |
+
help="List all available configuration files")
|
| 207 |
+
parser.add_argument("--show", metavar="CONFIG_PATH",
|
| 208 |
+
help="Show structure of specific config (e.g., agent.response_templates)")
|
| 209 |
+
parser.add_argument("--search", metavar="TERM",
|
| 210 |
+
help="Search for a term across all configurations")
|
| 211 |
+
parser.add_argument("--validate", action="store_true",
|
| 212 |
+
help="Validate all configuration files")
|
| 213 |
+
parser.add_argument("--relationships", action="store_true",
|
| 214 |
+
help="Show configuration relationships")
|
| 215 |
+
parser.add_argument("--depth", type=int, default=3,
|
| 216 |
+
help="Maximum depth for structure display (default: 3)")
|
| 217 |
+
|
| 218 |
+
args = parser.parse_args()
|
| 219 |
+
|
| 220 |
+
navigator = ConfigNavigator()
|
| 221 |
+
|
| 222 |
+
if args.list_all:
|
| 223 |
+
print("\n🗺️ **Available Configurations**")
|
| 224 |
+
print("=" * 60)
|
| 225 |
+
configs = navigator.list_all_configs()
|
| 226 |
+
for category, config_list in configs.items():
|
| 227 |
+
print(f"\n{category}:")
|
| 228 |
+
for config in config_list:
|
| 229 |
+
print(f" • {config}")
|
| 230 |
+
|
| 231 |
+
elif args.show:
|
| 232 |
+
navigator.show_config_structure(args.show, max_depth=args.depth)
|
| 233 |
+
|
| 234 |
+
elif args.search:
|
| 235 |
+
print(f"\n🔍 **Search Results for '{args.search}'**")
|
| 236 |
+
print("=" * 60)
|
| 237 |
+
results = navigator.find_in_configs(args.search)
|
| 238 |
+
if results:
|
| 239 |
+
for config_path, category in results:
|
| 240 |
+
print(f"📄 {config_path} ({category})")
|
| 241 |
+
else:
|
| 242 |
+
print("No results found.")
|
| 243 |
+
|
| 244 |
+
elif args.validate:
|
| 245 |
+
print("\n✅ **Configuration Validation**")
|
| 246 |
+
print("=" * 60)
|
| 247 |
+
results = navigator.validate_configs()
|
| 248 |
+
for status, configs in results.items():
|
| 249 |
+
print(f"\n{status}:")
|
| 250 |
+
for config in configs:
|
| 251 |
+
print(f" • {config}")
|
| 252 |
+
|
| 253 |
+
elif args.relationships:
|
| 254 |
+
navigator.show_config_relationships()
|
| 255 |
+
|
| 256 |
+
else:
|
| 257 |
+
parser.print_help()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
if __name__ == "__main__":
|
| 261 |
+
main()
|
test_integration_steps1to5.py
DELETED
|
@@ -1,170 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Integration Test: Steps 1-5 OMIRL Configuration Refactoring
|
| 4 |
-
|
| 5 |
-
This test verifies that all completed refactoring steps work together correctly:
|
| 6 |
-
- Step 1: Task validation from configuration
|
| 7 |
-
- Step 2: URLs from configuration
|
| 8 |
-
- Step 3: Time periods from configuration
|
| 9 |
-
- Step 4: Period mappings from configuration
|
| 10 |
-
- Step 5: Dynamic tool specification generation
|
| 11 |
-
|
| 12 |
-
The test simulates real agent usage patterns and validates end-to-end functionality.
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
import sys
|
| 16 |
-
import os
|
| 17 |
-
sys.path.append('/home/jeanbaptistebove/projects/operations')
|
| 18 |
-
|
| 19 |
-
from tools.omirl.adapter import omirl_tool, OMIRL_TOOL_SPEC
|
| 20 |
-
from tools.omirl.shared.validation import OMIRLValidator
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def test_integration():
|
| 24 |
-
"""Test integration of all refactoring steps"""
|
| 25 |
-
print("🧪 Integration Test: Steps 1-5 OMIRL Configuration Refactoring")
|
| 26 |
-
print("=" * 70)
|
| 27 |
-
|
| 28 |
-
# Test 1: Dynamic Tool Specification (Step 5)
|
| 29 |
-
print("\n1️⃣ Testing dynamic tool specification generation:")
|
| 30 |
-
print(f" Tool: {OMIRL_TOOL_SPEC['name']}")
|
| 31 |
-
print(f" Tasks available: {len(OMIRL_TOOL_SPEC['parameters']['properties']['task']['enum'])}")
|
| 32 |
-
print(f" Sensor types: {len(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['tipo_sensore']['enum'])}")
|
| 33 |
-
print(f" Time periods: {len(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['periodo']['enum'])}")
|
| 34 |
-
print(f" Alert zones: {len(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['zona_allerta']['enum'])}")
|
| 35 |
-
print(" ✅ Dynamic tool specification working")
|
| 36 |
-
|
| 37 |
-
# Test 2: Configuration-based validation (Steps 1-4)
|
| 38 |
-
print("\n2️⃣ Testing configuration-based validation:")
|
| 39 |
-
validator = OMIRLValidator()
|
| 40 |
-
|
| 41 |
-
# Test valid tasks (Step 1)
|
| 42 |
-
valid_tasks = validator.get_valid_tasks()
|
| 43 |
-
print(f" Valid tasks: {len(valid_tasks)} ({', '.join(valid_tasks[:3])}, ...)")
|
| 44 |
-
|
| 45 |
-
# Test task URLs (Step 2)
|
| 46 |
-
url = validator.get_task_url("massimi_precipitazione")
|
| 47 |
-
print(f" Task URL loaded: {url is not None}")
|
| 48 |
-
|
| 49 |
-
# Test time periods (Step 3)
|
| 50 |
-
periods = validator.get_time_periods()
|
| 51 |
-
print(f" Time periods: {len(periods)} loaded from configuration")
|
| 52 |
-
|
| 53 |
-
# Test period mappings (Step 4)
|
| 54 |
-
mappings = validator.get_period_mappings()
|
| 55 |
-
normalized = validator.normalize_periodo("60min")
|
| 56 |
-
print(f" Period mappings: {len(mappings)} entries, '60min' → '{normalized}'")
|
| 57 |
-
print(" ✅ Configuration-based validation working")
|
| 58 |
-
|
| 59 |
-
# Test 3: End-to-end tool execution with various scenarios
|
| 60 |
-
print("\n3️⃣ Testing end-to-end tool execution:")
|
| 61 |
-
|
| 62 |
-
# Test scenario 1: Valid task with period normalization
|
| 63 |
-
print(" Scenario 1: Valid task with period normalization...")
|
| 64 |
-
request1 = {
|
| 65 |
-
"task": "massimi_precipitazione",
|
| 66 |
-
"filters": {
|
| 67 |
-
"zona_allerta": "A",
|
| 68 |
-
"periodo": "60min" # Should be normalized to "1h"
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
try:
|
| 73 |
-
result1 = omirl_tool(request1)
|
| 74 |
-
success1 = "error" not in result1.get("summary_text", "").lower()
|
| 75 |
-
print(f" ✅ Scenario 1: {'Success' if success1 else 'Failed'}")
|
| 76 |
-
except Exception as e:
|
| 77 |
-
print(f" ⚠️ Scenario 1: Exception - {str(e)[:50]}...")
|
| 78 |
-
|
| 79 |
-
# Test scenario 2: Configuration-defined task validation
|
| 80 |
-
print(" Scenario 2: Configuration-defined task validation...")
|
| 81 |
-
request2 = {
|
| 82 |
-
"task": "invalid_task", # Should be rejected by config
|
| 83 |
-
"filters": {}
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
try:
|
| 87 |
-
result2 = omirl_tool(request2)
|
| 88 |
-
success2 = "non supportato" in result2.get("summary_text", "").lower() or "not supported" in result2.get("summary_text", "").lower()
|
| 89 |
-
print(f" ✅ Scenario 2: {'Success (rejected as expected)' if success2 else 'Failed'}")
|
| 90 |
-
except Exception as e:
|
| 91 |
-
print(f" ⚠️ Scenario 2: Exception - {str(e)[:50]}...")
|
| 92 |
-
|
| 93 |
-
# Test scenario 3: Dynamic sensor type validation
|
| 94 |
-
print(" Scenario 3: Dynamic sensor type validation...")
|
| 95 |
-
request3 = {
|
| 96 |
-
"task": "valori_stazioni",
|
| 97 |
-
"filters": {
|
| 98 |
-
"tipo_sensore": "Temperatura", # Should be valid from config
|
| 99 |
-
"provincia": "GE"
|
| 100 |
-
}
|
| 101 |
-
}
|
| 102 |
-
|
| 103 |
-
try:
|
| 104 |
-
result3 = omirl_tool(request3)
|
| 105 |
-
success3 = "error" not in result3.get("summary_text", "").lower()
|
| 106 |
-
print(f" ✅ Scenario 3: {'Success' if success3 else 'Failed'}")
|
| 107 |
-
except Exception as e:
|
| 108 |
-
print(f" ⚠️ Scenario 3: Exception - {str(e)[:50]}...")
|
| 109 |
-
|
| 110 |
-
print(" ✅ End-to-end tool execution tested")
|
| 111 |
-
|
| 112 |
-
# Test 4: Configuration consistency across components
|
| 113 |
-
print("\n4️⃣ Testing configuration consistency:")
|
| 114 |
-
|
| 115 |
-
# Check that tool spec and validator have consistent data
|
| 116 |
-
tool_tasks = set(OMIRL_TOOL_SPEC['parameters']['properties']['task']['enum'])
|
| 117 |
-
validator_tasks = set(validator.get_valid_tasks())
|
| 118 |
-
task_consistency = tool_tasks == validator_tasks
|
| 119 |
-
print(f" Task consistency: {task_consistency} ({len(tool_tasks)} vs {len(validator_tasks)})")
|
| 120 |
-
|
| 121 |
-
tool_sensors = set(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['tipo_sensore']['enum'])
|
| 122 |
-
validator_sensors = set(validator.parameters.get('sensor_types', []))
|
| 123 |
-
sensor_consistency = tool_sensors == validator_sensors
|
| 124 |
-
print(f" Sensor consistency: {sensor_consistency} ({len(tool_sensors)} vs {len(validator_sensors)})")
|
| 125 |
-
|
| 126 |
-
tool_zones = set(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['zona_allerta']['enum'])
|
| 127 |
-
validator_zones = set(validator.get_alert_zones())
|
| 128 |
-
zone_consistency = tool_zones == validator_zones
|
| 129 |
-
print(f" Zone consistency: {zone_consistency} ({len(tool_zones)} vs {len(validator_zones)})")
|
| 130 |
-
|
| 131 |
-
print(" ✅ Configuration consistency verified")
|
| 132 |
-
|
| 133 |
-
# Test 5: Backward compatibility
|
| 134 |
-
print("\n5️⃣ Testing backward compatibility:")
|
| 135 |
-
|
| 136 |
-
# Test that old request formats still work
|
| 137 |
-
legacy_request = {
|
| 138 |
-
"task": "massimi_precipitazione",
|
| 139 |
-
"filters": {
|
| 140 |
-
"zona_allerta": "A",
|
| 141 |
-
"periodo": "1h" # Standard format should still work
|
| 142 |
-
}
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
try:
|
| 146 |
-
legacy_result = omirl_tool(legacy_request)
|
| 147 |
-
legacy_success = "error" not in legacy_result.get("summary_text", "").lower()
|
| 148 |
-
print(f" Legacy format support: {'✅ Working' if legacy_success else '⚠️ Issues'}")
|
| 149 |
-
except Exception as e:
|
| 150 |
-
print(f" Legacy format support: ⚠️ Exception - {str(e)[:50]}...")
|
| 151 |
-
|
| 152 |
-
print(" ✅ Backward compatibility verified")
|
| 153 |
-
|
| 154 |
-
print(f"\n🎉 Integration Test PASSED!")
|
| 155 |
-
print(f" - Dynamic tool specification: ✅ Working")
|
| 156 |
-
print(f" - Configuration-based validation: ✅ Working")
|
| 157 |
-
print(f" - End-to-end execution: ✅ Working")
|
| 158 |
-
print(f" - Configuration consistency: ✅ Working")
|
| 159 |
-
print(f" - Backward compatibility: ✅ Working")
|
| 160 |
-
print(f"\n📊 Summary of configuration-driven improvements:")
|
| 161 |
-
print(f" - Tasks: {len(tool_tasks)} sourced from YAML (was 2 hardcoded)")
|
| 162 |
-
print(f" - Sensor types: {len(tool_sensors)} sourced from YAML (was 12 hardcoded)")
|
| 163 |
-
print(f" - Time periods: {len(OMIRL_TOOL_SPEC['parameters']['properties']['filters']['properties']['periodo']['enum'])} sourced from YAML (was 8 hardcoded)")
|
| 164 |
-
print(f" - Alert zones: {len(tool_zones)} sourced from global geography (was 7 hardcoded)")
|
| 165 |
-
print(f" - Period mappings: {len(validator.get_period_mappings())} normalization rules from YAML")
|
| 166 |
-
print(f" - URLs: Task-specific URLs loaded from YAML configuration")
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
if __name__ == "__main__":
|
| 170 |
-
test_integration()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_step1_task_config.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test Step 1: Task List Configuration
|
| 4 |
-
|
| 5 |
-
This test verifies that:
|
| 6 |
-
1. Valid tasks are loaded from parameters.yaml instead of being hardcoded
|
| 7 |
-
2. Task validation uses configuration-based validation
|
| 8 |
-
3. Default values are loaded from configuration
|
| 9 |
-
4. Invalid tasks are properly rejected with configuration-based error messages
|
| 10 |
-
|
| 11 |
-
Expected Results:
|
| 12 |
-
- Valid tasks should be accepted
|
| 13 |
-
- Invalid tasks should be rejected with updated error message
|
| 14 |
-
- Default periodo should be loaded from config for massimi_precipitazione
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import sys
|
| 18 |
-
import asyncio
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
|
| 21 |
-
# Add parent directories to path for imports
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
| 23 |
-
|
| 24 |
-
from tools.omirl.shared.validation import get_validator
|
| 25 |
-
from tools.omirl.adapter import omirl_tool
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
async def test_task_configuration():
|
| 29 |
-
"""Test task list configuration loading"""
|
| 30 |
-
print("🧪 Testing Step 1: Task List Configuration")
|
| 31 |
-
print("=" * 50)
|
| 32 |
-
|
| 33 |
-
# Test 1: Valid tasks from configuration
|
| 34 |
-
print("\n1️⃣ Testing valid tasks loading from configuration:")
|
| 35 |
-
validator = get_validator()
|
| 36 |
-
valid_tasks = validator.get_valid_tasks()
|
| 37 |
-
print(f" Valid tasks from config: {valid_tasks}")
|
| 38 |
-
|
| 39 |
-
expected_tasks = ["valori_stazioni", "massimi_precipitazione", "livelli_idrometrici"]
|
| 40 |
-
assert set(valid_tasks) >= set(expected_tasks[:2]), f"Missing required tasks: {expected_tasks[:2]}"
|
| 41 |
-
print(" ✅ Valid tasks loaded correctly from configuration")
|
| 42 |
-
|
| 43 |
-
# Test 2: Default values from configuration
|
| 44 |
-
print("\n2️⃣ Testing default values loading:")
|
| 45 |
-
default_periodo = validator.get_task_default("massimi_precipitazione", "periodo")
|
| 46 |
-
print(f" Default periodo for massimi_precipitazione: {default_periodo}")
|
| 47 |
-
|
| 48 |
-
assert default_periodo == "1h", f"Expected '1h', got '{default_periodo}'"
|
| 49 |
-
print(" ✅ Default values loaded correctly from configuration")
|
| 50 |
-
|
| 51 |
-
# Test 3: Valid task acceptance
|
| 52 |
-
print("\n3️⃣ Testing valid task acceptance:")
|
| 53 |
-
for task in ["valori_stazioni", "massimi_precipitazione"]:
|
| 54 |
-
try:
|
| 55 |
-
# Mock call to test validation (no actual data fetching)
|
| 56 |
-
result = await omirl_tool(task=task, filters={"test": "mock"})
|
| 57 |
-
# Should not fail at task validation stage
|
| 58 |
-
print(f" ✅ Task '{task}' accepted")
|
| 59 |
-
except Exception as e:
|
| 60 |
-
if "Task non supportato" in str(e) or "non supportato" in str(e):
|
| 61 |
-
print(f" ❌ Task '{task}' unexpectedly rejected: {e}")
|
| 62 |
-
raise
|
| 63 |
-
else:
|
| 64 |
-
# Other errors are expected (no real data fetching)
|
| 65 |
-
print(f" ✅ Task '{task}' accepted (later error expected)")
|
| 66 |
-
|
| 67 |
-
# Test 4: Invalid task rejection
|
| 68 |
-
print("\n4️⃣ Testing invalid task rejection:")
|
| 69 |
-
result = await omirl_tool(task="invalid_task", filters={})
|
| 70 |
-
|
| 71 |
-
# Should get error response
|
| 72 |
-
assert "Task non supportato" in result.get("summary_text", ""), "Should reject invalid task"
|
| 73 |
-
assert "invalid_task" in result.get("summary_text", ""), "Should mention the invalid task"
|
| 74 |
-
print(" ✅ Invalid task properly rejected with configuration-based validation")
|
| 75 |
-
|
| 76 |
-
# Test 5: Default periodo application
|
| 77 |
-
print("\n5️⃣ Testing default periodo application:")
|
| 78 |
-
# This will fail at later stages, but we can check the filters are properly set
|
| 79 |
-
try:
|
| 80 |
-
result = await omirl_tool(task="massimi_precipitazione", filters={})
|
| 81 |
-
# Even if it fails later, the periodo should be set
|
| 82 |
-
except Exception as e:
|
| 83 |
-
pass
|
| 84 |
-
|
| 85 |
-
print(" ✅ Default periodo application tested (detailed in adapter execution)")
|
| 86 |
-
|
| 87 |
-
print("\n🎉 Step 1 Configuration Tests PASSED!")
|
| 88 |
-
print(" - Task validation now uses parameters.yaml")
|
| 89 |
-
print(" - Default values loaded from configuration")
|
| 90 |
-
print(" - Error messages include valid task list from config")
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
if __name__ == "__main__":
|
| 94 |
-
asyncio.run(test_task_configuration())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_step2_url_config.py
DELETED
|
@@ -1,124 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test Step 2: Load Source URLs from Configuration
|
| 4 |
-
|
| 5 |
-
This test verifies that:
|
| 6 |
-
1. Source URLs are loaded from parameters.yaml instead of being hardcoded
|
| 7 |
-
2. Task implementations use configuration-based URLs
|
| 8 |
-
3. Fallback URLs work when configuration is missing
|
| 9 |
-
4. URLs are correctly returned in tool responses
|
| 10 |
-
|
| 11 |
-
Expected Results:
|
| 12 |
-
- URLs should be loaded from task_urls section in parameters.yaml
|
| 13 |
-
- Different tasks should return their respective URLs
|
| 14 |
-
- Fallback mechanisms should work for missing configurations
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import sys
|
| 18 |
-
import asyncio
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
|
| 21 |
-
# Add parent directories to path for imports
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
| 23 |
-
|
| 24 |
-
from tools.omirl.shared.validation import get_validator
|
| 25 |
-
from tools.omirl.adapter import omirl_tool
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
async def test_url_configuration():
|
| 29 |
-
"""Test URL loading from configuration"""
|
| 30 |
-
print("🧪 Testing Step 2: Source URL Configuration")
|
| 31 |
-
print("=" * 50)
|
| 32 |
-
|
| 33 |
-
# Test 1: URL loading from configuration
|
| 34 |
-
print("\n1️⃣ Testing URL loading from configuration:")
|
| 35 |
-
validator = get_validator()
|
| 36 |
-
|
| 37 |
-
valori_url = validator.get_task_url("valori_stazioni", "tables")
|
| 38 |
-
massimi_url = validator.get_task_url("massimi_precipitazione", "tables")
|
| 39 |
-
|
| 40 |
-
print(f" valori_stazioni URL: {valori_url}")
|
| 41 |
-
print(f" massimi_precipitazione URL: {massimi_url}")
|
| 42 |
-
|
| 43 |
-
expected_valori_url = "https://omirl.regione.liguria.it/#/sensorstable"
|
| 44 |
-
expected_massimi_url = "https://omirl.regione.liguria.it/#/maxtable"
|
| 45 |
-
|
| 46 |
-
assert valori_url == expected_valori_url, f"Expected '{expected_valori_url}', got '{valori_url}'"
|
| 47 |
-
assert massimi_url == expected_massimi_url, f"Expected '{expected_massimi_url}', got '{massimi_url}'"
|
| 48 |
-
print(" ✅ URLs loaded correctly from configuration")
|
| 49 |
-
|
| 50 |
-
# Test 2: URL usage in adapter responses
|
| 51 |
-
print("\n2️⃣ Testing URL usage in adapter responses:")
|
| 52 |
-
|
| 53 |
-
# Test valori_stazioni URL in response
|
| 54 |
-
try:
|
| 55 |
-
result = await omirl_tool(
|
| 56 |
-
task="valori_stazioni",
|
| 57 |
-
filters={"tipo_sensore": "Temperatura"},
|
| 58 |
-
language="it"
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
sources = result.get("sources", [])
|
| 62 |
-
print(f" valori_stazioni sources: {sources}")
|
| 63 |
-
|
| 64 |
-
assert len(sources) > 0, "Should have at least one source URL"
|
| 65 |
-
assert expected_valori_url in sources, f"Should contain '{expected_valori_url}'"
|
| 66 |
-
print(" ✅ valori_stazioni uses configuration-based URL")
|
| 67 |
-
|
| 68 |
-
except Exception as e:
|
| 69 |
-
print(f" ⚠️ valori_stazioni test had errors (may be expected): {e}")
|
| 70 |
-
# Continue - some errors are expected without full browser setup
|
| 71 |
-
|
| 72 |
-
# Test massimi_precipitazione URL in response
|
| 73 |
-
try:
|
| 74 |
-
result = await omirl_tool(
|
| 75 |
-
task="massimi_precipitazione",
|
| 76 |
-
filters={"zona_allerta": "A"},
|
| 77 |
-
language="it"
|
| 78 |
-
)
|
| 79 |
-
|
| 80 |
-
sources = result.get("sources", [])
|
| 81 |
-
print(f" massimi_precipitazione sources: {sources}")
|
| 82 |
-
|
| 83 |
-
assert len(sources) > 0, "Should have at least one source URL"
|
| 84 |
-
assert expected_massimi_url in sources, f"Should contain '{expected_massimi_url}'"
|
| 85 |
-
print(" ✅ massimi_precipitazione uses configuration-based URL")
|
| 86 |
-
|
| 87 |
-
except Exception as e:
|
| 88 |
-
print(f" ⚠️ massimi_precipitazione test had errors (may be expected): {e}")
|
| 89 |
-
# Continue - some errors are expected without full browser setup
|
| 90 |
-
|
| 91 |
-
# Test 3: URL variations for different modes
|
| 92 |
-
print("\n3️⃣ Testing URL variations for different modes:")
|
| 93 |
-
|
| 94 |
-
maps_valori_url = validator.get_task_url("stazioni", "maps")
|
| 95 |
-
maps_mappe_url = validator.get_task_url("mappe", "maps")
|
| 96 |
-
sensor_radar_url = validator.get_task_url("radar", "sensori")
|
| 97 |
-
|
| 98 |
-
print(f" maps/stazioni URL: {maps_valori_url}")
|
| 99 |
-
print(f" maps/mappe URL: {maps_mappe_url}")
|
| 100 |
-
print(f" sensori/radar URL: {sensor_radar_url}")
|
| 101 |
-
|
| 102 |
-
assert maps_valori_url == "https://omirl.regione.liguria.it/#/map"
|
| 103 |
-
assert maps_mappe_url == "https://omirl.regione.liguria.it/#/map"
|
| 104 |
-
assert sensor_radar_url == "https://omirl.regione.liguria.it/#/animations"
|
| 105 |
-
print(" ✅ Different mode URLs loaded correctly")
|
| 106 |
-
|
| 107 |
-
# Test 4: Fallback for missing URLs
|
| 108 |
-
print("\n4️⃣ Testing fallback for missing URLs:")
|
| 109 |
-
|
| 110 |
-
nonexistent_url = validator.get_task_url("nonexistent", "tables")
|
| 111 |
-
print(f" Nonexistent task URL: {nonexistent_url}")
|
| 112 |
-
|
| 113 |
-
assert nonexistent_url is None, "Should return None for nonexistent tasks"
|
| 114 |
-
print(" ✅ Missing URLs return None correctly")
|
| 115 |
-
|
| 116 |
-
print("\n🎉 Step 2 URL Configuration Tests PASSED!")
|
| 117 |
-
print(" - Source URLs now loaded from parameters.yaml")
|
| 118 |
-
print(" - Task implementations use configuration-based URLs")
|
| 119 |
-
print(" - Fallback mechanisms work correctly")
|
| 120 |
-
print(" - Different modes and tasks supported")
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
if __name__ == "__main__":
|
| 124 |
-
asyncio.run(test_url_configuration())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_step3_timeperiods_config.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test Step 3: Extract Time Periods to Configuration
|
| 4 |
-
|
| 5 |
-
This test verifies that:
|
| 6 |
-
1. Time periods are loaded from parameters.yaml instead of being hardcoded
|
| 7 |
-
2. Task implementations use configuration-based time periods
|
| 8 |
-
3. Fallback time periods work when configuration is missing
|
| 9 |
-
4. Time periods are correctly used in data processing and formatting
|
| 10 |
-
|
| 11 |
-
Expected Results:
|
| 12 |
-
- Time periods should be loaded from time_periods section in parameters.yaml
|
| 13 |
-
- Task implementations should use configuration-based time periods
|
| 14 |
-
- Fallback mechanisms should work for missing configurations
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import sys
|
| 18 |
-
import asyncio
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
|
| 21 |
-
# Add parent directories to path for imports
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
| 23 |
-
|
| 24 |
-
from tools.omirl.shared.validation import get_validator
|
| 25 |
-
from tools.omirl.adapter import omirl_tool
|
| 26 |
-
from tools.omirl.tables.massimi_precipitazione import format_precipitation_data_simple
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
async def test_time_periods_configuration():
|
| 30 |
-
"""Test time periods loading from configuration"""
|
| 31 |
-
print("🧪 Testing Step 3: Time Periods Configuration")
|
| 32 |
-
print("=" * 50)
|
| 33 |
-
|
| 34 |
-
# Test 1: Time periods loading from configuration
|
| 35 |
-
print("\n1️⃣ Testing time periods loading from configuration:")
|
| 36 |
-
validator = get_validator()
|
| 37 |
-
|
| 38 |
-
time_periods = validator.get_time_periods()
|
| 39 |
-
print(f" Time periods from config: {time_periods}")
|
| 40 |
-
|
| 41 |
-
expected_periods = ["5'", "15'", "30'", "1h", "3h", "6h", "12h", "24h"]
|
| 42 |
-
assert set(expected_periods).issubset(set(time_periods)), f"Missing expected periods: {expected_periods}"
|
| 43 |
-
print(" ✅ Time periods loaded correctly from configuration")
|
| 44 |
-
|
| 45 |
-
# Test 2: Time periods usage in task metadata
|
| 46 |
-
print("\n2️⃣ Testing time periods usage in task metadata:")
|
| 47 |
-
|
| 48 |
-
try:
|
| 49 |
-
result = await omirl_tool(
|
| 50 |
-
task="massimi_precipitazione",
|
| 51 |
-
filters={"zona_allerta": "A"},
|
| 52 |
-
language="it"
|
| 53 |
-
)
|
| 54 |
-
|
| 55 |
-
metadata = result.get("metadata", {})
|
| 56 |
-
metadata_time_periods = metadata.get("time_periods", [])
|
| 57 |
-
print(f" Metadata time periods: {metadata_time_periods}")
|
| 58 |
-
|
| 59 |
-
# Should contain the same periods from config
|
| 60 |
-
for period in expected_periods:
|
| 61 |
-
assert period in metadata_time_periods, f"Missing period '{period}' in metadata"
|
| 62 |
-
|
| 63 |
-
print(" ✅ Task metadata uses configuration-based time periods")
|
| 64 |
-
|
| 65 |
-
except Exception as e:
|
| 66 |
-
print(f" ⚠️ Task execution had errors (may be expected): {e}")
|
| 67 |
-
# Continue - some errors are expected without full browser setup
|
| 68 |
-
|
| 69 |
-
# Test 3: Time periods in formatting function
|
| 70 |
-
print("\n3️⃣ Testing time periods in formatting function:")
|
| 71 |
-
|
| 72 |
-
# Mock data for testing
|
| 73 |
-
mock_data = {
|
| 74 |
-
"zona_allerta": [{
|
| 75 |
-
"Max (mm)": "A",
|
| 76 |
-
"5'": "0.1 [14:25] Test Station",
|
| 77 |
-
"15'": "0.2 [14:30] Test Station",
|
| 78 |
-
"1h": "0.5 [14:40] Test Station",
|
| 79 |
-
"24h": "2.0 [15:00] Test Station"
|
| 80 |
-
}],
|
| 81 |
-
"province": []
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
filters = {"zona_allerta": "A"}
|
| 85 |
-
|
| 86 |
-
# Test with configuration-based time periods
|
| 87 |
-
result_with_config = format_precipitation_data_simple(mock_data, filters, time_periods)
|
| 88 |
-
print(f" Formatting with config periods: {len(result_with_config)} chars")
|
| 89 |
-
|
| 90 |
-
# Test with fallback time periods (None)
|
| 91 |
-
result_with_fallback = format_precipitation_data_simple(mock_data, filters, None)
|
| 92 |
-
print(f" Formatting with fallback periods: {len(result_with_fallback)} chars")
|
| 93 |
-
|
| 94 |
-
# Both should produce valid results
|
| 95 |
-
assert "Zona d'allerta A" in result_with_config
|
| 96 |
-
assert "Zona d'allerta A" in result_with_fallback
|
| 97 |
-
print(" ✅ Formatting function works with both config and fallback periods")
|
| 98 |
-
|
| 99 |
-
# Test 4: Extended time periods (configuration has more than basic set)
|
| 100 |
-
print("\n4️⃣ Testing extended time periods from configuration:")
|
| 101 |
-
|
| 102 |
-
# Check if config has the extended formats
|
| 103 |
-
extended_periods = ["5m", "15m", "30m", "1 ora", "3 ore", "ultima"]
|
| 104 |
-
config_extended = [p for p in time_periods if p in extended_periods]
|
| 105 |
-
print(f" Extended periods in config: {config_extended}")
|
| 106 |
-
|
| 107 |
-
if config_extended:
|
| 108 |
-
print(" ✅ Configuration contains extended period formats")
|
| 109 |
-
else:
|
| 110 |
-
print(" ℹ️ Configuration only contains basic period formats")
|
| 111 |
-
|
| 112 |
-
# Test 5: Consistency across different components
|
| 113 |
-
print("\n5️⃣ Testing consistency across different components:")
|
| 114 |
-
|
| 115 |
-
# All components should use the same time periods from config
|
| 116 |
-
validator_periods = set(validator.get_time_periods())
|
| 117 |
-
|
| 118 |
-
# Check that basic required periods are present
|
| 119 |
-
required_basic_periods = {"5'", "15'", "30'", "1h", "3h", "6h", "12h", "24h"}
|
| 120 |
-
missing_basic = required_basic_periods - validator_periods
|
| 121 |
-
|
| 122 |
-
if not missing_basic:
|
| 123 |
-
print(" ✅ All required basic time periods present in configuration")
|
| 124 |
-
else:
|
| 125 |
-
print(f" ❌ Missing basic time periods: {missing_basic}")
|
| 126 |
-
|
| 127 |
-
print("\n🎉 Step 3 Time Periods Configuration Tests PASSED!")
|
| 128 |
-
print(" - Time periods now loaded from parameters.yaml")
|
| 129 |
-
print(" - Task implementations use configuration-based time periods")
|
| 130 |
-
print(" - Fallback mechanisms work correctly")
|
| 131 |
-
print(" - Consistent time periods across all components")
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
if __name__ == "__main__":
|
| 135 |
-
asyncio.run(test_time_periods_configuration())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_step4_period_mappings.py
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test Step 4: Add Period Mappings to Configuration
|
| 4 |
-
|
| 5 |
-
This test verifies that:
|
| 6 |
-
1. Period mappings are loaded from parameters.yaml instead of being hardcoded
|
| 7 |
-
2. Period normalization uses configuration-based mappings
|
| 8 |
-
3. Various period formats are correctly normalized
|
| 9 |
-
4. Fallback mechanisms work when configuration is missing
|
| 10 |
-
|
| 11 |
-
Expected Results:
|
| 12 |
-
- Period mappings should be loaded from period_mappings section in parameters.yaml
|
| 13 |
-
- Different period formats should normalize to standard OMIRL formats
|
| 14 |
-
- Fallback mechanisms should work for missing configurations
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
import sys
|
| 18 |
-
import asyncio
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
|
| 21 |
-
# Add parent directories to path for imports
|
| 22 |
-
sys.path.insert(0, str(Path(__file__).parent))
|
| 23 |
-
|
| 24 |
-
from tools.omirl.shared.validation import get_validator
|
| 25 |
-
from tools.omirl.adapter import omirl_tool
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
async def test_period_mappings_configuration():
|
| 29 |
-
"""Test period mappings and normalization from configuration"""
|
| 30 |
-
print("🧪 Testing Step 4: Period Mappings Configuration")
|
| 31 |
-
print("=" * 50)
|
| 32 |
-
|
| 33 |
-
# Test 1: Period mappings loading from configuration
|
| 34 |
-
print("\n1️⃣ Testing period mappings loading from configuration:")
|
| 35 |
-
validator = get_validator()
|
| 36 |
-
|
| 37 |
-
period_mappings = validator.get_period_mappings()
|
| 38 |
-
print(f" Period mappings loaded: {len(period_mappings)} entries")
|
| 39 |
-
|
| 40 |
-
# Test some key mappings
|
| 41 |
-
expected_mappings = {
|
| 42 |
-
"5": "5'",
|
| 43 |
-
"15": "15'",
|
| 44 |
-
"30": "30'",
|
| 45 |
-
"1": "1h",
|
| 46 |
-
"60": "1h",
|
| 47 |
-
"60min": "1h",
|
| 48 |
-
"1 ora": "1h",
|
| 49 |
-
"24 ore": "24h"
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
for input_period, expected_output in expected_mappings.items():
|
| 53 |
-
assert input_period in period_mappings, f"Missing mapping for '{input_period}'"
|
| 54 |
-
assert period_mappings[input_period] == expected_output, f"Incorrect mapping for '{input_period}': expected '{expected_output}', got '{period_mappings[input_period]}'"
|
| 55 |
-
|
| 56 |
-
print(" ✅ Period mappings loaded correctly from configuration")
|
| 57 |
-
|
| 58 |
-
# Test 2: Period normalization functionality
|
| 59 |
-
print("\n2️⃣ Testing period normalization:")
|
| 60 |
-
|
| 61 |
-
test_cases = [
|
| 62 |
-
("5", "5'"),
|
| 63 |
-
("15", "15'"),
|
| 64 |
-
("30", "30'"),
|
| 65 |
-
("1", "1h"),
|
| 66 |
-
("60", "1h"),
|
| 67 |
-
("60min", "1h"),
|
| 68 |
-
("60m", "1h"),
|
| 69 |
-
("1h", "1h"), # Already normalized
|
| 70 |
-
("3h", "3h"), # Already normalized
|
| 71 |
-
("1 ora", "1h"), # Italian
|
| 72 |
-
("24 ore", "24h"), # Italian
|
| 73 |
-
("invalid", None), # Should return None
|
| 74 |
-
("", None), # Empty should return None
|
| 75 |
-
]
|
| 76 |
-
|
| 77 |
-
for input_period, expected in test_cases:
|
| 78 |
-
result = validator.normalize_periodo(input_period)
|
| 79 |
-
print(f" '{input_period}' → '{result}' (expected: '{expected}')")
|
| 80 |
-
assert result == expected, f"Normalization failed for '{input_period}': expected '{expected}', got '{result}'"
|
| 81 |
-
|
| 82 |
-
print(" ✅ Period normalization works correctly")
|
| 83 |
-
|
| 84 |
-
# Test 3: Case-insensitive normalization
|
| 85 |
-
print("\n3️⃣ Testing case-insensitive normalization:")
|
| 86 |
-
|
| 87 |
-
case_test_cases = [
|
| 88 |
-
("1H", "1h"),
|
| 89 |
-
("3H", "3h"),
|
| 90 |
-
("60MIN", "1h"),
|
| 91 |
-
("1 ORA", "1h"),
|
| 92 |
-
("24 ORE", "24h"),
|
| 93 |
-
]
|
| 94 |
-
|
| 95 |
-
for input_period, expected in case_test_cases:
|
| 96 |
-
result = validator.normalize_periodo(input_period)
|
| 97 |
-
print(f" '{input_period}' → '{result}' (expected: '{expected}')")
|
| 98 |
-
assert result == expected, f"Case-insensitive normalization failed for '{input_period}'"
|
| 99 |
-
|
| 100 |
-
print(" ✅ Case-insensitive normalization works correctly")
|
| 101 |
-
|
| 102 |
-
# Test 4: Integration with task execution
|
| 103 |
-
print("\n4️⃣ Testing integration with task execution:")
|
| 104 |
-
|
| 105 |
-
# Test different period formats in actual tool calls
|
| 106 |
-
period_variants = ["1", "60", "60min", "1h", "1 ora"]
|
| 107 |
-
|
| 108 |
-
for period_variant in period_variants:
|
| 109 |
-
try:
|
| 110 |
-
print(f" Testing period variant: '{period_variant}'")
|
| 111 |
-
result = await omirl_tool(
|
| 112 |
-
task="massimi_precipitazione",
|
| 113 |
-
filters={"zona_allerta": "A", "periodo": period_variant},
|
| 114 |
-
language="it"
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
# Should not have errors related to period validation
|
| 118 |
-
if "summary_text" in result and "⚠️" not in result["summary_text"]:
|
| 119 |
-
print(f" ✅ Period '{period_variant}' accepted and processed")
|
| 120 |
-
else:
|
| 121 |
-
print(f" ℹ️ Period '{period_variant}' processed with warnings (may be expected)")
|
| 122 |
-
|
| 123 |
-
except Exception as e:
|
| 124 |
-
print(f" ⚠️ Period '{period_variant}' test had errors: {e}")
|
| 125 |
-
# Continue - some errors are expected without full browser setup
|
| 126 |
-
|
| 127 |
-
# Test 5: Fallback behavior for unknown periods
|
| 128 |
-
print("\n5️⃣ Testing fallback behavior for unknown periods:")
|
| 129 |
-
|
| 130 |
-
unknown_periods = ["invalid", "xyz", "999"]
|
| 131 |
-
|
| 132 |
-
for unknown_period in unknown_periods:
|
| 133 |
-
result = validator.normalize_periodo(unknown_period)
|
| 134 |
-
print(f" Unknown period '{unknown_period}' → '{result}'")
|
| 135 |
-
assert result is None, f"Unknown period should return None, got '{result}'"
|
| 136 |
-
|
| 137 |
-
print(" ✅ Unknown periods correctly return None")
|
| 138 |
-
|
| 139 |
-
# Test 6: Comprehensive mapping coverage
|
| 140 |
-
print("\n6️⃣ Testing comprehensive mapping coverage:")
|
| 141 |
-
|
| 142 |
-
# Check that all standard OMIRL periods are covered
|
| 143 |
-
standard_periods = ["5'", "15'", "30'", "1h", "3h", "6h", "12h", "24h"]
|
| 144 |
-
|
| 145 |
-
for period in standard_periods:
|
| 146 |
-
# Should normalize to itself
|
| 147 |
-
result = validator.normalize_periodo(period)
|
| 148 |
-
assert result == period, f"Standard period '{period}' should normalize to itself"
|
| 149 |
-
|
| 150 |
-
# Should also have common variations
|
| 151 |
-
if period.endswith("'"):
|
| 152 |
-
minute_num = period[:-1]
|
| 153 |
-
assert validator.normalize_periodo(minute_num) == period, f"Number variant '{minute_num}' should map to '{period}'"
|
| 154 |
-
elif period.endswith("h"):
|
| 155 |
-
hour_num = period[:-1]
|
| 156 |
-
assert validator.normalize_periodo(hour_num) == period, f"Number variant '{hour_num}' should map to '{period}'"
|
| 157 |
-
|
| 158 |
-
print(" ✅ Comprehensive mapping coverage verified")
|
| 159 |
-
|
| 160 |
-
print("\n🎉 Step 4 Period Mappings Configuration Tests PASSED!")
|
| 161 |
-
print(" - Period mappings now loaded from parameters.yaml")
|
| 162 |
-
print(" - Period normalization uses configuration-based mappings")
|
| 163 |
-
print(" - Case-insensitive normalization supported")
|
| 164 |
-
print(" - Integration with task execution working")
|
| 165 |
-
print(" - Comprehensive coverage of period variants")
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
if __name__ == "__main__":
|
| 169 |
-
asyncio.run(test_period_mappings_configuration())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_step5_dynamic_toolspec.py
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""
|
| 3 |
-
Test Step 5: Dynamic Tool Specification Generation
|
| 4 |
-
|
| 5 |
-
This test verifies that the OMIRL tool specification is generated dynamically
|
| 6 |
-
from YAML configuration files instead of using hardcoded enumeration values.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import sys
|
| 10 |
-
import os
|
| 11 |
-
sys.path.append('/home/jeanbaptistebove/projects/operations')
|
| 12 |
-
|
| 13 |
-
from tools.omirl.adapter import OMIRL_TOOL_SPEC, _generate_dynamic_tool_spec
|
| 14 |
-
from tools.omirl.shared.validation import OMIRLValidator
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def test_dynamic_tool_spec():
|
| 18 |
-
"""Test that the tool specification is generated dynamically from configuration"""
|
| 19 |
-
print("🧪 Testing Step 5: Dynamic Tool Specification Generation")
|
| 20 |
-
print("=" * 60)
|
| 21 |
-
|
| 22 |
-
# Test 1: Verify tool spec is generated dynamically
|
| 23 |
-
print("\n1️⃣ Testing dynamic tool specification generation:")
|
| 24 |
-
|
| 25 |
-
# Get the current tool spec
|
| 26 |
-
tool_spec = OMIRL_TOOL_SPEC
|
| 27 |
-
print(f" Tool name: {tool_spec['name']}")
|
| 28 |
-
print(f" Tool description: {tool_spec['description']}")
|
| 29 |
-
|
| 30 |
-
# Verify basic structure
|
| 31 |
-
assert "parameters" in tool_spec, "Tool spec missing parameters"
|
| 32 |
-
assert "properties" in tool_spec["parameters"], "Tool spec missing properties"
|
| 33 |
-
|
| 34 |
-
properties = tool_spec["parameters"]["properties"]
|
| 35 |
-
assert "task" in properties, "Tool spec missing task property"
|
| 36 |
-
assert "filters" in properties, "Tool spec missing filters property"
|
| 37 |
-
|
| 38 |
-
print(" ✅ Tool specification structure is valid")
|
| 39 |
-
|
| 40 |
-
# Test 2: Verify tasks are loaded from configuration
|
| 41 |
-
print("\n2️⃣ Testing task enumeration from configuration:")
|
| 42 |
-
|
| 43 |
-
# Get tasks from tool spec
|
| 44 |
-
task_enum = properties["task"]["enum"]
|
| 45 |
-
print(f" Tasks in tool spec: {task_enum}")
|
| 46 |
-
|
| 47 |
-
# Verify against validator configuration
|
| 48 |
-
validator = OMIRLValidator()
|
| 49 |
-
config_tasks = validator.get_valid_tasks()
|
| 50 |
-
print(f" Tasks in configuration: {config_tasks}")
|
| 51 |
-
|
| 52 |
-
# Check that tool spec tasks match configuration
|
| 53 |
-
for task in task_enum:
|
| 54 |
-
assert task in config_tasks, f"Task '{task}' in tool spec but not in configuration"
|
| 55 |
-
print(f" ✅ All {len(task_enum)} tasks are sourced from configuration")
|
| 56 |
-
|
| 57 |
-
# Test 3: Verify sensor types are loaded from configuration
|
| 58 |
-
print("\n3️⃣ Testing sensor type enumeration from configuration:")
|
| 59 |
-
|
| 60 |
-
# Get sensor types from tool spec
|
| 61 |
-
filters_props = properties["filters"]["properties"]
|
| 62 |
-
sensor_enum = filters_props["tipo_sensore"]["enum"]
|
| 63 |
-
print(f" Sensor types in tool spec: {len(sensor_enum)} types")
|
| 64 |
-
|
| 65 |
-
# Verify against validator configuration
|
| 66 |
-
config_sensors = validator.parameters.get('sensor_types', [])
|
| 67 |
-
print(f" Sensor types in configuration: {len(config_sensors)} types")
|
| 68 |
-
|
| 69 |
-
# Check that tool spec sensors match configuration
|
| 70 |
-
for sensor in sensor_enum:
|
| 71 |
-
assert sensor in config_sensors, f"Sensor '{sensor}' in tool spec but not in configuration"
|
| 72 |
-
print(f" ✅ All {len(sensor_enum)} sensor types are sourced from configuration")
|
| 73 |
-
|
| 74 |
-
# Test 4: Verify time periods are loaded from configuration
|
| 75 |
-
print("\n4️⃣ Testing time period enumeration from configuration:")
|
| 76 |
-
|
| 77 |
-
# Get time periods from tool spec
|
| 78 |
-
period_enum = filters_props["periodo"]["enum"]
|
| 79 |
-
print(f" Time periods in tool spec: {period_enum}")
|
| 80 |
-
|
| 81 |
-
# Verify against validator configuration
|
| 82 |
-
config_periods = validator.parameters.get('time_periods', [])
|
| 83 |
-
# Filter to standard format periods only (what should be in enum)
|
| 84 |
-
standard_periods = [p for p in config_periods if "'" in p or ("h" in p and " " not in p)]
|
| 85 |
-
print(f" Standard periods in configuration: {standard_periods}")
|
| 86 |
-
|
| 87 |
-
# Check that tool spec periods are sourced from configuration
|
| 88 |
-
for period in period_enum:
|
| 89 |
-
assert period in standard_periods, f"Period '{period}' in tool spec but not in standard configuration"
|
| 90 |
-
print(f" ✅ All {len(period_enum)} time periods are sourced from configuration")
|
| 91 |
-
|
| 92 |
-
# Test 5: Verify alert zones are loaded from global geography
|
| 93 |
-
print("\n5️⃣ Testing alert zone enumeration from global geography:")
|
| 94 |
-
|
| 95 |
-
# Get alert zones from tool spec
|
| 96 |
-
zone_enum = filters_props["zona_allerta"]["enum"]
|
| 97 |
-
print(f" Alert zones in tool spec: {zone_enum}")
|
| 98 |
-
|
| 99 |
-
# Verify against validator (which loads from global geography)
|
| 100 |
-
config_zones = validator.get_alert_zones()
|
| 101 |
-
print(f" Alert zones in global geography: {config_zones}")
|
| 102 |
-
|
| 103 |
-
# Check that tool spec zones match configuration
|
| 104 |
-
for zone in zone_enum:
|
| 105 |
-
assert zone in config_zones, f"Zone '{zone}' in tool spec but not in global geography"
|
| 106 |
-
print(f" ✅ All {len(zone_enum)} alert zones are sourced from global geography")
|
| 107 |
-
|
| 108 |
-
# Test 6: Verify regeneration produces consistent results
|
| 109 |
-
print("\n6️⃣ Testing tool specification regeneration:")
|
| 110 |
-
|
| 111 |
-
# Generate tool spec again
|
| 112 |
-
regenerated_spec = _generate_dynamic_tool_spec()
|
| 113 |
-
|
| 114 |
-
# Compare key enumerations
|
| 115 |
-
assert regenerated_spec["parameters"]["properties"]["task"]["enum"] == task_enum, "Task enumeration not consistent"
|
| 116 |
-
assert regenerated_spec["parameters"]["properties"]["filters"]["properties"]["tipo_sensore"]["enum"] == sensor_enum, "Sensor enumeration not consistent"
|
| 117 |
-
assert regenerated_spec["parameters"]["properties"]["filters"]["properties"]["periodo"]["enum"] == period_enum, "Period enumeration not consistent"
|
| 118 |
-
assert regenerated_spec["parameters"]["properties"]["filters"]["properties"]["zona_allerta"]["enum"] == zone_enum, "Zone enumeration not consistent"
|
| 119 |
-
|
| 120 |
-
print(" ✅ Tool specification regeneration is consistent")
|
| 121 |
-
|
| 122 |
-
# Test 7: Verify fallback mechanism
|
| 123 |
-
print("\n7️⃣ Testing fallback mechanism for configuration errors:")
|
| 124 |
-
|
| 125 |
-
# This would require mocking a configuration error, so we'll just verify
|
| 126 |
-
# the fallback structure is reasonable
|
| 127 |
-
try:
|
| 128 |
-
# Try to generate spec (should succeed normally)
|
| 129 |
-
fallback_spec = _generate_dynamic_tool_spec()
|
| 130 |
-
fallback_tasks = fallback_spec["parameters"]["properties"]["task"]["enum"]
|
| 131 |
-
assert len(fallback_tasks) >= 2, "Fallback should have at least basic tasks"
|
| 132 |
-
print(" ✅ Fallback mechanism structure verified")
|
| 133 |
-
except Exception as e:
|
| 134 |
-
print(f" ⚠️ Fallback test issue: {e}")
|
| 135 |
-
|
| 136 |
-
print(f"\n🎉 Step 5 Dynamic Tool Specification Tests PASSED!")
|
| 137 |
-
print(f" - Tool specification now generated from YAML configuration")
|
| 138 |
-
print(f" - Tasks: {len(task_enum)} sourced from parameters.yaml")
|
| 139 |
-
print(f" - Sensor types: {len(sensor_enum)} sourced from parameters.yaml")
|
| 140 |
-
print(f" - Time periods: {len(period_enum)} sourced from parameters.yaml")
|
| 141 |
-
print(f" - Alert zones: {len(zone_enum)} sourced from global geography.yaml")
|
| 142 |
-
print(f" - Regeneration produces consistent results")
|
| 143 |
-
print(f" - Fallback mechanism available for configuration errors")
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
if __name__ == "__main__":
|
| 147 |
-
test_dynamic_tool_spec()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/agent/full/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Full Flow Testing Suite
|
| 2 |
+
|
| 3 |
+
This directory contains utilities for testing the complete application flow from user query to output generation. These tools are designed to help you understand how configuration changes affect the agent's behavior.
|
| 4 |
+
|
| 5 |
+
## Test Files
|
| 6 |
+
|
| 7 |
+
### 1. `test_full_flow.py` - Interactive Full Flow Test
|
| 8 |
+
|
| 9 |
+
The main interactive test that executes the complete application flow step by step.
|
| 10 |
+
|
| 11 |
+
**Usage:**
|
| 12 |
+
```bash
|
| 13 |
+
cd /home/jeanbaptistebove/projects/operations
|
| 14 |
+
python agent/tests/test_full_flow.py
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
**What it does:**
|
| 18 |
+
- Asks you for a user query (avoiding the need to run Streamlit)
|
| 19 |
+
- Executes the complete flow with detailed logging at each step
|
| 20 |
+
- Shows configuration impact on agent behavior
|
| 21 |
+
|
| 22 |
+
**Steps displayed:**
|
| 23 |
+
1. **Configuration Status** - Current LLM provider, model, tools available
|
| 24 |
+
2. **Query Validation** - Intent detection, parameter extraction, validation results
|
| 25 |
+
3. **Tool Selection** - Which tool was selected and why
|
| 26 |
+
4. **Task Execution** - Task execution with parameters, results, and artifacts
|
| 27 |
+
5. **LLM Response Generation** - Response generation process and preview
|
| 28 |
+
6. **Final Output** - Complete application output as user would see it
|
| 29 |
+
|
| 30 |
+
### 2. `config_viewer.py` - Configuration State Viewer
|
| 31 |
+
|
| 32 |
+
Quick utility to view current configuration state before testing.
|
| 33 |
+
|
| 34 |
+
**Usage:**
|
| 35 |
+
```bash
|
| 36 |
+
cd /home/jeanbaptistebove/projects/operations
|
| 37 |
+
python agent/tests/config_viewer.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
**What it shows:**
|
| 41 |
+
- LLM Router configuration (provider, model, fallback settings)
|
| 42 |
+
- Tool Registry (available tools and their status)
|
| 43 |
+
- Geography configuration (supported regions and provinces)
|
| 44 |
+
- OMIRL task configuration (valid tasks and requirements)
|
| 45 |
+
- Validation rules (validation settings and rules)
|
| 46 |
+
|
| 47 |
+
## Typical Testing Workflow
|
| 48 |
+
|
| 49 |
+
1. **Before making configuration changes:**
|
| 50 |
+
```bash
|
| 51 |
+
python agent/tests/config_viewer.py
|
| 52 |
+
```
|
| 53 |
+
This shows your current configuration state.
|
| 54 |
+
|
| 55 |
+
2. **Make your configuration changes** in the relevant YAML files.
|
| 56 |
+
|
| 57 |
+
3. **Test the impact:**
|
| 58 |
+
```bash
|
| 59 |
+
python agent/tests/test_full_flow.py
|
| 60 |
+
```
|
| 61 |
+
Enter test queries to see how the changes affect behavior.
|
| 62 |
+
|
| 63 |
+
4. **Compare results** with previous behavior to validate your changes.
|
| 64 |
+
|
| 65 |
+
## Example Test Queries
|
| 66 |
+
|
| 67 |
+
Here are some example queries you can use to test different aspects:
|
| 68 |
+
|
| 69 |
+
### OMIRL Data Extraction
|
| 70 |
+
- `"Mostra precipitazioni a Genova"`
|
| 71 |
+
- `"Temperatura a La Spezia"`
|
| 72 |
+
- `"Stazioni meteo in provincia di Savona"`
|
| 73 |
+
- `"Livelli idrometrici zona A"`
|
| 74 |
+
|
| 75 |
+
### Geographic Validation
|
| 76 |
+
- `"Dati meteo a Milano"` (should be rejected - not in Liguria)
|
| 77 |
+
- `"Precipitazioni in Toscana"` (should be rejected)
|
| 78 |
+
|
| 79 |
+
### Parameter Validation
|
| 80 |
+
- `"Precipitazioni"` (missing location - should ask for clarification)
|
| 81 |
+
- `"Sensore XYZ a Genova"` (invalid sensor type)
|
| 82 |
+
|
| 83 |
+
### Error Scenarios
|
| 84 |
+
- Malformed queries
|
| 85 |
+
- Missing required parameters
|
| 86 |
+
- Invalid parameter values
|
| 87 |
+
|
| 88 |
+
## Configuration Files Tested
|
| 89 |
+
|
| 90 |
+
The test suite monitors these configuration files for changes:
|
| 91 |
+
|
| 92 |
+
- `agent/config/llm_router_config.yaml` - LLM settings
|
| 93 |
+
- `agent/config/tool_registry.yaml` - Available tools
|
| 94 |
+
- `agent/config/geography.yaml` - Geographic data
|
| 95 |
+
- `agent/config/response_templates.yaml` - Response formatting
|
| 96 |
+
- `tools/omirl/config/tasks.yaml` - OMIRL task definitions
|
| 97 |
+
- `tools/omirl/config/validation_rules.yaml` - Validation behavior
|
| 98 |
+
|
| 99 |
+
## Understanding the Output
|
| 100 |
+
|
| 101 |
+
Each step in the flow shows:
|
| 102 |
+
|
| 103 |
+
- ✅ Success indicators
|
| 104 |
+
- ❌ Failure indicators
|
| 105 |
+
- 🔧 Configuration information
|
| 106 |
+
- 📝 User input
|
| 107 |
+
- 🛠️ Tool selection
|
| 108 |
+
- 📊 Data and parameters
|
| 109 |
+
- 🤖 LLM processing
|
| 110 |
+
- 📋 Final output
|
| 111 |
+
|
| 112 |
+
This detailed logging helps you understand exactly how configuration changes propagate through the system and affect the final user experience.
|
| 113 |
+
|
| 114 |
+
## Troubleshooting
|
| 115 |
+
|
| 116 |
+
If tests fail to run:
|
| 117 |
+
|
| 118 |
+
1. **Check Python environment** - Make sure you're in the correct conda environment
|
| 119 |
+
2. **Check dependencies** - Ensure all required packages are installed
|
| 120 |
+
3. **Check file paths** - Verify configuration files exist and are readable
|
| 121 |
+
4. **Check imports** - Make sure the project structure allows imports
|
| 122 |
+
|
| 123 |
+
Common issues:
|
| 124 |
+
- Missing configuration files
|
| 125 |
+
- Invalid YAML syntax in config files
|
| 126 |
+
- Missing environment variables (API keys, etc.)
|
| 127 |
+
- Python path issues
|
tests/agent/full/config_viewer.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Configuration Viewer for Full Flow Testing
|
| 4 |
+
==========================================
|
| 5 |
+
|
| 6 |
+
Quick utility to display current configuration state before running tests.
|
| 7 |
+
Useful for understanding how configuration changes will affect the agent.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python agent/tests/config_viewer.py
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import sys
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import yaml
|
| 17 |
+
import json
|
| 18 |
+
from typing import Dict, Any
|
| 19 |
+
|
| 20 |
+
# Add project root to path
|
| 21 |
+
project_root = Path(__file__).parent.parent.parent
|
| 22 |
+
sys.path.insert(0, str(project_root))
|
| 23 |
+
|
| 24 |
+
def load_yaml_config(config_path: Path) -> Dict[str, Any]:
|
| 25 |
+
"""Load a YAML configuration file."""
|
| 26 |
+
try:
|
| 27 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 28 |
+
return yaml.safe_load(f) or {}
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return {'error': f"Failed to load {config_path}: {e}"}
|
| 31 |
+
|
| 32 |
+
def display_llm_config():
|
| 33 |
+
"""Display LLM router configuration."""
|
| 34 |
+
print("🤖 LLM ROUTER CONFIGURATION")
|
| 35 |
+
print("=" * 50)
|
| 36 |
+
|
| 37 |
+
config_path = project_root / "agent" / "config" / "llm_router_config.yaml"
|
| 38 |
+
config = load_yaml_config(config_path)
|
| 39 |
+
|
| 40 |
+
if 'error' in config:
|
| 41 |
+
print(f"❌ {config['error']}")
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
# Main LLM settings
|
| 45 |
+
llm_config = config.get('llm', {})
|
| 46 |
+
print(f"Provider: {llm_config.get('provider', 'Not set')}")
|
| 47 |
+
print(f"Model: {llm_config.get('model', 'Not set')}")
|
| 48 |
+
|
| 49 |
+
# Fallback settings
|
| 50 |
+
fallback = llm_config.get('fallback', {})
|
| 51 |
+
if fallback.get('enabled'):
|
| 52 |
+
print(f"Fallback: {fallback.get('provider')} - {fallback.get('model')}")
|
| 53 |
+
else:
|
| 54 |
+
print("Fallback: Disabled")
|
| 55 |
+
|
| 56 |
+
# Available models
|
| 57 |
+
models = llm_config.get('models', {})
|
| 58 |
+
print(f"\nAvailable Models:")
|
| 59 |
+
for provider, provider_models in models.items():
|
| 60 |
+
print(f" {provider}:")
|
| 61 |
+
for model_type, model_name in provider_models.items():
|
| 62 |
+
print(f" {model_type}: {model_name}")
|
| 63 |
+
|
| 64 |
+
# Routing settings
|
| 65 |
+
routing = config.get('routing', {})
|
| 66 |
+
confidence = routing.get('confidence', {})
|
| 67 |
+
print(f"\nRouting Configuration:")
|
| 68 |
+
print(f" Execution threshold: {confidence.get('minimum_execution', 'Not set')}")
|
| 69 |
+
print(f" Clarification threshold: {confidence.get('clarification_threshold', 'Not set')}")
|
| 70 |
+
print(f" Primary language: {routing.get('language', {}).get('primary', 'Not set')}")
|
| 71 |
+
|
| 72 |
+
def display_tool_registry():
|
| 73 |
+
"""Display tool registry configuration."""
|
| 74 |
+
print("\n🛠️ TOOL REGISTRY CONFIGURATION")
|
| 75 |
+
print("=" * 50)
|
| 76 |
+
|
| 77 |
+
config_path = project_root / "agent" / "config" / "tool_registry.yaml"
|
| 78 |
+
config = load_yaml_config(config_path)
|
| 79 |
+
|
| 80 |
+
if 'error' in config:
|
| 81 |
+
print(f"❌ {config['error']}")
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
tools = config.get('tools', {})
|
| 85 |
+
print(f"Registered Tools: {len(tools)}")
|
| 86 |
+
|
| 87 |
+
for tool_name, tool_config in tools.items():
|
| 88 |
+
status = "✅ Enabled" if tool_config.get('enabled', False) else "❌ Disabled"
|
| 89 |
+
print(f"\n {tool_name} ({status})")
|
| 90 |
+
print(f" Description: {tool_config.get('description', 'No description')}")
|
| 91 |
+
print(f" Module: {tool_config.get('module_path', 'Not specified')}")
|
| 92 |
+
|
| 93 |
+
# Show supported tasks
|
| 94 |
+
tasks = tool_config.get('supported_tasks', [])
|
| 95 |
+
if tasks:
|
| 96 |
+
print(f" Supported tasks: {', '.join(tasks)}")
|
| 97 |
+
|
| 98 |
+
def display_geography_config():
|
| 99 |
+
"""Display geography configuration."""
|
| 100 |
+
print("\n🗺️ GEOGRAPHY CONFIGURATION")
|
| 101 |
+
print("=" * 50)
|
| 102 |
+
|
| 103 |
+
config_path = project_root / "agent" / "config" / "geography.yaml"
|
| 104 |
+
config = load_yaml_config(config_path)
|
| 105 |
+
|
| 106 |
+
if 'error' in config:
|
| 107 |
+
print(f"❌ {config['error']}")
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
# Supported regions
|
| 111 |
+
regions = config.get('supported_regions', {})
|
| 112 |
+
print(f"Supported Regions:")
|
| 113 |
+
for region_name, region_config in regions.items():
|
| 114 |
+
print(f" {region_name}:")
|
| 115 |
+
|
| 116 |
+
provinces = region_config.get('provinces', {})
|
| 117 |
+
if provinces:
|
| 118 |
+
print(f" Provinces: {len(provinces)}")
|
| 119 |
+
for prov_code, prov_name in list(provinces.items())[:3]:
|
| 120 |
+
print(f" {prov_code}: {prov_name}")
|
| 121 |
+
if len(provinces) > 3:
|
| 122 |
+
print(f" ... and {len(provinces) - 3} more")
|
| 123 |
+
|
| 124 |
+
def display_omirl_tasks():
|
| 125 |
+
"""Display OMIRL task configuration."""
|
| 126 |
+
print("\n🌊 OMIRL TASK CONFIGURATION")
|
| 127 |
+
print("=" * 50)
|
| 128 |
+
|
| 129 |
+
config_path = project_root / "tools" / "omirl" / "config" / "tasks.yaml"
|
| 130 |
+
config = load_yaml_config(config_path)
|
| 131 |
+
|
| 132 |
+
if 'error' in config:
|
| 133 |
+
print(f"❌ {config['error']}")
|
| 134 |
+
return
|
| 135 |
+
|
| 136 |
+
# Valid tasks
|
| 137 |
+
valid_tasks = config.get('valid_tasks', [])
|
| 138 |
+
print(f"Valid Tasks: {len(valid_tasks)}")
|
| 139 |
+
for task in valid_tasks:
|
| 140 |
+
print(f" • {task}")
|
| 141 |
+
|
| 142 |
+
# Task requirements
|
| 143 |
+
task_reqs = config.get('task_requirements', {})
|
| 144 |
+
print(f"\nTask Requirements:")
|
| 145 |
+
for task_name, requirements in task_reqs.items():
|
| 146 |
+
required_filters = requirements.get('required_filters', [])
|
| 147 |
+
optional_filters = requirements.get('optional_filters', [])
|
| 148 |
+
|
| 149 |
+
print(f" {task_name}:")
|
| 150 |
+
if required_filters:
|
| 151 |
+
print(f" Required: {', '.join(required_filters)}")
|
| 152 |
+
if optional_filters:
|
| 153 |
+
print(f" Optional: {', '.join(optional_filters)}")
|
| 154 |
+
print(f" Output: {requirements.get('primary_output', 'Not specified')}")
|
| 155 |
+
|
| 156 |
+
def display_validation_rules():
|
| 157 |
+
"""Display validation rules configuration."""
|
| 158 |
+
print("\n✅ VALIDATION RULES CONFIGURATION")
|
| 159 |
+
print("=" * 50)
|
| 160 |
+
|
| 161 |
+
config_path = project_root / "tools" / "omirl" / "config" / "validation_rules.yaml"
|
| 162 |
+
config = load_yaml_config(config_path)
|
| 163 |
+
|
| 164 |
+
if 'error' in config:
|
| 165 |
+
print(f"❌ {config['error']}")
|
| 166 |
+
return
|
| 167 |
+
|
| 168 |
+
# Validation settings
|
| 169 |
+
settings = config.get('validation_settings', {})
|
| 170 |
+
print(f"Validation Settings:")
|
| 171 |
+
print(f" Strict mode: {settings.get('strict_mode', 'Not set')}")
|
| 172 |
+
print(f" Auto-correct: {settings.get('auto_correct', 'Not set')}")
|
| 173 |
+
print(f" Provide suggestions: {settings.get('provide_suggestions', 'Not set')}")
|
| 174 |
+
|
| 175 |
+
# Rules summary
|
| 176 |
+
rules = config.get('rules', {})
|
| 177 |
+
print(f"\nValidation Rules: {len(rules)} configured")
|
| 178 |
+
for rule_name, rule_config in rules.items():
|
| 179 |
+
case_sensitive = rule_config.get('case_sensitive', True)
|
| 180 |
+
sensitivity = "Case sensitive" if case_sensitive else "Case insensitive"
|
| 181 |
+
print(f" {rule_name}: {sensitivity}")
|
| 182 |
+
|
| 183 |
+
def main():
|
| 184 |
+
"""Main entry point."""
|
| 185 |
+
print("📋 CONFIGURATION VIEWER")
|
| 186 |
+
print("=" * 60)
|
| 187 |
+
print("Current configuration state for full flow testing")
|
| 188 |
+
print("=" * 60)
|
| 189 |
+
|
| 190 |
+
try:
|
| 191 |
+
display_llm_config()
|
| 192 |
+
display_tool_registry()
|
| 193 |
+
display_geography_config()
|
| 194 |
+
display_omirl_tasks()
|
| 195 |
+
display_validation_rules()
|
| 196 |
+
|
| 197 |
+
print("\n" + "=" * 60)
|
| 198 |
+
print("✅ Configuration review complete!")
|
| 199 |
+
print("💡 Use this information to understand how your config changes")
|
| 200 |
+
print(" will affect the agent behavior in the full flow test.")
|
| 201 |
+
print("=" * 60)
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
print(f"❌ Error displaying configuration: {e}")
|
| 205 |
+
import traceback
|
| 206 |
+
traceback.print_exc()
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
main()
|
tests/agent/full/test_full_flow.py
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Full Flow Integration Test
|
| 4 |
+
=========================
|
| 5 |
+
|
| 6 |
+
Interactive test to validate the complete application flow from user query to final output.
|
| 7 |
+
Useful for testing configuration changes and observing agent behavior.
|
| 8 |
+
|
| 9 |
+
Usage:
|
| 10 |
+
python agent/tests/test_full_flow.py
|
| 11 |
+
|
| 12 |
+
The test will:
|
| 13 |
+
1. Ask for a user query
|
| 14 |
+
2. Execute the complete flow
|
| 15 |
+
3. Display detailed information at each step
|
| 16 |
+
4. Show how configuration changes affect behavior
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import sys
|
| 20 |
+
import os
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
from typing import Dict, Any, Optional, List
|
| 23 |
+
import json
|
| 24 |
+
from datetime import datetime
|
| 25 |
+
|
| 26 |
+
# Add project root to path
|
| 27 |
+
# File is in tests/agent/full/, so we need to go up 3 levels: full -> agent -> tests -> operations
|
| 28 |
+
current_file = Path(__file__)
|
| 29 |
+
project_root = current_file.parent.parent.parent.parent
|
| 30 |
+
sys.path.insert(0, str(project_root))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
from agent.agent import OperationsAgent
|
| 35 |
+
from agent.state import AgentState
|
| 36 |
+
from agent.llm_client import LLMClient
|
| 37 |
+
from agent.validator import ProposalValidator
|
| 38 |
+
from agent.registry import ToolRegistry
|
| 39 |
+
print("✅ All imports successful")
|
| 40 |
+
except ImportError as e:
|
| 41 |
+
print(f"❌ Import error: {e}")
|
| 42 |
+
print(f"Available directories in project root: {[d.name for d in project_root.iterdir() if d.is_dir()]}")
|
| 43 |
+
sys.exit(1)
|
| 44 |
+
|
| 45 |
+
class ConfigRecommender:
|
| 46 |
+
"""Analyzes test results and recommends configuration changes."""
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
self.recommendations = []
|
| 50 |
+
|
| 51 |
+
def analyze_flow_results(self, flow_data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 52 |
+
"""Analyze complete flow results and generate recommendations."""
|
| 53 |
+
recommendations = []
|
| 54 |
+
|
| 55 |
+
# Analyze validation results
|
| 56 |
+
validation_result = flow_data.get('validation_result', {})
|
| 57 |
+
if not validation_result.get('valid', False):
|
| 58 |
+
recommendations.extend(self._analyze_validation_failure(validation_result))
|
| 59 |
+
|
| 60 |
+
# Analyze agent processing
|
| 61 |
+
agent_processing = flow_data.get('agent_processing', {})
|
| 62 |
+
if not agent_processing.get('success', False):
|
| 63 |
+
recommendations.extend(self._analyze_processing_failure(agent_processing))
|
| 64 |
+
|
| 65 |
+
return recommendations
|
| 66 |
+
|
| 67 |
+
def _analyze_validation_failure(self, validation_result: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 68 |
+
"""Analyze validation failures and recommend fixes."""
|
| 69 |
+
recommendations = []
|
| 70 |
+
error = validation_result.get('error', '').lower()
|
| 71 |
+
|
| 72 |
+
if 'provincia' in error or 'region' in error:
|
| 73 |
+
recommendations.append({
|
| 74 |
+
'type': 'validation',
|
| 75 |
+
'priority': 'high',
|
| 76 |
+
'issue': 'Geographic validation failed',
|
| 77 |
+
'config_file': 'agent/config/geography.yaml',
|
| 78 |
+
'recommendation': 'Add missing provinces or regions to the supported_regions section',
|
| 79 |
+
'example': 'Add the requested province to the provinces list under the appropriate region'
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
return recommendations
|
| 83 |
+
|
| 84 |
+
def _analyze_processing_failure(self, agent_processing: Dict[str, Any]) -> List[Dict[str, Any]]:
|
| 85 |
+
"""Analyze agent processing failures."""
|
| 86 |
+
recommendations = []
|
| 87 |
+
error = str(agent_processing.get('error', '')).lower()
|
| 88 |
+
|
| 89 |
+
if 'timeout' in error:
|
| 90 |
+
recommendations.append({
|
| 91 |
+
'type': 'performance',
|
| 92 |
+
'priority': 'medium',
|
| 93 |
+
'issue': 'Agent processing timeout',
|
| 94 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 95 |
+
'recommendation': 'Increase timeout values in security or api sections',
|
| 96 |
+
'example': 'Increase timeout_seconds in security section or timeout in api section'
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
if 'api' in error or 'connection' in error:
|
| 100 |
+
recommendations.append({
|
| 101 |
+
'type': 'reliability',
|
| 102 |
+
'priority': 'high',
|
| 103 |
+
'issue': 'API connection issues',
|
| 104 |
+
'config_file': 'agent/config/llm_router_config.yaml',
|
| 105 |
+
'recommendation': 'Enable fallback provider or increase retry settings',
|
| 106 |
+
'example': 'Set fallback.enabled: true or increase api.max_retries'
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
return recommendations
|
| 110 |
+
|
| 111 |
+
def display_recommendations(self, recommendations: List[Dict[str, Any]]):
|
| 112 |
+
"""Display configuration recommendations in a user-friendly format."""
|
| 113 |
+
if not recommendations:
|
| 114 |
+
print("\n✅ No configuration issues detected!")
|
| 115 |
+
return
|
| 116 |
+
|
| 117 |
+
print(f"\n📋 CONFIGURATION RECOMMENDATIONS")
|
| 118 |
+
print("=" * 60)
|
| 119 |
+
print(f"Found {len(recommendations)} potential improvements:")
|
| 120 |
+
|
| 121 |
+
# Group by priority
|
| 122 |
+
high_priority = [r for r in recommendations if r.get('priority') == 'high']
|
| 123 |
+
medium_priority = [r for r in recommendations if r.get('priority') == 'medium']
|
| 124 |
+
|
| 125 |
+
for priority_group, priority_name in [(high_priority, 'HIGH PRIORITY'),
|
| 126 |
+
(medium_priority, 'MEDIUM PRIORITY')]:
|
| 127 |
+
if not priority_group:
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
print(f"\n🔥 {priority_name}")
|
| 131 |
+
print("-" * 40)
|
| 132 |
+
|
| 133 |
+
for i, rec in enumerate(priority_group, 1):
|
| 134 |
+
print(f"\n{i}. **{rec['issue']}**")
|
| 135 |
+
print(f" 📁 File: {rec['config_file']}")
|
| 136 |
+
print(f" 💡 Fix: {rec['recommendation']}")
|
| 137 |
+
print(f" 📝 Example: {rec['example']}")
|
| 138 |
+
|
| 139 |
+
print(f"\n💡 **Quick Fix Guide:**")
|
| 140 |
+
print(f"1. Open the mentioned config files")
|
| 141 |
+
print(f"2. Apply the recommended changes")
|
| 142 |
+
print(f"3. Re-run this test to verify fixes")
|
| 143 |
+
|
| 144 |
+
class FullFlowTester:
|
| 145 |
+
"""Interactive tester for the complete application flow."""
|
| 146 |
+
|
| 147 |
+
def __init__(self):
|
| 148 |
+
"""Initialize the tester with all required components."""
|
| 149 |
+
self.agent = None
|
| 150 |
+
self.llm_client = None
|
| 151 |
+
self.validator = None
|
| 152 |
+
self.tool_registry = None
|
| 153 |
+
self.recommender = ConfigRecommender()
|
| 154 |
+
self.test_session_id = f"test_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
| 155 |
+
self.flow_data = {} # Store data for recommendations
|
| 156 |
+
|
| 157 |
+
def setup_components(self) -> bool:
|
| 158 |
+
"""Initialize all application components."""
|
| 159 |
+
try:
|
| 160 |
+
print("🔧 Initializing application components...")
|
| 161 |
+
|
| 162 |
+
# Initialize LLM client
|
| 163 |
+
self.llm_client = LLMClient()
|
| 164 |
+
print(f" ✅ LLM Client initialized (Provider: {self.llm_client.provider})")
|
| 165 |
+
|
| 166 |
+
# Initialize tool registry
|
| 167 |
+
self.tool_registry = ToolRegistry()
|
| 168 |
+
available_tools = self.tool_registry.list_tools()
|
| 169 |
+
print(f" ✅ Tool Registry initialized ({len(available_tools)} tools available)")
|
| 170 |
+
|
| 171 |
+
# Initialize agent (this will include validation internally)
|
| 172 |
+
self.agent = OperationsAgent()
|
| 173 |
+
print(" ✅ Operations Agent initialized")
|
| 174 |
+
|
| 175 |
+
return True
|
| 176 |
+
|
| 177 |
+
except Exception as e:
|
| 178 |
+
print(f" ❌ Failed to initialize components: {e}")
|
| 179 |
+
return False
|
| 180 |
+
|
| 181 |
+
def get_user_query(self) -> Optional[str]:
|
| 182 |
+
"""Get user query from input."""
|
| 183 |
+
print("\n" + "="*60)
|
| 184 |
+
print("📝 USER QUERY INPUT")
|
| 185 |
+
print("="*60)
|
| 186 |
+
|
| 187 |
+
query = input("\nEnter your query (or 'quit' to exit): ").strip()
|
| 188 |
+
|
| 189 |
+
if query.lower() in ['quit', 'exit', 'q']:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
if not query:
|
| 193 |
+
print("⚠️ Empty query, please try again.")
|
| 194 |
+
return self.get_user_query()
|
| 195 |
+
|
| 196 |
+
return query
|
| 197 |
+
|
| 198 |
+
def display_step_header(self, step_number: int, step_name: str):
|
| 199 |
+
"""Display a formatted step header."""
|
| 200 |
+
print(f"\n{'='*60}")
|
| 201 |
+
print(f"STEP {step_number}: {step_name.upper()}")
|
| 202 |
+
print("="*60)
|
| 203 |
+
|
| 204 |
+
def display_config_info(self):
|
| 205 |
+
"""Display current configuration information."""
|
| 206 |
+
self.display_step_header(1, "Configuration Status")
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
# LLM Configuration
|
| 210 |
+
provider = self.llm_client.provider if self.llm_client else "Unknown"
|
| 211 |
+
model = self.llm_client.model if self.llm_client else "Unknown"
|
| 212 |
+
print(f"🤖 LLM Provider: {provider}")
|
| 213 |
+
print(f"🧠 Model: {model}")
|
| 214 |
+
|
| 215 |
+
# Tool Registry
|
| 216 |
+
if self.tool_registry:
|
| 217 |
+
tools = self.tool_registry.list_tools()
|
| 218 |
+
print(f"🛠️ Available Tools: {', '.join(tools)}")
|
| 219 |
+
|
| 220 |
+
# Agent Status
|
| 221 |
+
if self.agent:
|
| 222 |
+
print(f"✅ Agent: Active")
|
| 223 |
+
|
| 224 |
+
except Exception as e:
|
| 225 |
+
print(f"⚠️ Could not retrieve configuration info: {e}")
|
| 226 |
+
|
| 227 |
+
def execute_validation(self, query: str) -> tuple[bool, Dict[str, Any]]:
|
| 228 |
+
"""Execute query validation step."""
|
| 229 |
+
self.display_step_header(2, "Query Validation & Processing")
|
| 230 |
+
|
| 231 |
+
print(f"📝 User Query: '{query}'")
|
| 232 |
+
|
| 233 |
+
try:
|
| 234 |
+
# For now, we'll assume basic validation (the OperationsAgent will handle detailed validation)
|
| 235 |
+
validation_result = {
|
| 236 |
+
'valid': True,
|
| 237 |
+
'original_query': query,
|
| 238 |
+
'intent': 'data_extraction', # Simplified for demo
|
| 239 |
+
'extracted_params': {} # Will be extracted by the agent
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
print(f"✅ Validation Status: PASSED")
|
| 243 |
+
print(f"🎯 Query accepted for processing")
|
| 244 |
+
|
| 245 |
+
# Store validation data for recommendations
|
| 246 |
+
self.flow_data['validation_result'] = validation_result
|
| 247 |
+
|
| 248 |
+
return True, validation_result
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
print(f"❌ Validation failed with exception: {e}")
|
| 252 |
+
# Store validation failure data
|
| 253 |
+
self.flow_data['validation_result'] = {'valid': False, 'error': str(e)}
|
| 254 |
+
return False, {'error': str(e)}
|
| 255 |
+
|
| 256 |
+
def execute_full_agent_processing(self, query: str) -> tuple[bool, Dict[str, Any]]:
|
| 257 |
+
"""Execute the complete agent processing."""
|
| 258 |
+
self.display_step_header(3, "Agent Processing (Tool Selection + Task Execution)")
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
print(f"🚀 Processing query through OperationsAgent: '{query}'")
|
| 262 |
+
|
| 263 |
+
# Process the request using the agent's main interface
|
| 264 |
+
import asyncio
|
| 265 |
+
|
| 266 |
+
async def run_agent():
|
| 267 |
+
return await self.agent.process_request(
|
| 268 |
+
user_message=query,
|
| 269 |
+
user_id="test_user",
|
| 270 |
+
context={}
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
# Run the async method
|
| 274 |
+
result = asyncio.run(run_agent())
|
| 275 |
+
|
| 276 |
+
print(f"✅ Agent processing: SUCCESS")
|
| 277 |
+
print(f"📊 Response type: {type(result)}")
|
| 278 |
+
|
| 279 |
+
if isinstance(result, dict):
|
| 280 |
+
if 'response' in result:
|
| 281 |
+
response_text = result['response']
|
| 282 |
+
print(f"� Response preview: {response_text[:150]}{'...' if len(response_text) > 150 else ''}")
|
| 283 |
+
|
| 284 |
+
if 'metadata' in result:
|
| 285 |
+
metadata = result['metadata']
|
| 286 |
+
print(f"📋 Metadata keys: {list(metadata.keys())}")
|
| 287 |
+
|
| 288 |
+
# Show tool information if available
|
| 289 |
+
if 'selected_tool' in metadata:
|
| 290 |
+
print(f"🛠️ Selected Tool: {metadata['selected_tool']}")
|
| 291 |
+
|
| 292 |
+
if 'task' in metadata:
|
| 293 |
+
print(f"📋 Task: {metadata['task']}")
|
| 294 |
+
|
| 295 |
+
if 'artifacts' in metadata:
|
| 296 |
+
artifacts = metadata['artifacts']
|
| 297 |
+
print(f"📄 Generated artifacts: {len(artifacts)}")
|
| 298 |
+
for artifact in artifacts[:3]: # Show first 3
|
| 299 |
+
print(f" • {artifact}")
|
| 300 |
+
if len(artifacts) > 3:
|
| 301 |
+
print(f" • ... and {len(artifacts) - 3} more")
|
| 302 |
+
|
| 303 |
+
# Store processing data for recommendations
|
| 304 |
+
processing_result = {
|
| 305 |
+
'success': True,
|
| 306 |
+
'result': result,
|
| 307 |
+
'response': result.get('response', '') if isinstance(result, dict) else str(result),
|
| 308 |
+
'metadata': result.get('metadata', {}) if isinstance(result, dict) else {}
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
self.flow_data['agent_processing'] = processing_result
|
| 312 |
+
|
| 313 |
+
return True, processing_result
|
| 314 |
+
|
| 315 |
+
except Exception as e:
|
| 316 |
+
print(f"❌ Agent processing failed with exception: {e}")
|
| 317 |
+
import traceback
|
| 318 |
+
traceback.print_exc()
|
| 319 |
+
# Store processing failure
|
| 320 |
+
processing_result = {'success': False, 'error': str(e)}
|
| 321 |
+
self.flow_data['agent_processing'] = processing_result
|
| 322 |
+
return False, processing_result
|
| 323 |
+
|
| 324 |
+
def display_task_output(self, processing_result: Dict[str, Any]):
|
| 325 |
+
"""Display the raw task output before LLM processing."""
|
| 326 |
+
self.display_step_header(4, "Raw Task Output")
|
| 327 |
+
|
| 328 |
+
try:
|
| 329 |
+
result = processing_result.get('result', {})
|
| 330 |
+
metadata = processing_result.get('metadata', {})
|
| 331 |
+
|
| 332 |
+
print("📋 RAW TASK OUTPUT (before LLM processing):")
|
| 333 |
+
print("-" * 50)
|
| 334 |
+
|
| 335 |
+
# Extract the raw task data from omirl_data if available
|
| 336 |
+
if isinstance(result, dict) and 'metadata' in result:
|
| 337 |
+
result_metadata = result['metadata']
|
| 338 |
+
|
| 339 |
+
# Look for OMIRL-specific data
|
| 340 |
+
if 'omirl_data' in result_metadata:
|
| 341 |
+
omirl_data = result_metadata['omirl_data']
|
| 342 |
+
print("🌊 OMIRL Task Output:")
|
| 343 |
+
print(f" ✅ Status: {omirl_data.get('summary', 'No summary')}")
|
| 344 |
+
print(f" 📅 Extraction Time: {omirl_data.get('last_extraction', 'Unknown')}")
|
| 345 |
+
|
| 346 |
+
# Show task metadata
|
| 347 |
+
if 'metadata' in omirl_data:
|
| 348 |
+
task_meta = omirl_data['metadata']
|
| 349 |
+
print(f" 🎯 Task: {task_meta.get('task', 'Unknown')}")
|
| 350 |
+
print(f" 🔍 Filters Applied: {task_meta.get('filters_applied', 'None')}")
|
| 351 |
+
print(f" 🌐 Language: {task_meta.get('response_language', 'Not specified')}")
|
| 352 |
+
|
| 353 |
+
# Show the formatted task summary if available
|
| 354 |
+
if 'summary' in task_meta:
|
| 355 |
+
formatted_summary = task_meta['summary']
|
| 356 |
+
print(f"\n 📝 Formatted Task Summary:")
|
| 357 |
+
# Display the formatted summary with proper indentation
|
| 358 |
+
for line in formatted_summary.split('\n'):
|
| 359 |
+
print(f" {line}")
|
| 360 |
+
|
| 361 |
+
# Show generated artifacts/files
|
| 362 |
+
if 'artifacts' in omirl_data:
|
| 363 |
+
artifacts = omirl_data['artifacts']
|
| 364 |
+
print(f"\n 📄 Generated Files: {len(artifacts)}")
|
| 365 |
+
for artifact in artifacts:
|
| 366 |
+
print(f" • {artifact}")
|
| 367 |
+
|
| 368 |
+
# Show routing information if available
|
| 369 |
+
if 'routing_result' in result_metadata:
|
| 370 |
+
routing = result_metadata['routing_result']
|
| 371 |
+
print(f"\n🧭 Tool Selection Details:")
|
| 372 |
+
print(f" 📊 Status: {routing.get('status', 'Unknown')}")
|
| 373 |
+
if 'tool_calls' in routing and routing['tool_calls']:
|
| 374 |
+
tool_call = routing['tool_calls'][0] # Show first tool call
|
| 375 |
+
print(f" 🛠️ Tool: {tool_call.tool_name}")
|
| 376 |
+
print(f" 📋 Task: {tool_call.task}")
|
| 377 |
+
print(f" ⚙️ Parameters: {tool_call.parameters}")
|
| 378 |
+
print(f" 🤔 Reason: {tool_call.reason}")
|
| 379 |
+
|
| 380 |
+
print(f" 🎯 Confidence: {routing.get('routing_confidence', 'Unknown')}")
|
| 381 |
+
print(f" ⏱️ Routing Time: {routing.get('routing_time_ms', 'Unknown')}ms")
|
| 382 |
+
|
| 383 |
+
print("-" * 50)
|
| 384 |
+
print("📝 This raw output will now be processed by the LLM to generate user-friendly insights...")
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
print(f"⚠️ Could not display task output: {e}")
|
| 388 |
+
import traceback
|
| 389 |
+
traceback.print_exc()
|
| 390 |
+
|
| 391 |
+
def execute_task_execution(self, tool_name: str, validation_result: Dict[str, Any]) -> tuple[bool, Dict[str, Any]]:
|
| 392 |
+
"""Execute the selected task."""
|
| 393 |
+
self.display_step_header(4, "Task Execution")
|
| 394 |
+
|
| 395 |
+
try:
|
| 396 |
+
# Create agent state
|
| 397 |
+
state = AgentState(
|
| 398 |
+
user_query=validation_result.get('original_query', ''),
|
| 399 |
+
session_id=self.test_session_id
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
# Update state with validation results
|
| 403 |
+
state.intent = validation_result.get('intent', '')
|
| 404 |
+
state.extracted_params = validation_result.get('extracted_params', {})
|
| 405 |
+
state.selected_tool = tool_name
|
| 406 |
+
|
| 407 |
+
print(f"🚀 Executing tool: {tool_name}")
|
| 408 |
+
print(f"📊 Parameters:")
|
| 409 |
+
for key, value in state.extracted_params.items():
|
| 410 |
+
print(f" • {key}: {value}")
|
| 411 |
+
|
| 412 |
+
# Execute the task
|
| 413 |
+
execution_result = self.agent.execute_task(state)
|
| 414 |
+
|
| 415 |
+
if execution_result.get('success', False):
|
| 416 |
+
print(f"✅ Task execution: SUCCESS")
|
| 417 |
+
|
| 418 |
+
# Display results
|
| 419 |
+
if 'data' in execution_result:
|
| 420 |
+
data = execution_result['data']
|
| 421 |
+
if isinstance(data, list):
|
| 422 |
+
print(f"📊 Data records: {len(data)}")
|
| 423 |
+
elif isinstance(data, dict):
|
| 424 |
+
print(f"📊 Data keys: {list(data.keys())}")
|
| 425 |
+
else:
|
| 426 |
+
print(f"📊 Data type: {type(data).__name__}")
|
| 427 |
+
|
| 428 |
+
if 'artifacts' in execution_result:
|
| 429 |
+
artifacts = execution_result['artifacts']
|
| 430 |
+
print(f"📄 Generated artifacts: {len(artifacts)}")
|
| 431 |
+
for artifact in artifacts[:3]: # Show first 3
|
| 432 |
+
print(f" • {artifact}")
|
| 433 |
+
if len(artifacts) > 3:
|
| 434 |
+
print(f" • ... and {len(artifacts) - 3} more")
|
| 435 |
+
|
| 436 |
+
if 'metadata' in execution_result:
|
| 437 |
+
metadata = execution_result['metadata']
|
| 438 |
+
print(f"📋 Metadata: {list(metadata.keys())}")
|
| 439 |
+
|
| 440 |
+
else:
|
| 441 |
+
print(f"❌ Task execution: FAILED")
|
| 442 |
+
print(f" Error: {execution_result.get('error', 'Unknown error')}")
|
| 443 |
+
|
| 444 |
+
return execution_result.get('success', False), execution_result
|
| 445 |
+
|
| 446 |
+
except Exception as e:
|
| 447 |
+
print(f"❌ Task execution failed with exception: {e}")
|
| 448 |
+
return False, {'error': str(e)}
|
| 449 |
+
|
| 450 |
+
def execute_llm_generation(self, execution_result: Dict[str, Any], original_query: str) -> tuple[bool, str]:
|
| 451 |
+
"""Execute LLM response generation."""
|
| 452 |
+
self.display_step_header(5, "LLM Response Generation")
|
| 453 |
+
|
| 454 |
+
try:
|
| 455 |
+
print(f"🤖 Generating response using {self.llm_client.provider}")
|
| 456 |
+
|
| 457 |
+
# Prepare context for LLM
|
| 458 |
+
context = {
|
| 459 |
+
'user_query': original_query,
|
| 460 |
+
'execution_success': execution_result.get('success', False),
|
| 461 |
+
'data': execution_result.get('data'),
|
| 462 |
+
'artifacts': execution_result.get('artifacts', []),
|
| 463 |
+
'metadata': execution_result.get('metadata', {}),
|
| 464 |
+
'error': execution_result.get('error')
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
# Generate response
|
| 468 |
+
llm_response = self.agent.generate_response(context)
|
| 469 |
+
|
| 470 |
+
if llm_response:
|
| 471 |
+
print(f"✅ LLM response generated ({len(llm_response)} characters)")
|
| 472 |
+
print(f"📝 Response preview: {llm_response[:150]}{'...' if len(llm_response) > 150 else ''}")
|
| 473 |
+
return True, llm_response
|
| 474 |
+
else:
|
| 475 |
+
print("❌ Failed to generate LLM response")
|
| 476 |
+
return False, ""
|
| 477 |
+
|
| 478 |
+
except Exception as e:
|
| 479 |
+
print(f"❌ LLM generation failed: {e}")
|
| 480 |
+
return False, ""
|
| 481 |
+
|
| 482 |
+
def display_final_output(self, processing_result: Dict[str, Any]):
|
| 483 |
+
"""Display the final application output."""
|
| 484 |
+
self.display_step_header(5, "LLM-Generated User Output")
|
| 485 |
+
|
| 486 |
+
result = processing_result.get('result', {})
|
| 487 |
+
|
| 488 |
+
print("📋 FINAL USER-FACING OUTPUT (after LLM processing):")
|
| 489 |
+
print("-" * 50)
|
| 490 |
+
|
| 491 |
+
if isinstance(result, dict) and 'response' in result:
|
| 492 |
+
print(result['response'])
|
| 493 |
+
else:
|
| 494 |
+
print(str(result))
|
| 495 |
+
|
| 496 |
+
print("-" * 50)
|
| 497 |
+
|
| 498 |
+
# Additional information
|
| 499 |
+
metadata = processing_result.get('metadata', {})
|
| 500 |
+
if metadata:
|
| 501 |
+
print(f"\n Execution Metadata:")
|
| 502 |
+
for key, value in metadata.items():
|
| 503 |
+
if key not in ['omirl_data', 'routing_result']: # These were shown in task output
|
| 504 |
+
print(f" • {key}: {value}")
|
| 505 |
+
|
| 506 |
+
def run_test_flow(self):
|
| 507 |
+
"""Run the complete test flow."""
|
| 508 |
+
print("🧪 FULL FLOW INTEGRATION TEST")
|
| 509 |
+
print("=" * 60)
|
| 510 |
+
print("This test will execute the complete application flow and show")
|
| 511 |
+
print("detailed information at each step to help you understand how")
|
| 512 |
+
print("configuration changes affect the agent's behavior.")
|
| 513 |
+
|
| 514 |
+
# Setup components
|
| 515 |
+
if not self.setup_components():
|
| 516 |
+
print("❌ Failed to setup components. Exiting.")
|
| 517 |
+
return
|
| 518 |
+
|
| 519 |
+
while True:
|
| 520 |
+
# Get user query
|
| 521 |
+
query = self.get_user_query()
|
| 522 |
+
if query is None:
|
| 523 |
+
print("👋 Goodbye!")
|
| 524 |
+
break
|
| 525 |
+
|
| 526 |
+
print(f"\n🚀 Starting flow for query: '{query}'")
|
| 527 |
+
|
| 528 |
+
# Step 1: Show configuration
|
| 529 |
+
self.display_config_info()
|
| 530 |
+
|
| 531 |
+
# Step 2: Validation
|
| 532 |
+
validation_success, validation_result = self.execute_validation(query)
|
| 533 |
+
if not validation_success:
|
| 534 |
+
print("\n❌ Flow stopped due to validation failure.")
|
| 535 |
+
# Show recommendations for validation failure
|
| 536 |
+
self.display_step_header(6, "Configuration Recommendations")
|
| 537 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 538 |
+
self.recommender.display_recommendations(recommendations)
|
| 539 |
+
self.flow_data = {} # Reset for next iteration
|
| 540 |
+
continue
|
| 541 |
+
|
| 542 |
+
# Step 3: Agent Processing (includes tool selection, task execution, and response generation)
|
| 543 |
+
processing_success, processing_result = self.execute_full_agent_processing(query)
|
| 544 |
+
if not processing_success:
|
| 545 |
+
print("\n❌ Flow stopped due to agent processing failure.")
|
| 546 |
+
# Show recommendations for processing failure
|
| 547 |
+
self.display_step_header(6, "Configuration Recommendations")
|
| 548 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 549 |
+
self.recommender.display_recommendations(recommendations)
|
| 550 |
+
self.flow_data = {} # Reset for next iteration
|
| 551 |
+
continue
|
| 552 |
+
|
| 553 |
+
# Step 4: Raw Task Output Display
|
| 554 |
+
self.display_task_output(processing_result)
|
| 555 |
+
|
| 556 |
+
# Step 5: Final output display
|
| 557 |
+
self.display_final_output(processing_result)
|
| 558 |
+
|
| 559 |
+
# Step 6: Configuration recommendations
|
| 560 |
+
self.display_step_header(6, "Configuration Recommendations")
|
| 561 |
+
recommendations = self.recommender.analyze_flow_results(self.flow_data)
|
| 562 |
+
self.recommender.display_recommendations(recommendations)
|
| 563 |
+
|
| 564 |
+
print(f"\n✅ Flow completed successfully!")
|
| 565 |
+
print(f"🔄 Ready for next query...")
|
| 566 |
+
|
| 567 |
+
# Reset flow data for next iteration
|
| 568 |
+
self.flow_data = {}
|
| 569 |
+
|
| 570 |
+
def main():
|
| 571 |
+
"""Main entry point."""
|
| 572 |
+
tester = FullFlowTester()
|
| 573 |
+
try:
|
| 574 |
+
tester.run_test_flow()
|
| 575 |
+
except KeyboardInterrupt:
|
| 576 |
+
print("\n\n👋 Test interrupted by user. Goodbye!")
|
| 577 |
+
except Exception as e:
|
| 578 |
+
print(f"\n❌ Unexpected error: {e}")
|
| 579 |
+
import traceback
|
| 580 |
+
traceback.print_exc()
|
| 581 |
+
|
| 582 |
+
if __name__ == "__main__":
|
| 583 |
+
main()
|
tests/agent/test_refactored_agent.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/agent/test_refactored_agent.py
|
| 2 |
+
"""
|
| 3 |
+
Test refactored agent.py to ensure elimination of hard-coded values
|
| 4 |
+
|
| 5 |
+
This test suite verifies that:
|
| 6 |
+
1. Agent loads configuration from YAML files instead of using hard-coded values
|
| 7 |
+
2. Validation messages come from configuration
|
| 8 |
+
3. Error message templates come from configuration
|
| 9 |
+
4. OMIRL capabilities come from configuration
|
| 10 |
+
5. Agent settings come from configuration
|
| 11 |
+
6. No hard-coded provinces, sensor types, or timeouts remain
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import unittest
|
| 17 |
+
from unittest.mock import patch, MagicMock
|
| 18 |
+
|
| 19 |
+
# Add the agent module to the Python path
|
| 20 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
| 21 |
+
|
| 22 |
+
from agent.agent import OperationsAgent, ValidationError
|
| 23 |
+
from agent.config_loader import ConfigLoader
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class TestRefactoredAgent(unittest.TestCase):
|
| 27 |
+
"""Test agent configuration-driven behavior"""
|
| 28 |
+
|
| 29 |
+
def setUp(self):
|
| 30 |
+
"""Set up test environment"""
|
| 31 |
+
self.agent = OperationsAgent(lazy_init=True) # Don't initialize workflow
|
| 32 |
+
|
| 33 |
+
def test_agent_uses_config_loader(self):
|
| 34 |
+
"""Test that agent properly loads configuration"""
|
| 35 |
+
print("Testing agent uses ConfigLoader...")
|
| 36 |
+
|
| 37 |
+
# Verify agent has config loader
|
| 38 |
+
self.assertIsNotNone(self.agent.config_loader)
|
| 39 |
+
self.assertIsInstance(self.agent.config_loader, ConfigLoader)
|
| 40 |
+
|
| 41 |
+
print("✅ Agent uses ConfigLoader")
|
| 42 |
+
|
| 43 |
+
def test_agent_settings_from_config(self):
|
| 44 |
+
"""Test that agent settings come from configuration"""
|
| 45 |
+
print("Testing agent settings from config...")
|
| 46 |
+
|
| 47 |
+
# Mock config values
|
| 48 |
+
with patch.object(self.agent.config_loader, 'get_agent_settings') as mock_settings:
|
| 49 |
+
mock_settings.return_value = {
|
| 50 |
+
"default_language": "en",
|
| 51 |
+
"max_message_length": 5000,
|
| 52 |
+
"default_timeout": 45.0
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# Create new agent to test initialization
|
| 56 |
+
test_agent = OperationsAgent(lazy_init=True)
|
| 57 |
+
|
| 58 |
+
# Verify settings are used
|
| 59 |
+
self.assertEqual(test_agent.default_language, "en")
|
| 60 |
+
|
| 61 |
+
print("✅ Agent settings loaded from configuration")
|
| 62 |
+
|
| 63 |
+
def test_validation_messages_from_config(self):
|
| 64 |
+
"""Test that validation messages come from configuration"""
|
| 65 |
+
print("Testing validation messages from config...")
|
| 66 |
+
|
| 67 |
+
# Mock validation message
|
| 68 |
+
with patch.object(self.agent.config_loader, 'get_validation_message') as mock_validation:
|
| 69 |
+
mock_validation.return_value = "Custom validation error message"
|
| 70 |
+
|
| 71 |
+
# Test validation that should trigger config lookup
|
| 72 |
+
try:
|
| 73 |
+
self.agent._validate_request_input("")
|
| 74 |
+
self.fail("Should have raised ValidationError")
|
| 75 |
+
except ValidationError as e:
|
| 76 |
+
# Verify the mock was called
|
| 77 |
+
mock_validation.assert_called()
|
| 78 |
+
|
| 79 |
+
print("✅ Validation messages from configuration")
|
| 80 |
+
|
| 81 |
+
def test_omirl_capabilities_from_config(self):
|
| 82 |
+
"""Test that OMIRL capabilities come from configuration"""
|
| 83 |
+
print("Testing OMIRL capabilities from config...")
|
| 84 |
+
|
| 85 |
+
# Mock configuration values
|
| 86 |
+
with patch.object(self.agent.config_loader, 'get_omirl_capabilities') as mock_omirl, \
|
| 87 |
+
patch.object(self.agent.config_loader, 'get_supported_sensors') as mock_sensors, \
|
| 88 |
+
patch.object(self.agent.config_loader, 'get_valid_provinces') as mock_provinces:
|
| 89 |
+
|
| 90 |
+
mock_omirl.return_value = {
|
| 91 |
+
"data_source": "https://test.example.com",
|
| 92 |
+
"description": "Test OMIRL",
|
| 93 |
+
"features": ["Test feature"]
|
| 94 |
+
}
|
| 95 |
+
mock_sensors.return_value = ["Test Sensor"]
|
| 96 |
+
mock_provinces.return_value = ["TEST_PROVINCE"]
|
| 97 |
+
|
| 98 |
+
capabilities = self.agent.get_omirl_capabilities()
|
| 99 |
+
|
| 100 |
+
# Verify all config methods were called
|
| 101 |
+
mock_omirl.assert_called_once()
|
| 102 |
+
mock_sensors.assert_called_once()
|
| 103 |
+
mock_provinces.assert_called_once()
|
| 104 |
+
|
| 105 |
+
# Verify capabilities structure
|
| 106 |
+
self.assertIn("supported_sensors", capabilities)
|
| 107 |
+
self.assertIn("supported_provinces", capabilities)
|
| 108 |
+
self.assertEqual(capabilities["supported_sensors"], ["Test Sensor"])
|
| 109 |
+
self.assertEqual(capabilities["supported_provinces"], ["TEST_PROVINCE"])
|
| 110 |
+
|
| 111 |
+
print("✅ OMIRL capabilities from configuration")
|
| 112 |
+
|
| 113 |
+
def test_error_templates_from_config(self):
|
| 114 |
+
"""Test that error message templates come from configuration"""
|
| 115 |
+
print("Testing error templates from config...")
|
| 116 |
+
|
| 117 |
+
# Mock error template
|
| 118 |
+
with patch.object(self.agent.config_loader, 'get_error_message_template') as mock_template:
|
| 119 |
+
mock_template.return_value = "Custom error template: {error_message}"
|
| 120 |
+
|
| 121 |
+
error_response = self.agent._format_error_response("Test error", "Test message", "ValidationError")
|
| 122 |
+
|
| 123 |
+
# Verify template method was called
|
| 124 |
+
mock_template.assert_called_with("validation_error")
|
| 125 |
+
|
| 126 |
+
# Verify formatted response
|
| 127 |
+
self.assertIn("Custom error template: Test error", error_response["response"])
|
| 128 |
+
|
| 129 |
+
print("✅ Error templates from configuration")
|
| 130 |
+
|
| 131 |
+
def test_provinces_validation_from_config(self):
|
| 132 |
+
"""Test that province validation uses configuration"""
|
| 133 |
+
print("Testing province validation from config...")
|
| 134 |
+
|
| 135 |
+
# Mock valid provinces
|
| 136 |
+
with patch.object(self.agent.config_loader, 'get_valid_provinces') as mock_provinces, \
|
| 137 |
+
patch.object(self.agent.config_loader, 'get_validation_message') as mock_validation:
|
| 138 |
+
|
| 139 |
+
mock_provinces.return_value = ["GENOVA", "SAVONA"]
|
| 140 |
+
mock_validation.return_value = "Invalid province: {provincia}. Valid: {valid_provinces}"
|
| 141 |
+
|
| 142 |
+
# This should trigger validation
|
| 143 |
+
try:
|
| 144 |
+
import asyncio
|
| 145 |
+
# Mock the workflow initialization to avoid issues
|
| 146 |
+
with patch.object(self.agent, '_initialize_workflow'):
|
| 147 |
+
asyncio.run(self.agent.process_omirl_request(provincia="INVALID"))
|
| 148 |
+
except Exception:
|
| 149 |
+
pass # We just want to verify the config methods are called
|
| 150 |
+
|
| 151 |
+
# Verify config method was called
|
| 152 |
+
mock_provinces.assert_called()
|
| 153 |
+
|
| 154 |
+
print("✅ Province validation from configuration")
|
| 155 |
+
|
| 156 |
+
def test_conversation_history_limit_from_config(self):
|
| 157 |
+
"""Test that conversation history limit comes from configuration"""
|
| 158 |
+
print("Testing conversation history limit from config...")
|
| 159 |
+
|
| 160 |
+
# Mock config value
|
| 161 |
+
with patch.object(self.agent.config_loader, 'get_config_value') as mock_config:
|
| 162 |
+
mock_config.return_value = 25
|
| 163 |
+
|
| 164 |
+
# Test conversation history retrieval
|
| 165 |
+
history = self.agent.get_conversation_history()
|
| 166 |
+
|
| 167 |
+
# Verify config was accessed
|
| 168 |
+
mock_config.assert_called_with("agent_config", "agent.settings.conversation_history_limit", 50)
|
| 169 |
+
|
| 170 |
+
print("✅ Conversation history limit from configuration")
|
| 171 |
+
|
| 172 |
+
def test_no_hard_coded_values_in_agent(self):
|
| 173 |
+
"""Test that no hard-coded values remain in agent methods"""
|
| 174 |
+
print("Testing no hard-coded values remain...")
|
| 175 |
+
|
| 176 |
+
import inspect
|
| 177 |
+
|
| 178 |
+
# Get agent source code
|
| 179 |
+
agent_source = inspect.getsource(OperationsAgent)
|
| 180 |
+
|
| 181 |
+
# Check for hard-coded functional values (not docstrings/comments)
|
| 182 |
+
# Focus on actual code patterns that indicate hard-coded behavior
|
| 183 |
+
hard_coded_patterns = [
|
| 184 |
+
'timeout=60',
|
| 185 |
+
'timeout: float = 60',
|
| 186 |
+
'len(user_message) > 10000',
|
| 187 |
+
'conversation_history[-50:]',
|
| 188 |
+
'provincia.upper() not in ["GENOVA"', # Actual validation logic
|
| 189 |
+
'"GENOVA", "SAVONA", "IMPERIA", "LA SPEZIA"' # Hard-coded list
|
| 190 |
+
]
|
| 191 |
+
|
| 192 |
+
for pattern in hard_coded_patterns:
|
| 193 |
+
self.assertNotIn(pattern, agent_source, f"Found hard-coded value: {pattern}")
|
| 194 |
+
|
| 195 |
+
print("✅ No hard-coded functional values found in OperationsAgent")
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def run_tests():
|
| 199 |
+
"""Run all agent refactoring tests"""
|
| 200 |
+
print("🧪 TESTING REFACTORED AGENT.PY")
|
| 201 |
+
print("=" * 60)
|
| 202 |
+
|
| 203 |
+
# Create test suite
|
| 204 |
+
suite = unittest.TestLoader().loadTestsFromTestCase(TestRefactoredAgent)
|
| 205 |
+
runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w'))
|
| 206 |
+
result = runner.run(suite)
|
| 207 |
+
|
| 208 |
+
# Print results
|
| 209 |
+
tests_run = result.testsRun
|
| 210 |
+
failures = len(result.failures)
|
| 211 |
+
errors = len(result.errors)
|
| 212 |
+
success_count = tests_run - failures - errors
|
| 213 |
+
|
| 214 |
+
print(f"\nTEST SUMMARY: {success_count}/{tests_run} test suites passed")
|
| 215 |
+
|
| 216 |
+
if result.wasSuccessful():
|
| 217 |
+
print("🎉 All agent refactoring tests successful!")
|
| 218 |
+
print("✅ Agent now uses configurable values instead of hard-coded ones")
|
| 219 |
+
print("✅ Configuration loading system working properly")
|
| 220 |
+
print("✅ Error templates loaded from configuration")
|
| 221 |
+
print("✅ OMIRL capabilities loaded from configuration")
|
| 222 |
+
print("✅ Validation messages loaded from configuration")
|
| 223 |
+
else:
|
| 224 |
+
print("❌ Some tests failed!")
|
| 225 |
+
for failure in result.failures:
|
| 226 |
+
print(f"FAILURE: {failure[0]}")
|
| 227 |
+
print(f" {failure[1]}")
|
| 228 |
+
for error in result.errors:
|
| 229 |
+
print(f"ERROR: {error[0]}")
|
| 230 |
+
print(f" {error[1]}")
|
| 231 |
+
|
| 232 |
+
return result.wasSuccessful()
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
if __name__ == "__main__":
|
| 236 |
+
success = run_tests()
|
| 237 |
+
sys.exit(0 if success else 1)
|
tests/agent/test_refactored_nodes.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test Refactored Nodes with Response Handler
|
| 4 |
+
|
| 5 |
+
This test validates that the refactored nodes.py properly uses the
|
| 6 |
+
ResponseTemplateHandler instead of hard-coded response strings. It ensures:
|
| 7 |
+
|
| 8 |
+
1. Response generation node uses configurable templates
|
| 9 |
+
2. Error handling node uses configurable templates
|
| 10 |
+
3. No hard-coded response strings remain in nodes
|
| 11 |
+
4. Generated responses match expected format and content
|
| 12 |
+
|
| 13 |
+
Expected: All node response generation tests should pass, confirming
|
| 14 |
+
successful elimination of hard-coded response strings
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
import os
|
| 19 |
+
import asyncio
|
| 20 |
+
from unittest.mock import Mock
|
| 21 |
+
sys.path.append('/home/jeanbaptistebove/projects/operations')
|
| 22 |
+
|
| 23 |
+
from agent.nodes import response_generation_node, error_handling_node
|
| 24 |
+
from agent.state import AgentState, ToolResult, create_initial_state
|
| 25 |
+
from agent.response_handler import get_response_handler, reset_response_handler
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def create_mock_tool_result(tool_name: str, success: bool = True, summary_text: str = "", artifacts: list = None, warnings: list = None, sources: list = None, metadata: dict = None):
|
| 29 |
+
"""Create mock tool result for testing"""
|
| 30 |
+
return ToolResult(
|
| 31 |
+
tool_name=tool_name,
|
| 32 |
+
success=success,
|
| 33 |
+
summary_text=summary_text,
|
| 34 |
+
artifacts=artifacts or [],
|
| 35 |
+
sources=sources or [],
|
| 36 |
+
metadata=metadata or {},
|
| 37 |
+
warnings=warnings or []
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def test_success_response_generation():
|
| 42 |
+
"""Test that success responses are generated using templates"""
|
| 43 |
+
print("🧪 Testing Success Response Generation in Nodes")
|
| 44 |
+
print("=" * 60)
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
# Create test state with successful OMIRL result
|
| 48 |
+
state = create_initial_state("test query")
|
| 49 |
+
|
| 50 |
+
omirl_result = create_mock_tool_result(
|
| 51 |
+
tool_name="omirl_tool",
|
| 52 |
+
success=True,
|
| 53 |
+
summary_text="🌡️ **Temperatura Genova**: 21.5°C media in 13 stazioni attive",
|
| 54 |
+
artifacts=["temperature_genova_20250911.json"],
|
| 55 |
+
sources=["https://omirl.regione.liguria.it/#/sensorstable"],
|
| 56 |
+
metadata={"sensor_type": "Temperatura", "total_stations": 13}
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
state["tool_results"] = [omirl_result]
|
| 60 |
+
state["processing_status"] = "completed"
|
| 61 |
+
state["errors"] = []
|
| 62 |
+
|
| 63 |
+
# Generate response using node
|
| 64 |
+
result_state = await response_generation_node(state)
|
| 65 |
+
|
| 66 |
+
# Verify response was generated
|
| 67 |
+
assert "agent_response" in result_state, "Response should be generated"
|
| 68 |
+
response = result_state["agent_response"]
|
| 69 |
+
|
| 70 |
+
# Verify OMIRL-specific header (from template)
|
| 71 |
+
assert "🌊 **Estrazione Dati OMIRL Completata**" in response, "Should use OMIRL template header"
|
| 72 |
+
|
| 73 |
+
# Verify tool result content is included
|
| 74 |
+
assert "Temperatura Genova" in response, "Should include tool result summary"
|
| 75 |
+
|
| 76 |
+
# Verify artifact formatting (from template)
|
| 77 |
+
assert "File generato" in response, "Should include artifact information"
|
| 78 |
+
|
| 79 |
+
# Verify source formatting (from template)
|
| 80 |
+
assert "Fonti dati" in response, "Should include sources section"
|
| 81 |
+
|
| 82 |
+
# Verify no hard-coded strings are present
|
| 83 |
+
response_lower = response.lower()
|
| 84 |
+
hard_coded_indicators = ["⚠️ **errore nell'elaborazione**", "provincia di savona"]
|
| 85 |
+
for indicator in hard_coded_indicators:
|
| 86 |
+
assert indicator not in response_lower, f"Should not contain hard-coded string: {indicator}"
|
| 87 |
+
|
| 88 |
+
print("✅ OMIRL success response generated correctly using templates")
|
| 89 |
+
|
| 90 |
+
# Test generic tool response
|
| 91 |
+
generic_state = create_initial_state("test query")
|
| 92 |
+
generic_result = create_mock_tool_result(
|
| 93 |
+
tool_name="generic_tool",
|
| 94 |
+
success=True,
|
| 95 |
+
summary_text="Generic operation completed successfully"
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
generic_state["tool_results"] = [generic_result]
|
| 99 |
+
generic_state["processing_status"] = "completed"
|
| 100 |
+
generic_state["errors"] = []
|
| 101 |
+
|
| 102 |
+
generic_result_state = await response_generation_node(generic_state)
|
| 103 |
+
generic_response = generic_result_state["agent_response"]
|
| 104 |
+
|
| 105 |
+
# Should use generic template header
|
| 106 |
+
assert "✅ **Operazione Completata**" in generic_response, "Should use generic template header"
|
| 107 |
+
assert "Generic operation completed" in generic_response, "Should include tool summary"
|
| 108 |
+
|
| 109 |
+
print("✅ Generic success response generated correctly using templates")
|
| 110 |
+
|
| 111 |
+
return True
|
| 112 |
+
|
| 113 |
+
except Exception as e:
|
| 114 |
+
print(f"❌ Success response generation test failed: {e}")
|
| 115 |
+
return False
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
async def test_help_response_generation():
|
| 119 |
+
"""Test that help responses are generated using templates"""
|
| 120 |
+
print("\n🧪 Testing Help Response Generation in Nodes")
|
| 121 |
+
print("=" * 60)
|
| 122 |
+
|
| 123 |
+
try:
|
| 124 |
+
# Create state for help request
|
| 125 |
+
help_state = create_initial_state("aiuto")
|
| 126 |
+
help_state["tool_results"] = []
|
| 127 |
+
help_state["processing_status"] = "help_requested"
|
| 128 |
+
help_state["errors"] = []
|
| 129 |
+
|
| 130 |
+
result_state = await response_generation_node(help_state)
|
| 131 |
+
response = result_state["agent_response"]
|
| 132 |
+
|
| 133 |
+
# Verify help response uses templates
|
| 134 |
+
assert "Assistente Operazioni" in response and "Guida" in response, f"Should use help template header, got: {response[:100]}"
|
| 135 |
+
assert "Posso aiutarti con:" in response, "Should use capabilities intro from template"
|
| 136 |
+
assert "Esempi di richieste:" in response, "Should use examples intro from template"
|
| 137 |
+
|
| 138 |
+
# Verify no hard-coded examples remain
|
| 139 |
+
hard_coded_examples = [
|
| 140 |
+
"Mostra precipitazioni a Genova", # This specific one should be from config now
|
| 141 |
+
"Temperatura a La Spezia",
|
| 142 |
+
"Stazioni meteo in provincia di Savona"
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
# At least one example should be present (from configuration)
|
| 146 |
+
has_examples = any(example in response for example in ["Mostra precipitazioni", "Temperatura", "Stazioni meteo"])
|
| 147 |
+
assert has_examples, "Should include examples from configuration"
|
| 148 |
+
|
| 149 |
+
print("✅ Help response generated correctly using templates")
|
| 150 |
+
|
| 151 |
+
# Test clarification request
|
| 152 |
+
clarification_state = create_initial_state("dati genova")
|
| 153 |
+
clarification_state["tool_results"] = []
|
| 154 |
+
clarification_state["processing_status"] = "needs_clarification"
|
| 155 |
+
clarification_state["clarification_request"] = "Specifica il tipo di sensore"
|
| 156 |
+
clarification_state["errors"] = []
|
| 157 |
+
|
| 158 |
+
clarification_result_state = await response_generation_node(clarification_state)
|
| 159 |
+
clarification_response = clarification_result_state["agent_response"]
|
| 160 |
+
|
| 161 |
+
# Should include clarification from template
|
| 162 |
+
assert "Specifica meglio" in clarification_response, "Should use clarification template"
|
| 163 |
+
assert "tipo di sensore" in clarification_response, "Should include clarification text"
|
| 164 |
+
|
| 165 |
+
print("✅ Clarification response generated correctly using templates")
|
| 166 |
+
|
| 167 |
+
return True
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
print(f"❌ Help response generation test failed: {e}")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
async def test_error_response_generation():
|
| 175 |
+
"""Test that error responses are generated using templates"""
|
| 176 |
+
print("\n🧪 Testing Error Response Generation in Nodes")
|
| 177 |
+
print("=" * 60)
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
# Test error handling node directly
|
| 181 |
+
error_state = create_initial_state("test query")
|
| 182 |
+
|
| 183 |
+
failed_result = create_mock_tool_result(
|
| 184 |
+
tool_name="omirl_tool",
|
| 185 |
+
success=False,
|
| 186 |
+
summary_text="⚠️ Errore nell'estrazione dati: timeout connessione"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
error_state["tool_results"] = [failed_result]
|
| 190 |
+
error_state["errors"] = ["Errore di connessione al servizio OMIRL", "Timeout dopo 30 secondi"]
|
| 191 |
+
error_state["processing_status"] = "error"
|
| 192 |
+
|
| 193 |
+
result_state = await error_handling_node(error_state)
|
| 194 |
+
response = result_state["agent_response"]
|
| 195 |
+
|
| 196 |
+
# Verify error response uses templates
|
| 197 |
+
assert "⚠️ **Errore nell'Elaborazione**" in response, "Should use error template header"
|
| 198 |
+
|
| 199 |
+
# Verify error content is included
|
| 200 |
+
assert "timeout connessione" in response, "Should include tool error"
|
| 201 |
+
assert "Errore di connessione" in response, "Should include general errors"
|
| 202 |
+
assert "riformulare la richiesta" in response, "Should include helpful suggestion from template"
|
| 203 |
+
|
| 204 |
+
print("✅ Error response generated correctly using templates")
|
| 205 |
+
|
| 206 |
+
# Test response generation node with errors
|
| 207 |
+
error_state_2 = create_initial_state("test query")
|
| 208 |
+
error_state_2["tool_results"] = [failed_result]
|
| 209 |
+
error_state_2["errors"] = ["General error"]
|
| 210 |
+
error_state_2["processing_status"] = "error"
|
| 211 |
+
|
| 212 |
+
result_state_2 = await response_generation_node(error_state_2)
|
| 213 |
+
response_2 = result_state_2["agent_response"]
|
| 214 |
+
|
| 215 |
+
# Should also use error templates
|
| 216 |
+
assert "⚠️ **Errore nell'Elaborazione**" in response_2, "Response generation node should use error templates"
|
| 217 |
+
|
| 218 |
+
print("✅ Response generation node error handling uses templates")
|
| 219 |
+
|
| 220 |
+
return True
|
| 221 |
+
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f"❌ Error response generation test failed: {e}")
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
async def test_rejection_response_generation():
|
| 228 |
+
"""Test that rejection responses are generated using templates"""
|
| 229 |
+
print("\n🧪 Testing Rejection Response Generation in Nodes")
|
| 230 |
+
print("=" * 60)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
# Test geographic rejection
|
| 234 |
+
rejection_state = create_initial_state("dati milano")
|
| 235 |
+
rejection_state["tool_results"] = []
|
| 236 |
+
rejection_state["processing_status"] = "rejected"
|
| 237 |
+
rejection_state["rejection_reason"] = "Località non supportata: Milano"
|
| 238 |
+
rejection_state["errors"] = []
|
| 239 |
+
|
| 240 |
+
result_state = await response_generation_node(rejection_state)
|
| 241 |
+
response = result_state["agent_response"]
|
| 242 |
+
|
| 243 |
+
# Verify rejection response uses templates
|
| 244 |
+
assert "⚠️" in response and "Richiesta Non Supportata" in response, f"Should use rejection template header, got: {response[:100]}"
|
| 245 |
+
assert "Milano" in response, "Should include rejection reason"
|
| 246 |
+
assert "Liguria" in response, "Should mention supported region from template"
|
| 247 |
+
|
| 248 |
+
# Verify it's not using hard-coded province list
|
| 249 |
+
geographic_info_present = any(province in response for province in ["Genova", "Savona", "Imperia", "La Spezia"])
|
| 250 |
+
assert geographic_info_present, "Should include geographic information from template"
|
| 251 |
+
|
| 252 |
+
print("✅ Rejection response generated correctly using templates")
|
| 253 |
+
|
| 254 |
+
return True
|
| 255 |
+
|
| 256 |
+
except Exception as e:
|
| 257 |
+
print(f"❌ Rejection response generation test failed: {e}")
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
async def test_template_consistency():
|
| 262 |
+
"""Test that responses are consistent with template configuration"""
|
| 263 |
+
print("\n🧪 Testing Template Consistency")
|
| 264 |
+
print("=" * 60)
|
| 265 |
+
|
| 266 |
+
try:
|
| 267 |
+
# Get response handler to check templates directly
|
| 268 |
+
response_handler = get_response_handler()
|
| 269 |
+
|
| 270 |
+
# Test OMIRL emoji is used consistently
|
| 271 |
+
omirl_emoji = response_handler.get_tool_emoji("omirl_tool")
|
| 272 |
+
|
| 273 |
+
# Generate OMIRL response
|
| 274 |
+
omirl_state = create_initial_state("test query")
|
| 275 |
+
omirl_result = create_mock_tool_result("omirl_tool", True, "Test OMIRL data")
|
| 276 |
+
omirl_state["tool_results"] = [omirl_result]
|
| 277 |
+
omirl_state["processing_status"] = "completed"
|
| 278 |
+
omirl_state["errors"] = []
|
| 279 |
+
|
| 280 |
+
result_state = await response_generation_node(omirl_state)
|
| 281 |
+
response = result_state["agent_response"]
|
| 282 |
+
|
| 283 |
+
# Should use the emoji from configuration
|
| 284 |
+
assert omirl_emoji in response, f"Should use configured OMIRL emoji: {omirl_emoji}"
|
| 285 |
+
|
| 286 |
+
print(f"✅ Template consistency verified - OMIRL emoji: {omirl_emoji}")
|
| 287 |
+
|
| 288 |
+
# Test that different tools get different treatment
|
| 289 |
+
generic_state = create_initial_state("test query")
|
| 290 |
+
generic_result = create_mock_tool_result("other_tool", True, "Test other data")
|
| 291 |
+
generic_state["tool_results"] = [generic_result]
|
| 292 |
+
generic_state["processing_status"] = "completed"
|
| 293 |
+
generic_state["errors"] = []
|
| 294 |
+
|
| 295 |
+
generic_result_state = await response_generation_node(generic_state)
|
| 296 |
+
generic_response = generic_result_state["agent_response"]
|
| 297 |
+
|
| 298 |
+
# Should be different from OMIRL response
|
| 299 |
+
assert response != generic_response, "Different tools should generate different responses"
|
| 300 |
+
|
| 301 |
+
# Generic should use different emoji
|
| 302 |
+
default_emoji = response_handler.get_tool_emoji("other_tool")
|
| 303 |
+
assert default_emoji in generic_response, f"Should use default emoji: {default_emoji}"
|
| 304 |
+
|
| 305 |
+
print(f"✅ Tool-specific behavior verified - Default emoji: {default_emoji}")
|
| 306 |
+
|
| 307 |
+
return True
|
| 308 |
+
|
| 309 |
+
except Exception as e:
|
| 310 |
+
print(f"❌ Template consistency test failed: {e}")
|
| 311 |
+
return False
|
| 312 |
+
|
| 313 |
+
|
| 314 |
+
if __name__ == "__main__":
|
| 315 |
+
print("Starting Refactored Nodes Tests")
|
| 316 |
+
print("=" * 80)
|
| 317 |
+
|
| 318 |
+
# Reset response handler to ensure clean state
|
| 319 |
+
reset_response_handler()
|
| 320 |
+
|
| 321 |
+
async def run_all_tests():
|
| 322 |
+
# Run all tests
|
| 323 |
+
results = []
|
| 324 |
+
results.append(await test_success_response_generation())
|
| 325 |
+
results.append(await test_help_response_generation())
|
| 326 |
+
results.append(await test_error_response_generation())
|
| 327 |
+
results.append(await test_rejection_response_generation())
|
| 328 |
+
results.append(await test_template_consistency())
|
| 329 |
+
|
| 330 |
+
return results
|
| 331 |
+
|
| 332 |
+
# Run tests
|
| 333 |
+
results = asyncio.run(run_all_tests())
|
| 334 |
+
|
| 335 |
+
# Summary
|
| 336 |
+
passed = sum(results)
|
| 337 |
+
total = len(results)
|
| 338 |
+
|
| 339 |
+
print(f"\n{'='*80}")
|
| 340 |
+
print(f"TEST SUMMARY: {passed}/{total} test suites passed")
|
| 341 |
+
|
| 342 |
+
if passed == total:
|
| 343 |
+
print("🎉 All refactored nodes tests successful!")
|
| 344 |
+
print("✅ Nodes now use configurable response templates")
|
| 345 |
+
print("✅ Hard-coded response strings eliminated from nodes.py")
|
| 346 |
+
else:
|
| 347 |
+
print("❌ Some tests failed - review refactoring")
|
| 348 |
+
|
| 349 |
+
print("=" * 80)
|
tests/agent/test_response_handler.py
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test Response Template Handler
|
| 4 |
+
|
| 5 |
+
This test validates the ResponseTemplateHandler class that provides
|
| 6 |
+
tool-agnostic response generation. It ensures that:
|
| 7 |
+
|
| 8 |
+
1. Templates are properly loaded and applied
|
| 9 |
+
2. Success responses are generated correctly
|
| 10 |
+
3. Error and help responses work properly
|
| 11 |
+
4. Tool-specific logic is configuration-driven rather than hard-coded
|
| 12 |
+
|
| 13 |
+
Expected: All response generation tests should pass, demonstrating
|
| 14 |
+
that hard-coded response strings have been successfully eliminated
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
import os
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
sys.path.append('/home/jeanbaptistebove/projects/operations')
|
| 21 |
+
|
| 22 |
+
from agent.response_handler import ResponseTemplateHandler, ResponseContext, get_response_handler
|
| 23 |
+
from agent.state import ToolResult
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def create_mock_tool_result(tool_name: str, success: bool = True, summary_text: str = "", artifacts: list = None, warnings: list = None, sources: list = None, metadata: dict = None):
|
| 27 |
+
"""Create mock tool result for testing"""
|
| 28 |
+
return ToolResult(
|
| 29 |
+
tool_name=tool_name,
|
| 30 |
+
success=success,
|
| 31 |
+
summary_text=summary_text,
|
| 32 |
+
artifacts=artifacts or [],
|
| 33 |
+
sources=sources or [],
|
| 34 |
+
metadata=metadata or {},
|
| 35 |
+
warnings=warnings or []
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_response_handler_loading():
|
| 40 |
+
"""Test that ResponseTemplateHandler loads templates correctly"""
|
| 41 |
+
print("🧪 Testing Response Template Handler Loading")
|
| 42 |
+
print("=" * 60)
|
| 43 |
+
|
| 44 |
+
try:
|
| 45 |
+
# Test handler initialization
|
| 46 |
+
handler = ResponseTemplateHandler()
|
| 47 |
+
|
| 48 |
+
assert handler.templates is not None, "Templates should be loaded"
|
| 49 |
+
assert len(handler.templates) > 0, "Templates should not be empty"
|
| 50 |
+
|
| 51 |
+
# Check main sections exist
|
| 52 |
+
expected_sections = ['success_responses', 'help_responses', 'error_responses']
|
| 53 |
+
for section in expected_sections:
|
| 54 |
+
assert section in handler.templates, f"Missing section: {section}"
|
| 55 |
+
|
| 56 |
+
print("✅ Response handler loaded successfully")
|
| 57 |
+
print(f" - Loaded sections: {list(handler.templates.keys())}")
|
| 58 |
+
|
| 59 |
+
# Test global instance
|
| 60 |
+
global_handler = get_response_handler()
|
| 61 |
+
assert global_handler is not None, "Global handler should be available"
|
| 62 |
+
print("✅ Global handler instance works")
|
| 63 |
+
|
| 64 |
+
return True
|
| 65 |
+
|
| 66 |
+
except Exception as e:
|
| 67 |
+
print(f"❌ Response handler loading failed: {e}")
|
| 68 |
+
return False
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_success_response_generation():
|
| 72 |
+
"""Test generation of success responses for different tool types"""
|
| 73 |
+
print("\n🧪 Testing Success Response Generation")
|
| 74 |
+
print("=" * 60)
|
| 75 |
+
|
| 76 |
+
try:
|
| 77 |
+
handler = ResponseTemplateHandler()
|
| 78 |
+
|
| 79 |
+
# Test OMIRL tool success response
|
| 80 |
+
omirl_result = create_mock_tool_result(
|
| 81 |
+
tool_name="omirl_tool",
|
| 82 |
+
success=True,
|
| 83 |
+
summary_text="🌡️ **Temperatura Genova**: 21.5°C media in 13 stazioni attive",
|
| 84 |
+
artifacts=["temperature_genova_20250911.json"],
|
| 85 |
+
sources=["https://omirl.regione.liguria.it/#/sensorstable"]
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
context = ResponseContext(
|
| 89 |
+
tool_results=[omirl_result],
|
| 90 |
+
processing_status="completed",
|
| 91 |
+
errors=[]
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
response = handler.generate_success_response(context)
|
| 95 |
+
|
| 96 |
+
# Verify OMIRL-specific response
|
| 97 |
+
assert "🌊 **Estrazione Dati OMIRL Completata**" in response, "Should use OMIRL-specific header"
|
| 98 |
+
assert "Temperatura Genova" in response, "Should include tool result summary"
|
| 99 |
+
assert "File generato" in response, "Should include artifact information"
|
| 100 |
+
assert "Fonti dati" in response, "Should include sources"
|
| 101 |
+
print("✅ OMIRL success response generated correctly")
|
| 102 |
+
|
| 103 |
+
# Test generic tool success response
|
| 104 |
+
generic_result = create_mock_tool_result(
|
| 105 |
+
tool_name="generic_tool",
|
| 106 |
+
success=True,
|
| 107 |
+
summary_text="Operazione generica completata con successo"
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
generic_context = ResponseContext(
|
| 111 |
+
tool_results=[generic_result],
|
| 112 |
+
processing_status="completed",
|
| 113 |
+
errors=[]
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
generic_response = handler.generate_success_response(generic_context)
|
| 117 |
+
assert "✅ **Operazione Completata**" in generic_response, "Should use generic header for non-OMIRL tools"
|
| 118 |
+
print("✅ Generic success response generated correctly")
|
| 119 |
+
|
| 120 |
+
return True
|
| 121 |
+
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"❌ Success response generation failed: {e}")
|
| 124 |
+
return False
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def test_error_response_generation():
|
| 128 |
+
"""Test generation of error responses"""
|
| 129 |
+
print("\n🧪 Testing Error Response Generation")
|
| 130 |
+
print("=" * 60)
|
| 131 |
+
|
| 132 |
+
try:
|
| 133 |
+
handler = ResponseTemplateHandler()
|
| 134 |
+
|
| 135 |
+
# Test tool failure response
|
| 136 |
+
failed_result = create_mock_tool_result(
|
| 137 |
+
tool_name="omirl_tool",
|
| 138 |
+
success=False,
|
| 139 |
+
summary_text="⚠️ Errore nell'estrazione dati: timeout connessione"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
error_context = ResponseContext(
|
| 143 |
+
tool_results=[failed_result],
|
| 144 |
+
processing_status="error",
|
| 145 |
+
errors=["Errore di connessione al servizio OMIRL"]
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
error_response = handler.generate_error_response(error_context)
|
| 149 |
+
|
| 150 |
+
assert "⚠️ **Errore nell'Elaborazione**" in error_response, "Should have error header"
|
| 151 |
+
assert "timeout connessione" in error_response, "Should include tool error message"
|
| 152 |
+
assert "Errore di connessione" in error_response, "Should include general errors"
|
| 153 |
+
assert "riformulare la richiesta" in error_response, "Should include helpful suggestion"
|
| 154 |
+
print("✅ Error response generated correctly")
|
| 155 |
+
|
| 156 |
+
# Test rejection response
|
| 157 |
+
rejection_context = ResponseContext(
|
| 158 |
+
tool_results=[],
|
| 159 |
+
processing_status="rejected",
|
| 160 |
+
errors=[],
|
| 161 |
+
rejection_reason="Località non supportata: Milano"
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
rejection_response = handler.generate_rejection_response(rejection_context)
|
| 165 |
+
|
| 166 |
+
assert "⚠️ **Richiesta Non Supportata**" in rejection_response, "Should have rejection header"
|
| 167 |
+
assert "Milano" in rejection_response, "Should include rejection reason"
|
| 168 |
+
assert "Liguria" in rejection_response, "Should mention supported region"
|
| 169 |
+
print("✅ Rejection response generated correctly")
|
| 170 |
+
|
| 171 |
+
return True
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
print(f"❌ Error response generation failed: {e}")
|
| 175 |
+
return False
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def test_help_response_generation():
|
| 179 |
+
"""Test generation of help/guidance responses"""
|
| 180 |
+
print("\n🧪 Testing Help Response Generation")
|
| 181 |
+
print("=" * 60)
|
| 182 |
+
|
| 183 |
+
try:
|
| 184 |
+
handler = ResponseTemplateHandler()
|
| 185 |
+
|
| 186 |
+
# Test basic help response
|
| 187 |
+
help_context = ResponseContext(
|
| 188 |
+
tool_results=[],
|
| 189 |
+
processing_status="help_requested",
|
| 190 |
+
errors=[]
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
help_response = handler.generate_help_response(help_context)
|
| 194 |
+
|
| 195 |
+
assert "Assistente Operazioni - Guida" in help_response, "Should have help header"
|
| 196 |
+
assert "Posso aiutarti con:" in help_response, "Should have capabilities intro"
|
| 197 |
+
assert "Esempi di richieste:" in help_response, "Should have examples section"
|
| 198 |
+
print("✅ Basic help response generated correctly")
|
| 199 |
+
|
| 200 |
+
# Test help with clarification
|
| 201 |
+
clarification_context = ResponseContext(
|
| 202 |
+
tool_results=[],
|
| 203 |
+
processing_status="needs_clarification",
|
| 204 |
+
errors=[],
|
| 205 |
+
clarification_request="Specifica la provincia di interesse"
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
clarification_response = handler.generate_help_response(clarification_context)
|
| 209 |
+
|
| 210 |
+
assert "Specifica meglio" in clarification_response, "Should include clarification request"
|
| 211 |
+
assert "provincia di interesse" in clarification_response, "Should include specific clarification"
|
| 212 |
+
print("✅ Help with clarification generated correctly")
|
| 213 |
+
|
| 214 |
+
return True
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f"❌ Help response generation failed: {e}")
|
| 218 |
+
return False
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def test_dynamic_content_features():
|
| 222 |
+
"""Test dynamic content features like emojis and operation names"""
|
| 223 |
+
print("\n🧪 Testing Dynamic Content Features")
|
| 224 |
+
print("=" * 60)
|
| 225 |
+
|
| 226 |
+
try:
|
| 227 |
+
handler = ResponseTemplateHandler()
|
| 228 |
+
|
| 229 |
+
# Test tool emojis
|
| 230 |
+
omirl_emoji = handler.get_tool_emoji("omirl_tool")
|
| 231 |
+
assert omirl_emoji == "🌊", f"OMIRL emoji should be 🌊, got: {omirl_emoji}"
|
| 232 |
+
|
| 233 |
+
rag_emoji = handler.get_tool_emoji("rag_pipeline")
|
| 234 |
+
assert rag_emoji == "📋", f"RAG emoji should be 📋, got: {rag_emoji}"
|
| 235 |
+
|
| 236 |
+
default_emoji = handler.get_tool_emoji("unknown_tool")
|
| 237 |
+
assert default_emoji == "✅", f"Default emoji should be ✅, got: {default_emoji}"
|
| 238 |
+
|
| 239 |
+
print("✅ Tool emojis work correctly")
|
| 240 |
+
|
| 241 |
+
# Test operation names
|
| 242 |
+
data_extraction_name = handler.get_operation_name("data_extraction")
|
| 243 |
+
assert data_extraction_name == "Estrazione Dati", f"Should get Italian name, got: {data_extraction_name}"
|
| 244 |
+
|
| 245 |
+
unknown_operation = handler.get_operation_name("unknown_operation")
|
| 246 |
+
assert unknown_operation == "Unknown_Operation", f"Should title-case unknown operations, got: {unknown_operation}"
|
| 247 |
+
|
| 248 |
+
print("✅ Operation names work correctly")
|
| 249 |
+
|
| 250 |
+
return True
|
| 251 |
+
|
| 252 |
+
except Exception as e:
|
| 253 |
+
print(f"❌ Dynamic content features failed: {e}")
|
| 254 |
+
return False
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def test_template_agnostic_behavior():
|
| 258 |
+
"""Test that responses are truly tool-agnostic and configurable"""
|
| 259 |
+
print("\n🧪 Testing Tool-Agnostic Behavior")
|
| 260 |
+
print("=" * 60)
|
| 261 |
+
|
| 262 |
+
try:
|
| 263 |
+
handler = ResponseTemplateHandler()
|
| 264 |
+
|
| 265 |
+
# Test that the same handler can work with different tool types
|
| 266 |
+
tool_results = [
|
| 267 |
+
create_mock_tool_result("omirl_tool", True, "OMIRL data extracted"),
|
| 268 |
+
create_mock_tool_result("rag_pipeline", True, "Emergency procedure found"),
|
| 269 |
+
create_mock_tool_result("future_tool", True, "Future operation completed")
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
for result in tool_results:
|
| 273 |
+
context = ResponseContext(
|
| 274 |
+
tool_results=[result],
|
| 275 |
+
processing_status="completed",
|
| 276 |
+
errors=[]
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
response = handler.generate_success_response(context)
|
| 280 |
+
|
| 281 |
+
# Should generate valid response for any tool
|
| 282 |
+
assert len(response) > 20, f"Response too short for {result.tool_name}"
|
| 283 |
+
assert "**" in response, f"Response should be formatted for {result.tool_name}"
|
| 284 |
+
assert result.summary_text in response, f"Should include summary for {result.tool_name}"
|
| 285 |
+
|
| 286 |
+
print("✅ Handler works with any tool type")
|
| 287 |
+
|
| 288 |
+
# Test that specific tool detection works
|
| 289 |
+
omirl_context = ResponseContext(
|
| 290 |
+
tool_results=[create_mock_tool_result("omirl_tool", True, "Test")],
|
| 291 |
+
processing_status="completed",
|
| 292 |
+
errors=[]
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
non_omirl_context = ResponseContext(
|
| 296 |
+
tool_results=[create_mock_tool_result("other_tool", True, "Test")],
|
| 297 |
+
processing_status="completed",
|
| 298 |
+
errors=[]
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
omirl_response = handler.generate_success_response(omirl_context)
|
| 302 |
+
other_response = handler.generate_success_response(non_omirl_context)
|
| 303 |
+
|
| 304 |
+
# Should use different headers
|
| 305 |
+
assert "🌊 **Estrazione Dati OMIRL" in omirl_response, "OMIRL should get specific header"
|
| 306 |
+
assert "✅ **Operazione Completata" in other_response, "Other tools should get generic header"
|
| 307 |
+
assert omirl_response != other_response, "Different tools should get different responses"
|
| 308 |
+
|
| 309 |
+
print("✅ Tool-specific detection works correctly")
|
| 310 |
+
|
| 311 |
+
return True
|
| 312 |
+
|
| 313 |
+
except Exception as e:
|
| 314 |
+
print(f"❌ Tool-agnostic behavior test failed: {e}")
|
| 315 |
+
return False
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
if __name__ == "__main__":
|
| 319 |
+
print("Starting Response Template Handler Tests")
|
| 320 |
+
print("=" * 80)
|
| 321 |
+
|
| 322 |
+
# Run all tests
|
| 323 |
+
results = []
|
| 324 |
+
results.append(test_response_handler_loading())
|
| 325 |
+
results.append(test_success_response_generation())
|
| 326 |
+
results.append(test_error_response_generation())
|
| 327 |
+
results.append(test_help_response_generation())
|
| 328 |
+
results.append(test_dynamic_content_features())
|
| 329 |
+
results.append(test_template_agnostic_behavior())
|
| 330 |
+
|
| 331 |
+
# Summary
|
| 332 |
+
passed = sum(results)
|
| 333 |
+
total = len(results)
|
| 334 |
+
|
| 335 |
+
print(f"\n{'='*80}")
|
| 336 |
+
print(f"TEST SUMMARY: {passed}/{total} test suites passed")
|
| 337 |
+
|
| 338 |
+
if passed == total:
|
| 339 |
+
print("🎉 All response handler tests successful!")
|
| 340 |
+
print("✅ Response system is now truly tool-agnostic")
|
| 341 |
+
print("✅ Hard-coded response strings eliminated")
|
| 342 |
+
else:
|
| 343 |
+
print("❌ Some tests failed - review implementation")
|
| 344 |
+
|
| 345 |
+
print("=" * 80)
|
tests/agent/test_response_templates.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test Response Templates Configuration Loading
|
| 4 |
+
|
| 5 |
+
This test validates that the response templates configuration can be loaded
|
| 6 |
+
and used to generate tool-agnostic responses. It ensures that:
|
| 7 |
+
|
| 8 |
+
1. Templates are properly loaded from YAML
|
| 9 |
+
2. Dynamic content substitution works correctly
|
| 10 |
+
3. Tool-specific logic is configuration-driven
|
| 11 |
+
4. Multi-language support structure is in place
|
| 12 |
+
|
| 13 |
+
Expected: All template loading and substitution tests should pass
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import sys
|
| 17 |
+
import os
|
| 18 |
+
import yaml
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
sys.path.append('/home/jeanbaptistebove/projects/operations')
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_response_templates_loading():
|
| 24 |
+
"""Test that response templates can be loaded from YAML configuration"""
|
| 25 |
+
print("🧪 Testing Response Templates Configuration Loading")
|
| 26 |
+
print("=" * 60)
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
# Load response templates configuration
|
| 30 |
+
config_path = Path('/home/jeanbaptistebove/projects/operations/agent/config/response_templates.yaml')
|
| 31 |
+
|
| 32 |
+
print(f"1. Loading response templates from: {config_path}")
|
| 33 |
+
assert config_path.exists(), f"Response templates config not found: {config_path}"
|
| 34 |
+
|
| 35 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 36 |
+
templates_config = yaml.safe_load(f)
|
| 37 |
+
|
| 38 |
+
print("✅ Response templates loaded successfully")
|
| 39 |
+
|
| 40 |
+
# Test main sections exist
|
| 41 |
+
expected_sections = [
|
| 42 |
+
'success_responses', 'help_responses', 'error_responses',
|
| 43 |
+
'clarification_responses', 'result_formatting', 'dynamic_content'
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
print(f"2. Checking main sections: {expected_sections}")
|
| 47 |
+
for section in expected_sections:
|
| 48 |
+
assert section in templates_config, f"Missing section: {section}"
|
| 49 |
+
print(f" ✅ {section}")
|
| 50 |
+
|
| 51 |
+
# Test success response templates
|
| 52 |
+
print("3. Testing success response templates...")
|
| 53 |
+
success_templates = templates_config['success_responses']
|
| 54 |
+
|
| 55 |
+
# Test general completion template
|
| 56 |
+
general_template = success_templates.get('general_operation_completed', {})
|
| 57 |
+
assert 'template' in general_template, "Missing template in general_operation_completed"
|
| 58 |
+
template_text = general_template['template']
|
| 59 |
+
assert "Operazione Completata" in template_text, "Template should contain Italian text"
|
| 60 |
+
print(f" ✅ General template: {template_text.strip()}")
|
| 61 |
+
|
| 62 |
+
# Test OMIRL-specific template
|
| 63 |
+
omirl_template = success_templates.get('omirl_data_completed', {})
|
| 64 |
+
assert 'template' in omirl_template, "Missing template in omirl_data_completed"
|
| 65 |
+
assert 'condition' in omirl_template, "Missing condition in omirl_data_completed"
|
| 66 |
+
print(f" ✅ OMIRL template: {omirl_template['template'].strip()}")
|
| 67 |
+
|
| 68 |
+
# Test help response structure
|
| 69 |
+
print("4. Testing help response structure...")
|
| 70 |
+
help_responses = templates_config['help_responses']
|
| 71 |
+
|
| 72 |
+
general_help = help_responses.get('general_help', {})
|
| 73 |
+
required_help_keys = ['header', 'capabilities_intro', 'capabilities', 'examples_intro', 'examples']
|
| 74 |
+
for key in required_help_keys:
|
| 75 |
+
assert key in general_help, f"Missing key in general_help: {key}"
|
| 76 |
+
print(f" ✅ General help structure complete")
|
| 77 |
+
|
| 78 |
+
# Test tool-specific capabilities
|
| 79 |
+
capabilities_by_tool = help_responses.get('capabilities_by_tool', {})
|
| 80 |
+
assert 'omirl_tool' in capabilities_by_tool, "Missing omirl_tool capabilities"
|
| 81 |
+
|
| 82 |
+
omirl_caps = capabilities_by_tool['omirl_tool']
|
| 83 |
+
required_cap_keys = ['category', 'description', 'examples']
|
| 84 |
+
for key in required_cap_keys:
|
| 85 |
+
assert key in omirl_caps, f"Missing key in omirl_tool capabilities: {key}"
|
| 86 |
+
print(f" ✅ Tool-specific capabilities complete")
|
| 87 |
+
|
| 88 |
+
# Test dynamic content configuration
|
| 89 |
+
print("5. Testing dynamic content configuration...")
|
| 90 |
+
dynamic_content = templates_config['dynamic_content']
|
| 91 |
+
|
| 92 |
+
tool_emojis = dynamic_content.get('tool_specific_emojis', {})
|
| 93 |
+
assert 'omirl_tool' in tool_emojis, "Missing omirl_tool emoji"
|
| 94 |
+
assert 'default' in tool_emojis, "Missing default emoji"
|
| 95 |
+
print(f" ✅ Tool emojis: {tool_emojis}")
|
| 96 |
+
|
| 97 |
+
operation_names = dynamic_content.get('operation_names', {})
|
| 98 |
+
assert len(operation_names) > 0, "No operation names defined"
|
| 99 |
+
print(f" ✅ Operation names: {list(operation_names.keys())}")
|
| 100 |
+
|
| 101 |
+
print("\n✅ All response template tests passed!")
|
| 102 |
+
return True
|
| 103 |
+
|
| 104 |
+
except Exception as e:
|
| 105 |
+
print(f"❌ Response template test failed: {e}")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_template_substitution():
|
| 110 |
+
"""Test that template variables can be properly substituted"""
|
| 111 |
+
print("\n🧪 Testing Template Variable Substitution")
|
| 112 |
+
print("=" * 60)
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
# Test basic template substitution logic
|
| 116 |
+
template = "✅ **{operation_name} Completata**\n"
|
| 117 |
+
variables = {"operation_name": "Estrazione Dati OMIRL"}
|
| 118 |
+
|
| 119 |
+
# Simple substitution (would be done by a proper templating engine)
|
| 120 |
+
result = template.format(**variables)
|
| 121 |
+
expected = "✅ **Estrazione Dati OMIRL Completata**\n"
|
| 122 |
+
|
| 123 |
+
assert result == expected, f"Template substitution failed. Got: {result}"
|
| 124 |
+
print(f"✅ Basic substitution works: {result.strip()}")
|
| 125 |
+
|
| 126 |
+
# Test geographic rejection template
|
| 127 |
+
geo_template = "⚠️ **Richiesta Non Supportata**\n{rejection_reason}\n**Nota:** Posso fornire dati solo per la regione {supported_region}.\n**{geographic_scope_label}:** {supported_areas}"
|
| 128 |
+
geo_variables = {
|
| 129 |
+
"rejection_reason": "Località non supportata",
|
| 130 |
+
"supported_region": "Liguria",
|
| 131 |
+
"geographic_scope_label": "Province supportate",
|
| 132 |
+
"supported_areas": "Genova, Savona, Imperia, La Spezia"
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
geo_result = geo_template.format(**geo_variables)
|
| 136 |
+
assert "Liguria" in geo_result, "Geographic template should contain region"
|
| 137 |
+
assert "Province supportate" in geo_result, "Should contain scope label"
|
| 138 |
+
print(f"✅ Geographic template works")
|
| 139 |
+
|
| 140 |
+
print("\n✅ All template substitution tests passed!")
|
| 141 |
+
return True
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print(f"❌ Template substitution test failed: {e}")
|
| 145 |
+
return False
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def test_tool_agnostic_structure():
|
| 149 |
+
"""Test that the template structure supports tool-agnostic operation"""
|
| 150 |
+
print("\n🧪 Testing Tool-Agnostic Structure")
|
| 151 |
+
print("=" * 60)
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
config_path = Path('/home/jeanbaptistebove/projects/operations/agent/config/response_templates.yaml')
|
| 155 |
+
with open(config_path, 'r', encoding='utf-8') as f:
|
| 156 |
+
templates_config = yaml.safe_load(f)
|
| 157 |
+
|
| 158 |
+
# Test that templates use variables instead of hard-coded values
|
| 159 |
+
success_responses = templates_config['success_responses']
|
| 160 |
+
|
| 161 |
+
# Check data extraction template uses variables
|
| 162 |
+
data_extraction = success_responses.get('data_extraction_completed', {})
|
| 163 |
+
assert 'variables' in data_extraction, "data_extraction_completed should have variables definition"
|
| 164 |
+
variables = data_extraction['variables']
|
| 165 |
+
assert 'operation_name' in variables, "Should have operation_name variable"
|
| 166 |
+
print("✅ Data extraction template uses variables")
|
| 167 |
+
|
| 168 |
+
# Check dynamic content supports multiple tools
|
| 169 |
+
dynamic_content = templates_config['dynamic_content']
|
| 170 |
+
tool_emojis = dynamic_content['tool_specific_emojis']
|
| 171 |
+
|
| 172 |
+
# Should have entries for different tools, not just OMIRL
|
| 173 |
+
assert len(tool_emojis) >= 3, "Should support multiple tools" # omirl_tool, rag_pipeline, default
|
| 174 |
+
assert 'default' in tool_emojis, "Should have default fallback"
|
| 175 |
+
print("✅ Multiple tool support configured")
|
| 176 |
+
|
| 177 |
+
# Check help responses support multiple tools
|
| 178 |
+
help_responses = templates_config['help_responses']
|
| 179 |
+
capabilities_by_tool = help_responses['capabilities_by_tool']
|
| 180 |
+
|
| 181 |
+
# Should have capability definitions for different tools
|
| 182 |
+
assert 'omirl_tool' in capabilities_by_tool, "Should have OMIRL capabilities"
|
| 183 |
+
if 'rag_pipeline' in capabilities_by_tool: # May be disabled
|
| 184 |
+
print("✅ Multiple tool capabilities defined")
|
| 185 |
+
else:
|
| 186 |
+
print("ℹ️ RAG pipeline capabilities not yet defined (expected)")
|
| 187 |
+
|
| 188 |
+
print("\n✅ Tool-agnostic structure tests passed!")
|
| 189 |
+
return True
|
| 190 |
+
|
| 191 |
+
except Exception as e:
|
| 192 |
+
print(f"❌ Tool-agnostic structure test failed: {e}")
|
| 193 |
+
return False
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
if __name__ == "__main__":
|
| 197 |
+
print("Starting Response Templates Configuration Tests")
|
| 198 |
+
print("=" * 80)
|
| 199 |
+
|
| 200 |
+
# Run all tests
|
| 201 |
+
results = []
|
| 202 |
+
results.append(test_response_templates_loading())
|
| 203 |
+
results.append(test_template_substitution())
|
| 204 |
+
results.append(test_tool_agnostic_structure())
|
| 205 |
+
|
| 206 |
+
# Summary
|
| 207 |
+
passed = sum(results)
|
| 208 |
+
total = len(results)
|
| 209 |
+
|
| 210 |
+
print(f"\n{'='*80}")
|
| 211 |
+
print(f"TEST SUMMARY: {passed}/{total} test suites passed")
|
| 212 |
+
|
| 213 |
+
if passed == total:
|
| 214 |
+
print("🎉 All response template tests successful!")
|
| 215 |
+
print("✅ Configuration is ready for tool-agnostic operation")
|
| 216 |
+
else:
|
| 217 |
+
print("❌ Some tests failed - review configuration")
|
| 218 |
+
|
| 219 |
+
print("=" * 80)
|
tests/agent/test_simplified_agent.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tests/agent/test_simplified_agent.py
|
| 2 |
+
"""
|
| 3 |
+
Test simplified agent implementation
|
| 4 |
+
|
| 5 |
+
This test verifies that our lean approach:
|
| 6 |
+
1. Eliminates over-engineering while keeping good features
|
| 7 |
+
2. Uses Italian error messages
|
| 8 |
+
3. Uses existing config files without duplication
|
| 9 |
+
4. Maintains template system for responses
|
| 10 |
+
5. Works with existing tool configurations
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import os
|
| 14 |
+
import sys
|
| 15 |
+
import unittest
|
| 16 |
+
|
| 17 |
+
# Add the agent module to the Python path
|
| 18 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
|
| 19 |
+
|
| 20 |
+
from agent.agent import OperationsAgent, ValidationError, VALID_PROVINCES, SUPPORTED_SENSORS
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TestSimplifiedAgent(unittest.TestCase):
|
| 24 |
+
"""Test simplified, lean agent implementation"""
|
| 25 |
+
|
| 26 |
+
def setUp(self):
|
| 27 |
+
"""Set up test environment"""
|
| 28 |
+
self.agent = OperationsAgent(lazy_init=True)
|
| 29 |
+
|
| 30 |
+
def test_agent_initialization_simple(self):
|
| 31 |
+
"""Test that agent initializes without complex config loader"""
|
| 32 |
+
print("Testing simple agent initialization...")
|
| 33 |
+
|
| 34 |
+
# Verify agent initializes correctly
|
| 35 |
+
self.assertIsNotNone(self.agent.session_id)
|
| 36 |
+
self.assertEqual(self.agent.default_language, "it")
|
| 37 |
+
self.assertIsNone(self.agent.workflow) # Lazy init
|
| 38 |
+
|
| 39 |
+
print("✅ Agent initializes simply without over-engineering")
|
| 40 |
+
|
| 41 |
+
def test_italian_error_messages(self):
|
| 42 |
+
"""Test that error messages are in Italian"""
|
| 43 |
+
print("Testing Italian error messages...")
|
| 44 |
+
|
| 45 |
+
# Test validation error
|
| 46 |
+
try:
|
| 47 |
+
self.agent._validate_request_input("")
|
| 48 |
+
self.fail("Should have raised ValidationError")
|
| 49 |
+
except ValidationError as e:
|
| 50 |
+
error_msg = str(e)
|
| 51 |
+
# Verify it's in Italian
|
| 52 |
+
self.assertIn("messaggio utente", error_msg.lower())
|
| 53 |
+
|
| 54 |
+
print("✅ Error messages are in Italian")
|
| 55 |
+
|
| 56 |
+
def test_constants_from_existing_config(self):
|
| 57 |
+
"""Test that constants come from existing configurations"""
|
| 58 |
+
print("Testing constants from existing config...")
|
| 59 |
+
|
| 60 |
+
# Verify province constants
|
| 61 |
+
self.assertIn("GENOVA", VALID_PROVINCES)
|
| 62 |
+
self.assertIn("SAVONA", VALID_PROVINCES)
|
| 63 |
+
self.assertIn("IMPERIA", VALID_PROVINCES)
|
| 64 |
+
self.assertIn("LA SPEZIA", VALID_PROVINCES)
|
| 65 |
+
|
| 66 |
+
# Verify sensor constants
|
| 67 |
+
self.assertIn("Precipitazione", SUPPORTED_SENSORS)
|
| 68 |
+
self.assertIn("Temperatura", SUPPORTED_SENSORS)
|
| 69 |
+
|
| 70 |
+
print("✅ Constants properly extracted from existing configs")
|
| 71 |
+
|
| 72 |
+
def test_omirl_capabilities_italian(self):
|
| 73 |
+
"""Test that OMIRL capabilities are in Italian"""
|
| 74 |
+
print("Testing OMIRL capabilities in Italian...")
|
| 75 |
+
|
| 76 |
+
capabilities = self.agent.get_omirl_capabilities()
|
| 77 |
+
|
| 78 |
+
# Verify Italian descriptions
|
| 79 |
+
self.assertIn("Estrazione dati", capabilities["capabilities"][0])
|
| 80 |
+
self.assertIn("Filtraggio", capabilities["capabilities"][1])
|
| 81 |
+
self.assertIn("tempo reale", capabilities["capabilities"][3])
|
| 82 |
+
|
| 83 |
+
print("✅ OMIRL capabilities are in Italian")
|
| 84 |
+
|
| 85 |
+
def test_province_validation_italian(self):
|
| 86 |
+
"""Test that province validation uses Italian messages"""
|
| 87 |
+
print("Testing province validation with Italian messages...")
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
import asyncio
|
| 91 |
+
# This should trigger validation error
|
| 92 |
+
asyncio.run(self.agent.process_omirl_request(provincia="INVALID"))
|
| 93 |
+
except Exception as e:
|
| 94 |
+
# We expect some error, just verify it would use Italian
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
# Test direct validation
|
| 98 |
+
try:
|
| 99 |
+
self.agent._validate_request_input("test") # Valid
|
| 100 |
+
# Now test OMIRL validation by accessing the method indirectly
|
| 101 |
+
pass
|
| 102 |
+
except Exception:
|
| 103 |
+
pass
|
| 104 |
+
|
| 105 |
+
print("✅ Province validation ready for Italian messages")
|
| 106 |
+
|
| 107 |
+
def test_no_config_duplication(self):
|
| 108 |
+
"""Test that we don't have config duplication"""
|
| 109 |
+
print("Testing no config duplication...")
|
| 110 |
+
|
| 111 |
+
# Verify we're not creating duplicate config files
|
| 112 |
+
config_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'agent', 'config')
|
| 113 |
+
|
| 114 |
+
# These should NOT exist (over-engineered files)
|
| 115 |
+
self.assertFalse(os.path.exists(os.path.join(config_dir, 'agent_config.yaml')))
|
| 116 |
+
|
| 117 |
+
config_loader_path = os.path.join(os.path.dirname(__file__), '..', '..', 'agent', 'config_loader.py')
|
| 118 |
+
self.assertFalse(os.path.exists(config_loader_path))
|
| 119 |
+
|
| 120 |
+
# These SHOULD exist (good files)
|
| 121 |
+
self.assertTrue(os.path.exists(os.path.join(config_dir, 'response_templates.yaml')))
|
| 122 |
+
self.assertTrue(os.path.exists(os.path.join(config_dir, 'geography.yaml')))
|
| 123 |
+
|
| 124 |
+
print("✅ No config duplication - lean approach confirmed")
|
| 125 |
+
|
| 126 |
+
def test_error_response_italian(self):
|
| 127 |
+
"""Test that error responses are formatted in Italian"""
|
| 128 |
+
print("Testing error response formatting in Italian...")
|
| 129 |
+
|
| 130 |
+
error_response = self.agent._format_error_response(
|
| 131 |
+
"Test error",
|
| 132 |
+
"Test message",
|
| 133 |
+
"ValidationError"
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
# Verify Italian error formatting
|
| 137 |
+
response_text = error_response["response"]
|
| 138 |
+
self.assertIn("Errore di Validazione", response_text)
|
| 139 |
+
self.assertIn("Verifica che la richiesta", response_text)
|
| 140 |
+
|
| 141 |
+
print("✅ Error responses formatted in Italian")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def run_tests():
|
| 145 |
+
"""Run all simplified agent tests"""
|
| 146 |
+
print("🧪 TESTING SIMPLIFIED AGENT APPROACH")
|
| 147 |
+
print("=" * 60)
|
| 148 |
+
|
| 149 |
+
# Create test suite
|
| 150 |
+
suite = unittest.TestLoader().loadTestsFromTestCase(TestSimplifiedAgent)
|
| 151 |
+
runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w'))
|
| 152 |
+
result = runner.run(suite)
|
| 153 |
+
|
| 154 |
+
# Print results
|
| 155 |
+
tests_run = result.testsRun
|
| 156 |
+
failures = len(result.failures)
|
| 157 |
+
errors = len(result.errors)
|
| 158 |
+
success_count = tests_run - failures - errors
|
| 159 |
+
|
| 160 |
+
print(f"\nTEST SUMMARY: {success_count}/{tests_run} test suites passed")
|
| 161 |
+
|
| 162 |
+
if result.wasSuccessful():
|
| 163 |
+
print("🎉 Simplified agent approach successful!")
|
| 164 |
+
print("✅ Lean, elegant architecture without over-engineering")
|
| 165 |
+
print("✅ Italian error messages throughout")
|
| 166 |
+
print("✅ No config duplication")
|
| 167 |
+
print("✅ Simple constants from existing configs")
|
| 168 |
+
print("✅ Maintained good template system")
|
| 169 |
+
else:
|
| 170 |
+
print("❌ Some tests failed!")
|
| 171 |
+
for failure in result.failures:
|
| 172 |
+
print(f"FAILURE: {failure[0]}")
|
| 173 |
+
print(f" {failure[1]}")
|
| 174 |
+
for error in result.errors:
|
| 175 |
+
print(f"ERROR: {error[0]}")
|
| 176 |
+
print(f" {error[1]}")
|
| 177 |
+
|
| 178 |
+
return result.wasSuccessful()
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
if __name__ == "__main__":
|
| 182 |
+
success = run_tests()
|
| 183 |
+
sys.exit(0 if success else 1)
|
tools/omirl/config/tasks.yaml
CHANGED
|
@@ -1,17 +1,14 @@
|
|
| 1 |
-
# OMIRL
|
| 2 |
-
# Defines
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
sensori:
|
| 13 |
-
- "radar"
|
| 14 |
-
- "satellite"
|
| 15 |
|
| 16 |
# Task-specific parameter requirements
|
| 17 |
task_requirements:
|
|
@@ -28,7 +25,7 @@ task_requirements:
|
|
| 28 |
primary_output: "data"
|
| 29 |
description: "Extracts structured data from station time series tables with image capture and text generation"
|
| 30 |
|
| 31 |
-
|
| 32 |
required_filters: [] # Custom validation in task handles provincia OR zona_allerta
|
| 33 |
optional_filters:
|
| 34 |
- "provincia"
|
|
|
|
| 1 |
+
# OMIRL Task Configuration
|
| 2 |
+
# Defines valid tasks and their requirements
|
| 3 |
|
| 4 |
+
valid_tasks:
|
| 5 |
+
- "valori_stazioni"
|
| 6 |
+
- "massimi_precipitazione"
|
| 7 |
+
- "livelli_idrometrici"
|
| 8 |
+
- "stazioni"
|
| 9 |
+
- "mappe"
|
| 10 |
+
- "radar"
|
| 11 |
+
- "satellite"
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
# Task-specific parameter requirements
|
| 14 |
task_requirements:
|
|
|
|
| 25 |
primary_output: "data"
|
| 26 |
description: "Extracts structured data from station time series tables with image capture and text generation"
|
| 27 |
|
| 28 |
+
massimi_precipitazione:
|
| 29 |
required_filters: [] # Custom validation in task handles provincia OR zona_allerta
|
| 30 |
optional_filters:
|
| 31 |
- "provincia"
|
tools/omirl/shared/validation.py
CHANGED
|
@@ -370,14 +370,22 @@ class OMIRLValidator:
|
|
| 370 |
corrected_filters = filters.copy()
|
| 371 |
|
| 372 |
# Validate task exists (no mode validation needed)
|
| 373 |
-
valid_tasks = []
|
| 374 |
-
for mode_tasks in self.mode_tasks['valid_combinations'].values():
|
| 375 |
-
valid_tasks.extend(mode_tasks)
|
| 376 |
|
| 377 |
if task not in valid_tasks:
|
| 378 |
errors.append(f"Invalid task '{task}'. Valid tasks: {valid_tasks}")
|
| 379 |
return False, corrected_filters, errors
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
# Validate individual filters
|
| 382 |
validation_methods = {
|
| 383 |
'tipo_sensore': self.validate_sensor_type,
|
|
|
|
| 370 |
corrected_filters = filters.copy()
|
| 371 |
|
| 372 |
# Validate task exists (no mode validation needed)
|
| 373 |
+
valid_tasks = self.mode_tasks.get('valid_tasks', [])
|
|
|
|
|
|
|
| 374 |
|
| 375 |
if task not in valid_tasks:
|
| 376 |
errors.append(f"Invalid task '{task}'. Valid tasks: {valid_tasks}")
|
| 377 |
return False, corrected_filters, errors
|
| 378 |
|
| 379 |
+
# Validate filters are allowed for this task
|
| 380 |
+
task_requirements = self.get_task_requirements(task)
|
| 381 |
+
allowed_filters = set(task_requirements.get('required_filters', []) + task_requirements.get('optional_filters', []))
|
| 382 |
+
|
| 383 |
+
# Remove filters that are not allowed for this task
|
| 384 |
+
for filter_name in list(corrected_filters.keys()):
|
| 385 |
+
if filter_name not in allowed_filters:
|
| 386 |
+
print(f"⚠️ Removing unsupported filter '{filter_name}' for task '{task}'. Allowed filters: {list(allowed_filters)}")
|
| 387 |
+
del corrected_filters[filter_name]
|
| 388 |
+
|
| 389 |
# Validate individual filters
|
| 390 |
validation_methods = {
|
| 391 |
'tipo_sensore': self.validate_sensor_type,
|
tools/omirl/tables/massimi_precipitazione.py
CHANGED
|
@@ -104,14 +104,15 @@ async def fetch_massimi_precipitazione_async(filters: OMIRLFilterSet) -> OMIRLRe
|
|
| 104 |
result.data = filtered_data
|
| 105 |
result.message = f"Estratti dati precipitazione massima con filtri: {all_filters}"
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
# Generate simple formatted summary (no LLM dependency)
|
| 108 |
if filtered_data:
|
| 109 |
summary = format_precipitation_data_simple(filtered_data, all_filters, time_periods)
|
| 110 |
result.update_metadata(summary=summary)
|
| 111 |
|
| 112 |
-
# Add detailed metadata
|
| 113 |
-
source_url = validator.get_task_url("massimi_precipitazione", "tables") if validator else "https://omirl.regione.liguria.it/#/maxtable"
|
| 114 |
-
time_periods = validator.get_time_periods() if validator else ["5'", "15'", "30'", "1h", "3h", "6h", "12h", "24h"]
|
| 115 |
result.update_metadata(
|
| 116 |
filters_applied=all_filters,
|
| 117 |
zona_allerta_records=len(filtered_data.get("zona_allerta", [])),
|
|
|
|
| 104 |
result.data = filtered_data
|
| 105 |
result.message = f"Estratti dati precipitazione massima con filtri: {all_filters}"
|
| 106 |
|
| 107 |
+
# Get metadata values first
|
| 108 |
+
source_url = validator.get_task_url("massimi_precipitazione", "tables") if validator else "https://omirl.regione.liguria.it/#/maxtable"
|
| 109 |
+
time_periods = validator.get_time_periods() if validator else ["5'", "15'", "30'", "1h", "3h", "6h", "12h", "24h"]
|
| 110 |
+
|
| 111 |
# Generate simple formatted summary (no LLM dependency)
|
| 112 |
if filtered_data:
|
| 113 |
summary = format_precipitation_data_simple(filtered_data, all_filters, time_periods)
|
| 114 |
result.update_metadata(summary=summary)
|
| 115 |
|
|
|
|
|
|
|
|
|
|
| 116 |
result.update_metadata(
|
| 117 |
filters_applied=all_filters,
|
| 118 |
zona_allerta_records=len(filtered_data.get("zona_allerta", [])),
|