AI Assistant Contestualizzato - Architettura v2.0

AI Assistant Contestualizzato - Architettura v2.0

Data: 22 Ottobre 2025 Versione: 2.0 - Pragmatica & Context-Driven Sistema: sartUP - Modulo Industria 4.0

---

🎯 Filosofia Progettuale

Principi Fondamentali

1. AI come Ultimo Resort: Non usare LLM quando una risposta deterministica Γ¨ possibile 2. Knowledge Base Prima: 80% risposte da storico preparato, 20% AI generativa 3. Ambiti Contestuali: Ogni dominio ha logica, vocabolario e pattern propri 4. Costi Ottimizzati: Massimo 5-10€/mese per 50+ utenti 5. Risposte Precise: Zero allucinazioni, sempre fonti verificabili

---

πŸ“¦ Architettura Stratificata

``

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    FRONTEND - Chat Widget                        β”‚
β”‚  Query Utente β†’ Suggestions β†’ Storico Conversazione             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              LAYER 1: Intent Router (Deterministic)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Pattern Matching Regex β†’ Ambito Contestuale              β”‚   β”‚
β”‚  β”‚ β€’ "produzione di ieri" β†’ AMBITO_PRODUZIONE                β”‚   β”‚
β”‚  β”‚ β€’ "macchina in errore" β†’ AMBITO_DIAGNOSTICA              β”‚   β”‚
β”‚  β”‚ β€’ "qual Γ¨ il profilo" β†’ AMBITO_CONFIGURAZIONE            β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         LAYER 2: Context-Specific Knowledge Base                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Template Repository (JSON)                                β”‚   β”‚
β”‚  β”‚ β€’ Query predefinite con parametri                         β”‚   β”‚
β”‚  β”‚ β€’ Risposte con placeholder                                β”‚   β”‚
β”‚  β”‚ β€’ Threshold e regole business                             β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            LAYER 3: Data Execution Engine                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SQL Query Builder β†’ DB β†’ Response Formatter               β”‚   β”‚
β”‚  β”‚ β€’ Safe execution (whitelist, timeout)                     β”‚   β”‚
β”‚  β”‚ β€’ Cache intelligente                                      β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓ (SOLO se necessario)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              LAYER 4: LLM Fallback (GPT-4 Mini)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Fine-Tuned Model β†’ Prompt Compatto β†’ Response             β”‚   β”‚
β”‚  β”‚ β€’ Solo per query complesse/ambigue                        β”‚   β”‚
β”‚  β”‚ β€’ Context injection minimale                              β”‚   β”‚
β”‚  β”‚ β€’ Function calling per SQL generation                     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
`

---

πŸ—‚οΈ Ambiti Contestuali

1. AMBITO_PRODUZIONE

Dominio: Analisi quantitΓ  prodotte, trend, confronti temporali

EntitΓ  Chiave:

  • TotalPieces, NumberOfPlies, Quantity (da raw_data JSON)
  • timestamp, machine_id, profile_id
  • Aggregazioni: SUM, AVG, COUNT per periodo
  • Pattern Query Comuni: ` "produzione di {periodo}" "confronta {periodo1} con {periodo2}" "mostrami il trend {periodo}" "chi ha prodotto di piΓΉ" "pezzi totali {filtri}" `

    Template Esempio: `json { "pattern": "produzione di (ieri|oggi|questa settimana|questo mese)", "sql_template": "SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL)) as total FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to", "response_template": "La produzione di {periodo} Γ¨ stata di {total} pezzi, {comparazione}.", "requires_llm": false } `

    ---

    2. AMBITO_DIAGNOSTICA

    Dominio: Errori, anomalie, fermi macchina, performance problematiche

    EntitΓ  Chiave:

  • MachineStatus, ErrorCode, AlarmCode (se presenti)
  • Duration anomala (fuori range normale)
  • timestamp gaps (fermi non registrati)
  • Pattern Query Comuni: ` "perchΓ© la macchina si Γ¨ fermata" "errori di {periodo}" "qual Γ¨ il problema" "fermi macchina {periodo}" "macchina lenta oggi" `

    Logic Tree: ` 1. Verifica gaps timestamp > 30 min β†’ "Fermo macchina rilevato" 2. Verifica Duration > avg + 2Οƒ β†’ "Anomalia performance" 3. Verifica ErrorCode in raw_data β†’ "Errore registrato: {code}" 4. Verifica produzione < media - 20% β†’ "Produzione sotto media" `

    Richiede LLM: Solo per interpretare ErrorCode sconosciuti

    ---

    3. AMBITO_OPERATORI

    Dominio: Performance operatori, confronti, best practices

    EntitΓ  Chiave:

  • UserLog, MarkerName, OperatorID (campi operatore in raw_data)
  • Duration, TotalPieces per operatore
  • Metriche: pezzi/ora, avg_duration
  • Pattern Query Comuni: ` "chi Γ¨ l'operatore migliore" "performance di {nome_operatore}" "confronta {op1} con {op2}" "ranking operatori" "operatori piΓΉ veloci" `

    Template Esempio: `json { "pattern": "chi Γ¨ l'operatore (migliore|piΓΉ veloce|piΓΉ produttivo)", "sql_template": "SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{operator_field}')) as operator, COUNT(*) as operations, SUM(...) as total_pieces, AVG(...) as avg_duration FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY operator ORDER BY total_pieces DESC LIMIT 5", "response_template": "L'operatore piΓΉ produttivo Γ¨ {operator} con {total_pieces} pezzi in {operations} operazioni.", "requires_llm": false } `

    ---

    4. AMBITO_CONFIGURAZIONE

    Dominio: Info macchine, profili, setup, CSV imports

    EntitΓ  Chiave:

  • machines_i40.profiles_config
  • import_profiles.columns, column_names
  • csv_imports status
  • Pattern Query Comuni: ` "quali profili ha la macchina" "configurazione macchina" "ultimo import CSV" "campi disponibili" "come Γ¨ configurata la macchina" `

    Response Strategy: Query dirette su metadati (no LLM)

    ---

    5. AMBITO_PIANIFICAZIONE

    Dominio: Previsioni, capacitΓ  produttiva, deadline

    EntitΓ  Chiave:

  • Media produzione giornaliera/oraria
  • Working hours (business rules)
  • Proiezioni lineari
  • Pattern Query Comuni: ` "riusciremo a finire {quantitΓ } entro {data}" "quanto ci vuole per {quantitΓ }" "quando finiremo {ordine}" "capacitΓ  produttiva {periodo}" `

    Logic: 1. Calcola media produzione (ultimi 30 giorni) 2. Calcola working days rimanenti 3. Proiezione lineare: giorni_necessari = quantitΓ _rimanente / media_giornaliera 4. Confronta con deadline

    Richiede LLM: Solo per formulare risposta discorsiva

    ---

    6. AMBITO_COMPARAZIONE

    Dominio: Confronti macchine, periodi, operatori

    EntitΓ  Chiave:

  • Aggregazioni multi-macchina
  • Time series per confronto temporale
  • Delta percentuali
  • Pattern Query Comuni: ` "confronta macchina A con B" "questa settimana vs scorsa" "oggi vs ieri" "trend ultimi {n} giorni" `

    Template Esempio: `json { "pattern": "confronta (questa settimana|oggi) (con|vs) (la scorsa|ieri)", "sql_template": [ "SELECT SUM(...) as current FROM ... WHERE timestamp BETWEEN :current_from AND :current_to", "SELECT SUM(...) as previous FROM ... WHERE timestamp BETWEEN :previous_from AND :previous_to" ], "response_template": "Confronto {periodo_current} vs {periodo_previous}: {current} pezzi vs {previous} pezzi. Variazione: {delta}% ({emoji}).", "requires_llm": false } `

    ---

    7. AMBITO_OTTIMIZZAZIONE

    Dominio: Suggerimenti miglioramento, best practices, insights

    EntitΓ  Chiave:

  • Pattern temporali (orari piΓΉ produttivi)
  • Colli di bottiglia
  • OpportunitΓ  di miglioramento
  • Pattern Query Comuni: ` "come posso migliorare" "suggerimenti ottimizzazione" "quali sono i problemi" "come aumentare produttivitΓ " `

    Logic Tree: 1. Analizza distribuzione oraria β†’ Identifica picchi/valli 2. Confronta giorni settimana β†’ Identifica giorni migliori 3. Analizza durata operazioni β†’ Identifica outliers 4. Analizza performance operatori β†’ Identifica best practices

    Richiede LLM: Sì, per formulare suggerimenti articolati

    ---

    🧠 Intent Router - Pattern Matching

    Regex Dictionary (Italiano)

    `php // app/Services/I40/IntentPatterns.php

    return [ 'AMBITO_PRODUZIONE' => [ '/produz(?:ione|ione totale|ione di) (ieri|oggi|questa settimana|questo mese)/i', '/quant[iaoe]+ (pezzi|operazioni|lavorazioni)/i', '/totale (prodotto|lavorato)/i', '/(?:mostra|visualizza) (?:la )?produzione/i', ], 'AMBITO_DIAGNOSTICA' => [ '/(?:perch[eΓ©]|come mai|motivo) .*(ferm[oa]|problem[ai]|error[ei])/i', '/(?:errori?|alarm[ei]|warning)/i', '/(?:cosa|quale) (?:Γ¨ )?(?:il )?problem[ai]/i', '/macchina (?:ferma|bloccata|in errore)/i', '/performance (?:bassa|scadente|sotto media)/i', ], 'AMBITO_OPERATORI' => [ '/(?:chi|quale) (?:Γ¨ |ha ).*(operatore|utente)/i', '/performance (?:di |dell\'operatore)/i', '/ranking (?:operatori|utenti)/i', '/(?:operatore|utente) (?:migliore|peggiore|piΓΉ veloce|piΓΉ lento)/i', ], 'AMBITO_CONFIGURAZIONE' => [ '/(?:quali|mostra) profil[io]/i', '/configurazione (?:macchina|della macchina)/i', '/(?:ultimo|ultimi) (?:import|csv)/i', '/(?:come|in che modo) Γ¨ configurat[oa]/i', '/(?:campi|colonne) disponibili/i', ], 'AMBITO_PIANIFICAZIONE' => [ '/riuscir(?:emo|Γ²) .* entro/i', '/(?:quanto|quando) (?:tempo|ci vuole)/i', '/(?:capacitΓ |potenziale) produttiv[ao]/i', '/previsione/i', '/deadline/i', ], 'AMBITO_COMPARAZIONE' => [ '/confron(?:ta|to) .* (?:con|vs)/i', '/(?:differenza|delta) (?:tra|con)/i', '/(?:questa|oggi|ieri) (?:vs|con|rispetto)/i', '/trend (?:ultimi|degli ultimi)/i', ], 'AMBITO_OTTIMIZZAZIONE' => [ '/(?:come|in che modo) (?:posso |migliorare|ottimizzare|aumentare)/i', '/suggeriment[io]/i', '/(?:consigli|raccomandazioni)/i', '/(?:opportunitΓ |margini) (?:di |miglioramento)/i', ], ]; `

    ---

    πŸ“š Knowledge Base Templates

    Struttura Template

    `json { "template_id": "prod_daily", "ambito": "AMBITO_PRODUZIONE", "pattern": "produzione di (ieri|oggi)", "requires_llm": false, "cache_ttl": 3600, "parameters": { "machine_id": "required", "date_from": "auto", "date_to": "auto" }, "entity_extraction": { "periodo": "regex_group_1" }, "business_rules": [ { "condition": "periodo == 'ieri'", "action": "set_date_range('yesterday')" }, { "condition": "periodo == 'oggi'", "action": "set_date_range('today')" } ], "sql_query": { "query": "SELECT machine_id, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL(15,2))) as total_quantity, COUNT(*) as total_operations FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY machine_id", "timeout": 5, "fallback": "Impossibile recuperare dati produzione." }, "response_formatter": { "template": "Produzione di {periodo}:\n\nβœ… Totale pezzi: {total_quantity}\nπŸ“Š Operazioni: {total_operations}\n\n{context_comparison}", "context_comparison": { "type": "auto", "compare_with": "average_last_30_days", "template": "Rispetto alla media degli ultimi 30 giorni ({avg_30d} pezzi), sei {delta_percent}% {emoji}." } }, "follow_up_suggestions": [ "Mostrami il trend settimanale", "Confronta con ieri", "Chi ha prodotto di piΓΉ?" ] } `

    Database Schema Template Storage

    `sql -- Tabella: ai_knowledge_templates CREATE TABLE ai_knowledge_templates ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, template_id VARCHAR(64) UNIQUE NOT NULL, ambito VARCHAR(64) NOT NULL, pattern TEXT NOT NULL, requires_llm BOOLEAN DEFAULT FALSE, cache_ttl INT UNSIGNED DEFAULT 3600, template_config JSON NOT NULL, usage_count INT UNSIGNED DEFAULT 0, success_rate DECIMAL(5,2) DEFAULT 100.00, last_used_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_ambito (ambito), INDEX idx_usage (usage_count DESC) ); `

    ---

    πŸ”§ Services Architecture

    1. IntentRouterService

    `php // app/Services/I40/AI/IntentRouterService.php

    class IntentRouterService { protected $patterns; public function __construct() { $this->patterns = require app_path('Services/I40/AI/IntentPatterns.php'); } / * Rileva ambito dalla query utente */ public function detectAmbito(string $userQuery): array { $normalized = $this->normalize($userQuery); foreach ($this->patterns as $ambito => $regexList) { foreach ($regexList as $regex) { if (preg_match($regex, $normalized, $matches)) { return [ 'ambito' => $ambito, 'confidence' => 1.0, // Pattern match è deterministico 'matched_pattern' => $regex, 'extracted_entities' => $this->extractEntities($matches), ]; } } } // Fallback: usa LLM per rilevare ambito return [ 'ambito' => 'AMBITO_GENERALE', 'confidence' => 0.3, 'requires_llm_clarification' => true, ]; } protected function normalize(string $query): string { $query = strtolower($query); $query = preg_replace('/[^\w\sàèéìòù]/', '', $query); return trim($query); } protected function extractEntities(array $matches): array { $entities = []; // Estrai date/periodi $periodMap = [ 'ieri' => ['date_from' => now()->subDay()->startOfDay(), 'date_to' => now()->subDay()->endOfDay()], 'oggi' => ['date_from' => now()->startOfDay(), 'date_to' => now()->endOfDay()], 'questa settimana' => ['date_from' => now()->startOfWeek(), 'date_to' => now()->endOfWeek()], 'scorsa settimana' => ['date_from' => now()->subWeek()->startOfWeek(), 'date_to' => now()->subWeek()->endOfWeek()], 'questo mese' => ['date_from' => now()->startOfMonth(), 'date_to' => now()->endOfMonth()], ]; foreach ($matches as $match) { if (isset($periodMap[$match])) { $entities = array_merge($entities, $periodMap[$match]); $entities['periodo'] = $match; } } return $entities; } } `

    ---

    2. KnowledgeBaseService

    `php // app/Services/I40/AI/KnowledgeBaseService.php

    class KnowledgeBaseService { / * Trova template matching per query */ public function findTemplate(string $ambito, string $userQuery, array $entities): ?array { $templates = \DB::table('ai_knowledge_templates') ->where('ambito', $ambito) ->get(); foreach ($templates as $template) { $templateConfig = json_decode($template->template_config, true); $pattern = $templateConfig['pattern'] ?? ''; if (preg_match('/' . $pattern . '/i', $userQuery)) { // Incrementa usage counter \DB::table('ai_knowledge_templates') ->where('id', $template->id) ->increment('usage_count'); return array_merge((array)$template, $templateConfig); } } return null; } / * Esegue template e genera risposta */ public function executeTemplate(array $template, array $entities, Machine $machine): array { // 1) Prepara parametri query $params = $this->prepareParameters($template, $entities, $machine); // 2) Esegue SQL query $data = $this->executeSql($template['sql_query'], $params); // 3) Arricchisce con contesto (media, comparazioni) $enrichedData = $this->enrichWithContext($data, $params, $machine); // 4) Formatta risposta $response = $this->formatResponse($template['response_formatter'], $enrichedData); // 5) Genera follow-up suggestions $suggestions = $template['follow_up_suggestions'] ?? []; return [ 'content' => $response, 'data' => $enrichedData, 'suggestions' => $suggestions, 'requires_chart' => $this->shouldGenerateChart($template, $enrichedData), 'chart_config' => $this->getChartConfig($template, $enrichedData), 'metadata' => [ 'template_id' => $template['template_id'], 'ambito' => $template['ambito'], 'execution_time_ms' => 0, // TODO 'used_llm' => false, ], ]; } protected function enrichWithContext(array $data, array $params, Machine $machine): array { // Aggiunge media ultimi 30 giorni per confronto $avg30d = \DB::table('machine_operations_log') ->where('machine_id', $machine->id) ->whereBetween('timestamp', [now()->subDays(30), now()]) ->selectRaw('AVG(daily_total) as avg') ->from(\DB::raw('(SELECT DATE(timestamp) as date, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, \'$.TotalPieces\')) AS DECIMAL)) as daily_total FROM machine_operations_log GROUP BY date) as daily_stats')) ->value('avg'); $data['context'] = [ 'avg_last_30_days' => $avg30d, 'delta_percent' => $data['total_quantity'] ? (($data['total_quantity'] - $avg30d) / $avg30d * 100) : 0, 'emoji' => $data['total_quantity'] > $avg30d ? 'πŸ“ˆ' : 'πŸ“‰', ]; return $data; } } `

    ---

    3. LLMFallbackService (Solo quando necessario)

    `php // app/Services/I40/AI/LLMFallbackService.php

    class LLMFallbackService { / * Usa LLM solo se necessario */ public function generateResponse(string $ambito, string $userQuery, array $context, Machine $machine): array { // Prepara prompt minimale $systemPrompt = $this->buildMinimalPrompt($ambito, $machine); $userPrompt = $this->enhanceUserQuery($userQuery, $context); $response = \OpenAI::chat()->create([ 'model' => 'gpt-4-turbo-preview', // o gpt-4o-mini per costi minori 'messages' => [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userPrompt], ], 'temperature' => 0.3, // Basso per risposte deterministiche 'max_tokens' => 500, 'functions' => $this->getFunctions($ambito), ]); return [ 'content' => $response->choices[0]->message->content, 'tokens_used' => $response->usage->totalTokens, 'cost_estimate' => $this->estimateCost($response->usage->totalTokens), 'metadata' => [ 'used_llm' => true, 'ambito' => $ambito, ], ]; } protected function buildMinimalPrompt(string $ambito, Machine $machine): string { // Prompt compatto specifico per ambito (NON schema completo) $prompts = [ 'AMBITO_PRODUZIONE' => "Sei un assistente per analisi produzione. Macchina: {$machine->name}. Rispondi in italiano, max 3 frasi.", 'AMBITO_DIAGNOSTICA' => "Sei un assistente diagnostico macchine. Macchina: {$machine->name}. Identifica problemi e cause.", 'AMBITO_OTTIMIZZAZIONE' => "Sei un consulente ottimizzazione produzione. Suggerisci miglioramenti concreti e misurabili.", ]; return $prompts[$ambito] ?? "Sei un assistente produzione industriale. Rispondi brevemente in italiano."; } } `

    ---

    πŸ’Ύ Database Schema Completo

    `sql -- 1) Conversazioni (come roadmap originale) CREATE TABLE ai_conversations ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, machine_id BIGINT UNSIGNED NULL, ambito VARCHAR(64), -- Ambito prevalente conversazione title VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_machine (machine_id), INDEX idx_ambito (ambito), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE );

    -- 2) Messaggi CREATE TABLE ai_messages ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, conversation_id BIGINT UNSIGNED NOT NULL, role ENUM('user', 'assistant', 'system') NOT NULL, content TEXT NOT NULL, metadata JSON, -- { ambito, template_id, sql, execution_time, used_llm, cost } tokens_used INT UNSIGNED DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE );

    -- 3) Knowledge Base Templates CREATE TABLE ai_knowledge_templates ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, template_id VARCHAR(64) UNIQUE NOT NULL, ambito VARCHAR(64) NOT NULL, pattern TEXT NOT NULL, requires_llm BOOLEAN DEFAULT FALSE, cache_ttl INT UNSIGNED DEFAULT 3600, template_config JSON NOT NULL, usage_count INT UNSIGNED DEFAULT 0, success_rate DECIMAL(5,2) DEFAULT 100.00, avg_execution_time_ms INT UNSIGNED DEFAULT 0, last_used_at TIMESTAMP NULL, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_ambito (ambito), INDEX idx_usage (usage_count DESC), INDEX idx_active (is_active, ambito) );

    -- 4) Cache (ottimizzato) CREATE TABLE ai_cache ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, query_hash VARCHAR(64) UNIQUE NOT NULL, ambito VARCHAR(64), machine_id BIGINT UNSIGNED NULL, date_range VARCHAR(50), response_data JSON NOT NULL, hits INT UNSIGNED DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, INDEX idx_hash (query_hash), INDEX idx_expires (expires_at), INDEX idx_ambito (ambito) );

    -- 5) Feedback (+ ambito per analytics) CREATE TABLE ai_feedback ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, message_id BIGINT UNSIGNED NOT NULL, user_id BIGINT UNSIGNED NOT NULL, rating ENUM('up', 'down') NOT NULL, comment TEXT NULL, ambito VARCHAR(64), template_id VARCHAR(64) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (message_id) REFERENCES ai_messages(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_ambito (ambito), INDEX idx_template (template_id) );

    -- 6) Analytics (aggregati per dashboard) CREATE TABLE ai_usage_analytics ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, date DATE NOT NULL, ambito VARCHAR(64), total_queries INT UNSIGNED DEFAULT 0, queries_with_llm INT UNSIGNED DEFAULT 0, avg_response_time_ms INT UNSIGNED DEFAULT 0, total_tokens_used INT UNSIGNED DEFAULT 0, estimated_cost_usd DECIMAL(10,4) DEFAULT 0.0000, positive_feedback_count INT UNSIGNED DEFAULT 0, negative_feedback_count INT UNSIGNED DEFAULT 0, UNIQUE KEY unique_date_ambito (date, ambito), INDEX idx_date (date), INDEX idx_ambito (ambito) ); `

    ---

    πŸš€ Implementazione Step-by-Step

    Fase 1: Foundation (Settimana 1)

    1.1 Database Setup

    `bash php artisan make:migration create_ai_assistant_tables_v2 php artisan migrate `

    1.2 Models

    `bash php artisan make:model I40/AiConversation php artisan make:model I40/AiMessage php artisan make:model I40/AiKnowledgeTemplate php artisan make:model I40/AiCache `

    1.3 Seeders - Knowledge Base Iniziale

    `bash php artisan make:seeder AiKnowledgeTemplatesSeeder `

    File: database/seeders/AiKnowledgeTemplatesSeeder.php `php public function run() { $templates = [ // AMBITO_PRODUZIONE [ 'template_id' => 'prod_daily', 'ambito' => 'AMBITO_PRODUZIONE', 'pattern' => 'produzione di (ieri|oggi)', 'requires_llm' => false, 'cache_ttl' => 3600, 'template_config' => json_encode([ 'sql_query' => [ 'query' => 'SELECT ...', 'timeout' => 5, ], 'response_formatter' => [ 'template' => 'Produzione di {periodo}: {total_quantity} pezzi.', ], 'follow_up_suggestions' => [ 'Mostrami il trend settimanale', 'Confronta con ieri', ], ]), ], // ... altri 20-30 template per ambito ]; foreach ($templates as $template) { \DB::table('ai_knowledge_templates')->insert($template); } } `

    ---

    Fase 2: Services Layer (Settimana 2)

    2.1 Intent Router

    `bash php artisan make:service I40/AI/IntentRouterService `

    2.2 Knowledge Base Service

    `bash php artisan make:service I40/AI/KnowledgeBaseService `

    2.3 LLM Fallback Service

    `bash php artisan make:service I40/AI/LLMFallbackService `

    2.4 Orchestrator (Main Service)

    `bash php artisan make:service I40/AI/AiAssistantService `

    File: app/Services/I40/AI/AiAssistantService.php `php class AiAssistantService { public function __construct( protected IntentRouterService $intentRouter, protected KnowledgeBaseService $knowledgeBase, protected LLMFallbackService $llmFallback, ) {} public function handleQuery(string $userQuery, Machine $machine, User $user): array { // 1) Intent Detection $intent = $this->intentRouter->detectAmbito($userQuery); // 2) Check Cache $cached = $this->checkCache($userQuery, $machine->id, $intent['ambito']); if ($cached) { return $cached; } // 3) Find Knowledge Base Template $template = $this->knowledgeBase->findTemplate( $intent['ambito'], $userQuery, $intent['extracted_entities'] ); // 4a) Se template trovato β†’ Esegui senza LLM if ($template && !$template['requires_llm']) { $response = $this->knowledgeBase->executeTemplate($template, $intent['extracted_entities'], $machine); $this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response); return $response; } // 4b) Altrimenti β†’ Usa LLM $context = $this->buildContext($machine, $intent); $response = $this->llmFallback->generateResponse( $intent['ambito'], $userQuery, $context, $machine ); $this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response); return $response; } } `

    </p><p>---</p><p><h3>Fase 3: Controller &amp; Routes (Settimana 2)</h3></p><p><strong>Routes: </code>routes/i40.php<code></strong>
    </code>`<code>php
    Route::prefix(&#039;ai&#039;)-&gt;name(&#039;ai.&#039;)-&gt;middleware(&#039;auth&#039;)-&gt;group(function () {
        Route::post(&#039;/chat&#039;, [AiAssistantController::class, &#039;chat&#039;])-&gt;name(&#039;chat&#039;);
        Route::get(&#039;/conversations&#039;, [AiAssistantController::class, &#039;conversations&#039;])-&gt;name(&#039;conversations&#039;);
        Route::get(&#039;/conversations/{id}&#039;, [AiAssistantController::class, &#039;show&#039;])-&gt;name(&#039;conversations.show&#039;);
        Route::post(&#039;/feedback&#039;, [AiAssistantController::class, &#039;feedback&#039;])-&gt;name(&#039;feedback&#039;);
        
        // Analytics admin
        Route::get(&#039;/analytics&#039;, [AiAssistantController::class, &#039;analytics&#039;])-&gt;name(&#039;analytics&#039;)
            -&gt;middleware(&#039;role:admin&#039;);
    });
    </code>`<code></p><p>---</p><p><h3>Fase 4: Frontend Widget (Settimana 3)</h3></p><p><strong>Component Blade (Simplified - No Vue)</strong>
    </code>`<code>blade
    &lt;!-- resources/views/components/ai-chat-widget.blade.php --&gt;</p><p>&lt;div id=&quot;ai-chat-widget&quot; class=&quot;ai-chat-widget minimized&quot;&gt;
        &lt;div class=&quot;chat-header&quot; onclick=&quot;toggleChat()&quot;&gt;
            &lt;div&gt;
                &lt;span class=&quot;ai-avatar&quot;&gt;πŸ€–&lt;/span&gt;
                &lt;strong&gt;AI Assistant&lt;/strong&gt;
                &lt;small id=&quot;current-ambito&quot;&gt;&lt;/small&gt;
            &lt;/div&gt;
            &lt;i class=&quot;bi bi-chevron-up&quot;&gt;&lt;/i&gt;
        &lt;/div&gt;
        
        &lt;div class=&quot;chat-body&quot;&gt;
            &lt;div id=&quot;messages-container&quot; class=&quot;messages-container&quot;&gt;
                &lt;!-- Suggestions iniziali --&gt;
                &lt;div class=&quot;empty-state&quot;&gt;
                    &lt;h6&gt;Cosa vuoi sapere?&lt;/h6&gt;
                    &lt;div class=&quot;suggestions-grid&quot;&gt;
                        &lt;button onclick=&quot;sendMessage(&#039;Produzione di oggi&#039;)&quot;&gt;πŸ“Š Produzione oggi&lt;/button&gt;
                        &lt;button onclick=&quot;sendMessage(&#039;Chi Γ¨ l\&#039;operatore migliore?&#039;)&quot;&gt;πŸ‘€ Miglior operatore&lt;/button&gt;
                        &lt;button onclick=&quot;sendMessage(&#039;Confronta con ieri&#039;)&quot;&gt;πŸ“ˆ Confronto ieri&lt;/button&gt;
                        &lt;button onclick=&quot;sendMessage(&#039;Riusciremo a finire in tempo?&#039;)&quot;&gt;⏱️ Deadline&lt;/button&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;div class=&quot;chat-input&quot;&gt;
                &lt;input type=&quot;text&quot; id=&quot;user-input&quot; placeholder=&quot;Chiedi qualcosa...&quot; onkeypress=&quot;handleEnter(event)&quot;&gt;
                &lt;button onclick=&quot;sendMessage()&quot;&gt;&lt;i class=&quot;bi bi-send&quot;&gt;&lt;/i&gt;&lt;/button&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;</p><p>&lt;script&gt;
    let conversationId = null;
    const machineId = {{ $machine-&gt;id }};</p><p>async function sendMessage(text = null) {
        const message = text || document.getElementById(&#039;user-input&#039;).value;
        if (!message.trim()) return;
        
        // Mostra messaggio utente
        appendMessage(&#039;user&#039;, message);
        document.getElementById(&#039;user-input&#039;).value = &#039;&#039;;
        
        // Mostra loader
        showLoader();
        
        try {
            const response = await fetch(&#039;/admin/i40/ai/chat&#039;, {
                method: &#039;POST&#039;,
                headers: {
                    &#039;Content-Type&#039;: &#039;application/json&#039;,
                    &#039;X-CSRF-TOKEN&#039;: &#039;{{ csrf_token() }}&#039;,
                },
                body: JSON.stringify({
                    conversation_id: conversationId,
                    machine_id: machineId,
                    message: message,
                }),
            });
            
            const data = await response.json();
            conversationId = data.conversation_id;
            
            hideLoader();
            appendMessage(&#039;assistant&#039;, data.content, data.suggestions, data.chart_config);
            
            // Aggiorna badge ambito
            if (data.metadata?.ambito) {
                document.getElementById(&#039;current-ambito&#039;).textContent = formatAmbito(data.metadata.ambito);
            }
            
        } catch (error) {
            hideLoader();
            appendMessage(&#039;assistant&#039;, &#039;❌ Errore di connessione. Riprova.&#039;);
        }
    }</p><p>function appendMessage(role, content, suggestions = null, chartConfig = null) {
        const container = document.getElementById(&#039;messages-container&#039;);
        const msgDiv = document.createElement(&#039;div&#039;);
        msgDiv.className =
    message ${role}; msgDiv.innerHTML = <div class="message-avatar">${role === 'user' ? 'πŸ‘€' : 'πŸ€–'}</div> <div class="message-content"> <div class="message-text">${content.replace(/\n/g, '<br>')}</div> ${suggestions ? <div class="follow-up-suggestions"> ${suggestions.map(s => <button onclick="sendMessage('${s}')">${s}</button>
    ).join(&#039;&#039;)}
                    &lt;/div&gt;
                </code> : &#039;&#039;}
                ${chartConfig ? <code>&lt;button onclick=&quot;openChart(${JSON.stringify(chartConfig)})&quot;&gt;&lt;i class=&quot;bi bi-bar-chart&quot;&gt;&lt;/i&gt; Visualizza Grafico&lt;/button&gt;</code> : &#039;&#039;}
            &lt;/div&gt;
        <code>;
        
        container.appendChild(msgDiv);
        container.scrollTop = container.scrollHeight;
    }</p><p>function formatAmbito(ambito) {
        const map = {
            &#039;AMBITO_PRODUZIONE&#039;: &#039;πŸ“Š Produzione&#039;,
            &#039;AMBITO_DIAGNOSTICA&#039;: &#039;πŸ”§ Diagnostica&#039;,
            &#039;AMBITO_OPERATORI&#039;: &#039;πŸ‘₯ Operatori&#039;,
            &#039;AMBITO_PIANIFICAZIONE&#039;: &#039;πŸ“… Pianificazione&#039;,
            &#039;AMBITO_CONFIGURAZIONE&#039;: &#039;βš™οΈ Configurazione&#039;,
            &#039;AMBITO_COMPARAZIONE&#039;: &#039;βš–οΈ Confronti&#039;,
            &#039;AMBITO_OTTIMIZZAZIONE&#039;: &#039;πŸš€ Ottimizzazione&#039;,
        };
        return map[ambito] || &#039;&#039;;
    }
    &lt;/script&gt;
    `

    Includi in analyze.blade.php: `blade @include('components.ai-chat-widget', ['machine' => $selectedMachine]) `

    ---

    πŸ“Š Analytics & Monitoring

    Dashboard Metriche

    `php // app/Http/Controllers/Admin/I40/AiAssistantController.php

    public function analytics(Request $request) { $from = $request->input('from', now()->subDays(30)); $to = $request->input('to', now()); // Metriche globali $totalQueries = AiMessage::whereBetween('created_at', [$from, $to])->count(); $queriesWithLLM = AiMessage::whereBetween('created_at', [$from, $to]) ->whereJsonContains('metadata->used_llm', true)->count(); $llmUsagePercent = $totalQueries > 0 ? ($queriesWithLLM / $totalQueries * 100) : 0; // Breakdown per ambito $byAmbito = \DB::table('ai_usage_analytics') ->whereBetween('date', [$from, $to]) ->select('ambito', \DB::raw('SUM(total_queries) as total'), \DB::raw('SUM(queries_with_llm) as with_llm'), \DB::raw('AVG(avg_response_time_ms) as avg_time'), \DB::raw('SUM(estimated_cost_usd) as cost')) ->groupBy('ambito') ->get(); // Top templates $topTemplates = AiKnowledgeTemplate::orderBy('usage_count', 'desc')->limit(10)->get(); // Satisfaction rate $positiveFeedback = AiFeedback::where('rating', 'up')->whereBetween('created_at', [$from, $to])->count(); $negativeFeedback = AiFeedback::where('rating', 'down')->whereBetween('created_at', [$from, $to])->count(); $satisfactionRate = ($positiveFeedback + $negativeFeedback) > 0 ? ($positiveFeedback / ($positiveFeedback + $negativeFeedback) * 100) : 0; return view('admin.i40.ai.analytics', compact( 'totalQueries', 'llmUsagePercent', 'byAmbito', 'topTemplates', 'satisfactionRate' )); } `

    ---

    πŸ’° Stima Costi

    Scenario Realistico

    Setup:

  • 3 macchine
  • 10 utenti attivi
  • 30 query/utente/mese = 300 query totali/mese
  • Breakdown Uso LLM:

  • 80% query gestite da Knowledge Base (0 costi API) = 240 query
  • 20% query richiedono LLM = 60 query
  • Costo per Query LLM:

  • Input: 500 tokens Γ— $0.01 / 1M = $0.005
  • Output: 200 tokens Γ— $0.03 / 1M = $0.006
  • Totale per query: ~$0.011
  • Costo Mensile:

  • 60 query Γ— $0.011 = $0.66/mese
  • Con margine sicurezza (+50%): ~$1/mese

    ScalabilitΓ 

    | Utenti | Query/mese | Costo LLM (20%) | Costo Totale | |--------|------------|-----------------|--------------| | 10 | 300 | $0.66 | ~$1/mese | | 50 | 1.500 | $3.30 | ~$5/mese | | 100 | 3.000 | $6.60 | ~$10/mese |

    ---

    βœ… Vantaggi di Questo Approccio

    vs Roadmap Originale

    | Aspetto | Roadmap Originale | Questa Architettura | |---------|-------------------|---------------------| | Costi API | $25-40/mese | $1-5/mese | | Latency | 2-5s (sempre LLM) | <500ms (80% query) | | Precisione | 85% (LLM hallucinations) | 98% (template deterministici) | | Maintenance | Alta (prompt tuning) | Bassa (template JSON) | | ScalabilitΓ  | Lineare ($$) | Sub-lineare (cache) |

    PerchΓ© Funziona Meglio

    1. Knowledge Base > LLM: Le query ricorrenti (80%) hanno risposte deterministiche 2. Ambiti Contestuali: Ogni dominio ha vocabolario e pattern specifici 3. Cache Aggressiva: Stessa query = 0 costi se in cache 4. Feedback Loop: Templates migliorano con usage analytics 5. Costi Prevedibili: Budget mensile fisso, non variabile

    ---

    πŸŽ“ Training Iniziale

    Step 1: Seeding Knowledge Base (30-40 Template)

    Distribuzione Template:

  • AMBITO_PRODUZIONE: 8 template
  • AMBITO_DIAGNOSTICA: 6 template
  • AMBITO_OPERATORI: 5 template
  • AMBITO_CONFIGURAZIONE: 4 template
  • AMBITO_PIANIFICAZIONE: 5 template
  • AMBITO_COMPARAZIONE: 6 template
  • AMBITO_OTTIMIZZAZIONE: 4 template
  • Step 2: Fine-Tuning LLM (Opzionale, Fase 2)

    Una volta raccolti 500+ conversazioni: 1. Estrai query + risposte con feedback positivo 2. Crea dataset JSONL per fine-tuning 3. Fine-tune gpt-4o-mini su dominio specifico 4. Sostituisci LLM fallback con modello fine-tuned

    Benefici Fine-Tuning:

  • Costi ridotti ulteriormente (-50%)
  • Risposte piΓΉ coerenti col tuo dominio
  • Riduzione allucinazioni
  • ---

    πŸ“ Prossimi Step

    Settimana 1-2: Foundation

  • [ ] Setup database (migrations)
  • [ ] Seeding Knowledge Base (30 template iniziali)
  • [ ] Intent Router Service (pattern matching)
  • Settimana 3-4: Services Layer

  • [ ] Knowledge Base Service (template execution)
  • [ ] LLM Fallback Service (OpenAI integration)
  • [ ] Orchestrator Service (main logic)
  • [ ] Cache Service
  • Settimana 5: Controller & API

  • [ ] AiAssistantController (CRUD conversazioni)
  • [ ] Routes setup
  • [ ] API testing (Postman/Insomnia)
  • Settimana 6: Frontend

  • [ ] Chat Widget Blade component
  • [ ] Integrazione in analyze.blade.php`
  • [ ] Testing UX con utenti pilota
  • Settimana 7: Analytics & Refinement

  • [ ] Dashboard analytics admin
  • [ ] Usage tracking & optimization
  • [ ] Template refinement basato su feedback
  • Settimana 8: Production Ready

  • [ ] Load testing
  • [ ] Security audit
  • [ ] Documentazione utente
  • [ ] Soft launch
  • ---

    🚦 Criteri di Successo

    Metriche Tecniche

  • βœ… 80%+ query gestite senza LLM
  • βœ… Latency media < 1s
  • βœ… Costi < $10/mese (100 utenti)
  • βœ… Cache hit rate > 40%
  • Metriche Business

  • βœ… Satisfaction rate > 85%
  • βœ… 60%+ utenti usano AI settimanalmente
  • βœ… 50%+ query risolte senza supporto umano
  • Metriche QualitΓ 

  • βœ… Zero allucinazioni (risposte verificabili)
  • βœ… Precisione > 95%
  • βœ… Feedback negativo < 10%

---

Pronto per iniziare? πŸš€

Questa architettura Γ¨ production-ready, cost-effective e scalabile. La chiave Γ¨ partire con una Knowledge Base solida (30-40 template) e usare LLM solo come fallback intelligente.

Analisi Codice

Blocco 1
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    FRONTEND - Chat Widget                        β”‚
β”‚  Query Utente β†’ Suggestions β†’ Storico Conversazione             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              LAYER 1: Intent Router (Deterministic)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Pattern Matching Regex β†’ Ambito Contestuale              β”‚   β”‚
β”‚  β”‚ β€’ "produzione di ieri" β†’ AMBITO_PRODUZIONE                β”‚   β”‚
β”‚  β”‚ β€’ "macchina in errore" β†’ AMBITO_DIAGNOSTICA              β”‚   β”‚
β”‚  β”‚ β€’ "qual Γ¨ il profilo" β†’ AMBITO_CONFIGURAZIONE            β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         LAYER 2: Context-Specific Knowledge Base                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Template Repository (JSON)                                β”‚   β”‚
β”‚  β”‚ β€’ Query predefinite con parametri                         β”‚   β”‚
β”‚  β”‚ β€’ Risposte con placeholder                                β”‚   β”‚
β”‚  β”‚ β€’ Threshold e regole business                             β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚            LAYER 3: Data Execution Engine                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ SQL Query Builder β†’ DB β†’ Response Formatter               β”‚   β”‚
β”‚  β”‚ β€’ Safe execution (whitelist, timeout)                     β”‚   β”‚
β”‚  β”‚ β€’ Cache intelligente                                      β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
                         ↓ (SOLO se necessario)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              LAYER 4: LLM Fallback (GPT-4 Mini)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ Fine-Tuned Model β†’ Prompt Compatto β†’ Response             β”‚   β”‚
β”‚  β”‚ β€’ Solo per query complesse/ambigue                        β”‚   β”‚
β”‚  β”‚ β€’ Context injection minimale                              β”‚   β”‚
β”‚  β”‚ β€’ Function calling per SQL generation                     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Blocco 2
"produzione di {periodo}"
"confronta {periodo1} con {periodo2}"
"mostrami il trend {periodo}"
"chi ha prodotto di piΓΉ"
"pezzi totali {filtri}"
Blocco 3 json
{
  "pattern": "produzione di (ieri|oggi|questa settimana|questo mese)",
  "sql_template": "SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL)) as total FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to",
  "response_template": "La produzione di {periodo} Γ¨ stata di {total} pezzi, {comparazione}.",
  "requires_llm": false
}
Blocco 4
"perchΓ© la macchina si Γ¨ fermata"
"errori di {periodo}"
"qual Γ¨ il problema"
"fermi macchina {periodo}"
"macchina lenta oggi"
Blocco 5
1. Verifica gaps timestamp > 30 min β†’ "Fermo macchina rilevato"
2. Verifica Duration > avg + 2Οƒ β†’ "Anomalia performance"
3. Verifica ErrorCode in raw_data β†’ "Errore registrato: {code}"
4. Verifica produzione < media - 20% β†’ "Produzione sotto media"
Blocco 6
"chi Γ¨ l'operatore migliore"
"performance di {nome_operatore}"
"confronta {op1} con {op2}"
"ranking operatori"
"operatori piΓΉ veloci"
Blocco 7 json
{
  "pattern": "chi Γ¨ l'operatore (migliore|piΓΉ veloce|piΓΉ produttivo)",
  "sql_template": "SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{operator_field}')) as operator, COUNT(*) as operations, SUM(...) as total_pieces, AVG(...) as avg_duration FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY operator ORDER BY total_pieces DESC LIMIT 5",
  "response_template": "L'operatore piΓΉ produttivo Γ¨ {operator} con {total_pieces} pezzi in {operations} operazioni.",
  "requires_llm": false
}
Blocco 8
"quali profili ha la macchina"
"configurazione macchina"
"ultimo import CSV"
"campi disponibili"
"come Γ¨ configurata la macchina"
Blocco 9
"riusciremo a finire {quantitΓ } entro {data}"
"quanto ci vuole per {quantitΓ }"
"quando finiremo {ordine}"
"capacitΓ  produttiva {periodo}"
Blocco 10
"confronta macchina A con B"
"questa settimana vs scorsa"
"oggi vs ieri"
"trend ultimi {n} giorni"
Blocco 11 json
{
  "pattern": "confronta (questa settimana|oggi) (con|vs) (la scorsa|ieri)",
  "sql_template": [
    "SELECT SUM(...) as current FROM ... WHERE timestamp BETWEEN :current_from AND :current_to",
    "SELECT SUM(...) as previous FROM ... WHERE timestamp BETWEEN :previous_from AND :previous_to"
  ],
  "response_template": "Confronto {periodo_current} vs {periodo_previous}: {current} pezzi vs {previous} pezzi. Variazione: {delta}% ({emoji}).",
  "requires_llm": false
}
Blocco 12
"come posso migliorare"
"suggerimenti ottimizzazione"
"quali sono i problemi"
"come aumentare produttivitΓ "
Blocco 13 php
// app/Services/I40/IntentPatterns.php

return [
    'AMBITO_PRODUZIONE' => [
        '/produz(?:ione|ione totale|ione di) (ieri|oggi|questa settimana|questo mese)/i',
        '/quant[iaoe]+ (pezzi|operazioni|lavorazioni)/i',
        '/totale (prodotto|lavorato)/i',
        '/(?:mostra|visualizza) (?:la )?produzione/i',
    ],
    
    'AMBITO_DIAGNOSTICA' => [
        '/(?:perch[eΓ©]|come mai|motivo) .*(ferm[oa]|problem[ai]|error[ei])/i',
        '/(?:errori?|alarm[ei]|warning)/i',
        '/(?:cosa|quale) (?:Γ¨ )?(?:il )?problem[ai]/i',
        '/macchina (?:ferma|bloccata|in errore)/i',
        '/performance (?:bassa|scadente|sotto media)/i',
    ],
    
    'AMBITO_OPERATORI' => [
        '/(?:chi|quale) (?:Γ¨ |ha ).*(operatore|utente)/i',
        '/performance (?:di |dell\'operatore)/i',
        '/ranking (?:operatori|utenti)/i',
        '/(?:operatore|utente) (?:migliore|peggiore|piΓΉ veloce|piΓΉ lento)/i',
    ],
    
    'AMBITO_CONFIGURAZIONE' => [
        '/(?:quali|mostra) profil[io]/i',
        '/configurazione (?:macchina|della macchina)/i',
        '/(?:ultimo|ultimi) (?:import|csv)/i',
        '/(?:come|in che modo) Γ¨ configurat[oa]/i',
        '/(?:campi|colonne) disponibili/i',
    ],
    
    'AMBITO_PIANIFICAZIONE' => [
        '/riuscir(?:emo|Γ²) .* entro/i',
        '/(?:quanto|quando) (?:tempo|ci vuole)/i',
        '/(?:capacitΓ |potenziale) produttiv[ao]/i',
        '/previsione/i',
        '/deadline/i',
    ],
    
    'AMBITO_COMPARAZIONE' => [
        '/confron(?:ta|to) .* (?:con|vs)/i',
        '/(?:differenza|delta) (?:tra|con)/i',
        '/(?:questa|oggi|ieri) (?:vs|con|rispetto)/i',
        '/trend (?:ultimi|degli ultimi)/i',
    ],
    
    'AMBITO_OTTIMIZZAZIONE' => [
        '/(?:come|in che modo) (?:posso |migliorare|ottimizzare|aumentare)/i',
        '/suggeriment[io]/i',
        '/(?:consigli|raccomandazioni)/i',
        '/(?:opportunitΓ |margini) (?:di |miglioramento)/i',
    ],
];
Blocco 14 json
{
  "template_id": "prod_daily",
  "ambito": "AMBITO_PRODUZIONE",
  "pattern": "produzione di (ieri|oggi)",
  "requires_llm": false,
  "cache_ttl": 3600,
  "parameters": {
    "machine_id": "required",
    "date_from": "auto",
    "date_to": "auto"
  },
  "entity_extraction": {
    "periodo": "regex_group_1"
  },
  "business_rules": [
    {
      "condition": "periodo == 'ieri'",
      "action": "set_date_range('yesterday')"
    },
    {
      "condition": "periodo == 'oggi'",
      "action": "set_date_range('today')"
    }
  ],
  "sql_query": {
    "query": "SELECT machine_id, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL(15,2))) as total_quantity, COUNT(*) as total_operations FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY machine_id",
    "timeout": 5,
    "fallback": "Impossibile recuperare dati produzione."
  },
  "response_formatter": {
    "template": "**Produzione di {periodo}:**\n\nβœ… Totale pezzi: **{total_quantity}**\nπŸ“Š Operazioni: **{total_operations}**\n\n{context_comparison}",
    "context_comparison": {
      "type": "auto",
      "compare_with": "average_last_30_days",
      "template": "Rispetto alla media degli ultimi 30 giorni ({avg_30d} pezzi), sei {delta_percent}% {emoji}."
    }
  },
  "follow_up_suggestions": [
    "Mostrami il trend settimanale",
    "Confronta con ieri",
    "Chi ha prodotto di piΓΉ?"
  ]
}
Blocco 15 sql
-- Tabella: ai_knowledge_templates
CREATE TABLE ai_knowledge_templates (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  template_id VARCHAR(64) UNIQUE NOT NULL,
  ambito VARCHAR(64) NOT NULL,
  pattern TEXT NOT NULL,
  requires_llm BOOLEAN DEFAULT FALSE,
  cache_ttl INT UNSIGNED DEFAULT 3600,
  template_config JSON NOT NULL,
  usage_count INT UNSIGNED DEFAULT 0,
  success_rate DECIMAL(5,2) DEFAULT 100.00,
  last_used_at TIMESTAMP NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_ambito (ambito),
  INDEX idx_usage (usage_count DESC)
);
Blocco 16 php
// app/Services/I40/AI/IntentRouterService.php

class IntentRouterService
{
    protected $patterns;
    
    public function __construct()
    {
        $this->patterns = require app_path('Services/I40/AI/IntentPatterns.php');
    }
    
    /**
     * Rileva ambito dalla query utente
     */
    public function detectAmbito(string $userQuery): array
    {
        $normalized = $this->normalize($userQuery);
        
        foreach ($this->patterns as $ambito => $regexList) {
            foreach ($regexList as $regex) {
                if (preg_match($regex, $normalized, $matches)) {
                    return [
                        'ambito' => $ambito,
                        'confidence' => 1.0, // Pattern match Γ¨ deterministico
                        'matched_pattern' => $regex,
                        'extracted_entities' => $this->extractEntities($matches),
                    ];
                }
            }
        }
        
        // Fallback: usa LLM per rilevare ambito
        return [
            'ambito' => 'AMBITO_GENERALE',
            'confidence' => 0.3,
            'requires_llm_clarification' => true,
        ];
    }
    
    protected function normalize(string $query): string
    {
        $query = strtolower($query);
        $query = preg_replace('/[^\w\sàèéìòù]/', '', $query);
        return trim($query);
    }
    
    protected function extractEntities(array $matches): array
    {
        $entities = [];
        
        // Estrai date/periodi
        $periodMap = [
            'ieri' => ['date_from' => now()->subDay()->startOfDay(), 'date_to' => now()->subDay()->endOfDay()],
            'oggi' => ['date_from' => now()->startOfDay(), 'date_to' => now()->endOfDay()],
            'questa settimana' => ['date_from' => now()->startOfWeek(), 'date_to' => now()->endOfWeek()],
            'scorsa settimana' => ['date_from' => now()->subWeek()->startOfWeek(), 'date_to' => now()->subWeek()->endOfWeek()],
            'questo mese' => ['date_from' => now()->startOfMonth(), 'date_to' => now()->endOfMonth()],
        ];
        
        foreach ($matches as $match) {
            if (isset($periodMap[$match])) {
                $entities = array_merge($entities, $periodMap[$match]);
                $entities['periodo'] = $match;
            }
        }
        
        return $entities;
    }
}
Blocco 17 php
// app/Services/I40/AI/KnowledgeBaseService.php

class KnowledgeBaseService
{
    /**
     * Trova template matching per query
     */
    public function findTemplate(string $ambito, string $userQuery, array $entities): ?array
    {
        $templates = \DB::table('ai_knowledge_templates')
            ->where('ambito', $ambito)
            ->get();
        
        foreach ($templates as $template) {
            $templateConfig = json_decode($template->template_config, true);
            $pattern = $templateConfig['pattern'] ?? '';
            
            if (preg_match('/' . $pattern . '/i', $userQuery)) {
                // Incrementa usage counter
                \DB::table('ai_knowledge_templates')
                    ->where('id', $template->id)
                    ->increment('usage_count');
                
                return array_merge((array)$template, $templateConfig);
            }
        }
        
        return null;
    }
    
    /**
     * Esegue template e genera risposta
     */
    public function executeTemplate(array $template, array $entities, Machine $machine): array
    {
        // 1) Prepara parametri query
        $params = $this->prepareParameters($template, $entities, $machine);
        
        // 2) Esegue SQL query
        $data = $this->executeSql($template['sql_query'], $params);
        
        // 3) Arricchisce con contesto (media, comparazioni)
        $enrichedData = $this->enrichWithContext($data, $params, $machine);
        
        // 4) Formatta risposta
        $response = $this->formatResponse($template['response_formatter'], $enrichedData);
        
        // 5) Genera follow-up suggestions
        $suggestions = $template['follow_up_suggestions'] ?? [];
        
        return [
            'content' => $response,
            'data' => $enrichedData,
            'suggestions' => $suggestions,
            'requires_chart' => $this->shouldGenerateChart($template, $enrichedData),
            'chart_config' => $this->getChartConfig($template, $enrichedData),
            'metadata' => [
                'template_id' => $template['template_id'],
                'ambito' => $template['ambito'],
                'execution_time_ms' => 0, // TODO
                'used_llm' => false,
            ],
        ];
    }
    
    protected function enrichWithContext(array $data, array $params, Machine $machine): array
    {
        // Aggiunge media ultimi 30 giorni per confronto
        $avg30d = \DB::table('machine_operations_log')
            ->where('machine_id', $machine->id)
            ->whereBetween('timestamp', [now()->subDays(30), now()])
            ->selectRaw('AVG(daily_total) as avg')
            ->from(\DB::raw('(SELECT DATE(timestamp) as date, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, \'$.TotalPieces\')) AS DECIMAL)) as daily_total FROM machine_operations_log GROUP BY date) as daily_stats'))
            ->value('avg');
        
        $data['context'] = [
            'avg_last_30_days' => $avg30d,
            'delta_percent' => $data['total_quantity'] ? (($data['total_quantity'] - $avg30d) / $avg30d * 100) : 0,
            'emoji' => $data['total_quantity'] > $avg30d ? 'πŸ“ˆ' : 'πŸ“‰',
        ];
        
        return $data;
    }
}
Blocco 18 php
// app/Services/I40/AI/LLMFallbackService.php

class LLMFallbackService
{
    /**
     * Usa LLM solo se necessario
     */
    public function generateResponse(string $ambito, string $userQuery, array $context, Machine $machine): array
    {
        // Prepara prompt minimale
        $systemPrompt = $this->buildMinimalPrompt($ambito, $machine);
        $userPrompt = $this->enhanceUserQuery($userQuery, $context);
        
        $response = \OpenAI::chat()->create([
            'model' => 'gpt-4-turbo-preview', // o gpt-4o-mini per costi minori
            'messages' => [
                ['role' => 'system', 'content' => $systemPrompt],
                ['role' => 'user', 'content' => $userPrompt],
            ],
            'temperature' => 0.3, // Basso per risposte deterministiche
            'max_tokens' => 500,
            'functions' => $this->getFunctions($ambito),
        ]);
        
        return [
            'content' => $response->choices[0]->message->content,
            'tokens_used' => $response->usage->totalTokens,
            'cost_estimate' => $this->estimateCost($response->usage->totalTokens),
            'metadata' => [
                'used_llm' => true,
                'ambito' => $ambito,
            ],
        ];
    }
    
    protected function buildMinimalPrompt(string $ambito, Machine $machine): string
    {
        // Prompt compatto specifico per ambito (NON schema completo)
        $prompts = [
            'AMBITO_PRODUZIONE' => "Sei un assistente per analisi produzione. Macchina: {$machine->name}. Rispondi in italiano, max 3 frasi.",
            'AMBITO_DIAGNOSTICA' => "Sei un assistente diagnostico macchine. Macchina: {$machine->name}. Identifica problemi e cause.",
            'AMBITO_OTTIMIZZAZIONE' => "Sei un consulente ottimizzazione produzione. Suggerisci miglioramenti concreti e misurabili.",
        ];
        
        return $prompts[$ambito] ?? "Sei un assistente produzione industriale. Rispondi brevemente in italiano.";
    }
}
Blocco 19 sql
-- 1) Conversazioni (come roadmap originale)
CREATE TABLE ai_conversations (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  machine_id BIGINT UNSIGNED NULL,
  ambito VARCHAR(64), -- Ambito prevalente conversazione
  title VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_user (user_id),
  INDEX idx_machine (machine_id),
  INDEX idx_ambito (ambito),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- 2) Messaggi
CREATE TABLE ai_messages (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  conversation_id BIGINT UNSIGNED NOT NULL,
  role ENUM('user', 'assistant', 'system') NOT NULL,
  content TEXT NOT NULL,
  metadata JSON, -- { ambito, template_id, sql, execution_time, used_llm, cost }
  tokens_used INT UNSIGNED DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_conversation (conversation_id),
  FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE
);

-- 3) Knowledge Base Templates
CREATE TABLE ai_knowledge_templates (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  template_id VARCHAR(64) UNIQUE NOT NULL,
  ambito VARCHAR(64) NOT NULL,
  pattern TEXT NOT NULL,
  requires_llm BOOLEAN DEFAULT FALSE,
  cache_ttl INT UNSIGNED DEFAULT 3600,
  template_config JSON NOT NULL,
  usage_count INT UNSIGNED DEFAULT 0,
  success_rate DECIMAL(5,2) DEFAULT 100.00,
  avg_execution_time_ms INT UNSIGNED DEFAULT 0,
  last_used_at TIMESTAMP NULL,
  is_active BOOLEAN DEFAULT TRUE,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX idx_ambito (ambito),
  INDEX idx_usage (usage_count DESC),
  INDEX idx_active (is_active, ambito)
);

-- 4) Cache (ottimizzato)
CREATE TABLE ai_cache (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  query_hash VARCHAR(64) UNIQUE NOT NULL,
  ambito VARCHAR(64),
  machine_id BIGINT UNSIGNED NULL,
  date_range VARCHAR(50),
  response_data JSON NOT NULL,
  hits INT UNSIGNED DEFAULT 0,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  expires_at TIMESTAMP,
  INDEX idx_hash (query_hash),
  INDEX idx_expires (expires_at),
  INDEX idx_ambito (ambito)
);

-- 5) Feedback (+ ambito per analytics)
CREATE TABLE ai_feedback (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  message_id BIGINT UNSIGNED NOT NULL,
  user_id BIGINT UNSIGNED NOT NULL,
  rating ENUM('up', 'down') NOT NULL,
  comment TEXT NULL,
  ambito VARCHAR(64),
  template_id VARCHAR(64) NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (message_id) REFERENCES ai_messages(id) ON DELETE CASCADE,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_ambito (ambito),
  INDEX idx_template (template_id)
);

-- 6) Analytics (aggregati per dashboard)
CREATE TABLE ai_usage_analytics (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  date DATE NOT NULL,
  ambito VARCHAR(64),
  total_queries INT UNSIGNED DEFAULT 0,
  queries_with_llm INT UNSIGNED DEFAULT 0,
  avg_response_time_ms INT UNSIGNED DEFAULT 0,
  total_tokens_used INT UNSIGNED DEFAULT 0,
  estimated_cost_usd DECIMAL(10,4) DEFAULT 0.0000,
  positive_feedback_count INT UNSIGNED DEFAULT 0,
  negative_feedback_count INT UNSIGNED DEFAULT 0,
  UNIQUE KEY unique_date_ambito (date, ambito),
  INDEX idx_date (date),
  INDEX idx_ambito (ambito)
);
Blocco 20 bash
php artisan make:migration create_ai_assistant_tables_v2
php artisan migrate
Blocco 21 bash
php artisan make:model I40/AiConversation
php artisan make:model I40/AiMessage
php artisan make:model I40/AiKnowledgeTemplate
php artisan make:model I40/AiCache
Blocco 22 bash
php artisan make:seeder AiKnowledgeTemplatesSeeder
Blocco 23 php
public function run()
{
    $templates = [
        // AMBITO_PRODUZIONE
        [
            'template_id' => 'prod_daily',
            'ambito' => 'AMBITO_PRODUZIONE',
            'pattern' => 'produzione di (ieri|oggi)',
            'requires_llm' => false,
            'cache_ttl' => 3600,
            'template_config' => json_encode([
                'sql_query' => [
                    'query' => 'SELECT ...',
                    'timeout' => 5,
                ],
                'response_formatter' => [
                    'template' => 'Produzione di {periodo}: {total_quantity} pezzi.',
                ],
                'follow_up_suggestions' => [
                    'Mostrami il trend settimanale',
                    'Confronta con ieri',
                ],
            ]),
        ],
        // ... altri 20-30 template per ambito
    ];
    
    foreach ($templates as $template) {
        \DB::table('ai_knowledge_templates')->insert($template);
    }
}
Blocco 24 bash
php artisan make:service I40/AI/IntentRouterService
Blocco 25 bash
php artisan make:service I40/AI/KnowledgeBaseService
Blocco 26 bash
php artisan make:service I40/AI/LLMFallbackService
Blocco 27 bash
php artisan make:service I40/AI/AiAssistantService
Blocco 28 php
class AiAssistantService
{
    public function __construct(
        protected IntentRouterService $intentRouter,
        protected KnowledgeBaseService $knowledgeBase,
        protected LLMFallbackService $llmFallback,
    ) {}
    
    public function handleQuery(string $userQuery, Machine $machine, User $user): array
    {
        // 1) Intent Detection
        $intent = $this->intentRouter->detectAmbito($userQuery);
        
        // 2) Check Cache
        $cached = $this->checkCache($userQuery, $machine->id, $intent['ambito']);
        if ($cached) {
            return $cached;
        }
        
        // 3) Find Knowledge Base Template
        $template = $this->knowledgeBase->findTemplate(
            $intent['ambito'], 
            $userQuery, 
            $intent['extracted_entities']
        );
        
        // 4a) Se template trovato β†’ Esegui senza LLM
        if ($template && !$template['requires_llm']) {
            $response = $this->knowledgeBase->executeTemplate($template, $intent['extracted_entities'], $machine);
            $this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
            return $response;
        }
        
        // 4b) Altrimenti β†’ Usa LLM
        $context = $this->buildContext($machine, $intent);
        $response = $this->llmFallback->generateResponse(
            $intent['ambito'], 
            $userQuery, 
            $context, 
            $machine
        );
        
        $this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
        return $response;
    }
}
Blocco 29 php
Route::prefix('ai')->name('ai.')->middleware('auth')->group(function () {
    Route::post('/chat', [AiAssistantController::class, 'chat'])->name('chat');
    Route::get('/conversations', [AiAssistantController::class, 'conversations'])->name('conversations');
    Route::get('/conversations/{id}', [AiAssistantController::class, 'show'])->name('conversations.show');
    Route::post('/feedback', [AiAssistantController::class, 'feedback'])->name('feedback');
    
    // Analytics admin
    Route::get('/analytics', [AiAssistantController::class, 'analytics'])->name('analytics')
        ->middleware('role:admin');
});
Blocco 30 blade
<!-- resources/views/components/ai-chat-widget.blade.php -->

<div id="ai-chat-widget" class="ai-chat-widget minimized">
    <div class="chat-header" onclick="toggleChat()">
        <div>
            <span class="ai-avatar">πŸ€–</span>
            <strong>AI Assistant</strong>
            <small id="current-ambito"></small>
        </div>
        <i class="bi bi-chevron-up"></i>
    </div>
    
    <div class="chat-body">
        <div id="messages-container" class="messages-container">
            <!-- Suggestions iniziali -->
            <div class="empty-state">
                <h6>Cosa vuoi sapere?</h6>
                <div class="suggestions-grid">
                    <button onclick="sendMessage('Produzione di oggi')">πŸ“Š Produzione oggi</button>
                    <button onclick="sendMessage('Chi Γ¨ l\'operatore migliore?')">πŸ‘€ Miglior operatore</button>
                    <button onclick="sendMessage('Confronta con ieri')">πŸ“ˆ Confronto ieri</button>
                    <button onclick="sendMessage('Riusciremo a finire in tempo?')">⏱️ Deadline</button>
                </div>
            </div>
        </div>
        
        <div class="chat-input">
            <input type="text" id="user-input" placeholder="Chiedi qualcosa..." onkeypress="handleEnter(event)">
            <button onclick="sendMessage()"><i class="bi bi-send"></i></button>
        </div>
    </div>
</div>

<script>
let conversationId = null;
const machineId = {{ $machine->id }};

async function sendMessage(text = null) {
    const message = text || document.getElementById('user-input').value;
    if (!message.trim()) return;
    
    // Mostra messaggio utente
    appendMessage('user', message);
    document.getElementById('user-input').value = '';
    
    // Mostra loader
    showLoader();
    
    try {
        const response = await fetch('/admin/i40/ai/chat', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-TOKEN': '{{ csrf_token() }}',
            },
            body: JSON.stringify({
                conversation_id: conversationId,
                machine_id: machineId,
                message: message,
            }),
        });
        
        const data = await response.json();
        conversationId = data.conversation_id;
        
        hideLoader();
        appendMessage('assistant', data.content, data.suggestions, data.chart_config);
        
        // Aggiorna badge ambito
        if (data.metadata?.ambito) {
            document.getElementById('current-ambito').textContent = formatAmbito(data.metadata.ambito);
        }
        
    } catch (error) {
        hideLoader();
        appendMessage('assistant', '❌ Errore di connessione. Riprova.');
    }
}

function appendMessage(role, content, suggestions = null, chartConfig = null) {
    const container = document.getElementById('messages-container');
    const msgDiv = document.createElement('div');
    msgDiv.className = `message ${role}`;
    
    msgDiv.innerHTML = `
        <div class="message-avatar">${role === 'user' ? 'πŸ‘€' : 'πŸ€–'}</div>
        <div class="message-content">
            <div class="message-text">${content.replace(/\n/g, '<br>')}</div>
            ${suggestions ? `
                <div class="follow-up-suggestions">
                    ${suggestions.map(s => `<button onclick="sendMessage('${s}')">${s}</button>`).join('')}
                </div>
            ` : ''}
            ${chartConfig ? `<button onclick="openChart(${JSON.stringify(chartConfig)})"><i class="bi bi-bar-chart"></i> Visualizza Grafico</button>` : ''}
        </div>
    `;
    
    container.appendChild(msgDiv);
    container.scrollTop = container.scrollHeight;
}

function formatAmbito(ambito) {
    const map = {
        'AMBITO_PRODUZIONE': 'πŸ“Š Produzione',
        'AMBITO_DIAGNOSTICA': 'πŸ”§ Diagnostica',
        'AMBITO_OPERATORI': 'πŸ‘₯ Operatori',
        'AMBITO_PIANIFICAZIONE': 'πŸ“… Pianificazione',
        'AMBITO_CONFIGURAZIONE': 'βš™οΈ Configurazione',
        'AMBITO_COMPARAZIONE': 'βš–οΈ Confronti',
        'AMBITO_OTTIMIZZAZIONE': 'πŸš€ Ottimizzazione',
    };
    return map[ambito] || '';
}
</script>
Blocco 31 blade
@include('components.ai-chat-widget', ['machine' => $selectedMachine])
Blocco 32 php
// app/Http/Controllers/Admin/I40/AiAssistantController.php

public function analytics(Request $request)
{
    $from = $request->input('from', now()->subDays(30));
    $to = $request->input('to', now());
    
    // Metriche globali
    $totalQueries = AiMessage::whereBetween('created_at', [$from, $to])->count();
    $queriesWithLLM = AiMessage::whereBetween('created_at', [$from, $to])
        ->whereJsonContains('metadata->used_llm', true)->count();
    $llmUsagePercent = $totalQueries > 0 ? ($queriesWithLLM / $totalQueries * 100) : 0;
    
    // Breakdown per ambito
    $byAmbito = \DB::table('ai_usage_analytics')
        ->whereBetween('date', [$from, $to])
        ->select('ambito', 
            \DB::raw('SUM(total_queries) as total'),
            \DB::raw('SUM(queries_with_llm) as with_llm'),
            \DB::raw('AVG(avg_response_time_ms) as avg_time'),
            \DB::raw('SUM(estimated_cost_usd) as cost'))
        ->groupBy('ambito')
        ->get();
    
    // Top templates
    $topTemplates = AiKnowledgeTemplate::orderBy('usage_count', 'desc')->limit(10)->get();
    
    // Satisfaction rate
    $positiveFeedback = AiFeedback::where('rating', 'up')->whereBetween('created_at', [$from, $to])->count();
    $negativeFeedback = AiFeedback::where('rating', 'down')->whereBetween('created_at', [$from, $to])->count();
    $satisfactionRate = ($positiveFeedback + $negativeFeedback) > 0 
        ? ($positiveFeedback / ($positiveFeedback + $negativeFeedback) * 100) 
        : 0;
    
    return view('admin.i40.ai.analytics', compact(
        'totalQueries', 
        'llmUsagePercent', 
        'byAmbito', 
        'topTemplates', 
        'satisfactionRate'
    ));
}