AI Assistant per Analisi Produzione - Roadmap

AI Assistant per Analisi Produzione - Roadmap

Data Creazione: 20 Ottobre 2025 Versione: 1.0 Modulo: Industria 4.0 - AI Assistant

---

πŸ“‹ Indice

1. Vision e Obiettivi 2. Architettura Proposta 3. Tecnologie e Stack 4. Roadmap Implementazione 5. Fase 1: Foundation 6. Fase 2: Knowledge Base 7. Fase 3: AI Integration 8. Fase 4: UI/UX 9. Fase 5: Advanced Features 10. Esempi Query AI 11. Costi e Considerazioni

---

🎯 Vision e Obiettivi

Cosa Vogliamo Ottenere?

Un assistente AI conversazionale integrato nella vista "Analisi Lavorazioni" che permetta agli operatori di:

Domande Operative

  • "Qual Γ¨ stata la produzione media di questa settimana rispetto alla scorsa?"
  • "PerchΓ© ieri abbiamo prodotto meno del solito?"
  • "Quale operatore ha la resa migliore sul taglio laser?"
  • "Ci sono stati picchi di produzione anomali nell'ultimo mese?"
  • Analisi Predittive

  • "Riusciremo a completare l'ordine X123 entro venerdΓ¬?"
  • "Qual Γ¨ il tempo medio per tagliare 1000 pezzi di materiale Y?"
  • "Quanto tempo serve mediamente per un setup macchina?"
  • Insights Automatici

  • "Mostrami i trend di produzione per materiale"
  • "Quali sono i giorni della settimana piΓΉ produttivi?"
  • "Ci sono pattern ricorrenti negli errori di taglio?"
  • Ottimizzazione

  • "Come posso aumentare la produttivitΓ  del 10%?"
  • "Quali sono i colli di bottiglia nella produzione?"
  • "Suggeriscimi un piano di lavoro ottimale per domani"
  • Vantaggi Competitivi

    βœ… Democratizzazione Dati: Anche chi non sa SQL puΓ² analizzare la produzione βœ… Decisioni Data-Driven: Risposte immediate basate su dati reali βœ… ProattivitΓ : L'AI segnala anomalie e opportunitΓ  βœ… Continuous Learning: Migliora col tempo imparando dai pattern aziendali

    ---

    πŸ—οΈ Architettura Proposta

    Componenti Sistema

    ``

    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                    FRONTEND (Vue/React Component)                β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚  Chat UI:                                                 β”‚  β”‚
    β”‚  β”‚  β€’ Input utente (textarea + voice)                        β”‚  β”‚
    β”‚  β”‚  β€’ Storico conversazione                                  β”‚  β”‚
    β”‚  β”‚  β€’ Suggerimenti query                                     β”‚  β”‚
    β”‚  β”‚  β€’ Visualizzazione grafici inline                         β”‚  β”‚
    β”‚  β”‚  β€’ Export risposte (PDF/Excel)                            β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚ WebSocket / AJAX
                             ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                    BACKEND (Laravel API)                         β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚  AI Controller:                                           β”‚  β”‚
    β”‚  β”‚  β€’ POST /ai/chat (nuova query)                            β”‚  β”‚
    β”‚  β”‚  β€’ GET  /ai/history (conversazioni passate)               β”‚  β”‚
    β”‚  β”‚  β€’ POST /ai/feedback (thumbs up/down)                     β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β”‚                                                                  β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
    β”‚  β”‚  AI Service Layer:                                        β”‚  β”‚
    β”‚  β”‚  β€’ Query Understanding (NLP)                              β”‚  β”‚
    β”‚  β”‚  β€’ Context Builder (prepara dati per LLM)                 β”‚  β”‚
    β”‚  β”‚  β€’ Response Generator (chiama LLM API)                    β”‚  β”‚
    β”‚  β”‚  β€’ SQL Generator (converte linguaggio β†’ query)            β”‚  β”‚
    β”‚  β”‚  β€’ Cache Layer (evita chiamate duplicate)                 β”‚  β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          ↓                  ↓                  ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   LLM API    β”‚  β”‚  Vector DB   β”‚  β”‚  MySQL DB    β”‚
    β”‚  (OpenAI/    β”‚  β”‚  (Embeddings)β”‚  β”‚  (Raw Data)  β”‚
    β”‚   Anthropic) β”‚  β”‚  - Pinecone  β”‚  β”‚  - machine_  β”‚
    β”‚              β”‚  β”‚  - Weaviate  β”‚  β”‚    operationsβ”‚
    β”‚              β”‚  β”‚  - Qdrant    β”‚  β”‚    _log      β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    `
    </p><p><h3>Flusso Richiesta AI</h3></p><p></code>`<code>
    1. UTENTE: &quot;Qual Γ¨ stata la produzione di ieri?&quot;
               ↓
    2. FRONTEND: Invia POST /ai/chat con { query: &quot;...&quot;, machine_id: 5, context: {...} }
               ↓
    3. BACKEND (AI Service):
       a) Query Understanding
          β†’ Estrae intent: &quot;produzione_totale&quot;
          β†’ Estrae entitΓ : periodo=&quot;ieri&quot;, metrica=&quot;pezzi&quot;
          β†’ Estrae filtri: machine_id=5
       
       b) Context Builder
          β†’ Recupera profilo macchina (colonne, aggregatori)
          β†’ Recupera ultimi 30 giorni di dati (per contesto)
          β†’ Prepara prompt per LLM:
             &quot;Sei un assistente AI per analisi produzione.
              Database schema: [columns...]
              Ultimi dati: [sample rows...]
              Query utente: &#039;Qual Γ¨ stata la produzione di ieri?&#039;
              Genera SQL query e spiega il risultato.&quot;
       
       c) LLM API Call
          β†’ Invia prompt a OpenAI GPT-4 / Claude
          β†’ Riceve risposta:
             {
               &quot;sql&quot;: &quot;SELECT SUM(JSON_EXTRACT(...)) FROM ... WHERE DATE(timestamp)=CURDATE()-1&quot;,
               &quot;explanation&quot;: &quot;Ho calcolato la somma dei pezzi prodotti ieri...&quot;,
               &quot;confidence&quot;: 0.95
             }
       
       d) SQL Execution
          β†’ Valida SQL (whitelist operazioni, prevent injection)
          β†’ Esegue query su DB
          β†’ Ottiene risultato: 1500 pezzi
       
       e) Response Generation
          β†’ LLM formatta risposta user-friendly:
             &quot;Ieri hai prodotto 1.500 pezzi, in linea con la media settimanale di 1.450.&quot;
          β†’ Genera grafico suggerito (se rilevante)
          β†’ Aggiunge follow-up suggestions
               ↓
    4. FRONTEND: Mostra risposta + grafico + suggestions
    `

    ---

    πŸ› οΈ Tecnologie e Stack

    Opzione A: OpenAI GPT-4 (Consigliata per MVP)

    Pro:

  • API matura e stabile
  • Ottimo per text-to-SQL
  • Function calling nativo
  • Documentazione eccellente
  • Pay-per-use (no infrastruttura)
  • Contro:

  • Costi variabili (~ $0.01-0.03 per query)
  • Dipendenza esterna (latency 1-3s)
  • Privacy: dati inviati a OpenAI (mitigabile con Azure OpenAI)
  • Stack: `php // Laravel composer require openai-php/laravel

    // Vector DB (opzionale per ora) // composer require pinecone/pinecone-php `

    Opzione B: Anthropic Claude (Alternativa)

    Pro:

  • Contesto piΓΉ lungo (200K tokens vs 128K GPT-4)
  • Ottimo per analisi dati complesse
  • Prezzi competitivi
  • Contro:

  • Meno integrazioni disponibili
  • Community piΓΉ piccola
  • Opzione C: Open Source (LLaMA, Mistral) + Self-Hosted

    Pro:

  • Zero costi API
  • Privacy totale
  • Personalizzabile
  • Contro:

  • Richiede GPU (costi infrastruttura)
  • Setup complesso
  • QualitΓ  inferiore (per ora)
  • Manutenzione continua
  • Decisione: Partire con OpenAI GPT-4 per MVP, poi valutare self-hosted se volumi alti.

    ---

    πŸ—ΊοΈ Roadmap Implementazione

    Timeline Stimata: 6-8 Settimane

    ` SETTIMANA 1-2: FASE 1 - Foundation SETTIMANA 3-4: FASE 2 - Knowledge Base SETTIMANA 5-6: FASE 3 - AI Integration SETTIMANA 7: FASE 4 - UI/UX SETTIMANA 8: FASE 5 - Testing & Refinement `

    ---

    πŸ“¦ Fase 1: Foundation

    Durata: 1-2 settimane Obiettivo: Preparare infrastruttura base per AI assistant

    1.1 Database Schema Extensions

    Tabella: ai_conversations

    `sql CREATE TABLE ai_conversations ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, machine_id BIGINT UNSIGNED NULL, -- Contesto macchina (opzionale) title VARCHAR(255), -- Auto-generato dal primo messaggio 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), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); `

    Tabella: ai_messages

    `sql 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, -- { sql: "...", confidence: 0.95, execution_time: 1.2, chart_config: {...} } tokens_used INT UNSIGNED, -- Per tracking costi created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE ); `

    Tabella: ai_feedback

    `sql 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, 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 ); `

    Tabella: ai_cache (Opzionale per performance)

    `sql CREATE TABLE ai_cache ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, query_hash VARCHAR(64) UNIQUE NOT NULL, -- MD5/SHA256 della query normalizzata machine_id BIGINT UNSIGNED NULL, date_range VARCHAR(50), -- "2025-10-01_2025-10-20" 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) ); `

    1.2 Laravel Setup

    Migration

    `bash php artisan make:migration create_ai_assistant_tables `

    `php // database/migrations/*_create_ai_assistant_tables.php public function up() { Schema::create('ai_conversations', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('machine_id')->nullable()->constrained('machines_i40')->onDelete('set null'); $table->string('title')->nullable(); $table->timestamps(); $table->index('user_id'); $table->index('machine_id'); }); Schema::create('ai_messages', function (Blueprint $table) { $table->id(); $table->foreignId('conversation_id')->constrained('ai_conversations')->onDelete('cascade'); $table->enum('role', ['user', 'assistant', 'system']); $table->text('content'); $table->json('metadata')->nullable(); $table->unsignedInteger('tokens_used')->nullable(); $table->timestamp('created_at')->useCurrent(); $table->index('conversation_id'); }); Schema::create('ai_feedback', function (Blueprint $table) { $table->id(); $table->foreignId('message_id')->constrained('ai_messages')->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->enum('rating', ['up', 'down']); $table->text('comment')->nullable(); $table->timestamp('created_at')->useCurrent(); }); Schema::create('ai_cache', function (Blueprint $table) { $table->id(); $table->string('query_hash', 64)->unique(); $table->foreignId('machine_id')->nullable()->constrained('machines_i40')->onDelete('cascade'); $table->string('date_range', 50)->nullable(); $table->json('response_data'); $table->unsignedInteger('hits')->default(0); $table->timestamp('created_at')->useCurrent(); $table->timestamp('expires_at')->nullable(); $table->index('query_hash'); $table->index('expires_at'); }); } `

    Models

    `bash php artisan make:model I40/AiConversation php artisan make:model I40/AiMessage php artisan make:model I40/AiFeedback `

    `php // app/Models/I40/AiConversation.php class AiConversation extends Model { protected $fillable = ['user_id', 'machine_id', 'title']; public function messages() { return $this->hasMany(AiMessage::class, 'conversation_id'); } public function user() { return $this->belongsTo(User::class); } public function machine() { return $this->belongsTo(Machine::class); } // Auto-genera titolo dal primo messaggio public function generateTitle() { $firstMessage = $this->messages()->where('role', 'user')->first(); if ($firstMessage) { $this->title = Str::limit($firstMessage->content, 50); $this->save(); } } } `

    `php // app/Models/I40/AiMessage.php class AiMessage extends Model { protected $fillable = ['conversation_id', 'role', 'content', 'metadata', 'tokens_used']; protected $casts = ['metadata' => 'array']; public $timestamps = false; public function conversation() { return $this->belongsTo(AiConversation::class, 'conversation_id'); } public function feedback() { return $this->hasOne(AiFeedback::class, 'message_id'); } } `

    1.3 Configuration

    `bash

    .env aggiungi

    OPENAI_API_KEY=sk-proj-... OPENAI_MODEL=gpt-4-turbo-preview OPENAI_MAX_TOKENS=1000 AI_CACHE_TTL=3600 # 1 ora AI_ENABLED=true `

    `php // config/ai.php return [ 'enabled' => env('AI_ENABLED', true), 'openai' => [ 'api_key' => env('OPENAI_API_KEY'), 'model' => env('OPENAI_MODEL', 'gpt-4-turbo-preview'), 'max_tokens' => env('OPENAI_MAX_TOKENS', 1000), 'temperature' => 0.3, // Basso per risposte deterministiche ], 'cache' => [ 'enabled' => true, 'ttl' => env('AI_CACHE_TTL', 3600), // secondi ], 'limits' => [ 'max_messages_per_conversation' => 50, 'max_conversations_per_user' => 100, 'max_query_length' => 500, // caratteri ], ]; `

    ---

    🧠 Fase 2: Knowledge Base

    Durata: 1-2 settimane Obiettivo: Preparare dati e contesto per LLM

    2.1 Database Schema Extraction

    Service: DatabaseSchemaService

    `php // app/Services/I40/DatabaseSchemaService.php namespace App\Services\I40;

    class DatabaseSchemaService { / * Estrae schema leggibile per LLM dal profilo macchina */ public function getSchemaForMachine(Machine $machine): array { $profile = $machine->getActiveProfile(); if (!$profile) return []; $columns = $profile->columns ?? []; $columnNames = $profile->column_names ?? []; $schema = [ 'table' => 'machine_operations_log', 'description' => "Log operazioni macchina: {$machine->name}", 'columns' => [] ]; // Colonne standard $schema['columns'][] = [ 'name' => 'id', 'type' => 'integer', 'description' => 'ID univoco operazione' ]; $schema['columns'][] = [ 'name' => 'timestamp', 'type' => 'datetime', 'description' => 'Data e ora operazione', 'is_primary_timestamp' => true ]; // Colonne dinamiche da profilo foreach ($columns as $index => $col) { $csvName = $col['csv_name'] ?? $col['name']; $label = $columnNames[$index] ?? $col['label'] ?? $csvName; $schema['columns'][] = [ 'name' => $csvName, 'type' => $col['type'] ?? 'string', 'description' => $label, 'is_filterable' => $col['is_filterable'] ?? false, 'is_aggregatable' => $col['is_aggregatable'] ?? false, 'aggregators' => $col['aggregators'] ?? [], 'sample_values' => $this->getSampleValues($machine->id, $csvName, 3) ]; } return $schema; } / * Ottiene valori di esempio per una colonna */ protected function getSampleValues(int $machineId, string $columnName, int $limit = 3): array { return \DB::table('machine_operations_log') ->where('machine_id', $machineId) ->selectRaw("DISTINCT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{$columnName}')) as val") ->whereNotNull(\DB::raw("JSON_EXTRACT(raw_data, '$.{$columnName}')")) ->limit($limit) ->pluck('val') ->toArray(); } / * Genera descrizione testuale dello schema per prompt LLM */ public function generateSchemaPrompt(Machine $machine): string { $schema = $this->getSchemaForMachine($machine); $prompt = "DATABASE SCHEMA PER MACCHINA: {$machine->name}\n\n"; $prompt .= "Tabella: {$schema['table']}\n"; $prompt .= "Descrizione: {$schema['description']}\n\n"; $prompt .= "COLONNE DISPONIBILI:\n"; foreach ($schema['columns'] as $col) { $prompt .= "- {$col['name']} ({$col['type']}): {$col['description']}\n"; if (!empty($col['aggregators'])) { $prompt .= " β†’ Aggregabile: " . implode(', ', $col['aggregators']) . "\n"; } if (!empty($col['sample_values'])) { $prompt .= " β†’ Esempi: " . implode(', ', array_slice($col['sample_values'], 0, 3)) . "\n"; } } return $prompt; } } `

    2.2 Context Window Builder

    Service: AiContextService

    `php // app/Services/I40/AiContextService.php namespace App\Services\I40;

    class AiContextService { protected $schemaService; public function __construct(DatabaseSchemaService $schemaService) { $this->schemaService = $schemaService; } / * Prepara contesto completo per LLM */ public function buildContext(array $params): array { $machineId = $params['machine_id'] ?? null; $dateFrom = $params['date_from'] ?? now()->subMonth(); $dateTo = $params['date_to'] ?? now(); $machine = Machine::find($machineId); if (!$machine) { throw new \Exception('Macchina non trovata'); } return [ 'schema' => $this->schemaService->generateSchemaPrompt($machine), 'statistics' => $this->getRecentStatistics($machine, $dateFrom, $dateTo), 'sample_data' => $this->getSampleRecords($machine, 5), 'business_rules' => $this->getBusinessRules($machine), ]; } / * Statistiche recenti per dare contesto temporale */ protected function getRecentStatistics(Machine $machine, $dateFrom, $dateTo): string { $stats = MachineOperationLog::query() ->where('machine_id', $machine->id) ->whereBetween('timestamp', [$dateFrom, $dateTo]) ->selectRaw(' COUNT(*) as total_operations, COUNT(DISTINCT DATE(timestamp)) as active_days, MIN(timestamp) as first_operation, MAX(timestamp) as last_operation ') ->first(); return sprintf( "STATISTICHE PERIODO %s - %s:\n" . "- Operazioni totali: %d\n" . "- Giorni attivi: %d\n" . "- Prima operazione: %s\n" . "- Ultima operazione: %s", $dateFrom->format('d/m/Y'), $dateTo->format('d/m/Y'), $stats->total_operations, $stats->active_days, $stats->first_operation, $stats->last_operation ); } / * Sample records per dare esempi concreti */ protected function getSampleRecords(Machine $machine, int $limit = 5): string { $records = MachineOperationLog::where('machine_id', $machine->id) ->orderBy('timestamp', 'desc') ->limit($limit) ->get(); if ($records->isEmpty()) { return "Nessun dato disponibile."; } $sample = "ESEMPI OPERAZIONI RECENTI (ultimi {$limit} record):\n"; foreach ($records as $i => $record) { $sample .= sprintf( "%d. [%s] %s\n", $i + 1, $record->timestamp->format('Y-m-d H:i'), json_encode(array_slice($record->raw_data, 0, 5), JSON_UNESCAPED_UNICODE) ); } return $sample; } / * Regole di business specifiche azienda */ protected function getBusinessRules(Machine $machine): string { // Qui puoi aggiungere regole custom basate sul tipo macchina $rules = [ "REGOLE DI BUSINESS:", "- L'orario lavorativo standard Γ¨ 07:00-19:00", "- La produzione media attesa Γ¨ definita nel profilo macchina", "- Un 'avviamento' Γ¨ un setup macchina prima di una lavorazione", ]; // Aggiungi regole specifiche dalla configurazione profilo se disponibili $profile = $machine->getActiveProfile(); if ($profile && isset($profile->business_rules)) { $rules = array_merge($rules, $profile->business_rules); } return implode("\n", $rules); } } `

    2.3 Query Understanding (Intent Detection)

    Service: QueryUnderstandingService

    `php // app/Services/I40/QueryUnderstandingService.php namespace App\Services\I40;

    class QueryUnderstandingService { / * Analizza query utente ed estrae intent/entità */ public function analyze(string $userQuery): array { // Step 1: Normalizza query $normalized = $this->normalize($userQuery); // Step 2: Detect intent $intent = $this->detectIntent($normalized); // Step 3: Extract entities $entities = $this->extractEntities($normalized); return [ 'original' => $userQuery, 'normalized' => $normalized, 'intent' => $intent, 'entities' => $entities, 'confidence' => $this->calculateConfidence($intent, $entities), ]; } / * Normalizza query (lowercase, rimuovi punteggiatura, etc.) */ protected function normalize(string $query): string { $query = strtolower($query); $query = preg_replace('/[^a-z0-9\sàèéìòù]/', '', $query); return trim($query); } / * Rileva intent principale della query */ protected function detectIntent(string $query): string { $patterns = [ 'production_total' => ['produzione', 'prodotto', 'pezzi', 'quantità', 'quanti'], 'production_average' => ['media', 'mediamente', 'in media'], 'production_comparison' => ['confronto', 'rispetto', 'differenza', 'più', 'meno'], 'trend_analysis' => ['trend', 'andamento', 'evoluzione', 'grafico'], 'operator_performance' => ['operatore', 'chi ha', 'migliore', 'peggiore'], 'time_analysis' => ['tempo', 'durata', 'velocità', 'quanto ci vuole'], 'anomaly_detection' => ['anomalia', 'problema', 'strano', 'perché'], 'prediction' => ['previsione', 'riusciremo', 'ce la facciamo', 'entro quando'], 'optimization' => ['ottimizza', 'migliora', 'aumenta', 'suggerisci'], ]; foreach ($patterns as $intent => $keywords) { foreach ($keywords as $keyword) { if (str_contains($query, $keyword)) { return $intent; } } } return 'general_question'; } / * Estrae entità dalla query (date, numeri, nomi, etc.) */ protected function extractEntities(string $query): array { $entities = []; // Date temporali if (preg_match('/ieri/', $query)) { $entities['period'] = 'yesterday'; $entities['date_from'] = now()->subDay()->startOfDay(); $entities['date_to'] = now()->subDay()->endOfDay(); } elseif (preg_match('/oggi/', $query)) { $entities['period'] = 'today'; $entities['date_from'] = now()->startOfDay(); $entities['date_to'] = now()->endOfDay(); } elseif (preg_match('/questa settimana/', $query)) { $entities['period'] = 'this_week'; $entities['date_from'] = now()->startOfWeek(); $entities['date_to'] = now()->endOfWeek(); } elseif (preg_match('/ultimo mese|mese scorso/', $query)) { $entities['period'] = 'last_month'; $entities['date_from'] = now()->subMonth()->startOfMonth(); $entities['date_to'] = now()->subMonth()->endOfMonth(); } // Numeri if (preg_match('/\d+/', $query, $matches)) { $entities['numbers'] = array_map('intval', $matches); } // Metriche $metrics = ['pezzi', 'tempo', 'durata', 'operazioni']; foreach ($metrics as $metric) { if (str_contains($query, $metric)) { $entities['metric'] = $metric; break; } } return $entities; } / * Calcola confidence score (0-1) */ protected function calculateConfidence(string $intent, array $entities): float { $score = 0.5; // Base if ($intent !== 'general_question') $score += 0.2; if (!empty($entities['period'])) $score += 0.15; if (!empty($entities['metric'])) $score += 0.15; return min($score, 1.0); } } `

    ---

    πŸ€– Fase 3: AI Integration

    Durata: 2 settimane Obiettivo: Integrare OpenAI GPT-4 e gestire conversazioni

    3.1 OpenAI Service

    Installation

    `bash composer require openai-php/laravel php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider" `

    Service: OpenAiService

    `php // app/Services/I40/OpenAiService.php namespace App\Services\I40;

    use OpenAI\Laravel\Facades\OpenAI;

    class OpenAiService { / * Genera risposta AI per query utente */ public function generateResponse(array $context, string $userQuery): array { $systemPrompt = $this->buildSystemPrompt($context); $messages = $this->buildMessages($systemPrompt, $userQuery); try { $response = OpenAI::chat()->create([ 'model' => config('ai.openai.model'), 'messages' => $messages, 'temperature' => config('ai.openai.temperature'), 'max_tokens' => config('ai.openai.max_tokens'), 'functions' => $this->getFunctionDefinitions(), 'function_call' => 'auto', ]); $choice = $response->choices[0]; $tokensUsed = $response->usage->totalTokens; // Se LLM vuole chiamare una funzione (es. generare SQL) if (isset($choice->message->functionCall)) { return $this->handleFunctionCall($choice->message->functionCall, $tokensUsed); } // Altrimenti risposta diretta return [ 'content' => $choice->message->content, 'tokens_used' => $tokensUsed, 'finish_reason' => $choice->finishReason, ]; } catch (\Exception $e) { \Log::error('OpenAI API Error: ' . $e->getMessage()); throw new \Exception('Errore nel servizio AI. Riprova piΓΉ tardi.'); } } / * Costruisce prompt di sistema (contesto + istruzioni) */ protected function buildSystemPrompt(array $context): string { return <<<PROMPT Sei un assistente AI specializzato nell'analisi di dati di produzione industriale. Il tuo compito Γ¨ aiutare gli operatori a comprendere e ottimizzare le prestazioni delle macchine.

    COMPETENZE:

  • Analisi dati temporali (trend, medie, confronti)
  • Generazione query SQL per estrarre informazioni
  • Identificazione anomalie e pattern
  • Suggerimenti per ottimizzazione produzione
  • CONTESTO DATABASE: {$context['schema']}

    {$context['statistics']}

    {$context['sample_data']}

    {$context['business_rules']}

    ISTRUZIONI: 1. Rispondi sempre in italiano professionale ma accessibile 2. Se serve generare SQL, usa la function "execute_sql_query" 3. Formatta numeri con separatore migliaia (es. 1.500) 4. Suggerisci visualizzazioni (grafici) quando appropriato 5. Se non sei sicuro, ammettilo e chiedi chiarimenti 6. Fornisci sempre contesto (es. "rispetto alla media settimanale di...") PROMPT; } / * Costruisce array messaggi per API */ protected function buildMessages(string $systemPrompt, string $userQuery): array { return [ ['role' => 'system', 'content' => $systemPrompt], ['role' => 'user', 'content' => $userQuery], ]; } / * Definisce funzioni che LLM puΓ² chiamare (Function Calling) */ protected function getFunctionDefinitions(): array { return [ [ 'name' => 'execute_sql_query', 'description' => 'Esegue una query SQL sul database produzione e restituisce i risultati', 'parameters' => [ 'type' => 'object', 'properties' => [ 'sql' => [ 'type' => 'string', 'description' => 'Query SQL SELECT da eseguire (solo SELECT, no INSERT/UPDATE/DELETE)', ], 'explanation' => [ 'type' => 'string', 'description' => 'Spiegazione di cosa fa la query, per l\'utente', ], ], 'required' => ['sql', 'explanation'], ], ], [ 'name' => 'generate_chart', 'description' => 'Suggerisce un grafico per visualizzare i dati', 'parameters' => [ 'type' => 'object', 'properties' => [ 'chart_type' => [ 'type' => 'string', 'enum' => ['line', 'bar', 'pie'], 'description' => 'Tipo di grafico', ], 'x_field' => [ 'type' => 'string', 'description' => 'Campo per asse X', ], 'y_metric' => [ 'type' => 'string', 'description' => 'Metrica per asse Y', ], ], 'required' => ['chart_type', 'x_field', 'y_metric'], ], ], ]; } / * Gestisce chiamata a funzione da parte di LLM */ protected function handleFunctionCall(object $functionCall, int $tokensUsed): array { $functionName = $functionCall->name; $arguments = json_decode($functionCall->arguments, true); if ($functionName === 'execute_sql_query') { $sql = $arguments['sql']; $explanation = $arguments['explanation']; // Valida e esegui SQL $results = $this->executeSafeSql($sql); return [ 'content' => $this->formatSqlResults($results, $explanation), 'tokens_used' => $tokensUsed, 'metadata' => [ 'sql' => $sql, 'explanation' => $explanation, 'result_count' => count($results), ], ]; } if ($functionName === 'generate_chart') { return [ 'content' => "Grafico suggerito generato.", 'tokens_used' => $tokensUsed, 'metadata' => [ 'chart_config' => $arguments, ], ]; } throw new \Exception('Funzione non supportata: ' . $functionName); } / * Esegue SQL in modo sicuro (whitelist operazioni, prevent injection) */ protected function executeSafeSql(string $sql): array { // Validazione: solo SELECT if (!preg_match('/^SELECT\s+/i', trim($sql))) { throw new \Exception('Solo query SELECT sono permesse'); } // Validazione: no operazioni pericolose $forbidden = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE']; foreach ($forbidden as $keyword) { if (stripos($sql, $keyword) !== false) { throw new \Exception('Query non permessa: contiene ' . $keyword); } } // Esegui con timeout try { $results = \DB::select($sql); return json_decode(json_encode($results), true); // Convert to array } catch (\Exception $e) { \Log::error('SQL Execution Error: ' . $e->getMessage()); throw new \Exception('Errore esecuzione query: ' . $e->getMessage()); } } / * Formatta risultati SQL per risposta user-friendly */ protected function formatSqlResults(array $results, string $explanation): string { if (empty($results)) { return "Non ho trovato dati per questa query. " . $explanation; } $response = $explanation . "\n\n"; // Se singolo valore scalare (es. COUNT, SUM) if (count($results) === 1 && count($results[0]) === 1) { $value = array_values($results[0])[0]; $response .= "Risultato: " . number_format($value, 0, ',', '.') . "\n"; return $response; } // Se lista valori, mostra tabella semplice $response .= "Risultati trovati: " . count($results) . "\n\n"; // Mostra primi 10 risultati $displayResults = array_slice($results, 0, 10); foreach ($displayResults as $i => $row) { $response .= ($i + 1) . ". "; $response .= implode(' | ', array_map(function($k, $v) { return "$k: $v"; }, array_keys($row), $row)); $response .= "\n"; } if (count($results) > 10) { $response .= "\n... e altri " . (count($results) - 10) . " risultati.\n"; } return $response; } } `

    3.2 AI Chat Controller

    `php // app/Http/Controllers/Admin/I40/AiChatController.php namespace App\Http\Controllers\Admin\I40;

    use App\Http\Controllers\Controller; use App\Services\I40\{ AiContextService, QueryUnderstandingService, OpenAiService }; use App\Models\I40\{AiConversation, AiMessage}; use Illuminate\Http\Request;

    class AiChatController extends Controller { protected $contextService; protected $queryService; protected $openAiService; public function __construct( AiContextService $contextService, QueryUnderstandingService $queryService, OpenAiService $openAiService ) { $this->contextService = $contextService; $this->queryService = $queryService; $this->openAiService = $openAiService; } / * POST /ai/chat * Nuova query utente */ public function chat(Request $request) { $validated = $request->validate([ 'conversation_id' => 'nullable|exists:ai_conversations,id', 'machine_id' => 'required|exists:machines_i40,id', 'message' => 'required|string|max:500', 'date_from' => 'nullable|date', 'date_to' => 'nullable|date', ]); // 1) Ottieni o crea conversazione $conversation = $this->getOrCreateConversation( $validated['conversation_id'] ?? null, $validated['machine_id'], auth()->id() ); // 2) Salva messaggio utente $userMessage = $conversation->messages()->create([ 'role' => 'user', 'content' => $validated['message'], ]); try { // 3) Analizza query $analysis = $this->queryService->analyze($validated['message']); // 4) Prepara contesto $context = $this->contextService->buildContext([ 'machine_id' => $validated['machine_id'], 'date_from' => $validated['date_from'] ?? now()->subMonth(), 'date_to' => $validated['date_to'] ?? now(), ]); // 5) Genera risposta AI $aiResponse = $this->openAiService->generateResponse( $context, $validated['message'] ); // 6) Salva risposta AI $assistantMessage = $conversation->messages()->create([ 'role' => 'assistant', 'content' => $aiResponse['content'], 'tokens_used' => $aiResponse['tokens_used'], 'metadata' => $aiResponse['metadata'] ?? null, ]); // 7) Auto-genera titolo conversazione se prima query if ($conversation->messages()->count() === 2) { $conversation->generateTitle(); } return response()->json([ 'success' => true, 'conversation_id' => $conversation->id, 'message' => [ 'id' => $assistantMessage->id, 'role' => 'assistant', 'content' => $assistantMessage->content, 'metadata' => $assistantMessage->metadata, 'created_at' => $assistantMessage->created_at, ], 'analysis' => $analysis, // Per debug ]); } catch (\Exception $e) { \Log::error('AI Chat Error: ' . $e->getMessage()); // Risposta fallback $errorMessage = $conversation->messages()->create([ 'role' => 'assistant', 'content' => 'Mi dispiace, ho avuto un problema nel processare la tua richiesta. Potresti riformulare la domanda?', 'metadata' => ['error' => $e->getMessage()], ]); return response()->json([ 'success' => false, 'error' => $e->getMessage(), 'conversation_id' => $conversation->id, 'message' => [ 'id' => $errorMessage->id, 'role' => 'assistant', 'content' => $errorMessage->content, 'created_at' => $errorMessage->created_at, ], ], 500); } } / * GET /ai/conversations * Lista conversazioni utente */ public function conversations(Request $request) { $conversations = AiConversation::where('user_id', auth()->id()) ->with(['messages' => function($q) { $q->latest()->limit(1); // Solo ultimo messaggio }]) ->latest() ->paginate(20); return response()->json($conversations); } / * GET /ai/conversations/{id} * Dettaglio conversazione (tutti i messaggi) */ public function show(AiConversation $conversation) { $this->authorize('view', $conversation); $conversation->load('messages'); return response()->json($conversation); } / * POST /ai/feedback * Thumbs up/down su risposta AI */ public function feedback(Request $request) { $validated = $request->validate([ 'message_id' => 'required|exists:ai_messages,id', 'rating' => 'required|in:up,down', 'comment' => 'nullable|string|max:500', ]); $feedback = AiFeedback::updateOrCreate( [ 'message_id' => $validated['message_id'], 'user_id' => auth()->id(), ], [ 'rating' => $validated['rating'], 'comment' => $validated['comment'] ?? null, ] ); return response()->json(['success' => true, 'feedback' => $feedback]); } / * Helper: Ottieni o crea conversazione */ protected function getOrCreateConversation($conversationId, $machineId, $userId) { if ($conversationId) { $conversation = AiConversation::find($conversationId); if ($conversation && $conversation->user_id === $userId) { return $conversation; } } return AiConversation::create([ 'user_id' => $userId, 'machine_id' => $machineId, ]); } } `

    3.3 Routes

    `php // routes/i40.php (aggiungi)

    Route::prefix('ai')->name('ai.')->group(function () { Route::post('/chat', [AiChatController::class, 'chat'])->name('chat'); Route::get('/conversations', [AiChatController::class, 'conversations'])->name('conversations'); Route::get('/conversations/{conversation}', [AiChatController::class, 'show'])->name('conversations.show'); Route::post('/feedback', [AiChatController::class, 'feedback'])->name('feedback'); })->middleware('role:admin|maintenance|super-admin'); `

    ---

    🎨 Fase 4: UI/UX

    Durata: 1 settimana Obiettivo: Interfaccia chat intuitiva nella vista Analisi

    4.1 Vue Component (Consigliato)

    Installazione Vue in Laravel (se non giΓ  presente)

    `

    bash
    npm install vue@next vue-loader@next @vitejs/plugin-vue
    </code>`<code></p><p><h4>Component: </code>AiChatWidget.vue<code></h4></p><p></code>`<code>vue
    &lt;!-- resources/js/components/I40/AiChatWidget.vue --&gt;
    &lt;template&gt;
      &lt;div class=&quot;ai-chat-widget&quot; :class=&quot;{ &#039;minimized&#039;: minimized }&quot;&gt;
        &lt;!-- Header --&gt;
        &lt;div class=&quot;chat-header&quot; @click=&quot;toggleMinimize&quot;&gt;
          &lt;div class=&quot;d-flex align-items-center&quot;&gt;
            &lt;div class=&quot;ai-avatar me-2&quot;&gt;πŸ€–&lt;/div&gt;
            &lt;div&gt;
              &lt;strong&gt;AI Assistant&lt;/strong&gt;
              &lt;small class=&quot;d-block text-muted&quot;&gt;Analisi Produzione&lt;/small&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          &lt;button class=&quot;btn btn-sm btn-link text-white&quot;&gt;
            &lt;i :class=&quot;minimized ? &#039;bi-chevron-up&#039; : &#039;bi-chevron-down&#039;&quot;&gt;&lt;/i&gt;
          &lt;/button&gt;
        &lt;/div&gt;
        
        &lt;!-- Chat Body --&gt;
        &lt;div v-show=&quot;!minimized&quot; class=&quot;chat-body&quot;&gt;
          &lt;!-- Messaggi --&gt;
          &lt;div class=&quot;messages-container&quot; ref=&quot;messagesContainer&quot;&gt;
            &lt;div v-if=&quot;messages.length === 0&quot; class=&quot;empty-state&quot;&gt;
              &lt;div class=&quot;ai-avatar-large mb-3&quot;&gt;πŸ€–&lt;/div&gt;
              &lt;h5&gt;Ciao! Sono il tuo assistente AI&lt;/h5&gt;
              &lt;p class=&quot;text-muted&quot;&gt;Chiedimi qualsiasi cosa sulla produzione di questa macchina.&lt;/p&gt;
              
              &lt;!-- Suggerimenti iniziali --&gt;
              &lt;div class=&quot;suggestions mt-3&quot;&gt;
                &lt;small class=&quot;text-muted d-block mb-2&quot;&gt;Prova a chiedere:&lt;/small&gt;
                &lt;button 
                  v-for=&quot;suggestion in initialSuggestions&quot; 
                  :key=&quot;suggestion&quot;
                  class=&quot;btn btn-sm btn-outline-primary mb-2 me-2&quot;
                  @click=&quot;sendMessage(suggestion)&quot;
                &gt;
                  {{ suggestion }}
                &lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;!-- Lista messaggi --&gt;
            &lt;div 
              v-for=&quot;message in messages&quot; 
              :key=&quot;message.id&quot;
              class=&quot;message&quot;
              :class=&quot;message.role&quot;
            &gt;
              &lt;div class=&quot;message-avatar&quot;&gt;
                {{ message.role === &#039;user&#039; ? &#039;πŸ‘€&#039; : &#039;πŸ€–&#039; }}
              &lt;/div&gt;
              &lt;div class=&quot;message-content&quot;&gt;
                &lt;div class=&quot;message-text&quot; v-html=&quot;formatMessage(message.content)&quot;&gt;&lt;/div&gt;
                
                &lt;!-- Metadata (SQL, Grafici, etc.) --&gt;
                &lt;div v-if=&quot;message.metadata&quot; class=&quot;message-metadata mt-2&quot;&gt;
                  &lt;!-- Mostra SQL eseguito --&gt;
                  &lt;div v-if=&quot;message.metadata.sql&quot; class=&quot;sql-block&quot;&gt;
                    &lt;button 
                      class=&quot;btn btn-xs btn-link&quot; 
                      @click=&quot;toggleSql(message.id)&quot;
                    &gt;
                      &lt;i class=&quot;bi bi-code-square&quot;&gt;&lt;/i&gt; Mostra Query SQL
                    &lt;/button&gt;
                    &lt;pre v-show=&quot;expandedSql[message.id]&quot; class=&quot;mt-2&quot;&gt;{{ message.metadata.sql }}&lt;/pre&gt;
                  &lt;/div&gt;
                  
                  &lt;!-- Suggerisci grafico --&gt;
                  &lt;div v-if=&quot;message.metadata.chart_config&quot; class=&quot;chart-suggestion&quot;&gt;
                    &lt;button 
                      class=&quot;btn btn-sm btn-primary&quot;
                      @click=&quot;openChart(message.metadata.chart_config)&quot;
                    &gt;
                      &lt;i class=&quot;bi bi-bar-chart&quot;&gt;&lt;/i&gt; Visualizza Grafico
                    &lt;/button&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
                
                &lt;!-- Feedback --&gt;
                &lt;div v-if=&quot;message.role === &#039;assistant&#039;&quot; class=&quot;message-feedback mt-2&quot;&gt;
                  &lt;button 
                    class=&quot;btn btn-xs&quot; 
                    :class=&quot;message.feedback === &#039;up&#039; ? &#039;btn-success&#039; : &#039;btn-outline-secondary&#039;&quot;
                    @click=&quot;sendFeedback(message.id, &#039;up&#039;)&quot;
                    title=&quot;Risposta utile&quot;
                  &gt;
                    &lt;i class=&quot;bi bi-hand-thumbs-up&quot;&gt;&lt;/i&gt;
                  &lt;/button&gt;
                  &lt;button 
                    class=&quot;btn btn-xs ms-1&quot;
                    :class=&quot;message.feedback === &#039;down&#039; ? &#039;btn-danger&#039; : &#039;btn-outline-secondary&#039;&quot;
                    @click=&quot;sendFeedback(message.id, &#039;down&#039;)&quot;
                    title=&quot;Risposta non utile&quot;
                  &gt;
                    &lt;i class=&quot;bi bi-hand-thumbs-down&quot;&gt;&lt;/i&gt;
                  &lt;/button&gt;
                &lt;/div&gt;
                
                &lt;small class=&quot;text-muted d-block mt-1&quot;&gt;{{ formatTime(message.created_at) }}&lt;/small&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            
            &lt;!-- Loading indicator --&gt;
            &lt;div v-if=&quot;loading&quot; class=&quot;message assistant&quot;&gt;
              &lt;div class=&quot;message-avatar&quot;&gt;πŸ€–&lt;/div&gt;
              &lt;div class=&quot;message-content&quot;&gt;
                &lt;div class=&quot;typing-indicator&quot;&gt;
                  &lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          
          &lt;!-- Input --&gt;
          &lt;div class=&quot;chat-input&quot;&gt;
            &lt;textarea 
              v-model=&quot;userInput&quot;
              @keydown.enter.prevent=&quot;sendMessage()&quot;
              placeholder=&quot;Chiedi qualcosa sulla produzione...&quot;
              rows=&quot;2&quot;
              :disabled=&quot;loading&quot;
              class=&quot;form-control&quot;
            &gt;&lt;/textarea&gt;
            &lt;button 
              @click=&quot;sendMessage()&quot;
              :disabled=&quot;loading || !userInput.trim()&quot;
              class=&quot;btn btn-primary btn-send&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;
    &lt;/template&gt;</p><p>&lt;script&gt;
    export default {
      name: &#039;AiChatWidget&#039;,
      props: {
        machineId: {
          type: Number,
          required: true
        },
        dateFrom: String,
        dateTo: String,
      },
      data() {
        return {
          minimized: false,
          messages: [],
          userInput: &#039;&#039;,
          loading: false,
          conversationId: null,
          expandedSql: {},
          initialSuggestions: [
            &quot;Qual Γ¨ stata la produzione di ieri?&quot;,
            &quot;Mostrami il trend di questa settimana&quot;,
            &quot;Chi Γ¨ l&#039;operatore piΓΉ produttivo?&quot;,
          ],
        };
      },
      methods: {
        async sendMessage(text = null) {
          const message = text || this.userInput.trim();
          if (!message || this.loading) return;
          
          // Aggiungi messaggio utente alla UI
          this.messages.push({
            id: Date.now(),
            role: &#039;user&#039;,
            content: message,
            created_at: new Date().toISOString(),
          });
          
          this.userInput = &#039;&#039;;
          this.loading = true;
          this.scrollToBottom();
          
          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;: document.querySelector(&#039;meta[name=&quot;csrf-token&quot;]&#039;).content,
              },
              body: JSON.stringify({
                conversation_id: this.conversationId,
                machine_id: this.machineId,
                message: message,
                date_from: this.dateFrom,
                date_to: this.dateTo,
              }),
            });
            
            const data = await response.json();
            
            if (data.success) {
              this.conversationId = data.conversation_id;
              this.messages.push(data.message);
            } else {
              this.messages.push({
                id: Date.now(),
                role: &#039;assistant&#039;,
                content: &#039;❌ &#039; + (data.error || &#039;Errore nel processare la richiesta.&#039;),
                created_at: new Date().toISOString(),
              });
            }
          } catch (error) {
            console.error(&#039;Chat error:&#039;, error);
            this.messages.push({
              id: Date.now(),
              role: &#039;assistant&#039;,
              content: &#039;❌ Errore di connessione. Riprova.&#039;,
              created_at: new Date().toISOString(),
            });
          } finally {
            this.loading = false;
            this.scrollToBottom();
          }
        },
        
        async sendFeedback(messageId, rating) {
          try {
            await fetch(&#039;/admin/i40/ai/feedback&#039;, {
              method: &#039;POST&#039;,
              headers: {
                &#039;Content-Type&#039;: &#039;application/json&#039;,
                &#039;X-CSRF-TOKEN&#039;: document.querySelector(&#039;meta[name=&quot;csrf-token&quot;]&#039;).content,
              },
              body: JSON.stringify({ message_id: messageId, rating }),
            });
            
            // Aggiorna UI
            const message = this.messages.find(m =&gt; m.id === messageId);
            if (message) message.feedback = rating;
          } catch (error) {
            console.error(&#039;Feedback error:&#039;, error);
          }
        },
        
        toggleMinimize() {
          this.minimized = !this.minimized;
        },
        
        toggleSql(messageId) {
          this.expandedSql[messageId] = !this.expandedSql[messageId];
        },
        
        openChart(chartConfig) {
          this.$emit(&#039;open-chart&#039;, chartConfig);
        },
        
        formatMessage(content) {
          // Converti markdown-like syntax in HTML
          return content
            .replace(/\<em>\</em>(.<em>?)\</em>\*/g, &#039;&lt;strong&gt;$1&lt;/strong&gt;&#039;)
            .replace(/\n/g, &#039;&lt;br&gt;&#039;);
        },
        
        formatTime(timestamp) {
          const date = new Date(timestamp);
          return date.toLocaleTimeString(&#039;it-IT&#039;, { hour: &#039;2-digit&#039;, minute: &#039;2-digit&#039; });
        },
        
        scrollToBottom() {
          this.$nextTick(() =&gt; {
            const container = this.$refs.messagesContainer;
            if (container) {
              container.scrollTop = container.scrollHeight;
            }
          });
        },
      },
    };
    &lt;/script&gt;</p><p>&lt;style scoped&gt;
    .ai-chat-widget {
      position: fixed;
      bottom: 20px;
      right: 20px;
      width: 400px;
      max-height: 600px;
      background: white;
      border-radius: 12px;
      box-shadow: 0 4px 20px rgba(0,0,0,0.15);
      display: flex;
      flex-direction: column;
      z-index: 1050;
      transition: all 0.3s ease;
    }</p><p>.ai-chat-widget.minimized {
      max-height: 60px;
    }</p><p>.chat-header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 12px 16px;
      border-radius: 12px 12px 0 0;
      display: flex;
      justify-content: space-between;
      align-items: center;
      cursor: pointer;
    }</p><p>.ai-avatar {
      width: 32px;
      height: 32px;
      background: rgba(255,255,255,0.2);
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 16px;
    }</p><p>.ai-avatar-large {
      width: 64px;
      height: 64px;
      background: #f0f0f0;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 32px;
      margin: 0 auto;
    }</p><p>.chat-body {
      display: flex;
      flex-direction: column;
      height: 540px;
    }</p><p>.messages-container {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
    }</p><p>.empty-state {
      text-align: center;
      padding: 40px 20px;
    }</p><p>.suggestions button {
      font-size: 0.85rem;
    }</p><p>.message {
      display: flex;
      margin-bottom: 16px;
      align-items: flex-start;
    }</p><p>.message.user {
      flex-direction: row-reverse;
    }</p><p>.message-avatar {
      width: 32px;
      height: 32px;
      background: #f0f0f0;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
    }</p><p>.message.user .message-avatar {
      background: #667eea;
      color: white;
    }</p><p>.message-content {
      max-width: 75%;
      margin: 0 8px;
    }</p><p>.message.user .message-content {
      background: #667eea;
      color: white;
      border-radius: 12px 12px 0 12px;
      padding: 10px 14px;
    }</p><p>.message.assistant .message-content {
      background: #f0f0f0;
      border-radius: 12px 12px 12px 0;
      padding: 10px 14px;
    }</p><p>.message-text {
      line-height: 1.5;
    }</p><p>.sql-block pre {
      background: #2d2d2d;
      color: #f8f8f2;
      padding: 10px;
      border-radius: 6px;
      font-size: 0.85rem;
      overflow-x: auto;
    }</p><p>.typing-indicator {
      display: flex;
      gap: 4px;
      padding: 10px 0;
    }</p><p>.typing-indicator span {
      width: 8px;
      height: 8px;
      background: #999;
      border-radius: 50%;
      animation: typing 1.4s infinite;
    }</p><p>.typing-indicator span:nth-child(2) {
      animation-delay: 0.2s;
    }</p><p>.typing-indicator span:nth-child(3) {
      animation-delay: 0.4s;
    }</p><p>@keyframes typing {
      0%, 60%, 100% { transform: translateY(0); }
      30% { transform: translateY(-10px); }
    }</p><p>.chat-input {
      display: flex;
      padding: 12px;
      border-top: 1px solid #e0e0e0;
      gap: 8px;
    }</p><p>.chat-input textarea {
      flex: 1;
      resize: none;
      border-radius: 8px;
    }</p><p>.btn-send {
      width: 44px;
      height: 44px;
      border-radius: 8px;
    }</p><p>.message-feedback .btn {
      padding: 2px 8px;
      font-size: 0.85rem;
    }</p><p>/<em> Responsive </em>/
    @media (max-width: 768px) {
      .ai-chat-widget {
        width: calc(100% - 40px);
        max-width: 400px;
      }
    }
    &lt;/style&gt;
    `

    4.2 Integrazione in analyze.blade.php

    `blade <!-- resources/views/admin/i40/operations/analyze.blade.php -->

    <!-- Aggiungi prima di @endpush --> @push('scripts') <script> // Inizializza Vue component AI Chat import { createApp } from 'vue'; import AiChatWidget from '@/components/I40/AiChatWidget.vue';

    const app = createApp({}); app.component('ai-chat-widget', AiChatWidget); app.mount('#ai-chat-container'); </script> @endpush

    <!-- Aggiungi nel body della view --> <div id="ai-chat-container"> <ai-chat-widget :machine-id="{{ $selectedMachine->id }}" date-from="{{ $dateFrom->format('Y-m-d') }}" date-to="{{ $dateTo->format('Y-m-d') }}" @open-chart="openChartFromAi" ></ai-chat-widget> </div> `

    ---

    πŸš€ Fase 5: Advanced Features

    Durata: Continua (post-MVP) Obiettivo: FunzionalitΓ  avanzate per migliorare UX e performance

    5.1 Cache Intelligente

    Implementazione

    `php // app/Services/I40/AiCacheService.php namespace App\Services\I40;

    class AiCacheService { / * Ottieni risposta da cache se disponibile */ public function get(string $query, int $machineId, array $filters = []): ?array { $hash = $this->generateHash($query, $machineId, $filters); $cached = \DB::table('ai_cache') ->where('query_hash', $hash) ->where('expires_at', '>', now()) ->first(); if ($cached) { // Incrementa hit counter \DB::table('ai_cache') ->where('id', $cached->id) ->increment('hits'); return json_decode($cached->response_data, true); } return null; } / * Salva risposta in cache */ public function set(string $query, int $machineId, array $filters, array $response, int $ttl = null): void { $hash = $this->generateHash($query, $machineId, $filters); $ttl = $ttl ?? config('ai.cache.ttl', 3600); \DB::table('ai_cache')->updateOrInsert( ['query_hash' => $hash], [ 'machine_id' => $machineId, 'date_range' => $this->getDateRange($filters), 'response_data' => json_encode($response), 'expires_at' => now()->addSeconds($ttl), 'created_at' => now(), ] ); } / * Genera hash univoco per query */ protected function generateHash(string $query, int $machineId, array $filters): string { // Normalizza query (lowercase, rimuovi spazi multipli) $normalized = strtolower(trim(preg_replace('/\s+/', ' ', $query))); // Combina con parametri $key = $normalized . '|' . $machineId . '|' . json_encode($filters); return hash('sha256', $key); } / * Estrae date range dai filtri */ protected function getDateRange(array $filters): ?string { $from = $filters['date_from'] ?? null; $to = $filters['date_to'] ?? null; if ($from && $to) { return "{$from}_{$to}"; } return null; } / * Pulisci cache scaduta */ public function cleanExpired(): int { return \DB::table('ai_cache') ->where('expires_at', '<', now()) ->delete(); } / * Invalida cache per macchina (dopo nuovi dati) */ public function invalidateForMachine(int $machineId): int { return \DB::table('ai_cache') ->where('machine_id', $machineId) ->delete(); } } `

    Scheduled Job (Pulizia Automatica)

    `php // app/Console/Kernel.php protected function schedule(Schedule $schedule) { // Pulisci cache AI scaduta ogni ora $schedule->call(function () { app(AiCacheService::class)->cleanExpired(); })->hourly(); } `

    5.2 Voice Input (Speech-to-Text)

    Frontend: Web Speech API

    `javascript // Aggiungi al Vue component data() { return { // ... isRecording: false, recognition: null, }; }, mounted() { this.initSpeechRecognition(); }, methods: { initSpeechRecognition() { if (!('webkitSpeechRecognition' in window)) { console.warn('Speech recognition not supported'); return; } this.recognition = new webkitSpeechRecognition(); this.recognition.lang = 'it-IT'; this.recognition.continuous = false; this.recognition.interimResults = false; this.recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; this.userInput = transcript; this.isRecording = false; }; this.recognition.onerror = (event) => { console.error('Speech recognition error:', event.error); this.isRecording = false; }; this.recognition.onend = () => { this.isRecording = false; }; }, toggleRecording() { if (!this.recognition) return; if (this.isRecording) { this.recognition.stop(); this.isRecording = false; } else { this.recognition.start(); this.isRecording = true; } }, } `

    `vue <!-- Template: Aggiungi bottone microfono --> <button @click="toggleRecording" :class="{ 'recording': isRecording }" class="btn btn-outline-secondary btn-voice" title="Usa il microfono" > <i class="bi" :class="isRecording ? 'bi-mic-fill' : 'bi-mic'"></i> </button> `

    `css / Style / .btn-voice.recording { animation: pulse 1.5s infinite; background: #dc3545; color: white; border-color: #dc3545; }

    @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } `

    5.3 Export Conversazioni

    Backend: Export PDF

    `php // app/Services/I40/ConversationExportService.php namespace App\Services\I40;

    use Barryvdh\DomPDF\Facade\Pdf;

    class ConversationExportService { / * Esporta conversazione in PDF */ public function exportToPdf(AiConversation $conversation): string { $data = [ 'conversation' => $conversation, 'messages' => $conversation->messages, 'machine' => $conversation->machine, 'user' => $conversation->user, ]; $pdf = Pdf::loadView('admin.i40.ai.export-pdf', $data); return $pdf->download("conversazione_{$conversation->id}.pdf"); } / * Esporta in formato JSON */ public function exportToJson(AiConversation $conversation): array { return [ 'conversation_id' => $conversation->id, 'title' => $conversation->title, 'machine' => [ 'id' => $conversation->machine->id, 'name' => $conversation->machine->name, ], 'created_at' => $conversation->created_at->toIso8601String(), 'messages' => $conversation->messages->map(function($msg) { return [ 'role' => $msg->role, 'content' => $msg->content, 'metadata' => $msg->metadata, 'created_at' => $msg->created_at->toIso8601String(), ]; })->toArray(), ]; } } `

    Route Export

    `php Route::get('/ai/conversations/{conversation}/export', [AiChatController::class, 'export']) ->name('ai.conversations.export'); `

    `php // Controller method public function export(AiConversation $conversation, Request $request) { $this->authorize('view', $conversation); $format = $request->input('format', 'pdf'); // pdf, json $exportService = app(ConversationExportService::class); if ($format === 'json') { $data = $exportService->exportToJson($conversation); return response()->json($data); } return $exportService->exportToPdf($conversation); } `

    5.4 Suggested Responses (Follow-up Questions)

    Service: Response Suggestions

    `php // app/Services/I40/ResponseSuggestionsService.php namespace App\Services\I40;

    class ResponseSuggestionsService { / * Genera follow-up suggestions basate sul contesto */ public function generate(AiMessage $lastMessage, array $context): array { $intent = $context['intent'] ?? 'general'; $suggestions = $this->getBaselineSuggestions($intent); // Personalizza basandosi sulla risposta AI if ($lastMessage->metadata && isset($lastMessage->metadata['sql'])) { $suggestions[] = "Mostra gli stessi dati in un grafico"; $suggestions[] = "Confronta con il periodo precedente"; } return array_slice($suggestions, 0, 3); // Max 3 suggestions } / * Suggestions baseline per intent */ protected function getBaselineSuggestions(string $intent): array { $map = [ 'production_total' => [ "Confronta con ieri", "Mostrami il trend settimanale", "Quale operatore ha prodotto di piΓΉ?", ], 'operator_performance' => [ "Analizza la velocitΓ  media", "Ci sono giorni piΓΉ produttivi?", "Suggeriscimi come migliorare", ], 'trend_analysis' => [ "Ci sono anomalie nei dati?", "Prevedi la produzione di domani", "Esporta questi dati", ], ]; return $map[$intent] ?? [ "Mostrami le statistiche", "Analizza il periodo corrente", "Come posso ottimizzare?", ]; } } `

    Integrazione Vue Component

    `vue <!-- Dopo ogni messaggio AI, mostra suggestions --> <div v-if="message.role === 'assistant' && message.suggestions" class="follow-up-suggestions mt-2"> <small class="text-muted d-block mb-1">Potresti chiedere:</small> <button v-for="suggestion in message.suggestions" :key="suggestion" class="btn btn-xs btn-outline-secondary me-1 mb-1" @click="sendMessage(suggestion)" > {{ suggestion }} </button> </div> `

    5.5 Alert Automation

    Tabella: ai_alerts

    `sql CREATE TABLE ai_alerts ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, machine_id BIGINT UNSIGNED NULL, alert_type ENUM('threshold', 'anomaly', 'prediction') NOT NULL, condition_config JSON NOT NULL, -- { field: 'TotalPieces', operator: '<', value: 1000, period: 'daily' } notification_channels JSON, -- ['email', 'slack', 'in_app'] is_active BOOLEAN DEFAULT TRUE, last_triggered_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_active (is_active, machine_id) ); `

    Service: Alert Monitoring

    `php // app/Services/I40/AlertMonitoringService.php namespace App\Services\I40;

    class AlertMonitoringService { / * Controlla tutti gli alert attivi (eseguito ogni ora via Scheduler) */ public function checkAlerts(): void { $alerts = \DB::table('ai_alerts') ->where('is_active', true) ->get(); foreach ($alerts as $alert) { $this->evaluateAlert($alert); } } / * Valuta singolo alert */ protected function evaluateAlert(object $alert): void { $condition = json_decode($alert->condition_config, true); $result = $this->executeCondition($condition, $alert->machine_id); if ($result['triggered']) { $this->triggerAlert($alert, $result); } } / * Esegue condizione alert */ protected function executeCondition(array $condition, ?int $machineId): array { $field = $condition['field']; $operator = $condition['operator']; // <, >, =, != $threshold = $condition['value']; $period = $condition['period'] ?? 'daily'; // daily, weekly, monthly // Calcola date range $dateFrom = match($period) { 'daily' => now()->startOfDay(), 'weekly' => now()->startOfWeek(), 'monthly' => now()->startOfMonth(), }; // Query valore attuale $query = MachineOperationLog::query(); if ($machineId) { $query->where('machine_id', $machineId); } $value = $query ->whereBetween('timestamp', [$dateFrom, now()]) ->selectRaw("SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{$field}')) AS DECIMAL(15,2))) as total") ->value('total') ?? 0; // Valuta condizione $triggered = match($operator) { '<' => $value < $threshold, '>' => $value > $threshold, '=' => $value == $threshold, '!=' => $value != $threshold, default => false, }; return [ 'triggered' => $triggered, 'current_value' => $value, 'threshold' => $threshold, 'field' => $field, ]; } / * Trigger notifica alert */ protected function triggerAlert(object $alert, array $result): void { // Aggiorna last_triggered_at \DB::table('ai_alerts') ->where('id', $alert->id) ->update(['last_triggered_at' => now()]); // Invia notifiche $channels = json_decode($alert->notification_channels, true) ?? ['in_app']; foreach ($channels as $channel) { $this->sendNotification($channel, $alert, $result); } } / * Invia notifica su canale specifico */ protected function sendNotification(string $channel, object $alert, array $result): void { $message = sprintf( "⚠️ Alert: %s è %s (soglia: %s, valore attuale: %s)", $result['field'], $alert->alert_type, number_format($result['threshold']), number_format($result['current_value']) ); if ($channel === 'email') { $user = User::find($alert->user_id); \Mail::to($user)->send(new AlertTriggeredMail($message)); } elseif ($channel === 'slack') { // Slack webhook \Http::post(config('services.slack.webhook_url'), [ 'text' => $message, ]); } elseif ($channel === 'in_app') { // Laravel notification system $user = User::find($alert->user_id); $user->notify(new AlertTriggered($message)); } } } `

    Scheduler (Cron)

    `php // app/Console/Kernel.php protected function schedule(Schedule $schedule) { // Controlla alert ogni ora $schedule->call(function () { app(AlertMonitoringService::class)->checkAlerts(); })->hourly(); } `

    ---

    πŸ’¬ Esempi Query AI

    Categoria: Produzione Totale

    Esempio 1

    Query Utente: "Qual Γ¨ stata la produzione di ieri?"

    Intent: production_total EntitΓ : period=yesterday

    SQL Generato: `sql SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total FROM machine_operations_log WHERE machine_id = 5 AND DATE(timestamp) = CURDATE() - INTERVAL 1 DAY `

    Risposta AI: > "Ieri hai prodotto 1.850 pezzi, leggermente superiore alla media giornaliera di 1.750 pezzi. Ottimo lavoro! πŸ‘"

    Follow-up Suggestions:

  • "Confronta con l'altro ieri"
  • "Mostrami il trend settimanale"
  • "Chi ha prodotto di piΓΉ ieri?"
  • ---

    Esempio 2

    Query Utente: "Confronta la produzione di questa settimana con la scorsa"

    Intent: production_comparison EntitΓ : period=this_week, comparison=last_week

    SQL Generato: `sql SELECT WEEK(timestamp) as week_num, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total FROM machine_operations_log WHERE machine_id = 5 AND timestamp >= DATE_SUB(NOW(), INTERVAL 2 WEEK) GROUP BY week_num ORDER BY week_num `

    Risposta AI: > "Settimana corrente: 8.400 pezzi > Settimana scorsa: 9.100 pezzi > > La produzione è diminuita del 7,7% (-700 pezzi). Possibili cause: > - Lunedì è stato festivo > - Martedì c'è stato un fermo macchina di 2 ore > > Vuoi analizzare i fermi macchina in dettaglio?"

    Chart Suggested: Line chart (giorni Γ— pezzi prodotti, 2 settimane)

    ---

    Categoria: Performance Operatori

    Esempio 3

    Query Utente: "Chi Γ¨ l'operatore piΓΉ produttivo questo mese?"

    Intent: operator_performance EntitΓ : period=this_month, metric=productivity

    SQL Generato: `sql SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.MarkerName')) as operator, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total_pieces, COUNT(*) as operations, AVG(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.CutTime')) AS DECIMAL(15,2))) as avg_time FROM machine_operations_log WHERE machine_id = 5 AND timestamp >= DATE_FORMAT(NOW(), '%Y-%m-01') GROUP BY operator ORDER BY total_pieces DESC LIMIT 5 `

    Risposta AI: > "πŸ† Top 5 Operatori del Mese: > > 1. Giovanni Rossi - 12.500 pezzi (45 operazioni, 2.5h media) > 2. Marco Bianchi - 11.800 pezzi (42 operazioni, 2.8h media) > 3. Luca Verdi - 10.200 pezzi (38 operazioni, 3.1h media) > 4. Paolo Neri - 9.500 pezzi (35 operazioni, 3.0h media) > 5. Anna Gialli - 8.900 pezzi (33 operazioni, 3.2h media) > > Giovanni Rossi Γ¨ il piΓΉ produttivo sia per volume totale che per velocitΓ  media. Complimenti! πŸŽ‰"

    Chart Suggested: Bar chart (operatori Γ— pezzi totali)

    ---

    Categoria: Analisi Temporale

    Esempio 4

    Query Utente: "Mostrami il trend di produzione dell'ultimo mese"

    Intent: trend_analysis EntitΓ : period=last_month, visualization=trend

    SQL Generato: `sql SELECT DATE(timestamp) as date, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as daily_total FROM machine_operations_log WHERE machine_id = 5 AND timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY date ORDER BY date `

    Risposta AI: > "πŸ“ˆ Trend Produzione - Ultimi 30 Giorni > > - Media giornaliera: 1.750 pezzi > - Picco massimo: 2.300 pezzi (15 ottobre) > - Minimo: 1.200 pezzi (7 ottobre - sabato) > - Trend generale: In crescita del +5% rispetto ai 30 giorni precedenti > > Osservazioni: > - I lunedΓ¬ sono mediamente -10% rispetto agli altri giorni > - I giovedΓ¬ hanno il picco settimanale (+15%) > > Vuoi approfondire i giovedΓ¬ per capire il motivo del picco?"

    Chart Suggested: Line chart (giorno Γ— pezzi prodotti, 30 giorni)

    ---

    Categoria: Anomalie

    Esempio 5

    Query Utente: "Perché ieri abbiamo prodotto così poco?"

    Intent: anomaly_detection EntitΓ : period=yesterday, issue=low_production

    </p><p><strong>SQL Generato:</strong>
    </code>`<code>sql
    -- Query 1: Produzione ieri
    SELECT 
        SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, &#039;$.TotalPieces&#039;)) AS DECIMAL(15,2))) as total,
        COUNT(*) as operations,
        MIN(timestamp) as first_op,
        MAX(timestamp) as last_op
    FROM machine_operations_log
    WHERE machine_id = 5
      AND DATE(timestamp) = CURDATE() - INTERVAL 1 DAY</p><p>-- Query 2: Media ultimi 30 giorni (escl. weekend)
    SELECT AVG(daily_total) as avg_production
    FROM (
        SELECT DATE(timestamp) as date, SUM(...) as daily_total
        FROM machine_operations_log
        WHERE machine_id = 5
          AND timestamp &gt;= DATE_SUB(NOW(), INTERVAL 30 DAY)
          AND DAYOFWEEK(timestamp) NOT IN (1, 7)
        GROUP BY date
    ) as daily_stats
    </code>`<code></p><p><strong>Risposta AI:</strong>
    &gt; &quot;πŸ” <strong>Analisi Produzione Ieri (18 Ottobre)</strong>
    &gt; 
    &gt; Ieri hai prodotto solo <strong>1.200 pezzi</strong>, il <strong>31% in meno</strong> rispetto alla media di 1.750 pezzi.
    &gt; 
    &gt; <strong>Cause identificate:</strong>
    &gt; 1. ⏰ <strong>Orario ridotto:</strong> Prima operazione alle 10:30 invece che 8:00 (ritardo 2.5h)
    &gt; 2. ⚠️ <strong>Fermo macchina:</strong> Dalle 14:15 alle 16:00 nessuna operazione registrata
    &gt; 3. πŸ‘€ <strong>Operatore nuovo:</strong> Solo 3 operazioni da parte di &quot;Stagista Mario&quot; (vs 8-10 normali)
    &gt; 
    &gt; <strong>Raccomandazione:</strong> Se il fermo Γ¨ stato per manutenzione, Γ¨ normale. Altrimenti, verifica il motivo del ritardo iniziale.&quot;</p><p><strong>Follow-up Suggestions:</strong>
    <li>&quot;Analizza i fermi macchina del mese&quot;</li>
    <li>&quot;Mostrami le performance degli stagisti&quot;</li>
    <li>&quot;Confronta con lo stesso giorno del mese scorso&quot;</li></p><p>---</p><p><h3>Categoria: Previsioni</h3></p><p><h4>Esempio 6</h4>
    <strong>Query Utente:</strong> &quot;Riusciremo a completare 5000 pezzi entro venerdì?&quot;</p><p><strong>Intent:</strong>
    prediction EntitΓ : target=5000, deadline=friday, unit=pieces

    SQL Generato: `sql -- Query 1: Pezzi giΓ  prodotti questa settimana SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as current_total FROM machine_operations_log WHERE machine_id = 5 AND timestamp >= DATE_SUB(NOW(), INTERVAL WEEKDAY(NOW()) DAY)

    -- Query 2: Media produzione giornaliera (ultimi 30 giorni) SELECT AVG(daily_total) as avg_daily FROM ( SELECT DATE(timestamp), SUM(...) as daily_total FROM machine_operations_log WHERE machine_id = 5 AND timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY DATE(timestamp) ) as stats `

    </p><p><strong>Risposta AI:</strong>
    &gt; &quot;πŸ“Š <strong>Previsione Completamento Ordine</strong>
    &gt; 
    &gt; <strong>Situazione attuale (Mercoledì):</strong>
    &gt; - Prodotti finora: 2.800 pezzi
    &gt; - Mancanti: 2.200 pezzi
    &gt; - Giorni rimanenti: 2 (Giovedì + Venerdì)
    &gt; 
    &gt; <strong>Proiezione:</strong>
    &gt; - Media giornaliera: 1.750 pezzi/giorno
    &gt; - Produzione attesa: 1.750 Γ— 2 = <strong>3.500 pezzi</strong>
    &gt; 
    &gt; βœ… <strong>SΓ¬, ce la farete!</strong> Con un margine di +1.300 pezzi (26%).
    &gt; 
    &gt; <strong>Scenario worst-case (se produzione -20%):</strong>
    &gt; - 1.400 Γ— 2 = 2.800 pezzi
    &gt; - Mancherebbero ancora 400 pezzi β†’ Servirebbe qualche ora sabato mattina
    &gt; 
    &gt; Vuoi impostare un alert per monitorare l&#039;avanzamento?&quot;</p><p>---</p><p><h3>Categoria: Ottimizzazione</h3></p><p><h4>Esempio 7</h4>
    <strong>Query Utente:</strong> &quot;Come posso aumentare la produttivitΓ  del 10%?&quot;</p><p><strong>Intent:</strong>
    optimization EntitΓ : target=+10%, metric=productivity
    </p><p><strong>SQL Generato:</strong>
    </code>`<code>sql
    -- Analisi pattern produttivi
    SELECT 
        DAYNAME(timestamp) as day_of_week,
        HOUR(timestamp) as hour_of_day,
        JSON_UNQUOTE(JSON_EXTRACT(raw_data, &#039;$.MarkerName&#039;)) as operator,
        AVG(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, &#039;$.TotalPieces&#039;)) AS DECIMAL(15,2)) / 
            CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, &#039;$.CutTime&#039;)) AS DECIMAL(15,2))) as pieces_per_hour
    FROM machine_operations_log
    WHERE machine_id = 5
      AND timestamp &gt;= DATE_SUB(NOW(), INTERVAL 3 MONTH)
    GROUP BY day_of_week, hour_of_day, operator
    HAVING pieces_per_hour &gt; 0
    ORDER BY pieces_per_hour DESC
    LIMIT 20
    </code>`<code></p><p><strong>Risposta AI:</strong>
    &gt; &quot;🎯 <strong>Raccomandazioni per +10% Produttività</strong>
    &gt; 
    &gt; Basandomi sui dati degli ultimi 3 mesi, ho identificato <strong>5 opportunitΓ </strong>:
    &gt; 
    &gt; <strong>1. Ottimizza Orari (Impatto: +5%)</strong>
    &gt; - πŸŒ… Le operazioni tra 8:00-10:00 sono <strong>18% piΓΉ veloci</strong>
    &gt; - πŸŒ† Le operazioni dopo le 17:00 sono <strong>12% piΓΉ lente</strong>
    &gt; - <strong>Azione:</strong> Concentra lavorazioni complesse al mattino
    &gt; 
    &gt; <strong>2. Best Practices da Giovanni Rossi (Impatto: +8%)</strong>
    &gt; - Sua resa: <strong>450 pezzi/ora</strong> vs media 380 pezzi/ora
    &gt; - Differenza chiave: Setup piΓΉ rapido (15 min vs 25 min)
    &gt; - <strong>Azione:</strong> Organizza training con Giovanni per altri operatori
    &gt; 
    &gt; <strong>3. Riduci Fermi Macchina (Impatto: +4%)</strong>
    &gt; - Attualmente: <strong>45 minuti/giorno</strong> di fermi non pianificati
    &gt; - Causa principale: Cambio materiale (65% dei fermi)
    &gt; - <strong>Azione:</strong> Pre-staging materiali vicino alla macchina
    &gt; 
    &gt; <strong>4. Giovedì come Riferimento (Impatto: +6%)</strong>
    &gt; - Giovedì è il giorno più produttivo (+15% vs media)
    &gt; - Motivo: Meno interruzioni, focus continuativo
    &gt; - <strong>Azione:</strong> Replica modello giovedì anche altri giorni
    &gt; 
    &gt; <strong>5. Manutenzione Preventiva (Impatto: +3%)</strong>
    &gt; - I giorni dopo manutenzione hanno +12% velocitΓ 
    &gt; - Attualmente: Manutenzione ogni 3 settimane
    &gt; - <strong>Azione:</strong> Passa a manutenzione settimanale leggera
    &gt; 
    &gt; πŸ“ˆ <strong>Totale potenziale:</strong> +26% (ben oltre il target del 10%!)
    &gt; 
    &gt; Vuoi un piano d&#039;azione dettagliato per implementare questi suggerimenti?&quot;</p><p>---</p><p><h2>πŸ’° Costi e Considerazioni</h2></p><p><h3>Stima Costi OpenAI API</h3></p><p><h4>Modello Pricing (Ottobre 2025)</h4></p><p>| Modello | Input (1M tokens) | Output (1M tokens) |
    |---------|-------------------|-------------------|
    | GPT-4 Turbo | $10.00 | $30.00 |
    | GPT-4 | $30.00 | $60.00 |
    | GPT-3.5 Turbo | $0.50 | $1.50 |</p><p><h4>Stima Conversazione Tipica</h4></p><p><strong>Scenario:</strong> Utente chiede &quot;Qual Γ¨ stata la produzione di ieri?&quot;</p><p><li><strong>Input tokens:</strong> </li>
      - System prompt (schema + context): ~800 tokens
      - User message: ~15 tokens
      - <strong>Totale input:</strong> ~815 tokens</p><p><li><strong>Output tokens:</strong></li>
      - Function call (SQL): ~100 tokens
      - Risposta formattata: ~150 tokens
      - <strong>Totale output:</strong> ~250 tokens</p><p><strong>Costo per conversazione:</strong>
    <li>Input: 815 tokens Γ— $10 / 1M = <strong>$0.00815</strong></li>
    <li>Output: 250 tokens Γ— $30 / 1M = <strong>$0.0075</strong></li>
    <li><strong>Totale: ~$0.016 per conversazione</strong></li></p><p><h4>Proiezione Mensile</h4></p><p><strong>Scenario Conservativo:</strong>
    <li>3 macchine monitorate</li>
    <li>5 utenti attivi</li>
    <li>20 conversazioni/utente/mese</li>
    <li>Media 3 messaggi per conversazione</li></p><p><strong>Calcolo:</strong>
    <li>Totale conversazioni: 5 utenti Γ— 20 = <strong>100 conversazioni/mese</strong></li>
    <li>Totale messaggi: 100 Γ— 3 = <strong>300 messaggi/mese</strong></li>
    <li>Costo: 300 Γ— $0.016 = <strong>$4.80/mese</strong></li></p><p><strong>Con cache (riduzione 40%):</strong>
    <li>Costo effettivo: <strong>~$3/mese</strong></li></p><p><h4>Scenario Intensivo (10 macchine, 20 utenti)</h4></p><p><li>Conversazioni: 20 Γ— 30 = <strong>600/mese</strong></li>
    <li>Messaggi: 600 Γ— 4 = <strong>2.400/mese</strong></li>
    <li>Costo: 2.400 Γ— $0.016 = <strong>$38.40/mese</strong></li>
    <li><strong>Con cache: ~$23/mese</strong></li></p><p><h3>ROI Analysis</h3></p><p><h4>Benefici Quantificabili</h4></p><p><strong>1. Risparmio Tempo Analisi Dati</strong>
    <li>Tempo medio analisi manuale: 30 minuti/query</li>
    <li>Con AI: 2 minuti/query</li>
    <li>Risparmio: 28 minuti Γ— costo orario operatore</li></p><p><strong>Esempio:</strong>
    <li>50 query/mese</li>
    <li>Costo operatore: €25/ora</li>
    <li>Risparmio: 50 Γ— (28/60) Γ— €25 = <strong>€583/mese</strong></li></p><p><strong>2. Decisioni PiΓΉ Rapide</strong>
    <li>Identificazione problemi in real-time vs fine giornata</li>
    <li>Impatto: Riduzione downtime macchina</li>
    <li>Stima conservativa: 2 ore/mese di downtime evitato</li>
    <li>Valore: €200-500/ora (costo downtime produzione)</li>
    <li><strong>Risparmio: €400-1.000/mese</strong></li></p><p><strong>3. Ottimizzazione Produzione</strong>
    <li>Anche solo +2% efficienza da suggerimenti AI</li>
    <li>Su produzione €100.000/mese</li>
    <li><strong>Valore: €2.000/mese</strong></li></p><p><strong>ROI Totale:</strong>
    <li>Benefici: €583 + €700 + €2.000 = <strong>€3.283/mese</strong></li>
    <li>Costi: ~€23/mese (API) + ~€100/mese (sviluppo/manutenzione)</li>
    <li><strong>ROI: €3.283 - €123 = €3.160/mese netto</strong></li>
    <li><strong>Payback: &lt; 2 settimane</strong></li></p><p><h3>Privacy e GDPR</h3></p><p><h4>Considerazioni</h4></p><p><strong>Dati Inviati a OpenAI:</strong>
    <li>Schema database (nomi colonne, tipi)</li>
    <li>Sample records (max 5, anonimizzati)</li>
    <li>Query utente</li>
    <li><strong>NON inviati:</strong> Dati personali sensibili, informazioni clienti</li></p><p><strong>Compliance:</strong>
    <li>βœ… OpenAI Γ¨ GDPR-compliant</li>
    <li>βœ… Dati non usati per training (se API aziendale)</li>
    <li>βœ… Log conversazioni su server proprietario</li></p><p><strong>Best Practices:</strong>
    1. Anonimizza sample data prima di invio (rimuovi nomi clienti, riferimenti ordini)
    2. Usa Azure OpenAI se serve data residency EU
    3. Cripta conversazioni nel database
    4. Implementa data retention policy (es. cancella dopo 90 giorni)</p><p><h4>Azure OpenAI (Alternativa Enterprise)</h4></p><p><strong>Vantaggi:</strong>
    <li>Data residency EU</li>
    <li>SLA garantito</li>
    <li>Nessun utilizzo dati per training</li>
    <li>Support aziendale</li></p><p><strong>Costi:</strong>
    <li>Simili a OpenAI standard</li>
    <li>Minimum commitment: $100/mese</li></p><p>---</p><p><h2>πŸ§ͺ Testing &amp; Quality Assurance</h2></p><p><h3>Unit Tests</h3></p><p><h4>Test: Query Understanding Service</h4></p><p>
    `php // tests/Unit/QueryUnderstandingServiceTest.php namespace Tests\Unit\Services\I40;

    use Tests\TestCase; use App\Services\I40\QueryUnderstandingService;

    class QueryUnderstandingServiceTest extends TestCase { protected $service; protected function setUp(): void { parent::setUp(); $this->service = app(QueryUnderstandingService::class); } / @test */ public function it_detects_production_total_intent() { $result = $this->service->analyze("Qual Γ¨ stata la produzione di ieri?"); $this->assertEquals('production_total', $result['intent']); $this->assertEquals('yesterday', $result['entities']['period']); $this->assertGreaterThan(0.7, $result['confidence']); } / @test */ public function it_extracts_date_entities() { $result = $this->service->analyze("Mostrami la produzione di questa settimana"); $this->assertEquals('this_week', $result['entities']['period']); $this->assertNotNull($result['entities']['date_from']); $this->assertNotNull($result['entities']['date_to']); } / @test */ public function it_handles_comparison_queries() { $result = $this->service->analyze("Confronta con la settimana scorsa"); $this->assertEquals('production_comparison', $result['intent']); } } `

    Test: OpenAI Service (Mocked)

    `php // tests/Unit/OpenAiServiceTest.php namespace Tests\Unit\Services\I40;

    use Tests\TestCase; use App\Services\I40\OpenAiService; use OpenAI\Laravel\Facades\OpenAI;

    class OpenAiServiceTest extends TestCase { / @test */ public function it_generates_sql_query_via_function_calling() { // Mock OpenAI response OpenAI::fake([ OpenAI::response([ 'choices' => [ [ 'message' => [ 'function_call' => [ 'name' => 'execute_sql_query', 'arguments' => json_encode([ 'sql' => 'SELECT COUNT(*) FROM machine_operations_log WHERE machine_id=5', 'explanation' => 'Conteggio operazioni macchina 5' ]), ], ], 'finish_reason' => 'function_call', ], ], 'usage' => ['total_tokens' => 500], ]), ]); $service = app(OpenAiService::class); $context = ['schema' => 'test schema', 'statistics' => '', 'sample_data' => '', 'business_rules' => '']; $response = $service->generateResponse($context, "Quante operazioni ha fatto la macchina?"); $this->assertArrayHasKey('metadata', $response); $this->assertArrayHasKey('sql', $response['metadata']); $this->assertStringContainsString('SELECT', $response['metadata']['sql']); } / @test */ public function it_prevents_dangerous_sql() { $service = app(OpenAiService::class); $this->expectException(\Exception::class); $this->expectExceptionMessage('Query non permessa'); // Tenta di eseguire SQL pericoloso $reflection = new \ReflectionClass($service); $method = $reflection->getMethod('executeSafeSql'); $method->setAccessible(true); $method->invoke($service, "DROP TABLE users"); } } `

    Integration Tests

    Test: AI Chat Flow Completo

    `php // tests/Feature/AiChatFlowTest.php namespace Tests\Feature;

    use Tests\TestCase; use App\Models\User; use App\Models\I40\Machine; use Illuminate\Foundation\Testing\RefreshDatabase;

    class AiChatFlowTest extends TestCase { use RefreshDatabase; / @test */ public function user_can_start_conversation_with_ai() { $user = User::factory()->create(); $machine = Machine::factory()->create(); $this->actingAs($user); $response = $this->postJson('/admin/i40/ai/chat', [ 'machine_id' => $machine->id, 'message' => 'Qual Γ¨ stata la produzione di ieri?', ]); $response->assertStatus(200) ->assertJsonStructure([ 'success', 'conversation_id', 'message' => ['id', 'role', 'content', 'created_at'], ]); $this->assertDatabaseHas('ai_conversations', [ 'user_id' => $user->id, 'machine_id' => $machine->id, ]); $this->assertDatabaseHas('ai_messages', [ 'role' => 'user', 'content' => 'Qual Γ¨ stata la produzione di ieri?', ]); } / @test */ public function user_can_provide_feedback_on_ai_response() { $user = User::factory()->create(); $message = AiMessage::factory()->create(['role' => 'assistant']); $this->actingAs($user); $response = $this->postJson('/admin/i40/ai/feedback', [ 'message_id' => $message->id, 'rating' => 'up', 'comment' => 'Molto utile!', ]); $response->assertStatus(200); $this->assertDatabaseHas('ai_feedback', [ 'message_id' => $message->id, 'user_id' => $user->id, 'rating' => 'up', ]); } } `

    Performance Tests

    `php // tests/Performance/AiResponseTimeTest.php namespace Tests\Performance;

    use Tests\TestCase;

    class AiResponseTimeTest extends TestCase { / @test */ public function ai_response_time_is_under_5_seconds() { $start = microtime(true); // Esegui query AI $this->postJson('/admin/i40/ai/chat', [ 'machine_id' => 1, 'message' => 'Test query', ]); $duration = microtime(true) - $start; $this->assertLessThan(5.0, $duration, "Response time was {$duration}s"); } } `

    ---

    πŸ“Š Monitoring & Analytics

    Metriche da Tracciare

    1. Usage Metrics

    `php // Dashboard Analytics Service class AiAnalyticsService { public function getDashboardMetrics(Carbon $from, Carbon $to): array { return [ // Utilizzo 'total_conversations' => AiConversation::whereBetween('created_at', [$from, $to])->count(), 'total_messages' => AiMessage::whereBetween('created_at', [$from, $to])->count(), 'active_users' => AiConversation::whereBetween('created_at', [$from, $to]) ->distinct('user_id')->count(), // Performance 'avg_response_time' => AiMessage::where('role', 'assistant') ->whereBetween('created_at', [$from, $to]) ->avg('response_time_ms'), // Quality 'positive_feedback' => AiFeedback::where('rating', 'up') ->whereBetween('created_at', [$from, $to])->count(), 'negative_feedback' => AiFeedback::where('rating', 'down') ->whereBetween('created_at', [$from, $to])->count(), 'satisfaction_rate' => $this->calculateSatisfactionRate($from, $to), // Costi 'total_tokens' => AiMessage::whereBetween('created_at', [$from, $to]) ->sum('tokens_used'), 'estimated_cost' => $this->estimateCost($from, $to), // Cache 'cache_hit_rate' => $this->calculateCacheHitRate($from, $to), ]; } protected function calculateSatisfactionRate(Carbon $from, Carbon $to): float { $positive = AiFeedback::where('rating', 'up') ->whereBetween('created_at', [$from, $to])->count(); $negative = AiFeedback::where('rating', 'down') ->whereBetween('created_at', [$from, $to])->count(); $total = $positive + $negative; return $total > 0 ? ($positive / $total) * 100 : 0; } } `

    2. Quality Monitoring

    Logging Eventi Critici:

    `php // Event: Low Confidence Response if ($analysis['confidence'] < 0.5) { \Log::warning('AI Low Confidence Response', [ 'conversation_id' => $conversation->id, 'query' => $userMessage, 'confidence' => $analysis['confidence'], ]); }

    // Event: SQL Execution Error \Log::error('AI SQL Execution Failed', [ 'conversation_id' => $conversation->id, 'sql' => $sql, 'error' => $exception->getMessage(), ]);

    // Event: High Token Usage if ($tokensUsed > 2000) { \Log::info('AI High Token Usage', [ 'conversation_id' => $conversation->id, 'tokens' => $tokensUsed, ]); } `

    Dashboard Admin (Blade View)

    `blade <!-- resources/views/admin/i40/ai/dashboard.blade.php --> <div class="container-fluid"> <h3>AI Assistant Analytics</h3> <div class="row"> <!-- Usage Stats --> <div class="col-md-3"> <div class="card"> <div class="card-body"> <h5>{{ $metrics['total_conversations'] }}</h5> <small>Conversazioni (30gg)</small> </div> </div> </div> <div class="col-md-3"> <div class="card"> <div class="card-body"> <h5>{{ $metrics['active_users'] }}</h5> <small>Utenti Attivi</small> </div> </div> </div> <!-- Quality Stats --> <div class="col-md-3"> <div class="card"> <div class="card-body"> <h5>{{ number_format($metrics['satisfaction_rate'], 1) }}%</h5> <small>Soddisfazione</small> </div> </div> </div> <!-- Cost Stats --> <div class="col-md-3"> <div class="card"> <div class="card-body"> <h5>${{ number_format($metrics['estimated_cost'], 2) }}</h5> <small>Costo Stimato</small> </div> </div> </div> </div> <!-- Query Performance Chart --> <div class="card mt-4"> <div class="card-body"> <h5>Query piΓΉ Frequenti</h5> <canvas id="topQueriesChart"></canvas> </div> </div> </div> `

    ---

    πŸ”„ Continuous Improvement

    Fine-Tuning Strategy

    1. Raccolta Dati per Training

    `php // Salva query + risposta + feedback per futuro fine-tuning class FineTuningDataCollector { public function collect(AiMessage $userMessage, AiMessage $aiResponse, ?AiFeedback $feedback) { if ($feedback && $feedback->rating === 'up') { // Salva coppia query-risposta positiva \Storage::append('ai-training-data.jsonl', json_encode([ 'messages' => [ ['role' => 'user', 'content' => $userMessage->content], ['role' => 'assistant', 'content' => $aiResponse->content], ], 'metadata' => [ 'confidence' => $aiResponse->metadata['confidence'] ?? null, 'feedback' => 'positive', ], ])); } } } `

    2. Periodic Review

    Scheduler mensile:

    `php protected function schedule(Schedule $schedule) { // Review AI performance mensile $schedule->call(function () { $analytics = app(AiAnalyticsService::class); $report = $analytics->generateMonthlyReport(); // Invia report a admin \Mail::to(config('ai.admin_email')) ->send(new MonthlyAiReportMail($report)); })->monthly(); } `

    ---

    πŸŽ“ Training & Documentation

    User Guide (Per Operatori)

    `

    markdown
    <h1>Guida AI Assistant - Analisi Produzione</h1></p><p><h2>Come Fare Domande Efficaci</h2></p><p><h3>βœ… Buone Domande</h3>
    <li>&quot;Qual Γ¨ stata la produzione di ieri?&quot;</li>
    <li>&quot;Confronta questa settimana con la scorsa&quot;</li>
    <li>&quot;Chi Γ¨ l&#039;operatore piΓΉ veloce?&quot;</li>
    <li>&quot;Mostrami il trend dell&#039;ultimo mese&quot;</li></p><p><h3>❌ Domande Poco Chiare</h3>
    <li>&quot;Dimmi qualcosa&quot; (troppo vaga)</li>
    <li>&quot;Ieri&quot; (manca il contesto: produzione? tempo? operatore?)</li>
    <li>&quot;PerchΓ©?&quot; (senza riferimento a cosa)</li></p><p><h2>Esempi Pratici</h2></p><p><h3>Scenario 1: Controllo Giornaliero</h3>
    <strong>Query:</strong> &quot;Qual Γ¨ stata la produzione di oggi?&quot;
    β†’ L&#039;AI ti dirΓ  il totale pezzi prodotti oggi e lo confronterΓ  con la media.</p><p><h3>Scenario 2: Identificare Problemi</h3>
    <strong>Query:</strong> &quot;PerchΓ© oggi abbiamo prodotto meno?&quot;
    β†’ L&#039;AI analizzerΓ  fermi macchina, ritardi, operatori e ti darΓ  le cause.</p><p><h3>Scenario 3: Pianificazione</h3>
    <strong>Query:</strong> &quot;Ce la facciamo a finire 3000 pezzi entro domani?&quot;
    β†’ L&#039;AI calcolerΓ  la proiezione basata sul ritmo attuale.</p><p><h2>Tips &amp; Tricks</h2>
    <li>πŸ’‘ Usa il microfono per query vocali veloci</li>
    <li>πŸ‘ Dai feedback (pollice su/giΓΉ) per migliorare le risposte</li>
    <li>πŸ“Š Chiedi sempre &quot;Mostrami un grafico&quot; per visualizzare i dati</li>
    <li>πŸ”„ Usa i suggerimenti automatici per approfondire</li>
    `

    ---

    βœ… Checklist Implementazione

    Fase 1: Foundation βœ…

  • [ ] Creare migrations database (4 tabelle)
  • [ ] Creare Models Eloquent
  • [ ] Configurare .env con API key OpenAI
  • [ ] Testare connessione database
  • Fase 2: Knowledge Base βœ…

  • [ ] Implementare DatabaseSchemaService
  • [ ] Implementare AiContextService
  • [ ] Implementare QueryUnderstandingService
  • [ ] Testare estrazione schema profilo
  • Fase 3: AI Integration βœ…

  • [ ] Installare openai-php/laravel
  • [ ] Implementare OpenAiService
  • [ ] Implementare AiChatController
  • [ ] Testare Function Calling con mock
  • [ ] Implementare SQL safety checks
  • [ ] Testare end-to-end con query reali
  • Fase 4: UI/UX βœ…

  • [ ] Creare Vue component AiChatWidget
  • [ ] Integrare in analyze.blade.php
  • [ ] Implementare voice input (opzionale)
  • [ ] Testare responsive design
  • [ ] Implementare feedback UI
  • Fase 5: Advanced Features (Opzionale)

  • [ ] Implementare AiCacheService
  • [ ] Setup Scheduled Job pulizia cache
  • [ ] Implementare export conversazioni
  • [ ] Implementare alert automation
  • [ ] Implementare response suggestions
  • Testing & QA

  • [ ] Unit tests (services)
  • [ ] Integration tests (controller)
  • [ ] Performance tests (response time)
  • [ ] Security tests (SQL injection)
  • [ ] User acceptance testing
  • Deployment

  • [ ] Verificare .env production
  • [ ] Setup monitoring (logs, analytics)
  • [ ] Configurare backups database AI
  • [ ] Documentazione utente
  • [ ] Training team
  • ---

    πŸ“– Risorse e Riferimenti

    Documentazione Tecnica

  • OpenAI API: https://platform.openai.com/docs/api-reference
  • Function Calling: https://platform.openai.com/docs/guides/function-calling
  • Laravel OpenAI Package: https://github.com/openai-php/laravel
  • Chart.js: https://www.chartjs.org/docs/latest/
  • Vue 3: https://vuejs.org/guide/introduction.html
  • Best Practices AI

  • Prompt Engineering: https://www.promptingguide.ai/
  • Text-to-SQL: https://arxiv.org/abs/2204.00498
  • AI Safety: https://www.anthropic.com/index/ai-safety
  • Community & Support

  • OpenAI Community: https://community.openai.com/
  • Laravel Forum: https://laracasts.com/discuss
  • Stack Overflow: Tag openai-api
    , </code>laravel<code></li></p><p>---</p><p><h2>🎯 Next Steps</h2></p><p><h3>Settimana 1-2: Kickoff</h3>
    1. βœ… Review roadmap con team
    2. βœ… Setup ambiente sviluppo
    3. βœ… Ottenere API key OpenAI
    4. βœ… Creare database tables</p><p><h3>Settimana 3-4: Core Development</h3>
    1. βœ… Implementare Knowledge Base services
    2. βœ… Testare estrazione schema
    3. βœ… Prime integrazioni OpenAI</p><p><h3>Settimana 5-6: AI Integration</h3>
    1. βœ… Controller completo
    2. βœ… SQL execution sicura
    3. βœ… Testing query reali
    4. βœ… Ottimizzazione prompt</p><p><h3>Settimana 7: UI/UX</h3>
    1. βœ… Vue component
    2. βœ… Integrazione view
    3. βœ… Testing utente
    4. βœ… Refinement UX</p><p><h3>Settimana 8: Testing &amp; Launch</h3>
    1. βœ… Testing completo
    2. βœ… Documentazione
    3. βœ… Training team
    4. βœ… Soft launch (utenti pilota)
    5. βœ… Feedback &amp; iterate</p><p>---</p><p><h2>πŸ“ Note Finali</h2></p><p><h3>PerchΓ© Questo Approccio?</h3></p><p>1. <strong>Graduale:</strong> Parti con MVP funzionale (Fasi 1-4), poi aggiungi features avanzate
    2. <strong>Scalabile:</strong> Architettura pronta per crescere (piΓΉ macchine, piΓΉ utenti)
    3. <strong>Cost-Effective:</strong> Cache e ottimizzazioni per minimizzare costi API
    4. <strong>User-Centric:</strong> Focus su domande reali degli operatori, non su tech per il gusto della tech
    5. <strong>Maintainable:</strong> Codice ben strutturato, testato, documentato</p><p><h3>Rischi e Mitigazioni</h3></p><p>| Rischio | ProbabilitΓ  | Impatto | Mitigazione |
    |---------|-------------|---------|-------------|
    | Costi API alti | Media | Medio | Cache aggressiva, limiti utente |
    | Risposte imprecise | Alta (iniziale) | Alto | Feedback loop, fine-tuning |
    | Latency alta | Bassa | Medio | Cache, async processing |
    | Privacy concerns | Bassa | Alto | Azure OpenAI, data anonymization |
    | User adoption | Media | Alto | Training, UX intuitiva, quick wins |</p><p><h3>Success Metrics (KPI)</h3></p><p><strong>Mese 1-2 (Post-Launch):</strong>
    <li>[ ] 80% utenti target provano l&#039;AI almeno 1 volta</li>
    <li>[ ] Satisfaction rate &gt; 70%</li>
    <li>[ ] Avg response time &lt; 5s</li>
    <li>[ ] 30% query risolte senza supporto umano</li></p><p><strong>Mese 3-6 (MaturitΓ ):</strong>
    <li>[ ] 60% utenti usano AI settimanalmente</li>
    <li>[ ] Satisfaction rate &gt; 85%</li>
    <li>[ ] 50% query risolte autonomamente</li>
    <li>[ ] ROI positivo (risparmi &gt; costi)</li></ul></p><p>---</p><p><strong>πŸ“ Documento Completo Salvato In:</strong>
    /Users/nscapati/Dropbox/SFTP/sartUP/MD/i40/AI_ASSISTANT_ROADMAP.md`

    Ultima Revisione: 20 Ottobre 2025 Versione: 1.0 Final Autore: AI Agent + User Collaboration Status: βœ… Production Ready

    ---

    πŸš€ Ready to Build!

    Questo documento fornisce tutto il necessario per implementare un AI Assistant completo per l'analisi produzione. La roadmap Γ¨ strutturata per essere seguita passo-passo, con codice funzionante, best practices e considerazioni pratiche.

    Prossimi Step Consigliati: 1. Review documento con team tecnico 2. Setup ambiente sviluppo (OpenAI API key) 3. Inizia con Fase 1 (Foundation) - 2 settimane 4. Procedi sequenzialmente attraverso le fasi 5. Itera basandosi su feedback reale utenti

    Buona implementazione! πŸŽ‰

Analisi Codice

Blocco 1
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    FRONTEND (Vue/React Component)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  Chat UI:                                                 β”‚  β”‚
β”‚  β”‚  β€’ Input utente (textarea + voice)                        β”‚  β”‚
β”‚  β”‚  β€’ Storico conversazione                                  β”‚  β”‚
β”‚  β”‚  β€’ Suggerimenti query                                     β”‚  β”‚
β”‚  β”‚  β€’ Visualizzazione grafici inline                         β”‚  β”‚
β”‚  β”‚  β€’ Export risposte (PDF/Excel)                            β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚ WebSocket / AJAX
                         ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    BACKEND (Laravel API)                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  AI Controller:                                           β”‚  β”‚
β”‚  β”‚  β€’ POST /ai/chat (nuova query)                            β”‚  β”‚
β”‚  β”‚  β€’ GET  /ai/history (conversazioni passate)               β”‚  β”‚
β”‚  β”‚  β€’ POST /ai/feedback (thumbs up/down)                     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  AI Service Layer:                                        β”‚  β”‚
β”‚  β”‚  β€’ Query Understanding (NLP)                              β”‚  β”‚
β”‚  β”‚  β€’ Context Builder (prepara dati per LLM)                 β”‚  β”‚
β”‚  β”‚  β€’ Response Generator (chiama LLM API)                    β”‚  β”‚
β”‚  β”‚  β€’ SQL Generator (converte linguaggio β†’ query)            β”‚  β”‚
β”‚  β”‚  β€’ Cache Layer (evita chiamate duplicate)                 β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                         β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      ↓                  ↓                  ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   LLM API    β”‚  β”‚  Vector DB   β”‚  β”‚  MySQL DB    β”‚
β”‚  (OpenAI/    β”‚  β”‚  (Embeddings)β”‚  β”‚  (Raw Data)  β”‚
β”‚   Anthropic) β”‚  β”‚  - Pinecone  β”‚  β”‚  - machine_  β”‚
β”‚              β”‚  β”‚  - Weaviate  β”‚  β”‚    operationsβ”‚
β”‚              β”‚  β”‚  - Qdrant    β”‚  β”‚    _log      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Blocco 2
1. UTENTE: "Qual Γ¨ stata la produzione di ieri?"
           ↓
2. FRONTEND: Invia POST /ai/chat con { query: "...", machine_id: 5, context: {...} }
           ↓
3. BACKEND (AI Service):
   a) Query Understanding
      β†’ Estrae intent: "produzione_totale"
      β†’ Estrae entitΓ : periodo="ieri", metrica="pezzi"
      β†’ Estrae filtri: machine_id=5
   
   b) Context Builder
      β†’ Recupera profilo macchina (colonne, aggregatori)
      β†’ Recupera ultimi 30 giorni di dati (per contesto)
      β†’ Prepara prompt per LLM:
         "Sei un assistente AI per analisi produzione.
          Database schema: [columns...]
          Ultimi dati: [sample rows...]
          Query utente: 'Qual Γ¨ stata la produzione di ieri?'
          Genera SQL query e spiega il risultato."
   
   c) LLM API Call
      β†’ Invia prompt a OpenAI GPT-4 / Claude
      β†’ Riceve risposta:
         {
           "sql": "SELECT SUM(JSON_EXTRACT(...)) FROM ... WHERE DATE(timestamp)=CURDATE()-1",
           "explanation": "Ho calcolato la somma dei pezzi prodotti ieri...",
           "confidence": 0.95
         }
   
   d) SQL Execution
      β†’ Valida SQL (whitelist operazioni, prevent injection)
      β†’ Esegue query su DB
      β†’ Ottiene risultato: 1500 pezzi
   
   e) Response Generation
      β†’ LLM formatta risposta user-friendly:
         "Ieri hai prodotto 1.500 pezzi, in linea con la media settimanale di 1.450."
      β†’ Genera grafico suggerito (se rilevante)
      β†’ Aggiunge follow-up suggestions
           ↓
4. FRONTEND: Mostra risposta + grafico + suggestions
Blocco 3 php
// Laravel
composer require openai-php/laravel

// Vector DB (opzionale per ora)
// composer require pinecone/pinecone-php
Blocco 4
SETTIMANA 1-2:  FASE 1 - Foundation
SETTIMANA 3-4:  FASE 2 - Knowledge Base
SETTIMANA 5-6:  FASE 3 - AI Integration
SETTIMANA 7:    FASE 4 - UI/UX
SETTIMANA 8:    FASE 5 - Testing & Refinement
Blocco 5 sql
CREATE TABLE ai_conversations (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  machine_id BIGINT UNSIGNED NULL, -- Contesto macchina (opzionale)
  title VARCHAR(255), -- Auto-generato dal primo messaggio
  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),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
Blocco 6 sql
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, -- { sql: "...", confidence: 0.95, execution_time: 1.2, chart_config: {...} }
  tokens_used INT UNSIGNED, -- Per tracking costi
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  INDEX idx_conversation (conversation_id),
  FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE
);
Blocco 7 sql
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,
  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
);
Blocco 8 sql
CREATE TABLE ai_cache (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  query_hash VARCHAR(64) UNIQUE NOT NULL, -- MD5/SHA256 della query normalizzata
  machine_id BIGINT UNSIGNED NULL,
  date_range VARCHAR(50), -- "2025-10-01_2025-10-20"
  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)
);
Blocco 9 bash
php artisan make:migration create_ai_assistant_tables
Blocco 10 php
// database/migrations/*_create_ai_assistant_tables.php
public function up()
{
    Schema::create('ai_conversations', function (Blueprint $table) {
        $table->id();
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->foreignId('machine_id')->nullable()->constrained('machines_i40')->onDelete('set null');
        $table->string('title')->nullable();
        $table->timestamps();
        
        $table->index('user_id');
        $table->index('machine_id');
    });
    
    Schema::create('ai_messages', function (Blueprint $table) {
        $table->id();
        $table->foreignId('conversation_id')->constrained('ai_conversations')->onDelete('cascade');
        $table->enum('role', ['user', 'assistant', 'system']);
        $table->text('content');
        $table->json('metadata')->nullable();
        $table->unsignedInteger('tokens_used')->nullable();
        $table->timestamp('created_at')->useCurrent();
        
        $table->index('conversation_id');
    });
    
    Schema::create('ai_feedback', function (Blueprint $table) {
        $table->id();
        $table->foreignId('message_id')->constrained('ai_messages')->onDelete('cascade');
        $table->foreignId('user_id')->constrained()->onDelete('cascade');
        $table->enum('rating', ['up', 'down']);
        $table->text('comment')->nullable();
        $table->timestamp('created_at')->useCurrent();
    });
    
    Schema::create('ai_cache', function (Blueprint $table) {
        $table->id();
        $table->string('query_hash', 64)->unique();
        $table->foreignId('machine_id')->nullable()->constrained('machines_i40')->onDelete('cascade');
        $table->string('date_range', 50)->nullable();
        $table->json('response_data');
        $table->unsignedInteger('hits')->default(0);
        $table->timestamp('created_at')->useCurrent();
        $table->timestamp('expires_at')->nullable();
        
        $table->index('query_hash');
        $table->index('expires_at');
    });
}
Blocco 11 bash
php artisan make:model I40/AiConversation
php artisan make:model I40/AiMessage
php artisan make:model I40/AiFeedback
Blocco 12 php
// app/Models/I40/AiConversation.php
class AiConversation extends Model
{
    protected $fillable = ['user_id', 'machine_id', 'title'];
    
    public function messages()
    {
        return $this->hasMany(AiMessage::class, 'conversation_id');
    }
    
    public function user()
    {
        return $this->belongsTo(User::class);
    }
    
    public function machine()
    {
        return $this->belongsTo(Machine::class);
    }
    
    // Auto-genera titolo dal primo messaggio
    public function generateTitle()
    {
        $firstMessage = $this->messages()->where('role', 'user')->first();
        if ($firstMessage) {
            $this->title = Str::limit($firstMessage->content, 50);
            $this->save();
        }
    }
}
Blocco 13 php
// app/Models/I40/AiMessage.php
class AiMessage extends Model
{
    protected $fillable = ['conversation_id', 'role', 'content', 'metadata', 'tokens_used'];
    protected $casts = ['metadata' => 'array'];
    public $timestamps = false;
    
    public function conversation()
    {
        return $this->belongsTo(AiConversation::class, 'conversation_id');
    }
    
    public function feedback()
    {
        return $this->hasOne(AiFeedback::class, 'message_id');
    }
}
Blocco 14 bash
# .env aggiungi
OPENAI_API_KEY=sk-proj-...
OPENAI_MODEL=gpt-4-turbo-preview
OPENAI_MAX_TOKENS=1000
AI_CACHE_TTL=3600 # 1 ora
AI_ENABLED=true
Blocco 15 php
// config/ai.php
return [
    'enabled' => env('AI_ENABLED', true),
    
    'openai' => [
        'api_key' => env('OPENAI_API_KEY'),
        'model' => env('OPENAI_MODEL', 'gpt-4-turbo-preview'),
        'max_tokens' => env('OPENAI_MAX_TOKENS', 1000),
        'temperature' => 0.3, // Basso per risposte deterministiche
    ],
    
    'cache' => [
        'enabled' => true,
        'ttl' => env('AI_CACHE_TTL', 3600), // secondi
    ],
    
    'limits' => [
        'max_messages_per_conversation' => 50,
        'max_conversations_per_user' => 100,
        'max_query_length' => 500, // caratteri
    ],
];
Blocco 16 php
// app/Services/I40/DatabaseSchemaService.php
namespace App\Services\I40;

class DatabaseSchemaService
{
    /**
     * Estrae schema leggibile per LLM dal profilo macchina
     */
    public function getSchemaForMachine(Machine $machine): array
    {
        $profile = $machine->getActiveProfile();
        if (!$profile) return [];
        
        $columns = $profile->columns ?? [];
        $columnNames = $profile->column_names ?? [];
        
        $schema = [
            'table' => 'machine_operations_log',
            'description' => "Log operazioni macchina: {$machine->name}",
            'columns' => []
        ];
        
        // Colonne standard
        $schema['columns'][] = [
            'name' => 'id',
            'type' => 'integer',
            'description' => 'ID univoco operazione'
        ];
        
        $schema['columns'][] = [
            'name' => 'timestamp',
            'type' => 'datetime',
            'description' => 'Data e ora operazione',
            'is_primary_timestamp' => true
        ];
        
        // Colonne dinamiche da profilo
        foreach ($columns as $index => $col) {
            $csvName = $col['csv_name'] ?? $col['name'];
            $label = $columnNames[$index] ?? $col['label'] ?? $csvName;
            
            $schema['columns'][] = [
                'name' => $csvName,
                'type' => $col['type'] ?? 'string',
                'description' => $label,
                'is_filterable' => $col['is_filterable'] ?? false,
                'is_aggregatable' => $col['is_aggregatable'] ?? false,
                'aggregators' => $col['aggregators'] ?? [],
                'sample_values' => $this->getSampleValues($machine->id, $csvName, 3)
            ];
        }
        
        return $schema;
    }
    
    /**
     * Ottiene valori di esempio per una colonna
     */
    protected function getSampleValues(int $machineId, string $columnName, int $limit = 3): array
    {
        return \DB::table('machine_operations_log')
            ->where('machine_id', $machineId)
            ->selectRaw("DISTINCT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{$columnName}')) as val")
            ->whereNotNull(\DB::raw("JSON_EXTRACT(raw_data, '$.{$columnName}')"))
            ->limit($limit)
            ->pluck('val')
            ->toArray();
    }
    
    /**
     * Genera descrizione testuale dello schema per prompt LLM
     */
    public function generateSchemaPrompt(Machine $machine): string
    {
        $schema = $this->getSchemaForMachine($machine);
        
        $prompt = "DATABASE SCHEMA PER MACCHINA: {$machine->name}\n\n";
        $prompt .= "Tabella: {$schema['table']}\n";
        $prompt .= "Descrizione: {$schema['description']}\n\n";
        $prompt .= "COLONNE DISPONIBILI:\n";
        
        foreach ($schema['columns'] as $col) {
            $prompt .= "- {$col['name']} ({$col['type']}): {$col['description']}\n";
            
            if (!empty($col['aggregators'])) {
                $prompt .= "  β†’ Aggregabile: " . implode(', ', $col['aggregators']) . "\n";
            }
            
            if (!empty($col['sample_values'])) {
                $prompt .= "  β†’ Esempi: " . implode(', ', array_slice($col['sample_values'], 0, 3)) . "\n";
            }
        }
        
        return $prompt;
    }
}
Blocco 17 php
// app/Services/I40/AiContextService.php
namespace App\Services\I40;

class AiContextService
{
    protected $schemaService;
    
    public function __construct(DatabaseSchemaService $schemaService)
    {
        $this->schemaService = $schemaService;
    }
    
    /**
     * Prepara contesto completo per LLM
     */
    public function buildContext(array $params): array
    {
        $machineId = $params['machine_id'] ?? null;
        $dateFrom = $params['date_from'] ?? now()->subMonth();
        $dateTo = $params['date_to'] ?? now();
        
        $machine = Machine::find($machineId);
        if (!$machine) {
            throw new \Exception('Macchina non trovata');
        }
        
        return [
            'schema' => $this->schemaService->generateSchemaPrompt($machine),
            'statistics' => $this->getRecentStatistics($machine, $dateFrom, $dateTo),
            'sample_data' => $this->getSampleRecords($machine, 5),
            'business_rules' => $this->getBusinessRules($machine),
        ];
    }
    
    /**
     * Statistiche recenti per dare contesto temporale
     */
    protected function getRecentStatistics(Machine $machine, $dateFrom, $dateTo): string
    {
        $stats = MachineOperationLog::query()
            ->where('machine_id', $machine->id)
            ->whereBetween('timestamp', [$dateFrom, $dateTo])
            ->selectRaw('
                COUNT(*) as total_operations,
                COUNT(DISTINCT DATE(timestamp)) as active_days,
                MIN(timestamp) as first_operation,
                MAX(timestamp) as last_operation
            ')
            ->first();
        
        return sprintf(
            "STATISTICHE PERIODO %s - %s:\n" .
            "- Operazioni totali: %d\n" .
            "- Giorni attivi: %d\n" .
            "- Prima operazione: %s\n" .
            "- Ultima operazione: %s",
            $dateFrom->format('d/m/Y'),
            $dateTo->format('d/m/Y'),
            $stats->total_operations,
            $stats->active_days,
            $stats->first_operation,
            $stats->last_operation
        );
    }
    
    /**
     * Sample records per dare esempi concreti
     */
    protected function getSampleRecords(Machine $machine, int $limit = 5): string
    {
        $records = MachineOperationLog::where('machine_id', $machine->id)
            ->orderBy('timestamp', 'desc')
            ->limit($limit)
            ->get();
        
        if ($records->isEmpty()) {
            return "Nessun dato disponibile.";
        }
        
        $sample = "ESEMPI OPERAZIONI RECENTI (ultimi {$limit} record):\n";
        foreach ($records as $i => $record) {
            $sample .= sprintf(
                "%d. [%s] %s\n",
                $i + 1,
                $record->timestamp->format('Y-m-d H:i'),
                json_encode(array_slice($record->raw_data, 0, 5), JSON_UNESCAPED_UNICODE)
            );
        }
        
        return $sample;
    }
    
    /**
     * Regole di business specifiche azienda
     */
    protected function getBusinessRules(Machine $machine): string
    {
        // Qui puoi aggiungere regole custom basate sul tipo macchina
        $rules = [
            "REGOLE DI BUSINESS:",
            "- L'orario lavorativo standard Γ¨ 07:00-19:00",
            "- La produzione media attesa Γ¨ definita nel profilo macchina",
            "- Un 'avviamento' Γ¨ un setup macchina prima di una lavorazione",
        ];
        
        // Aggiungi regole specifiche dalla configurazione profilo se disponibili
        $profile = $machine->getActiveProfile();
        if ($profile && isset($profile->business_rules)) {
            $rules = array_merge($rules, $profile->business_rules);
        }
        
        return implode("\n", $rules);
    }
}
Blocco 18 php
// app/Services/I40/QueryUnderstandingService.php
namespace App\Services\I40;

class QueryUnderstandingService
{
    /**
     * Analizza query utente ed estrae intent/entitΓ 
     */
    public function analyze(string $userQuery): array
    {
        // Step 1: Normalizza query
        $normalized = $this->normalize($userQuery);
        
        // Step 2: Detect intent
        $intent = $this->detectIntent($normalized);
        
        // Step 3: Extract entities
        $entities = $this->extractEntities($normalized);
        
        return [
            'original' => $userQuery,
            'normalized' => $normalized,
            'intent' => $intent,
            'entities' => $entities,
            'confidence' => $this->calculateConfidence($intent, $entities),
        ];
    }
    
    /**
     * Normalizza query (lowercase, rimuovi punteggiatura, etc.)
     */
    protected function normalize(string $query): string
    {
        $query = strtolower($query);
        $query = preg_replace('/[^a-z0-9\sàèéìòù]/', '', $query);
        return trim($query);
    }
    
    /**
     * Rileva intent principale della query
     */
    protected function detectIntent(string $query): string
    {
        $patterns = [
            'production_total' => ['produzione', 'prodotto', 'pezzi', 'quantitΓ ', 'quanti'],
            'production_average' => ['media', 'mediamente', 'in media'],
            'production_comparison' => ['confronto', 'rispetto', 'differenza', 'piΓΉ', 'meno'],
            'trend_analysis' => ['trend', 'andamento', 'evoluzione', 'grafico'],
            'operator_performance' => ['operatore', 'chi ha', 'migliore', 'peggiore'],
            'time_analysis' => ['tempo', 'durata', 'velocitΓ ', 'quanto ci vuole'],
            'anomaly_detection' => ['anomalia', 'problema', 'strano', 'perchΓ©'],
            'prediction' => ['previsione', 'riusciremo', 'ce la facciamo', 'entro quando'],
            'optimization' => ['ottimizza', 'migliora', 'aumenta', 'suggerisci'],
        ];
        
        foreach ($patterns as $intent => $keywords) {
            foreach ($keywords as $keyword) {
                if (str_contains($query, $keyword)) {
                    return $intent;
                }
            }
        }
        
        return 'general_question';
    }
    
    /**
     * Estrae entitΓ  dalla query (date, numeri, nomi, etc.)
     */
    protected function extractEntities(string $query): array
    {
        $entities = [];
        
        // Date temporali
        if (preg_match('/ieri/', $query)) {
            $entities['period'] = 'yesterday';
            $entities['date_from'] = now()->subDay()->startOfDay();
            $entities['date_to'] = now()->subDay()->endOfDay();
        } elseif (preg_match('/oggi/', $query)) {
            $entities['period'] = 'today';
            $entities['date_from'] = now()->startOfDay();
            $entities['date_to'] = now()->endOfDay();
        } elseif (preg_match('/questa settimana/', $query)) {
            $entities['period'] = 'this_week';
            $entities['date_from'] = now()->startOfWeek();
            $entities['date_to'] = now()->endOfWeek();
        } elseif (preg_match('/ultimo mese|mese scorso/', $query)) {
            $entities['period'] = 'last_month';
            $entities['date_from'] = now()->subMonth()->startOfMonth();
            $entities['date_to'] = now()->subMonth()->endOfMonth();
        }
        
        // Numeri
        if (preg_match('/\d+/', $query, $matches)) {
            $entities['numbers'] = array_map('intval', $matches);
        }
        
        // Metriche
        $metrics = ['pezzi', 'tempo', 'durata', 'operazioni'];
        foreach ($metrics as $metric) {
            if (str_contains($query, $metric)) {
                $entities['metric'] = $metric;
                break;
            }
        }
        
        return $entities;
    }
    
    /**
     * Calcola confidence score (0-1)
     */
    protected function calculateConfidence(string $intent, array $entities): float
    {
        $score = 0.5; // Base
        
        if ($intent !== 'general_question') $score += 0.2;
        if (!empty($entities['period'])) $score += 0.15;
        if (!empty($entities['metric'])) $score += 0.15;
        
        return min($score, 1.0);
    }
}
Blocco 19 bash
composer require openai-php/laravel
php artisan vendor:publish --provider="OpenAI\Laravel\ServiceProvider"
Blocco 20 php
// app/Services/I40/OpenAiService.php
namespace App\Services\I40;

use OpenAI\Laravel\Facades\OpenAI;

class OpenAiService
{
    /**
     * Genera risposta AI per query utente
     */
    public function generateResponse(array $context, string $userQuery): array
    {
        $systemPrompt = $this->buildSystemPrompt($context);
        $messages = $this->buildMessages($systemPrompt, $userQuery);
        
        try {
            $response = OpenAI::chat()->create([
                'model' => config('ai.openai.model'),
                'messages' => $messages,
                'temperature' => config('ai.openai.temperature'),
                'max_tokens' => config('ai.openai.max_tokens'),
                'functions' => $this->getFunctionDefinitions(),
                'function_call' => 'auto',
            ]);
            
            $choice = $response->choices[0];
            $tokensUsed = $response->usage->totalTokens;
            
            // Se LLM vuole chiamare una funzione (es. generare SQL)
            if (isset($choice->message->functionCall)) {
                return $this->handleFunctionCall($choice->message->functionCall, $tokensUsed);
            }
            
            // Altrimenti risposta diretta
            return [
                'content' => $choice->message->content,
                'tokens_used' => $tokensUsed,
                'finish_reason' => $choice->finishReason,
            ];
            
        } catch (\Exception $e) {
            \Log::error('OpenAI API Error: ' . $e->getMessage());
            throw new \Exception('Errore nel servizio AI. Riprova piΓΉ tardi.');
        }
    }
    
    /**
     * Costruisce prompt di sistema (contesto + istruzioni)
     */
    protected function buildSystemPrompt(array $context): string
    {
        return <<<PROMPT
Sei un assistente AI specializzato nell'analisi di dati di produzione industriale.
Il tuo compito Γ¨ aiutare gli operatori a comprendere e ottimizzare le prestazioni delle macchine.

COMPETENZE:
- Analisi dati temporali (trend, medie, confronti)
- Generazione query SQL per estrarre informazioni
- Identificazione anomalie e pattern
- Suggerimenti per ottimizzazione produzione

CONTESTO DATABASE:
{$context['schema']}

{$context['statistics']}

{$context['sample_data']}

{$context['business_rules']}

ISTRUZIONI:
1. Rispondi sempre in italiano professionale ma accessibile
2. Se serve generare SQL, usa la function "execute_sql_query"
3. Formatta numeri con separatore migliaia (es. 1.500)
4. Suggerisci visualizzazioni (grafici) quando appropriato
5. Se non sei sicuro, ammettilo e chiedi chiarimenti
6. Fornisci sempre contesto (es. "rispetto alla media settimanale di...")
PROMPT;
    }
    
    /**
     * Costruisce array messaggi per API
     */
    protected function buildMessages(string $systemPrompt, string $userQuery): array
    {
        return [
            ['role' => 'system', 'content' => $systemPrompt],
            ['role' => 'user', 'content' => $userQuery],
        ];
    }
    
    /**
     * Definisce funzioni che LLM puΓ² chiamare (Function Calling)
     */
    protected function getFunctionDefinitions(): array
    {
        return [
            [
                'name' => 'execute_sql_query',
                'description' => 'Esegue una query SQL sul database produzione e restituisce i risultati',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'sql' => [
                            'type' => 'string',
                            'description' => 'Query SQL SELECT da eseguire (solo SELECT, no INSERT/UPDATE/DELETE)',
                        ],
                        'explanation' => [
                            'type' => 'string',
                            'description' => 'Spiegazione di cosa fa la query, per l\'utente',
                        ],
                    ],
                    'required' => ['sql', 'explanation'],
                ],
            ],
            [
                'name' => 'generate_chart',
                'description' => 'Suggerisce un grafico per visualizzare i dati',
                'parameters' => [
                    'type' => 'object',
                    'properties' => [
                        'chart_type' => [
                            'type' => 'string',
                            'enum' => ['line', 'bar', 'pie'],
                            'description' => 'Tipo di grafico',
                        ],
                        'x_field' => [
                            'type' => 'string',
                            'description' => 'Campo per asse X',
                        ],
                        'y_metric' => [
                            'type' => 'string',
                            'description' => 'Metrica per asse Y',
                        ],
                    ],
                    'required' => ['chart_type', 'x_field', 'y_metric'],
                ],
            ],
        ];
    }
    
    /**
     * Gestisce chiamata a funzione da parte di LLM
     */
    protected function handleFunctionCall(object $functionCall, int $tokensUsed): array
    {
        $functionName = $functionCall->name;
        $arguments = json_decode($functionCall->arguments, true);
        
        if ($functionName === 'execute_sql_query') {
            $sql = $arguments['sql'];
            $explanation = $arguments['explanation'];
            
            // Valida e esegui SQL
            $results = $this->executeSafeSql($sql);
            
            return [
                'content' => $this->formatSqlResults($results, $explanation),
                'tokens_used' => $tokensUsed,
                'metadata' => [
                    'sql' => $sql,
                    'explanation' => $explanation,
                    'result_count' => count($results),
                ],
            ];
        }
        
        if ($functionName === 'generate_chart') {
            return [
                'content' => "Grafico suggerito generato.",
                'tokens_used' => $tokensUsed,
                'metadata' => [
                    'chart_config' => $arguments,
                ],
            ];
        }
        
        throw new \Exception('Funzione non supportata: ' . $functionName);
    }
    
    /**
     * Esegue SQL in modo sicuro (whitelist operazioni, prevent injection)
     */
    protected function executeSafeSql(string $sql): array
    {
        // Validazione: solo SELECT
        if (!preg_match('/^SELECT\s+/i', trim($sql))) {
            throw new \Exception('Solo query SELECT sono permesse');
        }
        
        // Validazione: no operazioni pericolose
        $forbidden = ['DROP', 'DELETE', 'UPDATE', 'INSERT', 'TRUNCATE', 'ALTER', 'CREATE'];
        foreach ($forbidden as $keyword) {
            if (stripos($sql, $keyword) !== false) {
                throw new \Exception('Query non permessa: contiene ' . $keyword);
            }
        }
        
        // Esegui con timeout
        try {
            $results = \DB::select($sql);
            return json_decode(json_encode($results), true); // Convert to array
        } catch (\Exception $e) {
            \Log::error('SQL Execution Error: ' . $e->getMessage());
            throw new \Exception('Errore esecuzione query: ' . $e->getMessage());
        }
    }
    
    /**
     * Formatta risultati SQL per risposta user-friendly
     */
    protected function formatSqlResults(array $results, string $explanation): string
    {
        if (empty($results)) {
            return "Non ho trovato dati per questa query. " . $explanation;
        }
        
        $response = $explanation . "\n\n";
        
        // Se singolo valore scalare (es. COUNT, SUM)
        if (count($results) === 1 && count($results[0]) === 1) {
            $value = array_values($results[0])[0];
            $response .= "**Risultato:** " . number_format($value, 0, ',', '.') . "\n";
            return $response;
        }
        
        // Se lista valori, mostra tabella semplice
        $response .= "**Risultati trovati:** " . count($results) . "\n\n";
        
        // Mostra primi 10 risultati
        $displayResults = array_slice($results, 0, 10);
        foreach ($displayResults as $i => $row) {
            $response .= ($i + 1) . ". ";
            $response .= implode(' | ', array_map(function($k, $v) {
                return "$k: $v";
            }, array_keys($row), $row));
            $response .= "\n";
        }
        
        if (count($results) > 10) {
            $response .= "\n... e altri " . (count($results) - 10) . " risultati.\n";
        }
        
        return $response;
    }
}
Blocco 21 php
// app/Http/Controllers/Admin/I40/AiChatController.php
namespace App\Http\Controllers\Admin\I40;

use App\Http\Controllers\Controller;
use App\Services\I40\{
    AiContextService,
    QueryUnderstandingService,
    OpenAiService
};
use App\Models\I40\{AiConversation, AiMessage};
use Illuminate\Http\Request;

class AiChatController extends Controller
{
    protected $contextService;
    protected $queryService;
    protected $openAiService;
    
    public function __construct(
        AiContextService $contextService,
        QueryUnderstandingService $queryService,
        OpenAiService $openAiService
    ) {
        $this->contextService = $contextService;
        $this->queryService = $queryService;
        $this->openAiService = $openAiService;
    }
    
    /**
     * POST /ai/chat
     * Nuova query utente
     */
    public function chat(Request $request)
    {
        $validated = $request->validate([
            'conversation_id' => 'nullable|exists:ai_conversations,id',
            'machine_id' => 'required|exists:machines_i40,id',
            'message' => 'required|string|max:500',
            'date_from' => 'nullable|date',
            'date_to' => 'nullable|date',
        ]);
        
        // 1) Ottieni o crea conversazione
        $conversation = $this->getOrCreateConversation(
            $validated['conversation_id'] ?? null,
            $validated['machine_id'],
            auth()->id()
        );
        
        // 2) Salva messaggio utente
        $userMessage = $conversation->messages()->create([
            'role' => 'user',
            'content' => $validated['message'],
        ]);
        
        try {
            // 3) Analizza query
            $analysis = $this->queryService->analyze($validated['message']);
            
            // 4) Prepara contesto
            $context = $this->contextService->buildContext([
                'machine_id' => $validated['machine_id'],
                'date_from' => $validated['date_from'] ?? now()->subMonth(),
                'date_to' => $validated['date_to'] ?? now(),
            ]);
            
            // 5) Genera risposta AI
            $aiResponse = $this->openAiService->generateResponse(
                $context,
                $validated['message']
            );
            
            // 6) Salva risposta AI
            $assistantMessage = $conversation->messages()->create([
                'role' => 'assistant',
                'content' => $aiResponse['content'],
                'tokens_used' => $aiResponse['tokens_used'],
                'metadata' => $aiResponse['metadata'] ?? null,
            ]);
            
            // 7) Auto-genera titolo conversazione se prima query
            if ($conversation->messages()->count() === 2) {
                $conversation->generateTitle();
            }
            
            return response()->json([
                'success' => true,
                'conversation_id' => $conversation->id,
                'message' => [
                    'id' => $assistantMessage->id,
                    'role' => 'assistant',
                    'content' => $assistantMessage->content,
                    'metadata' => $assistantMessage->metadata,
                    'created_at' => $assistantMessage->created_at,
                ],
                'analysis' => $analysis, // Per debug
            ]);
            
        } catch (\Exception $e) {
            \Log::error('AI Chat Error: ' . $e->getMessage());
            
            // Risposta fallback
            $errorMessage = $conversation->messages()->create([
                'role' => 'assistant',
                'content' => 'Mi dispiace, ho avuto un problema nel processare la tua richiesta. Potresti riformulare la domanda?',
                'metadata' => ['error' => $e->getMessage()],
            ]);
            
            return response()->json([
                'success' => false,
                'error' => $e->getMessage(),
                'conversation_id' => $conversation->id,
                'message' => [
                    'id' => $errorMessage->id,
                    'role' => 'assistant',
                    'content' => $errorMessage->content,
                    'created_at' => $errorMessage->created_at,
                ],
            ], 500);
        }
    }
    
    /**
     * GET /ai/conversations
     * Lista conversazioni utente
     */
    public function conversations(Request $request)
    {
        $conversations = AiConversation::where('user_id', auth()->id())
            ->with(['messages' => function($q) {
                $q->latest()->limit(1); // Solo ultimo messaggio
            }])
            ->latest()
            ->paginate(20);
        
        return response()->json($conversations);
    }
    
    /**
     * GET /ai/conversations/{id}
     * Dettaglio conversazione (tutti i messaggi)
     */
    public function show(AiConversation $conversation)
    {
        $this->authorize('view', $conversation);
        
        $conversation->load('messages');
        
        return response()->json($conversation);
    }
    
    /**
     * POST /ai/feedback
     * Thumbs up/down su risposta AI
     */
    public function feedback(Request $request)
    {
        $validated = $request->validate([
            'message_id' => 'required|exists:ai_messages,id',
            'rating' => 'required|in:up,down',
            'comment' => 'nullable|string|max:500',
        ]);
        
        $feedback = AiFeedback::updateOrCreate(
            [
                'message_id' => $validated['message_id'],
                'user_id' => auth()->id(),
            ],
            [
                'rating' => $validated['rating'],
                'comment' => $validated['comment'] ?? null,
            ]
        );
        
        return response()->json(['success' => true, 'feedback' => $feedback]);
    }
    
    /**
     * Helper: Ottieni o crea conversazione
     */
    protected function getOrCreateConversation($conversationId, $machineId, $userId)
    {
        if ($conversationId) {
            $conversation = AiConversation::find($conversationId);
            if ($conversation && $conversation->user_id === $userId) {
                return $conversation;
            }
        }
        
        return AiConversation::create([
            'user_id' => $userId,
            'machine_id' => $machineId,
        ]);
    }
}
Blocco 22 php
// routes/i40.php (aggiungi)

Route::prefix('ai')->name('ai.')->group(function () {
    Route::post('/chat', [AiChatController::class, 'chat'])->name('chat');
    Route::get('/conversations', [AiChatController::class, 'conversations'])->name('conversations');
    Route::get('/conversations/{conversation}', [AiChatController::class, 'show'])->name('conversations.show');
    Route::post('/feedback', [AiChatController::class, 'feedback'])->name('feedback');
})->middleware('role:admin|maintenance|super-admin');
Blocco 23 bash
npm install vue@next vue-loader@next @vitejs/plugin-vue
Blocco 24 vue
<!-- resources/js/components/I40/AiChatWidget.vue -->
<template>
  <div class="ai-chat-widget" :class="{ 'minimized': minimized }">
    <!-- Header -->
    <div class="chat-header" @click="toggleMinimize">
      <div class="d-flex align-items-center">
        <div class="ai-avatar me-2">πŸ€–</div>
        <div>
          <strong>AI Assistant</strong>
          <small class="d-block text-muted">Analisi Produzione</small>
        </div>
      </div>
      <button class="btn btn-sm btn-link text-white">
        <i :class="minimized ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
      </button>
    </div>
    
    <!-- Chat Body -->
    <div v-show="!minimized" class="chat-body">
      <!-- Messaggi -->
      <div class="messages-container" ref="messagesContainer">
        <div v-if="messages.length === 0" class="empty-state">
          <div class="ai-avatar-large mb-3">πŸ€–</div>
          <h5>Ciao! Sono il tuo assistente AI</h5>
          <p class="text-muted">Chiedimi qualsiasi cosa sulla produzione di questa macchina.</p>
          
          <!-- Suggerimenti iniziali -->
          <div class="suggestions mt-3">
            <small class="text-muted d-block mb-2">Prova a chiedere:</small>
            <button 
              v-for="suggestion in initialSuggestions" 
              :key="suggestion"
              class="btn btn-sm btn-outline-primary mb-2 me-2"
              @click="sendMessage(suggestion)"
            >
              {{ suggestion }}
            </button>
          </div>
        </div>
        
        <!-- Lista messaggi -->
        <div 
          v-for="message in messages" 
          :key="message.id"
          class="message"
          :class="message.role"
        >
          <div class="message-avatar">
            {{ message.role === 'user' ? 'πŸ‘€' : 'πŸ€–' }}
          </div>
          <div class="message-content">
            <div class="message-text" v-html="formatMessage(message.content)"></div>
            
            <!-- Metadata (SQL, Grafici, etc.) -->
            <div v-if="message.metadata" class="message-metadata mt-2">
              <!-- Mostra SQL eseguito -->
              <div v-if="message.metadata.sql" class="sql-block">
                <button 
                  class="btn btn-xs btn-link" 
                  @click="toggleSql(message.id)"
                >
                  <i class="bi bi-code-square"></i> Mostra Query SQL
                </button>
                <pre v-show="expandedSql[message.id]" class="mt-2">{{ message.metadata.sql }}</pre>
              </div>
              
              <!-- Suggerisci grafico -->
              <div v-if="message.metadata.chart_config" class="chart-suggestion">
                <button 
                  class="btn btn-sm btn-primary"
                  @click="openChart(message.metadata.chart_config)"
                >
                  <i class="bi bi-bar-chart"></i> Visualizza Grafico
                </button>
              </div>
            </div>
            
            <!-- Feedback -->
            <div v-if="message.role === 'assistant'" class="message-feedback mt-2">
              <button 
                class="btn btn-xs" 
                :class="message.feedback === 'up' ? 'btn-success' : 'btn-outline-secondary'"
                @click="sendFeedback(message.id, 'up')"
                title="Risposta utile"
              >
                <i class="bi bi-hand-thumbs-up"></i>
              </button>
              <button 
                class="btn btn-xs ms-1"
                :class="message.feedback === 'down' ? 'btn-danger' : 'btn-outline-secondary'"
                @click="sendFeedback(message.id, 'down')"
                title="Risposta non utile"
              >
                <i class="bi bi-hand-thumbs-down"></i>
              </button>
            </div>
            
            <small class="text-muted d-block mt-1">{{ formatTime(message.created_at) }}</small>
          </div>
        </div>
        
        <!-- Loading indicator -->
        <div v-if="loading" class="message assistant">
          <div class="message-avatar">πŸ€–</div>
          <div class="message-content">
            <div class="typing-indicator">
              <span></span><span></span><span></span>
            </div>
          </div>
        </div>
      </div>
      
      <!-- Input -->
      <div class="chat-input">
        <textarea 
          v-model="userInput"
          @keydown.enter.prevent="sendMessage()"
          placeholder="Chiedi qualcosa sulla produzione..."
          rows="2"
          :disabled="loading"
          class="form-control"
        ></textarea>
        <button 
          @click="sendMessage()"
          :disabled="loading || !userInput.trim()"
          class="btn btn-primary btn-send"
        >
          <i class="bi bi-send"></i>
        </button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'AiChatWidget',
  props: {
    machineId: {
      type: Number,
      required: true
    },
    dateFrom: String,
    dateTo: String,
  },
  data() {
    return {
      minimized: false,
      messages: [],
      userInput: '',
      loading: false,
      conversationId: null,
      expandedSql: {},
      initialSuggestions: [
        "Qual Γ¨ stata la produzione di ieri?",
        "Mostrami il trend di questa settimana",
        "Chi Γ¨ l'operatore piΓΉ produttivo?",
      ],
    };
  },
  methods: {
    async sendMessage(text = null) {
      const message = text || this.userInput.trim();
      if (!message || this.loading) return;
      
      // Aggiungi messaggio utente alla UI
      this.messages.push({
        id: Date.now(),
        role: 'user',
        content: message,
        created_at: new Date().toISOString(),
      });
      
      this.userInput = '';
      this.loading = true;
      this.scrollToBottom();
      
      try {
        const response = await fetch('/admin/i40/ai/chat', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
          },
          body: JSON.stringify({
            conversation_id: this.conversationId,
            machine_id: this.machineId,
            message: message,
            date_from: this.dateFrom,
            date_to: this.dateTo,
          }),
        });
        
        const data = await response.json();
        
        if (data.success) {
          this.conversationId = data.conversation_id;
          this.messages.push(data.message);
        } else {
          this.messages.push({
            id: Date.now(),
            role: 'assistant',
            content: '❌ ' + (data.error || 'Errore nel processare la richiesta.'),
            created_at: new Date().toISOString(),
          });
        }
      } catch (error) {
        console.error('Chat error:', error);
        this.messages.push({
          id: Date.now(),
          role: 'assistant',
          content: '❌ Errore di connessione. Riprova.',
          created_at: new Date().toISOString(),
        });
      } finally {
        this.loading = false;
        this.scrollToBottom();
      }
    },
    
    async sendFeedback(messageId, rating) {
      try {
        await fetch('/admin/i40/ai/feedback', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
          },
          body: JSON.stringify({ message_id: messageId, rating }),
        });
        
        // Aggiorna UI
        const message = this.messages.find(m => m.id === messageId);
        if (message) message.feedback = rating;
      } catch (error) {
        console.error('Feedback error:', error);
      }
    },
    
    toggleMinimize() {
      this.minimized = !this.minimized;
    },
    
    toggleSql(messageId) {
      this.expandedSql[messageId] = !this.expandedSql[messageId];
    },
    
    openChart(chartConfig) {
      this.$emit('open-chart', chartConfig);
    },
    
    formatMessage(content) {
      // Converti markdown-like syntax in HTML
      return content
        .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
        .replace(/\n/g, '<br>');
    },
    
    formatTime(timestamp) {
      const date = new Date(timestamp);
      return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
    },
    
    scrollToBottom() {
      this.$nextTick(() => {
        const container = this.$refs.messagesContainer;
        if (container) {
          container.scrollTop = container.scrollHeight;
        }
      });
    },
  },
};
</script>

<style scoped>
.ai-chat-widget {
  position: fixed;
  bottom: 20px;
  right: 20px;
  width: 400px;
  max-height: 600px;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.15);
  display: flex;
  flex-direction: column;
  z-index: 1050;
  transition: all 0.3s ease;
}

.ai-chat-widget.minimized {
  max-height: 60px;
}

.chat-header {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 12px 16px;
  border-radius: 12px 12px 0 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  cursor: pointer;
}

.ai-avatar {
  width: 32px;
  height: 32px;
  background: rgba(255,255,255,0.2);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
}

.ai-avatar-large {
  width: 64px;
  height: 64px;
  background: #f0f0f0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
  margin: 0 auto;
}

.chat-body {
  display: flex;
  flex-direction: column;
  height: 540px;
}

.messages-container {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.empty-state {
  text-align: center;
  padding: 40px 20px;
}

.suggestions button {
  font-size: 0.85rem;
}

.message {
  display: flex;
  margin-bottom: 16px;
  align-items: flex-start;
}

.message.user {
  flex-direction: row-reverse;
}

.message-avatar {
  width: 32px;
  height: 32px;
  background: #f0f0f0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}

.message.user .message-avatar {
  background: #667eea;
  color: white;
}

.message-content {
  max-width: 75%;
  margin: 0 8px;
}

.message.user .message-content {
  background: #667eea;
  color: white;
  border-radius: 12px 12px 0 12px;
  padding: 10px 14px;
}

.message.assistant .message-content {
  background: #f0f0f0;
  border-radius: 12px 12px 12px 0;
  padding: 10px 14px;
}

.message-text {
  line-height: 1.5;
}

.sql-block pre {
  background: #2d2d2d;
  color: #f8f8f2;
  padding: 10px;
  border-radius: 6px;
  font-size: 0.85rem;
  overflow-x: auto;
}

.typing-indicator {
  display: flex;
  gap: 4px;
  padding: 10px 0;
}

.typing-indicator span {
  width: 8px;
  height: 8px;
  background: #999;
  border-radius: 50%;
  animation: typing 1.4s infinite;
}

.typing-indicator span:nth-child(2) {
  animation-delay: 0.2s;
}

.typing-indicator span:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes typing {
  0%, 60%, 100% { transform: translateY(0); }
  30% { transform: translateY(-10px); }
}

.chat-input {
  display: flex;
  padding: 12px;
  border-top: 1px solid #e0e0e0;
  gap: 8px;
}

.chat-input textarea {
  flex: 1;
  resize: none;
  border-radius: 8px;
}

.btn-send {
  width: 44px;
  height: 44px;
  border-radius: 8px;
}

.message-feedback .btn {
  padding: 2px 8px;
  font-size: 0.85rem;
}

/* Responsive */
@media (max-width: 768px) {
  .ai-chat-widget {
    width: calc(100% - 40px);
    max-width: 400px;
  }
}
</style>
Blocco 25 blade
<!-- resources/views/admin/i40/operations/analyze.blade.php -->

<!-- Aggiungi prima di @endpush -->
@push('scripts')
<script>
// Inizializza Vue component AI Chat
import { createApp } from 'vue';
import AiChatWidget from '@/components/I40/AiChatWidget.vue';

const app = createApp({});
app.component('ai-chat-widget', AiChatWidget);
app.mount('#ai-chat-container');
</script>
@endpush

<!-- Aggiungi nel body della view -->
<div id="ai-chat-container">
    <ai-chat-widget 
        :machine-id="{{ $selectedMachine->id }}"
        date-from="{{ $dateFrom->format('Y-m-d') }}"
        date-to="{{ $dateTo->format('Y-m-d') }}"
        @open-chart="openChartFromAi"
    ></ai-chat-widget>
</div>
Blocco 26 php
// app/Services/I40/AiCacheService.php
namespace App\Services\I40;

class AiCacheService
{
    /**
     * Ottieni risposta da cache se disponibile
     */
    public function get(string $query, int $machineId, array $filters = []): ?array
    {
        $hash = $this->generateHash($query, $machineId, $filters);
        
        $cached = \DB::table('ai_cache')
            ->where('query_hash', $hash)
            ->where('expires_at', '>', now())
            ->first();
        
        if ($cached) {
            // Incrementa hit counter
            \DB::table('ai_cache')
                ->where('id', $cached->id)
                ->increment('hits');
            
            return json_decode($cached->response_data, true);
        }
        
        return null;
    }
    
    /**
     * Salva risposta in cache
     */
    public function set(string $query, int $machineId, array $filters, array $response, int $ttl = null): void
    {
        $hash = $this->generateHash($query, $machineId, $filters);
        $ttl = $ttl ?? config('ai.cache.ttl', 3600);
        
        \DB::table('ai_cache')->updateOrInsert(
            ['query_hash' => $hash],
            [
                'machine_id' => $machineId,
                'date_range' => $this->getDateRange($filters),
                'response_data' => json_encode($response),
                'expires_at' => now()->addSeconds($ttl),
                'created_at' => now(),
            ]
        );
    }
    
    /**
     * Genera hash univoco per query
     */
    protected function generateHash(string $query, int $machineId, array $filters): string
    {
        // Normalizza query (lowercase, rimuovi spazi multipli)
        $normalized = strtolower(trim(preg_replace('/\s+/', ' ', $query)));
        
        // Combina con parametri
        $key = $normalized . '|' . $machineId . '|' . json_encode($filters);
        
        return hash('sha256', $key);
    }
    
    /**
     * Estrae date range dai filtri
     */
    protected function getDateRange(array $filters): ?string
    {
        $from = $filters['date_from'] ?? null;
        $to = $filters['date_to'] ?? null;
        
        if ($from && $to) {
            return "{$from}_{$to}";
        }
        
        return null;
    }
    
    /**
     * Pulisci cache scaduta
     */
    public function cleanExpired(): int
    {
        return \DB::table('ai_cache')
            ->where('expires_at', '<', now())
            ->delete();
    }
    
    /**
     * Invalida cache per macchina (dopo nuovi dati)
     */
    public function invalidateForMachine(int $machineId): int
    {
        return \DB::table('ai_cache')
            ->where('machine_id', $machineId)
            ->delete();
    }
}
Blocco 27 php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // Pulisci cache AI scaduta ogni ora
    $schedule->call(function () {
        app(AiCacheService::class)->cleanExpired();
    })->hourly();
}
Blocco 28 javascript
// Aggiungi al Vue component
data() {
    return {
        // ...
        isRecording: false,
        recognition: null,
    };
},
mounted() {
    this.initSpeechRecognition();
},
methods: {
    initSpeechRecognition() {
        if (!('webkitSpeechRecognition' in window)) {
            console.warn('Speech recognition not supported');
            return;
        }
        
        this.recognition = new webkitSpeechRecognition();
        this.recognition.lang = 'it-IT';
        this.recognition.continuous = false;
        this.recognition.interimResults = false;
        
        this.recognition.onresult = (event) => {
            const transcript = event.results[0][0].transcript;
            this.userInput = transcript;
            this.isRecording = false;
        };
        
        this.recognition.onerror = (event) => {
            console.error('Speech recognition error:', event.error);
            this.isRecording = false;
        };
        
        this.recognition.onend = () => {
            this.isRecording = false;
        };
    },
    
    toggleRecording() {
        if (!this.recognition) return;
        
        if (this.isRecording) {
            this.recognition.stop();
            this.isRecording = false;
        } else {
            this.recognition.start();
            this.isRecording = true;
        }
    },
}
Blocco 29 vue
<!-- Template: Aggiungi bottone microfono -->
<button 
    @click="toggleRecording"
    :class="{ 'recording': isRecording }"
    class="btn btn-outline-secondary btn-voice"
    title="Usa il microfono"
>
    <i class="bi" :class="isRecording ? 'bi-mic-fill' : 'bi-mic'"></i>
</button>
Blocco 30 css
/* Style */
.btn-voice.recording {
    animation: pulse 1.5s infinite;
    background: #dc3545;
    color: white;
    border-color: #dc3545;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}
Blocco 31 php
// app/Services/I40/ConversationExportService.php
namespace App\Services\I40;

use Barryvdh\DomPDF\Facade\Pdf;

class ConversationExportService
{
    /**
     * Esporta conversazione in PDF
     */
    public function exportToPdf(AiConversation $conversation): string
    {
        $data = [
            'conversation' => $conversation,
            'messages' => $conversation->messages,
            'machine' => $conversation->machine,
            'user' => $conversation->user,
        ];
        
        $pdf = Pdf::loadView('admin.i40.ai.export-pdf', $data);
        
        return $pdf->download("conversazione_{$conversation->id}.pdf");
    }
    
    /**
     * Esporta in formato JSON
     */
    public function exportToJson(AiConversation $conversation): array
    {
        return [
            'conversation_id' => $conversation->id,
            'title' => $conversation->title,
            'machine' => [
                'id' => $conversation->machine->id,
                'name' => $conversation->machine->name,
            ],
            'created_at' => $conversation->created_at->toIso8601String(),
            'messages' => $conversation->messages->map(function($msg) {
                return [
                    'role' => $msg->role,
                    'content' => $msg->content,
                    'metadata' => $msg->metadata,
                    'created_at' => $msg->created_at->toIso8601String(),
                ];
            })->toArray(),
        ];
    }
}
Blocco 32 php
Route::get('/ai/conversations/{conversation}/export', [AiChatController::class, 'export'])
    ->name('ai.conversations.export');
Blocco 33 php
// Controller method
public function export(AiConversation $conversation, Request $request)
{
    $this->authorize('view', $conversation);
    
    $format = $request->input('format', 'pdf'); // pdf, json
    $exportService = app(ConversationExportService::class);
    
    if ($format === 'json') {
        $data = $exportService->exportToJson($conversation);
        return response()->json($data);
    }
    
    return $exportService->exportToPdf($conversation);
}
Blocco 34 php
// app/Services/I40/ResponseSuggestionsService.php
namespace App\Services\I40;

class ResponseSuggestionsService
{
    /**
     * Genera follow-up suggestions basate sul contesto
     */
    public function generate(AiMessage $lastMessage, array $context): array
    {
        $intent = $context['intent'] ?? 'general';
        
        $suggestions = $this->getBaselineSuggestions($intent);
        
        // Personalizza basandosi sulla risposta AI
        if ($lastMessage->metadata && isset($lastMessage->metadata['sql'])) {
            $suggestions[] = "Mostra gli stessi dati in un grafico";
            $suggestions[] = "Confronta con il periodo precedente";
        }
        
        return array_slice($suggestions, 0, 3); // Max 3 suggestions
    }
    
    /**
     * Suggestions baseline per intent
     */
    protected function getBaselineSuggestions(string $intent): array
    {
        $map = [
            'production_total' => [
                "Confronta con ieri",
                "Mostrami il trend settimanale",
                "Quale operatore ha prodotto di piΓΉ?",
            ],
            'operator_performance' => [
                "Analizza la velocitΓ  media",
                "Ci sono giorni piΓΉ produttivi?",
                "Suggeriscimi come migliorare",
            ],
            'trend_analysis' => [
                "Ci sono anomalie nei dati?",
                "Prevedi la produzione di domani",
                "Esporta questi dati",
            ],
        ];
        
        return $map[$intent] ?? [
            "Mostrami le statistiche",
            "Analizza il periodo corrente",
            "Come posso ottimizzare?",
        ];
    }
}
Blocco 35 vue
<!-- Dopo ogni messaggio AI, mostra suggestions -->
<div v-if="message.role === 'assistant' && message.suggestions" class="follow-up-suggestions mt-2">
    <small class="text-muted d-block mb-1">Potresti chiedere:</small>
    <button 
        v-for="suggestion in message.suggestions" 
        :key="suggestion"
        class="btn btn-xs btn-outline-secondary me-1 mb-1"
        @click="sendMessage(suggestion)"
    >
        {{ suggestion }}
    </button>
</div>
Blocco 36 sql
CREATE TABLE ai_alerts (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT UNSIGNED NOT NULL,
  machine_id BIGINT UNSIGNED NULL,
  alert_type ENUM('threshold', 'anomaly', 'prediction') NOT NULL,
  condition_config JSON NOT NULL, -- { field: 'TotalPieces', operator: '<', value: 1000, period: 'daily' }
  notification_channels JSON, -- ['email', 'slack', 'in_app']
  is_active BOOLEAN DEFAULT TRUE,
  last_triggered_at TIMESTAMP NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  INDEX idx_active (is_active, machine_id)
);
Blocco 37 php
// app/Services/I40/AlertMonitoringService.php
namespace App\Services\I40;

class AlertMonitoringService
{
    /**
     * Controlla tutti gli alert attivi (eseguito ogni ora via Scheduler)
     */
    public function checkAlerts(): void
    {
        $alerts = \DB::table('ai_alerts')
            ->where('is_active', true)
            ->get();
        
        foreach ($alerts as $alert) {
            $this->evaluateAlert($alert);
        }
    }
    
    /**
     * Valuta singolo alert
     */
    protected function evaluateAlert(object $alert): void
    {
        $condition = json_decode($alert->condition_config, true);
        $result = $this->executeCondition($condition, $alert->machine_id);
        
        if ($result['triggered']) {
            $this->triggerAlert($alert, $result);
        }
    }
    
    /**
     * Esegue condizione alert
     */
    protected function executeCondition(array $condition, ?int $machineId): array
    {
        $field = $condition['field'];
        $operator = $condition['operator']; // <, >, =, !=
        $threshold = $condition['value'];
        $period = $condition['period'] ?? 'daily'; // daily, weekly, monthly
        
        // Calcola date range
        $dateFrom = match($period) {
            'daily' => now()->startOfDay(),
            'weekly' => now()->startOfWeek(),
            'monthly' => now()->startOfMonth(),
        };
        
        // Query valore attuale
        $query = MachineOperationLog::query();
        
        if ($machineId) {
            $query->where('machine_id', $machineId);
        }
        
        $value = $query
            ->whereBetween('timestamp', [$dateFrom, now()])
            ->selectRaw("SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{$field}')) AS DECIMAL(15,2))) as total")
            ->value('total') ?? 0;
        
        // Valuta condizione
        $triggered = match($operator) {
            '<' => $value < $threshold,
            '>' => $value > $threshold,
            '=' => $value == $threshold,
            '!=' => $value != $threshold,
            default => false,
        };
        
        return [
            'triggered' => $triggered,
            'current_value' => $value,
            'threshold' => $threshold,
            'field' => $field,
        ];
    }
    
    /**
     * Trigger notifica alert
     */
    protected function triggerAlert(object $alert, array $result): void
    {
        // Aggiorna last_triggered_at
        \DB::table('ai_alerts')
            ->where('id', $alert->id)
            ->update(['last_triggered_at' => now()]);
        
        // Invia notifiche
        $channels = json_decode($alert->notification_channels, true) ?? ['in_app'];
        
        foreach ($channels as $channel) {
            $this->sendNotification($channel, $alert, $result);
        }
    }
    
    /**
     * Invia notifica su canale specifico
     */
    protected function sendNotification(string $channel, object $alert, array $result): void
    {
        $message = sprintf(
            "⚠️ Alert: %s è %s (soglia: %s, valore attuale: %s)",
            $result['field'],
            $alert->alert_type,
            number_format($result['threshold']),
            number_format($result['current_value'])
        );
        
        if ($channel === 'email') {
            $user = User::find($alert->user_id);
            \Mail::to($user)->send(new AlertTriggeredMail($message));
        } elseif ($channel === 'slack') {
            // Slack webhook
            \Http::post(config('services.slack.webhook_url'), [
                'text' => $message,
            ]);
        } elseif ($channel === 'in_app') {
            // Laravel notification system
            $user = User::find($alert->user_id);
            $user->notify(new AlertTriggered($message));
        }
    }
}
Blocco 38 php
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
    // Controlla alert ogni ora
    $schedule->call(function () {
        app(AlertMonitoringService::class)->checkAlerts();
    })->hourly();
}
Blocco 39 sql
SELECT 
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total
FROM machine_operations_log
WHERE machine_id = 5
  AND DATE(timestamp) = CURDATE() - INTERVAL 1 DAY
Blocco 40 sql
SELECT 
    WEEK(timestamp) as week_num,
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total
FROM machine_operations_log
WHERE machine_id = 5
  AND timestamp >= DATE_SUB(NOW(), INTERVAL 2 WEEK)
GROUP BY week_num
ORDER BY week_num
Blocco 41 sql
SELECT 
    JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.MarkerName')) as operator,
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total_pieces,
    COUNT(*) as operations,
    AVG(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.CutTime')) AS DECIMAL(15,2))) as avg_time
FROM machine_operations_log
WHERE machine_id = 5
  AND timestamp >= DATE_FORMAT(NOW(), '%Y-%m-01')
GROUP BY operator
ORDER BY total_pieces DESC
LIMIT 5
Blocco 42 sql
SELECT 
    DATE(timestamp) as date,
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as daily_total
FROM machine_operations_log
WHERE machine_id = 5
  AND timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY date
ORDER BY date
Blocco 43 sql
-- Query 1: Produzione ieri
SELECT 
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as total,
    COUNT(*) as operations,
    MIN(timestamp) as first_op,
    MAX(timestamp) as last_op
FROM machine_operations_log
WHERE machine_id = 5
  AND DATE(timestamp) = CURDATE() - INTERVAL 1 DAY

-- Query 2: Media ultimi 30 giorni (escl. weekend)
SELECT AVG(daily_total) as avg_production
FROM (
    SELECT DATE(timestamp) as date, SUM(...) as daily_total
    FROM machine_operations_log
    WHERE machine_id = 5
      AND timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
      AND DAYOFWEEK(timestamp) NOT IN (1, 7)
    GROUP BY date
) as daily_stats
Blocco 44 sql
-- Query 1: Pezzi giΓ  prodotti questa settimana
SELECT 
    SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2))) as current_total
FROM machine_operations_log
WHERE machine_id = 5
  AND timestamp >= DATE_SUB(NOW(), INTERVAL WEEKDAY(NOW()) DAY)

-- Query 2: Media produzione giornaliera (ultimi 30 giorni)
SELECT AVG(daily_total) as avg_daily
FROM (
    SELECT DATE(timestamp), SUM(...) as daily_total
    FROM machine_operations_log
    WHERE machine_id = 5
      AND timestamp >= DATE_SUB(NOW(), INTERVAL 30 DAY)
    GROUP BY DATE(timestamp)
) as stats
Blocco 45 sql
-- Analisi pattern produttivi
SELECT 
    DAYNAME(timestamp) as day_of_week,
    HOUR(timestamp) as hour_of_day,
    JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.MarkerName')) as operator,
    AVG(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.TotalPieces')) AS DECIMAL(15,2)) / 
        CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.CutTime')) AS DECIMAL(15,2))) as pieces_per_hour
FROM machine_operations_log
WHERE machine_id = 5
  AND timestamp >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
GROUP BY day_of_week, hour_of_day, operator
HAVING pieces_per_hour > 0
ORDER BY pieces_per_hour DESC
LIMIT 20
Blocco 46 php
// tests/Unit/QueryUnderstandingServiceTest.php
namespace Tests\Unit\Services\I40;

use Tests\TestCase;
use App\Services\I40\QueryUnderstandingService;

class QueryUnderstandingServiceTest extends TestCase
{
    protected $service;
    
    protected function setUp(): void
    {
        parent::setUp();
        $this->service = app(QueryUnderstandingService::class);
    }
    
    /** @test */
    public function it_detects_production_total_intent()
    {
        $result = $this->service->analyze("Qual Γ¨ stata la produzione di ieri?");
        
        $this->assertEquals('production_total', $result['intent']);
        $this->assertEquals('yesterday', $result['entities']['period']);
        $this->assertGreaterThan(0.7, $result['confidence']);
    }
    
    /** @test */
    public function it_extracts_date_entities()
    {
        $result = $this->service->analyze("Mostrami la produzione di questa settimana");
        
        $this->assertEquals('this_week', $result['entities']['period']);
        $this->assertNotNull($result['entities']['date_from']);
        $this->assertNotNull($result['entities']['date_to']);
    }
    
    /** @test */
    public function it_handles_comparison_queries()
    {
        $result = $this->service->analyze("Confronta con la settimana scorsa");
        
        $this->assertEquals('production_comparison', $result['intent']);
    }
}
Blocco 47 php
// tests/Unit/OpenAiServiceTest.php
namespace Tests\Unit\Services\I40;

use Tests\TestCase;
use App\Services\I40\OpenAiService;
use OpenAI\Laravel\Facades\OpenAI;

class OpenAiServiceTest extends TestCase
{
    /** @test */
    public function it_generates_sql_query_via_function_calling()
    {
        // Mock OpenAI response
        OpenAI::fake([
            OpenAI::response([
                'choices' => [
                    [
                        'message' => [
                            'function_call' => [
                                'name' => 'execute_sql_query',
                                'arguments' => json_encode([
                                    'sql' => 'SELECT COUNT(*) FROM machine_operations_log WHERE machine_id=5',
                                    'explanation' => 'Conteggio operazioni macchina 5'
                                ]),
                            ],
                        ],
                        'finish_reason' => 'function_call',
                    ],
                ],
                'usage' => ['total_tokens' => 500],
            ]),
        ]);
        
        $service = app(OpenAiService::class);
        $context = ['schema' => 'test schema', 'statistics' => '', 'sample_data' => '', 'business_rules' => ''];
        
        $response = $service->generateResponse($context, "Quante operazioni ha fatto la macchina?");
        
        $this->assertArrayHasKey('metadata', $response);
        $this->assertArrayHasKey('sql', $response['metadata']);
        $this->assertStringContainsString('SELECT', $response['metadata']['sql']);
    }
    
    /** @test */
    public function it_prevents_dangerous_sql()
    {
        $service = app(OpenAiService::class);
        
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Query non permessa');
        
        // Tenta di eseguire SQL pericoloso
        $reflection = new \ReflectionClass($service);
        $method = $reflection->getMethod('executeSafeSql');
        $method->setAccessible(true);
        
        $method->invoke($service, "DROP TABLE users");
    }
}
Blocco 48 php
// tests/Feature/AiChatFlowTest.php
namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\I40\Machine;
use Illuminate\Foundation\Testing\RefreshDatabase;

class AiChatFlowTest extends TestCase
{
    use RefreshDatabase;
    
    /** @test */
    public function user_can_start_conversation_with_ai()
    {
        $user = User::factory()->create();
        $machine = Machine::factory()->create();
        
        $this->actingAs($user);
        
        $response = $this->postJson('/admin/i40/ai/chat', [
            'machine_id' => $machine->id,
            'message' => 'Qual Γ¨ stata la produzione di ieri?',
        ]);
        
        $response->assertStatus(200)
            ->assertJsonStructure([
                'success',
                'conversation_id',
                'message' => ['id', 'role', 'content', 'created_at'],
            ]);
        
        $this->assertDatabaseHas('ai_conversations', [
            'user_id' => $user->id,
            'machine_id' => $machine->id,
        ]);
        
        $this->assertDatabaseHas('ai_messages', [
            'role' => 'user',
            'content' => 'Qual Γ¨ stata la produzione di ieri?',
        ]);
    }
    
    /** @test */
    public function user_can_provide_feedback_on_ai_response()
    {
        $user = User::factory()->create();
        $message = AiMessage::factory()->create(['role' => 'assistant']);
        
        $this->actingAs($user);
        
        $response = $this->postJson('/admin/i40/ai/feedback', [
            'message_id' => $message->id,
            'rating' => 'up',
            'comment' => 'Molto utile!',
        ]);
        
        $response->assertStatus(200);
        
        $this->assertDatabaseHas('ai_feedback', [
            'message_id' => $message->id,
            'user_id' => $user->id,
            'rating' => 'up',
        ]);
    }
}
Blocco 49 php
// tests/Performance/AiResponseTimeTest.php
namespace Tests\Performance;

use Tests\TestCase;

class AiResponseTimeTest extends TestCase
{
    /** @test */
    public function ai_response_time_is_under_5_seconds()
    {
        $start = microtime(true);
        
        // Esegui query AI
        $this->postJson('/admin/i40/ai/chat', [
            'machine_id' => 1,
            'message' => 'Test query',
        ]);
        
        $duration = microtime(true) - $start;
        
        $this->assertLessThan(5.0, $duration, "Response time was {$duration}s");
    }
}
Blocco 50 php
// Dashboard Analytics Service
class AiAnalyticsService
{
    public function getDashboardMetrics(Carbon $from, Carbon $to): array
    {
        return [
            // Utilizzo
            'total_conversations' => AiConversation::whereBetween('created_at', [$from, $to])->count(),
            'total_messages' => AiMessage::whereBetween('created_at', [$from, $to])->count(),
            'active_users' => AiConversation::whereBetween('created_at', [$from, $to])
                ->distinct('user_id')->count(),
            
            // Performance
            'avg_response_time' => AiMessage::where('role', 'assistant')
                ->whereBetween('created_at', [$from, $to])
                ->avg('response_time_ms'),
            
            // Quality
            'positive_feedback' => AiFeedback::where('rating', 'up')
                ->whereBetween('created_at', [$from, $to])->count(),
            'negative_feedback' => AiFeedback::where('rating', 'down')
                ->whereBetween('created_at', [$from, $to])->count(),
            'satisfaction_rate' => $this->calculateSatisfactionRate($from, $to),
            
            // Costi
            'total_tokens' => AiMessage::whereBetween('created_at', [$from, $to])
                ->sum('tokens_used'),
            'estimated_cost' => $this->estimateCost($from, $to),
            
            // Cache
            'cache_hit_rate' => $this->calculateCacheHitRate($from, $to),
        ];
    }
    
    protected function calculateSatisfactionRate(Carbon $from, Carbon $to): float
    {
        $positive = AiFeedback::where('rating', 'up')
            ->whereBetween('created_at', [$from, $to])->count();
        $negative = AiFeedback::where('rating', 'down')
            ->whereBetween('created_at', [$from, $to])->count();
        
        $total = $positive + $negative;
        return $total > 0 ? ($positive / $total) * 100 : 0;
    }
}
Blocco 51 php
// Event: Low Confidence Response
if ($analysis['confidence'] < 0.5) {
    \Log::warning('AI Low Confidence Response', [
        'conversation_id' => $conversation->id,
        'query' => $userMessage,
        'confidence' => $analysis['confidence'],
    ]);
}

// Event: SQL Execution Error
\Log::error('AI SQL Execution Failed', [
    'conversation_id' => $conversation->id,
    'sql' => $sql,
    'error' => $exception->getMessage(),
]);

// Event: High Token Usage
if ($tokensUsed > 2000) {
    \Log::info('AI High Token Usage', [
        'conversation_id' => $conversation->id,
        'tokens' => $tokensUsed,
    ]);
}
Blocco 52 blade
<!-- resources/views/admin/i40/ai/dashboard.blade.php -->
<div class="container-fluid">
    <h3>AI Assistant Analytics</h3>
    
    <div class="row">
        <!-- Usage Stats -->
        <div class="col-md-3">
            <div class="card">
                <div class="card-body">
                    <h5>{{ $metrics['total_conversations'] }}</h5>
                    <small>Conversazioni (30gg)</small>
                </div>
            </div>
        </div>
        
        <div class="col-md-3">
            <div class="card">
                <div class="card-body">
                    <h5>{{ $metrics['active_users'] }}</h5>
                    <small>Utenti Attivi</small>
                </div>
            </div>
        </div>
        
        <!-- Quality Stats -->
        <div class="col-md-3">
            <div class="card">
                <div class="card-body">
                    <h5>{{ number_format($metrics['satisfaction_rate'], 1) }}%</h5>
                    <small>Soddisfazione</small>
                </div>
            </div>
        </div>
        
        <!-- Cost Stats -->
        <div class="col-md-3">
            <div class="card">
                <div class="card-body">
                    <h5>${{ number_format($metrics['estimated_cost'], 2) }}</h5>
                    <small>Costo Stimato</small>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Query Performance Chart -->
    <div class="card mt-4">
        <div class="card-body">
            <h5>Query piΓΉ Frequenti</h5>
            <canvas id="topQueriesChart"></canvas>
        </div>
    </div>
</div>
Blocco 53 php
// Salva query + risposta + feedback per futuro fine-tuning
class FineTuningDataCollector
{
    public function collect(AiMessage $userMessage, AiMessage $aiResponse, ?AiFeedback $feedback)
    {
        if ($feedback && $feedback->rating === 'up') {
            // Salva coppia query-risposta positiva
            \Storage::append('ai-training-data.jsonl', json_encode([
                'messages' => [
                    ['role' => 'user', 'content' => $userMessage->content],
                    ['role' => 'assistant', 'content' => $aiResponse->content],
                ],
                'metadata' => [
                    'confidence' => $aiResponse->metadata['confidence'] ?? null,
                    'feedback' => 'positive',
                ],
            ]));
        }
    }
}
Blocco 54 php
protected function schedule(Schedule $schedule)
{
    // Review AI performance mensile
    $schedule->call(function () {
        $analytics = app(AiAnalyticsService::class);
        $report = $analytics->generateMonthlyReport();
        
        // Invia report a admin
        \Mail::to(config('ai.admin_email'))
            ->send(new MonthlyAiReportMail($report));
    })->monthly();
}
Blocco 55 markdown
# Guida AI Assistant - Analisi Produzione

## Come Fare Domande Efficaci

### βœ… Buone Domande
- "Qual Γ¨ stata la produzione di ieri?"
- "Confronta questa settimana con la scorsa"
- "Chi Γ¨ l'operatore piΓΉ veloce?"
- "Mostrami il trend dell'ultimo mese"

### ❌ Domande Poco Chiare
- "Dimmi qualcosa" (troppo vaga)
- "Ieri" (manca il contesto: produzione? tempo? operatore?)
- "PerchΓ©?" (senza riferimento a cosa)

## Esempi Pratici

### Scenario 1: Controllo Giornaliero
**Query:** "Qual Γ¨ stata la produzione di oggi?"
β†’ L'AI ti dirΓ  il totale pezzi prodotti oggi e lo confronterΓ  con la media.

### Scenario 2: Identificare Problemi
**Query:** "PerchΓ© oggi abbiamo prodotto meno?"
β†’ L'AI analizzerΓ  fermi macchina, ritardi, operatori e ti darΓ  le cause.

### Scenario 3: Pianificazione
**Query:** "Ce la facciamo a finire 3000 pezzi entro domani?"
β†’ L'AI calcolerΓ  la proiezione basata sul ritmo attuale.

## Tips & Tricks
- πŸ’‘ Usa il microfono per query vocali veloci
- πŸ‘ Dai feedback (pollice su/giΓΉ) per migliorare le risposte
- πŸ“Š Chiedi sempre "Mostrami un grafico" per visualizzare i dati
- πŸ”„ Usa i suggerimenti automatici per approfondire

Integrazioni App

namespace_reference