AI Assistant Contestualizzato - Architettura v2.0
AI Assistant Contestualizzato - Architettura v2.0
Data: 22 Ottobre 2025 Versione: 2.0 - Pragmatica & Context-Driven Sistema: sartUP - Modulo Industria 4.0
---
π― Filosofia Progettuale
Principi Fondamentali
1. AI come Ultimo Resort: Non usare LLM quando una risposta deterministica Γ¨ possibile 2. Knowledge Base Prima: 80% risposte da storico preparato, 20% AI generativa 3. Ambiti Contestuali: Ogni dominio ha logica, vocabolario e pattern propri 4. Costi Ottimizzati: Massimo 5-10β¬/mese per 50+ utenti 5. Risposte Precise: Zero allucinazioni, sempre fonti verificabili
---
π¦ Architettura Stratificata
``
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND - Chat Widget β
β Query Utente β Suggestions β Storico Conversazione β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 1: Intent Router (Deterministic) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Pattern Matching Regex β Ambito Contestuale β β
β β β’ "produzione di ieri" β AMBITO_PRODUZIONE β β
β β β’ "macchina in errore" β AMBITO_DIAGNOSTICA β β
β β β’ "qual Γ¨ il profilo" β AMBITO_CONFIGURAZIONE β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 2: Context-Specific Knowledge Base β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Template Repository (JSON) β β
β β β’ Query predefinite con parametri β β
β β β’ Risposte con placeholder β β
β β β’ Threshold e regole business β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 3: Data Execution Engine β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SQL Query Builder β DB β Response Formatter β β
β β β’ Safe execution (whitelist, timeout) β β
β β β’ Cache intelligente β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β (SOLO se necessario)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 4: LLM Fallback (GPT-4 Mini) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Fine-Tuned Model β Prompt Compatto β Response β β
β β β’ Solo per query complesse/ambigue β β
β β β’ Context injection minimale β β
β β β’ Function calling per SQL generation β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ---
ποΈ Ambiti Contestuali
1. AMBITO_PRODUZIONE
Dominio: Analisi quantitΓ prodotte, trend, confronti temporali
EntitΓ Chiave:
TotalPieces, NumberOfPlies, Quantity (da raw_data JSON)
timestamp, machine_id, profile_id
Aggregazioni: SUM, AVG, COUNT per periodo Pattern Query Comuni:
`
"produzione di {periodo}"
"confronta {periodo1} con {periodo2}"
"mostrami il trend {periodo}"
"chi ha prodotto di piΓΉ"
"pezzi totali {filtri}"
`Template Esempio:
`json
{
"pattern": "produzione di (ieri|oggi|questa settimana|questo mese)",
"sql_template": "SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL)) as total FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to",
"response_template": "La produzione di {periodo} Γ¨ stata di {total} pezzi, {comparazione}.",
"requires_llm": false
}
`---
2. AMBITO_DIAGNOSTICA
Dominio: Errori, anomalie, fermi macchina, performance problematiche
EntitΓ Chiave:
MachineStatus, ErrorCode, AlarmCode (se presenti)
Duration anomala (fuori range normale)
timestamp gaps (fermi non registrati)Pattern Query Comuni:
`
"perchΓ© la macchina si Γ¨ fermata"
"errori di {periodo}"
"qual Γ¨ il problema"
"fermi macchina {periodo}"
"macchina lenta oggi"
`Logic Tree:
`
1. Verifica gaps timestamp > 30 min β "Fermo macchina rilevato"
2. Verifica Duration > avg + 2Ο β "Anomalia performance"
3. Verifica ErrorCode in raw_data β "Errore registrato: {code}"
4. Verifica produzione < media - 20% β "Produzione sotto media"
`Richiede LLM: Solo per interpretare ErrorCode sconosciuti
---
3. AMBITO_OPERATORI
Dominio: Performance operatori, confronti, best practices
EntitΓ Chiave:
UserLog, MarkerName, OperatorID (campi operatore in raw_data)
Duration, TotalPieces per operatore
Metriche: pezzi/ora, avg_duration Pattern Query Comuni:
`
"chi Γ¨ l'operatore migliore"
"performance di {nome_operatore}"
"confronta {op1} con {op2}"
"ranking operatori"
"operatori piΓΉ veloci"
`Template Esempio:
`json
{
"pattern": "chi Γ¨ l'operatore (migliore|piΓΉ veloce|piΓΉ produttivo)",
"sql_template": "SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{operator_field}')) as operator, COUNT(*) as operations, SUM(...) as total_pieces, AVG(...) as avg_duration FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY operator ORDER BY total_pieces DESC LIMIT 5",
"response_template": "L'operatore piΓΉ produttivo Γ¨ {operator} con {total_pieces} pezzi in {operations} operazioni.",
"requires_llm": false
}
`---
4. AMBITO_CONFIGURAZIONE
Dominio: Info macchine, profili, setup, CSV imports
EntitΓ Chiave:
machines_i40.profiles_config
import_profiles.columns, column_names
csv_imports statusPattern Query Comuni:
`
"quali profili ha la macchina"
"configurazione macchina"
"ultimo import CSV"
"campi disponibili"
"come Γ¨ configurata la macchina"
`Response Strategy: Query dirette su metadati (no LLM)
---
5. AMBITO_PIANIFICAZIONE
Dominio: Previsioni, capacitΓ produttiva, deadline
EntitΓ Chiave:
Media produzione giornaliera/oraria
Working hours (business rules)
Proiezioni lineari Pattern Query Comuni:
`
"riusciremo a finire {quantitΓ } entro {data}"
"quanto ci vuole per {quantitΓ }"
"quando finiremo {ordine}"
"capacitΓ produttiva {periodo}"
`Logic:
1. Calcola media produzione (ultimi 30 giorni)
2. Calcola working days rimanenti
3. Proiezione lineare:
giorni_necessari = quantitΓ _rimanente / media_giornaliera
4. Confronta con deadlineRichiede LLM: Solo per formulare risposta discorsiva
---
6. AMBITO_COMPARAZIONE
Dominio: Confronti macchine, periodi, operatori
EntitΓ Chiave:
Aggregazioni multi-macchina
Time series per confronto temporale
Delta percentuali Pattern Query Comuni:
`
"confronta macchina A con B"
"questa settimana vs scorsa"
"oggi vs ieri"
"trend ultimi {n} giorni"
`Template Esempio:
`json
{
"pattern": "confronta (questa settimana|oggi) (con|vs) (la scorsa|ieri)",
"sql_template": [
"SELECT SUM(...) as current FROM ... WHERE timestamp BETWEEN :current_from AND :current_to",
"SELECT SUM(...) as previous FROM ... WHERE timestamp BETWEEN :previous_from AND :previous_to"
],
"response_template": "Confronto {periodo_current} vs {periodo_previous}: {current} pezzi vs {previous} pezzi. Variazione: {delta}% ({emoji}).",
"requires_llm": false
}
`---
7. AMBITO_OTTIMIZZAZIONE
Dominio: Suggerimenti miglioramento, best practices, insights
EntitΓ Chiave:
Pattern temporali (orari piΓΉ produttivi)
Colli di bottiglia
OpportunitΓ di miglioramento Pattern Query Comuni:
`
"come posso migliorare"
"suggerimenti ottimizzazione"
"quali sono i problemi"
"come aumentare produttivitΓ "
`Logic Tree:
1. Analizza distribuzione oraria β Identifica picchi/valli
2. Confronta giorni settimana β Identifica giorni migliori
3. Analizza durata operazioni β Identifica outliers
4. Analizza performance operatori β Identifica best practices
Richiede LLM: Sì, per formulare suggerimenti articolati
---
π§ Intent Router - Pattern Matching
Regex Dictionary (Italiano)
`php
// app/Services/I40/IntentPatterns.phpreturn [
'AMBITO_PRODUZIONE' => [
'/produz(?:ione|ione totale|ione di) (ieri|oggi|questa settimana|questo mese)/i',
'/quant[iaoe]+ (pezzi|operazioni|lavorazioni)/i',
'/totale (prodotto|lavorato)/i',
'/(?:mostra|visualizza) (?:la )?produzione/i',
],
'AMBITO_DIAGNOSTICA' => [
'/(?:perch[eΓ©]|come mai|motivo) .*(ferm[oa]|problem[ai]|error[ei])/i',
'/(?:errori?|alarm[ei]|warning)/i',
'/(?:cosa|quale) (?:Γ¨ )?(?:il )?problem[ai]/i',
'/macchina (?:ferma|bloccata|in errore)/i',
'/performance (?:bassa|scadente|sotto media)/i',
],
'AMBITO_OPERATORI' => [
'/(?:chi|quale) (?:Γ¨ |ha ).*(operatore|utente)/i',
'/performance (?:di |dell\'operatore)/i',
'/ranking (?:operatori|utenti)/i',
'/(?:operatore|utente) (?:migliore|peggiore|piΓΉ veloce|piΓΉ lento)/i',
],
'AMBITO_CONFIGURAZIONE' => [
'/(?:quali|mostra) profil[io]/i',
'/configurazione (?:macchina|della macchina)/i',
'/(?:ultimo|ultimi) (?:import|csv)/i',
'/(?:come|in che modo) Γ¨ configurat[oa]/i',
'/(?:campi|colonne) disponibili/i',
],
'AMBITO_PIANIFICAZIONE' => [
'/riuscir(?:emo|Γ²) .* entro/i',
'/(?:quanto|quando) (?:tempo|ci vuole)/i',
'/(?:capacitΓ |potenziale) produttiv[ao]/i',
'/previsione/i',
'/deadline/i',
],
'AMBITO_COMPARAZIONE' => [
'/confron(?:ta|to) .* (?:con|vs)/i',
'/(?:differenza|delta) (?:tra|con)/i',
'/(?:questa|oggi|ieri) (?:vs|con|rispetto)/i',
'/trend (?:ultimi|degli ultimi)/i',
],
'AMBITO_OTTIMIZZAZIONE' => [
'/(?:come|in che modo) (?:posso |migliorare|ottimizzare|aumentare)/i',
'/suggeriment[io]/i',
'/(?:consigli|raccomandazioni)/i',
'/(?:opportunitΓ |margini) (?:di |miglioramento)/i',
],
];
`---
π Knowledge Base Templates
Struttura Template
`json
{
"template_id": "prod_daily",
"ambito": "AMBITO_PRODUZIONE",
"pattern": "produzione di (ieri|oggi)",
"requires_llm": false,
"cache_ttl": 3600,
"parameters": {
"machine_id": "required",
"date_from": "auto",
"date_to": "auto"
},
"entity_extraction": {
"periodo": "regex_group_1"
},
"business_rules": [
{
"condition": "periodo == 'ieri'",
"action": "set_date_range('yesterday')"
},
{
"condition": "periodo == 'oggi'",
"action": "set_date_range('today')"
}
],
"sql_query": {
"query": "SELECT machine_id, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL(15,2))) as total_quantity, COUNT(*) as total_operations FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY machine_id",
"timeout": 5,
"fallback": "Impossibile recuperare dati produzione."
},
"response_formatter": {
"template": "Produzione di {periodo}:\n\nβ
Totale pezzi: {total_quantity}\nπ Operazioni: {total_operations}\n\n{context_comparison}",
"context_comparison": {
"type": "auto",
"compare_with": "average_last_30_days",
"template": "Rispetto alla media degli ultimi 30 giorni ({avg_30d} pezzi), sei {delta_percent}% {emoji}."
}
},
"follow_up_suggestions": [
"Mostrami il trend settimanale",
"Confronta con ieri",
"Chi ha prodotto di piΓΉ?"
]
}
`Database Schema Template Storage
`sql
-- Tabella: ai_knowledge_templates
CREATE TABLE ai_knowledge_templates (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
template_id VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64) NOT NULL,
pattern TEXT NOT NULL,
requires_llm BOOLEAN DEFAULT FALSE,
cache_ttl INT UNSIGNED DEFAULT 3600,
template_config JSON NOT NULL,
usage_count INT UNSIGNED DEFAULT 0,
success_rate DECIMAL(5,2) DEFAULT 100.00,
last_used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ambito (ambito),
INDEX idx_usage (usage_count DESC)
);
`---
π§ Services Architecture
1. IntentRouterService
`php
// app/Services/I40/AI/IntentRouterService.phpclass IntentRouterService
{
protected $patterns;
public function __construct()
{
$this->patterns = require app_path('Services/I40/AI/IntentPatterns.php');
}
/
* Rileva ambito dalla query utente
*/
public function detectAmbito(string $userQuery): array
{
$normalized = $this->normalize($userQuery);
foreach ($this->patterns as $ambito => $regexList) {
foreach ($regexList as $regex) {
if (preg_match($regex, $normalized, $matches)) {
return [
'ambito' => $ambito,
'confidence' => 1.0, // Pattern match Γ¨ deterministico
'matched_pattern' => $regex,
'extracted_entities' => $this->extractEntities($matches),
];
}
}
}
// Fallback: usa LLM per rilevare ambito
return [
'ambito' => 'AMBITO_GENERALE',
'confidence' => 0.3,
'requires_llm_clarification' => true,
];
}
protected function normalize(string $query): string
{
$query = strtolower($query);
$query = preg_replace('/[^\w\sà èéìòù]/', '', $query);
return trim($query);
}
protected function extractEntities(array $matches): array
{
$entities = [];
// Estrai date/periodi
$periodMap = [
'ieri' => ['date_from' => now()->subDay()->startOfDay(), 'date_to' => now()->subDay()->endOfDay()],
'oggi' => ['date_from' => now()->startOfDay(), 'date_to' => now()->endOfDay()],
'questa settimana' => ['date_from' => now()->startOfWeek(), 'date_to' => now()->endOfWeek()],
'scorsa settimana' => ['date_from' => now()->subWeek()->startOfWeek(), 'date_to' => now()->subWeek()->endOfWeek()],
'questo mese' => ['date_from' => now()->startOfMonth(), 'date_to' => now()->endOfMonth()],
];
foreach ($matches as $match) {
if (isset($periodMap[$match])) {
$entities = array_merge($entities, $periodMap[$match]);
$entities['periodo'] = $match;
}
}
return $entities;
}
}
`---
2. KnowledgeBaseService
`php
// app/Services/I40/AI/KnowledgeBaseService.phpclass KnowledgeBaseService
{
/
* Trova template matching per query
*/
public function findTemplate(string $ambito, string $userQuery, array $entities): ?array
{
$templates = \DB::table('ai_knowledge_templates')
->where('ambito', $ambito)
->get();
foreach ($templates as $template) {
$templateConfig = json_decode($template->template_config, true);
$pattern = $templateConfig['pattern'] ?? '';
if (preg_match('/' . $pattern . '/i', $userQuery)) {
// Incrementa usage counter
\DB::table('ai_knowledge_templates')
->where('id', $template->id)
->increment('usage_count');
return array_merge((array)$template, $templateConfig);
}
}
return null;
}
/
* Esegue template e genera risposta
*/
public function executeTemplate(array $template, array $entities, Machine $machine): array
{
// 1) Prepara parametri query
$params = $this->prepareParameters($template, $entities, $machine);
// 2) Esegue SQL query
$data = $this->executeSql($template['sql_query'], $params);
// 3) Arricchisce con contesto (media, comparazioni)
$enrichedData = $this->enrichWithContext($data, $params, $machine);
// 4) Formatta risposta
$response = $this->formatResponse($template['response_formatter'], $enrichedData);
// 5) Genera follow-up suggestions
$suggestions = $template['follow_up_suggestions'] ?? [];
return [
'content' => $response,
'data' => $enrichedData,
'suggestions' => $suggestions,
'requires_chart' => $this->shouldGenerateChart($template, $enrichedData),
'chart_config' => $this->getChartConfig($template, $enrichedData),
'metadata' => [
'template_id' => $template['template_id'],
'ambito' => $template['ambito'],
'execution_time_ms' => 0, // TODO
'used_llm' => false,
],
];
}
protected function enrichWithContext(array $data, array $params, Machine $machine): array
{
// Aggiunge media ultimi 30 giorni per confronto
$avg30d = \DB::table('machine_operations_log')
->where('machine_id', $machine->id)
->whereBetween('timestamp', [now()->subDays(30), now()])
->selectRaw('AVG(daily_total) as avg')
->from(\DB::raw('(SELECT DATE(timestamp) as date, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, \'$.TotalPieces\')) AS DECIMAL)) as daily_total FROM machine_operations_log GROUP BY date) as daily_stats'))
->value('avg');
$data['context'] = [
'avg_last_30_days' => $avg30d,
'delta_percent' => $data['total_quantity'] ? (($data['total_quantity'] - $avg30d) / $avg30d * 100) : 0,
'emoji' => $data['total_quantity'] > $avg30d ? 'π' : 'π',
];
return $data;
}
}
`---
3. LLMFallbackService (Solo quando necessario)
`php
// app/Services/I40/AI/LLMFallbackService.phpclass LLMFallbackService
{
/
* Usa LLM solo se necessario
*/
public function generateResponse(string $ambito, string $userQuery, array $context, Machine $machine): array
{
// Prepara prompt minimale
$systemPrompt = $this->buildMinimalPrompt($ambito, $machine);
$userPrompt = $this->enhanceUserQuery($userQuery, $context);
$response = \OpenAI::chat()->create([
'model' => 'gpt-4-turbo-preview', // o gpt-4o-mini per costi minori
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt],
],
'temperature' => 0.3, // Basso per risposte deterministiche
'max_tokens' => 500,
'functions' => $this->getFunctions($ambito),
]);
return [
'content' => $response->choices[0]->message->content,
'tokens_used' => $response->usage->totalTokens,
'cost_estimate' => $this->estimateCost($response->usage->totalTokens),
'metadata' => [
'used_llm' => true,
'ambito' => $ambito,
],
];
}
protected function buildMinimalPrompt(string $ambito, Machine $machine): string
{
// Prompt compatto specifico per ambito (NON schema completo)
$prompts = [
'AMBITO_PRODUZIONE' => "Sei un assistente per analisi produzione. Macchina: {$machine->name}. Rispondi in italiano, max 3 frasi.",
'AMBITO_DIAGNOSTICA' => "Sei un assistente diagnostico macchine. Macchina: {$machine->name}. Identifica problemi e cause.",
'AMBITO_OTTIMIZZAZIONE' => "Sei un consulente ottimizzazione produzione. Suggerisci miglioramenti concreti e misurabili.",
];
return $prompts[$ambito] ?? "Sei un assistente produzione industriale. Rispondi brevemente in italiano.";
}
}
`---
πΎ Database Schema Completo
`sql
-- 1) Conversazioni (come roadmap originale)
CREATE TABLE ai_conversations (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
machine_id BIGINT UNSIGNED NULL,
ambito VARCHAR(64), -- Ambito prevalente conversazione
title VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_machine (machine_id),
INDEX idx_ambito (ambito),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);-- 2) Messaggi
CREATE TABLE ai_messages (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT UNSIGNED NOT NULL,
role ENUM('user', 'assistant', 'system') NOT NULL,
content TEXT NOT NULL,
metadata JSON, -- { ambito, template_id, sql, execution_time, used_llm, cost }
tokens_used INT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation (conversation_id),
FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE
);
-- 3) Knowledge Base Templates
CREATE TABLE ai_knowledge_templates (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
template_id VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64) NOT NULL,
pattern TEXT NOT NULL,
requires_llm BOOLEAN DEFAULT FALSE,
cache_ttl INT UNSIGNED DEFAULT 3600,
template_config JSON NOT NULL,
usage_count INT UNSIGNED DEFAULT 0,
success_rate DECIMAL(5,2) DEFAULT 100.00,
avg_execution_time_ms INT UNSIGNED DEFAULT 0,
last_used_at TIMESTAMP NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ambito (ambito),
INDEX idx_usage (usage_count DESC),
INDEX idx_active (is_active, ambito)
);
-- 4) Cache (ottimizzato)
CREATE TABLE ai_cache (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
query_hash VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64),
machine_id BIGINT UNSIGNED NULL,
date_range VARCHAR(50),
response_data JSON NOT NULL,
hits INT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
INDEX idx_hash (query_hash),
INDEX idx_expires (expires_at),
INDEX idx_ambito (ambito)
);
-- 5) Feedback (+ ambito per analytics)
CREATE TABLE ai_feedback (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
message_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
rating ENUM('up', 'down') NOT NULL,
comment TEXT NULL,
ambito VARCHAR(64),
template_id VARCHAR(64) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES ai_messages(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_ambito (ambito),
INDEX idx_template (template_id)
);
-- 6) Analytics (aggregati per dashboard)
CREATE TABLE ai_usage_analytics (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
date DATE NOT NULL,
ambito VARCHAR(64),
total_queries INT UNSIGNED DEFAULT 0,
queries_with_llm INT UNSIGNED DEFAULT 0,
avg_response_time_ms INT UNSIGNED DEFAULT 0,
total_tokens_used INT UNSIGNED DEFAULT 0,
estimated_cost_usd DECIMAL(10,4) DEFAULT 0.0000,
positive_feedback_count INT UNSIGNED DEFAULT 0,
negative_feedback_count INT UNSIGNED DEFAULT 0,
UNIQUE KEY unique_date_ambito (date, ambito),
INDEX idx_date (date),
INDEX idx_ambito (ambito)
);
`---
π Implementazione Step-by-Step
Fase 1: Foundation (Settimana 1)
1.1 Database Setup
`bash
php artisan make:migration create_ai_assistant_tables_v2
php artisan migrate
`1.2 Models
`bash
php artisan make:model I40/AiConversation
php artisan make:model I40/AiMessage
php artisan make:model I40/AiKnowledgeTemplate
php artisan make:model I40/AiCache
`1.3 Seeders - Knowledge Base Iniziale
`bash
php artisan make:seeder AiKnowledgeTemplatesSeeder
`File:
database/seeders/AiKnowledgeTemplatesSeeder.php
`php
public function run()
{
$templates = [
// AMBITO_PRODUZIONE
[
'template_id' => 'prod_daily',
'ambito' => 'AMBITO_PRODUZIONE',
'pattern' => 'produzione di (ieri|oggi)',
'requires_llm' => false,
'cache_ttl' => 3600,
'template_config' => json_encode([
'sql_query' => [
'query' => 'SELECT ...',
'timeout' => 5,
],
'response_formatter' => [
'template' => 'Produzione di {periodo}: {total_quantity} pezzi.',
],
'follow_up_suggestions' => [
'Mostrami il trend settimanale',
'Confronta con ieri',
],
]),
],
// ... altri 20-30 template per ambito
];
foreach ($templates as $template) {
\DB::table('ai_knowledge_templates')->insert($template);
}
}
`---
Fase 2: Services Layer (Settimana 2)
2.1 Intent Router
`bash
php artisan make:service I40/AI/IntentRouterService
`2.2 Knowledge Base Service
`bash
php artisan make:service I40/AI/KnowledgeBaseService
`2.3 LLM Fallback Service
`bash
php artisan make:service I40/AI/LLMFallbackService
`2.4 Orchestrator (Main Service)
`bash
php artisan make:service I40/AI/AiAssistantService
`File:
app/Services/I40/AI/AiAssistantService.php
`php
class AiAssistantService
{
public function __construct(
protected IntentRouterService $intentRouter,
protected KnowledgeBaseService $knowledgeBase,
protected LLMFallbackService $llmFallback,
) {}
public function handleQuery(string $userQuery, Machine $machine, User $user): array
{
// 1) Intent Detection
$intent = $this->intentRouter->detectAmbito($userQuery);
// 2) Check Cache
$cached = $this->checkCache($userQuery, $machine->id, $intent['ambito']);
if ($cached) {
return $cached;
}
// 3) Find Knowledge Base Template
$template = $this->knowledgeBase->findTemplate(
$intent['ambito'],
$userQuery,
$intent['extracted_entities']
);
// 4a) Se template trovato β Esegui senza LLM
if ($template && !$template['requires_llm']) {
$response = $this->knowledgeBase->executeTemplate($template, $intent['extracted_entities'], $machine);
$this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
return $response;
}
// 4b) Altrimenti β Usa LLM
$context = $this->buildContext($machine, $intent);
$response = $this->llmFallback->generateResponse(
$intent['ambito'],
$userQuery,
$context,
$machine
);
$this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
return $response;
}
}
`</p><p>---</p><p><h3>Fase 3: Controller & Routes (Settimana 2)</h3></p><p><strong>Routes: </code>routes/i40.php<code></strong>
</code>`<code>php
Route::prefix('ai')->name('ai.')->middleware('auth')->group(function () {
Route::post('/chat', [AiAssistantController::class, 'chat'])->name('chat');
Route::get('/conversations', [AiAssistantController::class, 'conversations'])->name('conversations');
Route::get('/conversations/{id}', [AiAssistantController::class, 'show'])->name('conversations.show');
Route::post('/feedback', [AiAssistantController::class, 'feedback'])->name('feedback');
// Analytics admin
Route::get('/analytics', [AiAssistantController::class, 'analytics'])->name('analytics')
->middleware('role:admin');
});
</code>`<code></p><p>---</p><p><h3>Fase 4: Frontend Widget (Settimana 3)</h3></p><p><strong>Component Blade (Simplified - No Vue)</strong>
</code>`<code>blade
<!-- resources/views/components/ai-chat-widget.blade.php --></p><p><div id="ai-chat-widget" class="ai-chat-widget minimized">
<div class="chat-header" onclick="toggleChat()">
<div>
<span class="ai-avatar">π€</span>
<strong>AI Assistant</strong>
<small id="current-ambito"></small>
</div>
<i class="bi bi-chevron-up"></i>
</div>
<div class="chat-body">
<div id="messages-container" class="messages-container">
<!-- Suggestions iniziali -->
<div class="empty-state">
<h6>Cosa vuoi sapere?</h6>
<div class="suggestions-grid">
<button onclick="sendMessage('Produzione di oggi')">π Produzione oggi</button>
<button onclick="sendMessage('Chi Γ¨ l\'operatore migliore?')">π€ Miglior operatore</button>
<button onclick="sendMessage('Confronta con ieri')">π Confronto ieri</button>
<button onclick="sendMessage('Riusciremo a finire in tempo?')">β±οΈ Deadline</button>
</div>
</div>
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="Chiedi qualcosa..." onkeypress="handleEnter(event)">
<button onclick="sendMessage()"><i class="bi bi-send"></i></button>
</div>
</div>
</div></p><p><script>
let conversationId = null;
const machineId = {{ $machine->id }};</p><p>async function sendMessage(text = null) {
const message = text || document.getElementById('user-input').value;
if (!message.trim()) return;
// Mostra messaggio utente
appendMessage('user', message);
document.getElementById('user-input').value = '';
// Mostra loader
showLoader();
try {
const response = await fetch('/admin/i40/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify({
conversation_id: conversationId,
machine_id: machineId,
message: message,
}),
});
const data = await response.json();
conversationId = data.conversation_id;
hideLoader();
appendMessage('assistant', data.content, data.suggestions, data.chart_config);
// Aggiorna badge ambito
if (data.metadata?.ambito) {
document.getElementById('current-ambito').textContent = formatAmbito(data.metadata.ambito);
}
} catch (error) {
hideLoader();
appendMessage('assistant', 'β Errore di connessione. Riprova.');
}
}</p><p>function appendMessage(role, content, suggestions = null, chartConfig = null) {
const container = document.getElementById('messages-container');
const msgDiv = document.createElement('div');
msgDiv.className =;
msgDiv.innerHTML =
<div class="message-avatar">${role === 'user' ? 'π€' : 'π€'}</div>
<div class="message-content">
<div class="message-text">${content.replace(/\n/g, '<br>')}</div>
${suggestions ?
<div class="follow-up-suggestions">
${suggestions.map(s => <button onclick="sendMessage('${s}')">${s}</button>).join('')}
</div>
</code> : ''}
${chartConfig ? <code><button onclick="openChart(${JSON.stringify(chartConfig)})"><i class="bi bi-bar-chart"></i> Visualizza Grafico</button></code> : ''}
</div>
<code>;
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}</p><p>function formatAmbito(ambito) {
const map = {
'AMBITO_PRODUZIONE': 'π Produzione',
'AMBITO_DIAGNOSTICA': 'π§ Diagnostica',
'AMBITO_OPERATORI': 'π₯ Operatori',
'AMBITO_PIANIFICAZIONE': 'π
Pianificazione',
'AMBITO_CONFIGURAZIONE': 'βοΈ Configurazione',
'AMBITO_COMPARAZIONE': 'βοΈ Confronti',
'AMBITO_OTTIMIZZAZIONE': 'π Ottimizzazione',
};
return map[ambito] || '';
}
</script>Includi in
analyze.blade.php:
`blade
@include('components.ai-chat-widget', ['machine' => $selectedMachine])
`---
π Analytics & Monitoring
Dashboard Metriche
`php
// app/Http/Controllers/Admin/I40/AiAssistantController.phppublic function analytics(Request $request)
{
$from = $request->input('from', now()->subDays(30));
$to = $request->input('to', now());
// Metriche globali
$totalQueries = AiMessage::whereBetween('created_at', [$from, $to])->count();
$queriesWithLLM = AiMessage::whereBetween('created_at', [$from, $to])
->whereJsonContains('metadata->used_llm', true)->count();
$llmUsagePercent = $totalQueries > 0 ? ($queriesWithLLM / $totalQueries * 100) : 0;
// Breakdown per ambito
$byAmbito = \DB::table('ai_usage_analytics')
->whereBetween('date', [$from, $to])
->select('ambito',
\DB::raw('SUM(total_queries) as total'),
\DB::raw('SUM(queries_with_llm) as with_llm'),
\DB::raw('AVG(avg_response_time_ms) as avg_time'),
\DB::raw('SUM(estimated_cost_usd) as cost'))
->groupBy('ambito')
->get();
// Top templates
$topTemplates = AiKnowledgeTemplate::orderBy('usage_count', 'desc')->limit(10)->get();
// Satisfaction rate
$positiveFeedback = AiFeedback::where('rating', 'up')->whereBetween('created_at', [$from, $to])->count();
$negativeFeedback = AiFeedback::where('rating', 'down')->whereBetween('created_at', [$from, $to])->count();
$satisfactionRate = ($positiveFeedback + $negativeFeedback) > 0
? ($positiveFeedback / ($positiveFeedback + $negativeFeedback) * 100)
: 0;
return view('admin.i40.ai.analytics', compact(
'totalQueries',
'llmUsagePercent',
'byAmbito',
'topTemplates',
'satisfactionRate'
));
}
`---
π° Stima Costi
Scenario Realistico
Setup:
3 macchine
10 utenti attivi
30 query/utente/mese = 300 query totali/mese Breakdown Uso LLM:
80% query gestite da Knowledge Base (0 costi API) = 240 query
20% query richiedono LLM = 60 query Costo per Query LLM:
Input: 500 tokens Γ $0.01 / 1M = $0.005
Output: 200 tokens Γ $0.03 / 1M = $0.006
Totale per query: ~$0.011 Costo Mensile:
60 query Γ $0.011 = $0.66/mese Con margine sicurezza (+50%): ~$1/mese
ScalabilitΓ
| Utenti | Query/mese | Costo LLM (20%) | Costo Totale |
|--------|------------|-----------------|--------------|
| 10 | 300 | $0.66 | ~$1/mese |
| 50 | 1.500 | $3.30 | ~$5/mese |
| 100 | 3.000 | $6.60 | ~$10/mese |
---
β
Vantaggi di Questo Approccio
vs Roadmap Originale
| Aspetto | Roadmap Originale | Questa Architettura |
|---------|-------------------|---------------------|
| Costi API | $25-40/mese | $1-5/mese |
| Latency | 2-5s (sempre LLM) | <500ms (80% query) |
| Precisione | 85% (LLM hallucinations) | 98% (template deterministici) |
| Maintenance | Alta (prompt tuning) | Bassa (template JSON) |
| ScalabilitΓ | Lineare ($$) | Sub-lineare (cache) |
PerchΓ© Funziona Meglio
1. Knowledge Base > LLM: Le query ricorrenti (80%) hanno risposte deterministiche
2. Ambiti Contestuali: Ogni dominio ha vocabolario e pattern specifici
3. Cache Aggressiva: Stessa query = 0 costi se in cache
4. Feedback Loop: Templates migliorano con usage analytics
5. Costi Prevedibili: Budget mensile fisso, non variabile
---
π Training Iniziale
Step 1: Seeding Knowledge Base (30-40 Template)
Distribuzione Template:
AMBITO_PRODUZIONE: 8 template
AMBITO_DIAGNOSTICA: 6 template
AMBITO_OPERATORI: 5 template
AMBITO_CONFIGURAZIONE: 4 template
AMBITO_PIANIFICAZIONE: 5 template
AMBITO_COMPARAZIONE: 6 template
AMBITO_OTTIMIZZAZIONE: 4 template Step 2: Fine-Tuning LLM (Opzionale, Fase 2)
Una volta raccolti 500+ conversazioni:
1. Estrai query + risposte con feedback positivo
2. Crea dataset JSONL per fine-tuning
3. Fine-tune
gpt-4o-mini su dominio specifico
4. Sostituisci LLM fallback con modello fine-tunedBenefici Fine-Tuning:
Costi ridotti ulteriormente (-50%)
Risposte piΓΉ coerenti col tuo dominio
Riduzione allucinazioni ---
π Prossimi Step
Settimana 1-2: Foundation
[ ] Setup database (migrations)
[ ] Seeding Knowledge Base (30 template iniziali)
[ ] Intent Router Service (pattern matching) Settimana 3-4: Services Layer
[ ] Knowledge Base Service (template execution)
[ ] LLM Fallback Service (OpenAI integration)
[ ] Orchestrator Service (main logic)
[ ] Cache Service Settimana 5: Controller & API
[ ] AiAssistantController (CRUD conversazioni)
[ ] Routes setup
[ ] API testing (Postman/Insomnia) Settimana 6: Frontend
[ ] Chat Widget Blade component
[ ] Integrazione in analyze.blade.php`
Settimana 7: Analytics & Refinement
Settimana 8: Production Ready
---
π¦ Criteri di Successo
Metriche Tecniche
Metriche Business
Metriche QualitΓ
---
Pronto per iniziare? π
Questa architettura Γ¨ production-ready, cost-effective e scalabile. La chiave Γ¨ partire con una Knowledge Base solida (30-40 template) e usare LLM solo come fallback intelligente.
Analisi Codice
Blocco 1
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FRONTEND - Chat Widget β
β Query Utente β Suggestions β Storico Conversazione β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 1: Intent Router (Deterministic) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Pattern Matching Regex β Ambito Contestuale β β
β β β’ "produzione di ieri" β AMBITO_PRODUZIONE β β
β β β’ "macchina in errore" β AMBITO_DIAGNOSTICA β β
β β β’ "qual Γ¨ il profilo" β AMBITO_CONFIGURAZIONE β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 2: Context-Specific Knowledge Base β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Template Repository (JSON) β β
β β β’ Query predefinite con parametri β β
β β β’ Risposte con placeholder β β
β β β’ Threshold e regole business β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 3: Data Execution Engine β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β SQL Query Builder β DB β Response Formatter β β
β β β’ Safe execution (whitelist, timeout) β β
β β β’ Cache intelligente β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββ
β
β (SOLO se necessario)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β LAYER 4: LLM Fallback (GPT-4 Mini) β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Fine-Tuned Model β Prompt Compatto β Response β β
β β β’ Solo per query complesse/ambigue β β
β β β’ Context injection minimale β β
β β β’ Function calling per SQL generation β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Blocco 2
"produzione di {periodo}"
"confronta {periodo1} con {periodo2}"
"mostrami il trend {periodo}"
"chi ha prodotto di piΓΉ"
"pezzi totali {filtri}"
Blocco 3 json
{
"pattern": "produzione di (ieri|oggi|questa settimana|questo mese)",
"sql_template": "SELECT SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL)) as total FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to",
"response_template": "La produzione di {periodo} Γ¨ stata di {total} pezzi, {comparazione}.",
"requires_llm": false
}
Blocco 4
"perchΓ© la macchina si Γ¨ fermata"
"errori di {periodo}"
"qual Γ¨ il problema"
"fermi macchina {periodo}"
"macchina lenta oggi"
Blocco 5
1. Verifica gaps timestamp > 30 min β "Fermo macchina rilevato"
2. Verifica Duration > avg + 2Ο β "Anomalia performance"
3. Verifica ErrorCode in raw_data β "Errore registrato: {code}"
4. Verifica produzione < media - 20% β "Produzione sotto media"
Blocco 6
"chi Γ¨ l'operatore migliore"
"performance di {nome_operatore}"
"confronta {op1} con {op2}"
"ranking operatori"
"operatori piΓΉ veloci"
Blocco 7 json
{
"pattern": "chi Γ¨ l'operatore (migliore|piΓΉ veloce|piΓΉ produttivo)",
"sql_template": "SELECT JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{operator_field}')) as operator, COUNT(*) as operations, SUM(...) as total_pieces, AVG(...) as avg_duration FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY operator ORDER BY total_pieces DESC LIMIT 5",
"response_template": "L'operatore piΓΉ produttivo Γ¨ {operator} con {total_pieces} pezzi in {operations} operazioni.",
"requires_llm": false
}
Blocco 8
"quali profili ha la macchina"
"configurazione macchina"
"ultimo import CSV"
"campi disponibili"
"come Γ¨ configurata la macchina"
Blocco 9
"riusciremo a finire {quantitΓ } entro {data}"
"quanto ci vuole per {quantitΓ }"
"quando finiremo {ordine}"
"capacitΓ produttiva {periodo}"
Blocco 10
"confronta macchina A con B"
"questa settimana vs scorsa"
"oggi vs ieri"
"trend ultimi {n} giorni"
Blocco 11 json
{
"pattern": "confronta (questa settimana|oggi) (con|vs) (la scorsa|ieri)",
"sql_template": [
"SELECT SUM(...) as current FROM ... WHERE timestamp BETWEEN :current_from AND :current_to",
"SELECT SUM(...) as previous FROM ... WHERE timestamp BETWEEN :previous_from AND :previous_to"
],
"response_template": "Confronto {periodo_current} vs {periodo_previous}: {current} pezzi vs {previous} pezzi. Variazione: {delta}% ({emoji}).",
"requires_llm": false
}
Blocco 12
"come posso migliorare"
"suggerimenti ottimizzazione"
"quali sono i problemi"
"come aumentare produttivitΓ "
Blocco 13 php
// app/Services/I40/IntentPatterns.php
return [
'AMBITO_PRODUZIONE' => [
'/produz(?:ione|ione totale|ione di) (ieri|oggi|questa settimana|questo mese)/i',
'/quant[iaoe]+ (pezzi|operazioni|lavorazioni)/i',
'/totale (prodotto|lavorato)/i',
'/(?:mostra|visualizza) (?:la )?produzione/i',
],
'AMBITO_DIAGNOSTICA' => [
'/(?:perch[eΓ©]|come mai|motivo) .*(ferm[oa]|problem[ai]|error[ei])/i',
'/(?:errori?|alarm[ei]|warning)/i',
'/(?:cosa|quale) (?:Γ¨ )?(?:il )?problem[ai]/i',
'/macchina (?:ferma|bloccata|in errore)/i',
'/performance (?:bassa|scadente|sotto media)/i',
],
'AMBITO_OPERATORI' => [
'/(?:chi|quale) (?:Γ¨ |ha ).*(operatore|utente)/i',
'/performance (?:di |dell\'operatore)/i',
'/ranking (?:operatori|utenti)/i',
'/(?:operatore|utente) (?:migliore|peggiore|piΓΉ veloce|piΓΉ lento)/i',
],
'AMBITO_CONFIGURAZIONE' => [
'/(?:quali|mostra) profil[io]/i',
'/configurazione (?:macchina|della macchina)/i',
'/(?:ultimo|ultimi) (?:import|csv)/i',
'/(?:come|in che modo) Γ¨ configurat[oa]/i',
'/(?:campi|colonne) disponibili/i',
],
'AMBITO_PIANIFICAZIONE' => [
'/riuscir(?:emo|Γ²) .* entro/i',
'/(?:quanto|quando) (?:tempo|ci vuole)/i',
'/(?:capacitΓ |potenziale) produttiv[ao]/i',
'/previsione/i',
'/deadline/i',
],
'AMBITO_COMPARAZIONE' => [
'/confron(?:ta|to) .* (?:con|vs)/i',
'/(?:differenza|delta) (?:tra|con)/i',
'/(?:questa|oggi|ieri) (?:vs|con|rispetto)/i',
'/trend (?:ultimi|degli ultimi)/i',
],
'AMBITO_OTTIMIZZAZIONE' => [
'/(?:come|in che modo) (?:posso |migliorare|ottimizzare|aumentare)/i',
'/suggeriment[io]/i',
'/(?:consigli|raccomandazioni)/i',
'/(?:opportunitΓ |margini) (?:di |miglioramento)/i',
],
];
Blocco 14 json
{
"template_id": "prod_daily",
"ambito": "AMBITO_PRODUZIONE",
"pattern": "produzione di (ieri|oggi)",
"requires_llm": false,
"cache_ttl": 3600,
"parameters": {
"machine_id": "required",
"date_from": "auto",
"date_to": "auto"
},
"entity_extraction": {
"periodo": "regex_group_1"
},
"business_rules": [
{
"condition": "periodo == 'ieri'",
"action": "set_date_range('yesterday')"
},
{
"condition": "periodo == 'oggi'",
"action": "set_date_range('today')"
}
],
"sql_query": {
"query": "SELECT machine_id, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, '$.{quantity_field}')) AS DECIMAL(15,2))) as total_quantity, COUNT(*) as total_operations FROM machine_operations_log WHERE machine_id = :machine_id AND timestamp BETWEEN :date_from AND :date_to GROUP BY machine_id",
"timeout": 5,
"fallback": "Impossibile recuperare dati produzione."
},
"response_formatter": {
"template": "**Produzione di {periodo}:**\n\nβ
Totale pezzi: **{total_quantity}**\nπ Operazioni: **{total_operations}**\n\n{context_comparison}",
"context_comparison": {
"type": "auto",
"compare_with": "average_last_30_days",
"template": "Rispetto alla media degli ultimi 30 giorni ({avg_30d} pezzi), sei {delta_percent}% {emoji}."
}
},
"follow_up_suggestions": [
"Mostrami il trend settimanale",
"Confronta con ieri",
"Chi ha prodotto di piΓΉ?"
]
}
Blocco 15 sql
-- Tabella: ai_knowledge_templates
CREATE TABLE ai_knowledge_templates (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
template_id VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64) NOT NULL,
pattern TEXT NOT NULL,
requires_llm BOOLEAN DEFAULT FALSE,
cache_ttl INT UNSIGNED DEFAULT 3600,
template_config JSON NOT NULL,
usage_count INT UNSIGNED DEFAULT 0,
success_rate DECIMAL(5,2) DEFAULT 100.00,
last_used_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ambito (ambito),
INDEX idx_usage (usage_count DESC)
);
Blocco 16 php
// app/Services/I40/AI/IntentRouterService.php
class IntentRouterService
{
protected $patterns;
public function __construct()
{
$this->patterns = require app_path('Services/I40/AI/IntentPatterns.php');
}
/**
* Rileva ambito dalla query utente
*/
public function detectAmbito(string $userQuery): array
{
$normalized = $this->normalize($userQuery);
foreach ($this->patterns as $ambito => $regexList) {
foreach ($regexList as $regex) {
if (preg_match($regex, $normalized, $matches)) {
return [
'ambito' => $ambito,
'confidence' => 1.0, // Pattern match Γ¨ deterministico
'matched_pattern' => $regex,
'extracted_entities' => $this->extractEntities($matches),
];
}
}
}
// Fallback: usa LLM per rilevare ambito
return [
'ambito' => 'AMBITO_GENERALE',
'confidence' => 0.3,
'requires_llm_clarification' => true,
];
}
protected function normalize(string $query): string
{
$query = strtolower($query);
$query = preg_replace('/[^\w\sà èéìòù]/', '', $query);
return trim($query);
}
protected function extractEntities(array $matches): array
{
$entities = [];
// Estrai date/periodi
$periodMap = [
'ieri' => ['date_from' => now()->subDay()->startOfDay(), 'date_to' => now()->subDay()->endOfDay()],
'oggi' => ['date_from' => now()->startOfDay(), 'date_to' => now()->endOfDay()],
'questa settimana' => ['date_from' => now()->startOfWeek(), 'date_to' => now()->endOfWeek()],
'scorsa settimana' => ['date_from' => now()->subWeek()->startOfWeek(), 'date_to' => now()->subWeek()->endOfWeek()],
'questo mese' => ['date_from' => now()->startOfMonth(), 'date_to' => now()->endOfMonth()],
];
foreach ($matches as $match) {
if (isset($periodMap[$match])) {
$entities = array_merge($entities, $periodMap[$match]);
$entities['periodo'] = $match;
}
}
return $entities;
}
}
Blocco 17 php
// app/Services/I40/AI/KnowledgeBaseService.php
class KnowledgeBaseService
{
/**
* Trova template matching per query
*/
public function findTemplate(string $ambito, string $userQuery, array $entities): ?array
{
$templates = \DB::table('ai_knowledge_templates')
->where('ambito', $ambito)
->get();
foreach ($templates as $template) {
$templateConfig = json_decode($template->template_config, true);
$pattern = $templateConfig['pattern'] ?? '';
if (preg_match('/' . $pattern . '/i', $userQuery)) {
// Incrementa usage counter
\DB::table('ai_knowledge_templates')
->where('id', $template->id)
->increment('usage_count');
return array_merge((array)$template, $templateConfig);
}
}
return null;
}
/**
* Esegue template e genera risposta
*/
public function executeTemplate(array $template, array $entities, Machine $machine): array
{
// 1) Prepara parametri query
$params = $this->prepareParameters($template, $entities, $machine);
// 2) Esegue SQL query
$data = $this->executeSql($template['sql_query'], $params);
// 3) Arricchisce con contesto (media, comparazioni)
$enrichedData = $this->enrichWithContext($data, $params, $machine);
// 4) Formatta risposta
$response = $this->formatResponse($template['response_formatter'], $enrichedData);
// 5) Genera follow-up suggestions
$suggestions = $template['follow_up_suggestions'] ?? [];
return [
'content' => $response,
'data' => $enrichedData,
'suggestions' => $suggestions,
'requires_chart' => $this->shouldGenerateChart($template, $enrichedData),
'chart_config' => $this->getChartConfig($template, $enrichedData),
'metadata' => [
'template_id' => $template['template_id'],
'ambito' => $template['ambito'],
'execution_time_ms' => 0, // TODO
'used_llm' => false,
],
];
}
protected function enrichWithContext(array $data, array $params, Machine $machine): array
{
// Aggiunge media ultimi 30 giorni per confronto
$avg30d = \DB::table('machine_operations_log')
->where('machine_id', $machine->id)
->whereBetween('timestamp', [now()->subDays(30), now()])
->selectRaw('AVG(daily_total) as avg')
->from(\DB::raw('(SELECT DATE(timestamp) as date, SUM(CAST(JSON_UNQUOTE(JSON_EXTRACT(raw_data, \'$.TotalPieces\')) AS DECIMAL)) as daily_total FROM machine_operations_log GROUP BY date) as daily_stats'))
->value('avg');
$data['context'] = [
'avg_last_30_days' => $avg30d,
'delta_percent' => $data['total_quantity'] ? (($data['total_quantity'] - $avg30d) / $avg30d * 100) : 0,
'emoji' => $data['total_quantity'] > $avg30d ? 'π' : 'π',
];
return $data;
}
}
Blocco 18 php
// app/Services/I40/AI/LLMFallbackService.php
class LLMFallbackService
{
/**
* Usa LLM solo se necessario
*/
public function generateResponse(string $ambito, string $userQuery, array $context, Machine $machine): array
{
// Prepara prompt minimale
$systemPrompt = $this->buildMinimalPrompt($ambito, $machine);
$userPrompt = $this->enhanceUserQuery($userQuery, $context);
$response = \OpenAI::chat()->create([
'model' => 'gpt-4-turbo-preview', // o gpt-4o-mini per costi minori
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt],
],
'temperature' => 0.3, // Basso per risposte deterministiche
'max_tokens' => 500,
'functions' => $this->getFunctions($ambito),
]);
return [
'content' => $response->choices[0]->message->content,
'tokens_used' => $response->usage->totalTokens,
'cost_estimate' => $this->estimateCost($response->usage->totalTokens),
'metadata' => [
'used_llm' => true,
'ambito' => $ambito,
],
];
}
protected function buildMinimalPrompt(string $ambito, Machine $machine): string
{
// Prompt compatto specifico per ambito (NON schema completo)
$prompts = [
'AMBITO_PRODUZIONE' => "Sei un assistente per analisi produzione. Macchina: {$machine->name}. Rispondi in italiano, max 3 frasi.",
'AMBITO_DIAGNOSTICA' => "Sei un assistente diagnostico macchine. Macchina: {$machine->name}. Identifica problemi e cause.",
'AMBITO_OTTIMIZZAZIONE' => "Sei un consulente ottimizzazione produzione. Suggerisci miglioramenti concreti e misurabili.",
];
return $prompts[$ambito] ?? "Sei un assistente produzione industriale. Rispondi brevemente in italiano.";
}
}
Blocco 19 sql
-- 1) Conversazioni (come roadmap originale)
CREATE TABLE ai_conversations (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
machine_id BIGINT UNSIGNED NULL,
ambito VARCHAR(64), -- Ambito prevalente conversazione
title VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_machine (machine_id),
INDEX idx_ambito (ambito),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 2) Messaggi
CREATE TABLE ai_messages (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
conversation_id BIGINT UNSIGNED NOT NULL,
role ENUM('user', 'assistant', 'system') NOT NULL,
content TEXT NOT NULL,
metadata JSON, -- { ambito, template_id, sql, execution_time, used_llm, cost }
tokens_used INT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation (conversation_id),
FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE
);
-- 3) Knowledge Base Templates
CREATE TABLE ai_knowledge_templates (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
template_id VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64) NOT NULL,
pattern TEXT NOT NULL,
requires_llm BOOLEAN DEFAULT FALSE,
cache_ttl INT UNSIGNED DEFAULT 3600,
template_config JSON NOT NULL,
usage_count INT UNSIGNED DEFAULT 0,
success_rate DECIMAL(5,2) DEFAULT 100.00,
avg_execution_time_ms INT UNSIGNED DEFAULT 0,
last_used_at TIMESTAMP NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_ambito (ambito),
INDEX idx_usage (usage_count DESC),
INDEX idx_active (is_active, ambito)
);
-- 4) Cache (ottimizzato)
CREATE TABLE ai_cache (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
query_hash VARCHAR(64) UNIQUE NOT NULL,
ambito VARCHAR(64),
machine_id BIGINT UNSIGNED NULL,
date_range VARCHAR(50),
response_data JSON NOT NULL,
hits INT UNSIGNED DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
INDEX idx_hash (query_hash),
INDEX idx_expires (expires_at),
INDEX idx_ambito (ambito)
);
-- 5) Feedback (+ ambito per analytics)
CREATE TABLE ai_feedback (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
message_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
rating ENUM('up', 'down') NOT NULL,
comment TEXT NULL,
ambito VARCHAR(64),
template_id VARCHAR(64) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES ai_messages(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_ambito (ambito),
INDEX idx_template (template_id)
);
-- 6) Analytics (aggregati per dashboard)
CREATE TABLE ai_usage_analytics (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
date DATE NOT NULL,
ambito VARCHAR(64),
total_queries INT UNSIGNED DEFAULT 0,
queries_with_llm INT UNSIGNED DEFAULT 0,
avg_response_time_ms INT UNSIGNED DEFAULT 0,
total_tokens_used INT UNSIGNED DEFAULT 0,
estimated_cost_usd DECIMAL(10,4) DEFAULT 0.0000,
positive_feedback_count INT UNSIGNED DEFAULT 0,
negative_feedback_count INT UNSIGNED DEFAULT 0,
UNIQUE KEY unique_date_ambito (date, ambito),
INDEX idx_date (date),
INDEX idx_ambito (ambito)
);
Blocco 20 bash
php artisan make:migration create_ai_assistant_tables_v2
php artisan migrate
Blocco 21 bash
php artisan make:model I40/AiConversation
php artisan make:model I40/AiMessage
php artisan make:model I40/AiKnowledgeTemplate
php artisan make:model I40/AiCache
Blocco 22 bash
php artisan make:seeder AiKnowledgeTemplatesSeeder
Blocco 23 php
public function run()
{
$templates = [
// AMBITO_PRODUZIONE
[
'template_id' => 'prod_daily',
'ambito' => 'AMBITO_PRODUZIONE',
'pattern' => 'produzione di (ieri|oggi)',
'requires_llm' => false,
'cache_ttl' => 3600,
'template_config' => json_encode([
'sql_query' => [
'query' => 'SELECT ...',
'timeout' => 5,
],
'response_formatter' => [
'template' => 'Produzione di {periodo}: {total_quantity} pezzi.',
],
'follow_up_suggestions' => [
'Mostrami il trend settimanale',
'Confronta con ieri',
],
]),
],
// ... altri 20-30 template per ambito
];
foreach ($templates as $template) {
\DB::table('ai_knowledge_templates')->insert($template);
}
}
Blocco 24 bash
php artisan make:service I40/AI/IntentRouterService
Blocco 25 bash
php artisan make:service I40/AI/KnowledgeBaseService
Blocco 26 bash
php artisan make:service I40/AI/LLMFallbackService
Blocco 27 bash
php artisan make:service I40/AI/AiAssistantService
Blocco 28 php
class AiAssistantService
{
public function __construct(
protected IntentRouterService $intentRouter,
protected KnowledgeBaseService $knowledgeBase,
protected LLMFallbackService $llmFallback,
) {}
public function handleQuery(string $userQuery, Machine $machine, User $user): array
{
// 1) Intent Detection
$intent = $this->intentRouter->detectAmbito($userQuery);
// 2) Check Cache
$cached = $this->checkCache($userQuery, $machine->id, $intent['ambito']);
if ($cached) {
return $cached;
}
// 3) Find Knowledge Base Template
$template = $this->knowledgeBase->findTemplate(
$intent['ambito'],
$userQuery,
$intent['extracted_entities']
);
// 4a) Se template trovato β Esegui senza LLM
if ($template && !$template['requires_llm']) {
$response = $this->knowledgeBase->executeTemplate($template, $intent['extracted_entities'], $machine);
$this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
return $response;
}
// 4b) Altrimenti β Usa LLM
$context = $this->buildContext($machine, $intent);
$response = $this->llmFallback->generateResponse(
$intent['ambito'],
$userQuery,
$context,
$machine
);
$this->saveToCache($userQuery, $machine->id, $intent['ambito'], $response);
return $response;
}
}
Blocco 29 php
Route::prefix('ai')->name('ai.')->middleware('auth')->group(function () {
Route::post('/chat', [AiAssistantController::class, 'chat'])->name('chat');
Route::get('/conversations', [AiAssistantController::class, 'conversations'])->name('conversations');
Route::get('/conversations/{id}', [AiAssistantController::class, 'show'])->name('conversations.show');
Route::post('/feedback', [AiAssistantController::class, 'feedback'])->name('feedback');
// Analytics admin
Route::get('/analytics', [AiAssistantController::class, 'analytics'])->name('analytics')
->middleware('role:admin');
});
Blocco 30 blade
<!-- resources/views/components/ai-chat-widget.blade.php -->
<div id="ai-chat-widget" class="ai-chat-widget minimized">
<div class="chat-header" onclick="toggleChat()">
<div>
<span class="ai-avatar">π€</span>
<strong>AI Assistant</strong>
<small id="current-ambito"></small>
</div>
<i class="bi bi-chevron-up"></i>
</div>
<div class="chat-body">
<div id="messages-container" class="messages-container">
<!-- Suggestions iniziali -->
<div class="empty-state">
<h6>Cosa vuoi sapere?</h6>
<div class="suggestions-grid">
<button onclick="sendMessage('Produzione di oggi')">π Produzione oggi</button>
<button onclick="sendMessage('Chi Γ¨ l\'operatore migliore?')">π€ Miglior operatore</button>
<button onclick="sendMessage('Confronta con ieri')">π Confronto ieri</button>
<button onclick="sendMessage('Riusciremo a finire in tempo?')">β±οΈ Deadline</button>
</div>
</div>
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="Chiedi qualcosa..." onkeypress="handleEnter(event)">
<button onclick="sendMessage()"><i class="bi bi-send"></i></button>
</div>
</div>
</div>
<script>
let conversationId = null;
const machineId = {{ $machine->id }};
async function sendMessage(text = null) {
const message = text || document.getElementById('user-input').value;
if (!message.trim()) return;
// Mostra messaggio utente
appendMessage('user', message);
document.getElementById('user-input').value = '';
// Mostra loader
showLoader();
try {
const response = await fetch('/admin/i40/ai/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}',
},
body: JSON.stringify({
conversation_id: conversationId,
machine_id: machineId,
message: message,
}),
});
const data = await response.json();
conversationId = data.conversation_id;
hideLoader();
appendMessage('assistant', data.content, data.suggestions, data.chart_config);
// Aggiorna badge ambito
if (data.metadata?.ambito) {
document.getElementById('current-ambito').textContent = formatAmbito(data.metadata.ambito);
}
} catch (error) {
hideLoader();
appendMessage('assistant', 'β Errore di connessione. Riprova.');
}
}
function appendMessage(role, content, suggestions = null, chartConfig = null) {
const container = document.getElementById('messages-container');
const msgDiv = document.createElement('div');
msgDiv.className = `message ${role}`;
msgDiv.innerHTML = `
<div class="message-avatar">${role === 'user' ? 'π€' : 'π€'}</div>
<div class="message-content">
<div class="message-text">${content.replace(/\n/g, '<br>')}</div>
${suggestions ? `
<div class="follow-up-suggestions">
${suggestions.map(s => `<button onclick="sendMessage('${s}')">${s}</button>`).join('')}
</div>
` : ''}
${chartConfig ? `<button onclick="openChart(${JSON.stringify(chartConfig)})"><i class="bi bi-bar-chart"></i> Visualizza Grafico</button>` : ''}
</div>
`;
container.appendChild(msgDiv);
container.scrollTop = container.scrollHeight;
}
function formatAmbito(ambito) {
const map = {
'AMBITO_PRODUZIONE': 'π Produzione',
'AMBITO_DIAGNOSTICA': 'π§ Diagnostica',
'AMBITO_OPERATORI': 'π₯ Operatori',
'AMBITO_PIANIFICAZIONE': 'π
Pianificazione',
'AMBITO_CONFIGURAZIONE': 'βοΈ Configurazione',
'AMBITO_COMPARAZIONE': 'βοΈ Confronti',
'AMBITO_OTTIMIZZAZIONE': 'π Ottimizzazione',
};
return map[ambito] || '';
}
</script>
Blocco 31 blade
@include('components.ai-chat-widget', ['machine' => $selectedMachine])
Blocco 32 php
// app/Http/Controllers/Admin/I40/AiAssistantController.php
public function analytics(Request $request)
{
$from = $request->input('from', now()->subDays(30));
$to = $request->input('to', now());
// Metriche globali
$totalQueries = AiMessage::whereBetween('created_at', [$from, $to])->count();
$queriesWithLLM = AiMessage::whereBetween('created_at', [$from, $to])
->whereJsonContains('metadata->used_llm', true)->count();
$llmUsagePercent = $totalQueries > 0 ? ($queriesWithLLM / $totalQueries * 100) : 0;
// Breakdown per ambito
$byAmbito = \DB::table('ai_usage_analytics')
->whereBetween('date', [$from, $to])
->select('ambito',
\DB::raw('SUM(total_queries) as total'),
\DB::raw('SUM(queries_with_llm) as with_llm'),
\DB::raw('AVG(avg_response_time_ms) as avg_time'),
\DB::raw('SUM(estimated_cost_usd) as cost'))
->groupBy('ambito')
->get();
// Top templates
$topTemplates = AiKnowledgeTemplate::orderBy('usage_count', 'desc')->limit(10)->get();
// Satisfaction rate
$positiveFeedback = AiFeedback::where('rating', 'up')->whereBetween('created_at', [$from, $to])->count();
$negativeFeedback = AiFeedback::where('rating', 'down')->whereBetween('created_at', [$from, $to])->count();
$satisfactionRate = ($positiveFeedback + $negativeFeedback) > 0
? ($positiveFeedback / ($positiveFeedback + $negativeFeedback) * 100)
: 0;
return view('admin.i40.ai.analytics', compact(
'totalQueries',
'llmUsagePercent',
'byAmbito',
'topTemplates',
'satisfactionRate'
));
}