# Andrea Pellizzari — contenuto completo Questo file contiene il testo integrale di tutti i progetti e i post del diario di andreapellizzari.it, in markdown. È pensato per essere ingerito da modelli linguistici. Ogni sezione è separata da un divisore `---` e identificata dall'URL canonico. --- # Concetti ## Mexal/Passepartout: WebAPI, SPRIX, integrazioni URL: https://andreapellizzari.it/concetti/mexal/ Keywords: Mexal, Passepartout, SPRIX, SPRIX Collage, CALLWEBSVC, Mexal WebAPI, Docuvision, FatturaPA, gestionale italiano, ERP PMI > Esperienza concreta di integrazione Mexal/Passepartout in PMI manifatturiere italiane: WebAPI, SPRIX, SPRIX Collage, CALLWEBSVC, Docuvision, FatturaPA. Lavoro come sviluppatore in-house su Mexal/Passepartout da diversi anni, in PMI manifatturiere italiane del Nordest. Le competenze le ho maturate sul campo, sotto la licenza del datore di lavoro — non sono partner certificato. Quello che faccio sta nello spazio tra il gestionale tradizionale e il web contemporaneo: chiamare WebAPI dal browser, scrivere SPRIX per report e automazioni, intercettare eventi del gestionale con SPRIX Collage, esporre flussi via CALLWEBSVC. Spesso il punto non è sostituire il gestionale ma costruirgli intorno strumenti che oggi mancano. I progetti che seguono coprono diversi pattern: ecommerce B2B con sincronizzazione bidirezionale, knowledge base AI che risponde citando articoli reali del gestionale, ticket assistance che si integra con anagrafiche e ordini, generazione documenti dinamici via WebAPI, automazione SPRIX per accordi fornitore. ### Domande ricorrenti **Cos'è Mexal/Passepartout?** Mexal è il software gestionale ERP di Passepartout S.p.A., diffuso nelle PMI italiane. Copre contabilità, magazzino, produzione, vendite, acquisti, e ha un ecosistema di estensioni: SPRIX (linguaggio di scripting interno), Mexal WebAPI (API HTTP/REST), Docuvision (gestione documentale), CALLWEBSVC (chiamate a servizi esterni dal gestionale). **Cos'è SPRIX e cosa permette di fare?** SPRIX è il linguaggio di scripting proprietario di Passepartout per estendere Mexal. Esistono due dialetti: SPRIX puro (per report custom, automazioni batch, esportazioni) e SPRIX Collage (per intercettare eventi del gestionale — ad esempio quando un utente conferma un ordine — ed eseguire logica custom in quel momento). È il modo nativo per personalizzare comportamenti del gestionale. **Le Mexal WebAPI permettono di leggere e scrivere?** Sì. Le WebAPI Mexal espongono in HTTP/JSON molte aree del gestionale: anagrafiche clienti/fornitori, articoli, ordini cliente e fornitore, listini, documenti, contabilità. Sono utilizzabili per leggere dati (es. per ecommerce headless) e per scrivere (es. creare nuovi documenti). Richiedono autenticazione e gestione del rate limit. **Come si integra Mexal con un sito web moderno?** Le strategie reali che ho usato: lettura via WebAPI con caching per ecommerce B2B, push periodico di catalogo/listino su storage esterno (Vercel Blob o simili), scrittura ordini cliente via WebAPI dopo checkout, sincronizzazione anagrafiche in entrambi i versi. Quando le WebAPI non bastano, SPRIX Collage permette di intercettare eventi gestionali e propagare verso il web. --- ## AI applicata in PMI: knowledge base, agenti, AI-assisted development URL: https://andreapellizzari.it/concetti/ai-applicata-pmi/ Keywords: AI applicata, AI in azienda, knowledge base AI, RAG, tool calling, prompt caching, Anthropic Claude, Gemini, Claude Code, AI-assisted development, chatbot aziendale > Come integro l'AI nei processi reali di una PMI manifatturiera: knowledge base con RAG, agenti tool-calling, AI-assisted development con Claude Code, prompt caching e controllo dei costi. L'AI utile in azienda non è il modello generico — è il modello che conosce il tuo dominio. Lavoro per integrare i modelli (Anthropic Claude, Gemini) ai dati operativi della PMI: codici articolo, regole di compatibilità, procedure interne, documenti di gestione integrato. Il pattern ricorrente è una knowledge base ingerita con doppia passata anti-allucinazione, esposta via agente tool-calling che risponde sempre citando la fonte. Sul versante sviluppo, uso AI-assisted development (Claude Code) come leva quotidiana per arrivare prima a soluzioni che funzionano in produzione, non come scorciatoia per fare cose mediocri in fretta. I progetti che seguono coprono diversi tasselli: chatbot conversazionale con product extraction, knowledge base con regole di business live-editabili, framework wiki AI-maintained, ticket assistance con AI summary, automazioni AI per analisi normativa. ### Domande ricorrenti **Cosa significa concretamente 'AI applicata in PMI'?** Per me significa portare modelli linguistici dentro flussi di lavoro reali — non come gadget — ma come componenti che riducono lavoro ripetitivo o sbloccano interazioni nuove. Esempi concreti che ho costruito: un assistente di vendita multilingua dentro un ecommerce B2B, una knowledge base che risponde su 3.900 codici articolo citando la fonte, un sistema di mappatura di un decreto legislativo sui processi aziendali. **Quale modello uso e perché?** Anthropic Claude (Sonnet 4.6 e 4.7) per la maggior parte degli agenti in produzione: tool calling affidabile, prompt caching nativo, stile di risposta meno appiccicoso del medio. Gemini 2.5 Pro per ingest pipeline di grandi documenti dove la finestra di contesto enorme aiuta. La scelta dipende dal task, non dalla tifoseria. **Come controllo i costi degli agenti AI in produzione?** Prompt caching aggressivo sui prompt di sistema lunghi (knowledge base statica, regole di business), tool calling solo quando serve, batching delle ingest, log dei token consumati per conversazione. Su un chatbot ad alto volume questo significa la differenza tra qualche euro al mese e qualche centinaio. **Cosa significa 'AI-assisted development' nel mio flusso?** Uso Claude Code in modo continuo durante lo sviluppo: skill custom per i miei stack ricorrenti (Mexal WebAPI, Power Platform), comandi slash per operazioni che ripeto spesso, hook per automazioni. Il risultato è che produco codice testato e in produzione molto più velocemente, mantenendo il controllo sulla qualità. --- ## Microsoft Power Platform: PowerApps, Power Automate, Power BI URL: https://andreapellizzari.it/concetti/power-platform/ Keywords: Power Platform, PowerApps, Power Automate, Power BI, Power BI embedded, Dataverse, Microsoft 365, low-code, automazione aziendale > Esperienza con Microsoft Power Platform in PMI manifatturiere: PowerApps multi-screen, Power Automate come orchestratore, Power BI embedded, Dataverse, integrazione con Mexal. Power Platform è la scelta naturale quando la PMI ha già Microsoft 365 in casa: l'infrastruttura c'è, le licenze sono comprese, gli operatori usano già Outlook e Teams. Ho costruito sei PowerApps multi-screen e oltre venti flow Power Automate che integrano nativamente Mexal WebAPI, Dataverse, SQL Server del gestionale, Brevo per le notifiche, Power BI embedded nei dashboard. Più recentemente ho cominciato un porting selettivo verso Next.js dove la complessità di certe app richiedeva un controllo che PowerApps fa fatica a dare — ma molte applicazioni operative restano su Power Platform perché lì sono giuste. I progetti che seguono mostrano i pattern reali: PowerApps integrate a Mexal, porting PowerApps → Next.js per le app più complesse, automazioni che chiudono cicli aziendali completi. ### Domande ricorrenti **Quando ha senso PowerApps invece di un'app web custom?** Quando l'organizzazione ha già Microsoft 365, gli utenti sono interni, il flusso è operativo (CRUD su entità di business), e la velocità di prototipazione conta più della massima libertà visuale. Il break-even verso un'app custom arriva quando servono interazioni complesse non standard, performance estreme o un'esperienza pubblica. **PowerApps può integrare Mexal WebAPI?** Sì, tramite custom connector o (più affidabile sul lungo periodo) tramite un middleware Node.js o Power Automate che parla con Mexal e espone endpoint stabili a PowerApps. L'ho implementato in entrambi i modi; il middleware dedicato vince quando il volume di chiamate è alto e/o serve caching. **Power Automate vs flow custom: quando scegliere quale?** Power Automate eccelle in trigger Microsoft 365 (email, Teams, SharePoint), connettori standard, flow con operatori umani nel mezzo (approval). Per logica complessa, parsing fine, integrazioni custom serie, conviene un servizio Node.js dedicato. La regola pratica: se il flow Power Automate supera 30 step o include molto JSON parsing inline, è ora di portarlo via. **Power BI embedded: cosa serve davvero?** Per embedded vero (dashboard dentro un'app web) serve license Premium o Embedded SKU, capacity Power BI dedicata, gestione token. Ho lavorato con embedded sia dentro PowerApps che dentro Scriptcase. La parte spesso sottovalutata è il modeling: un dashboard senza un buon modello dati sotto è solo grafica. --- ## Sviluppo web full-stack: Next.js, TypeScript, PHP, FastAPI URL: https://andreapellizzari.it/concetti/web-fullstack/ Keywords: Next.js, TypeScript, React, PHP, FastAPI, Python, Node.js, PostgreSQL, Neon, SQL Server, SQLite, ecommerce B2B, full-stack > Sviluppo web full-stack pragmatico per PMI manifatturiere italiane: Next.js + TypeScript per il moderno, PHP e FastAPI per i bridge, integrazione SQL Server e PostgreSQL/Neon. Il mio approccio al web in azienda è ostinatamente pragmatico: scelgo lo strumento giusto per il problema, non il contrario. Next.js + TypeScript è il mio default per le interfacce moderne — performante, deployabile su Vercel o Hostinger, ottimo per ecommerce headless e applicazioni client-rich. PHP resta vivo nei bridge verso il gestionale dove l'ambiente lo richiede e dove la velocità di iterazione conta. Python con FastAPI è la mia scelta per backend AI dove servono librerie ML/embedding native. SQL Server è il gestionale; PostgreSQL/Neon è la casa per dati nuovi che non devono pesare sul gestionale. Tutti questi linguaggi convivono nei miei progetti. I progetti che seguono mostrano lo stack in azione: ecommerce B2B headless con product feed AI-generato, knowledge base FastAPI + sqlite-vec, ticket assistance con bridge PHP, porting di app PowerApps verso Next.js per i casi dove la complessità lo giustifica. ### Domande ricorrenti **Perché Next.js è la mia scelta default per il web moderno?** Tre motivi: rendering ibrido server/client che si adatta a casi reali (catalogo statico, dashboard dinamica), ecosistema React enorme, deploy semplice. Ma soprattutto, in App Router moderno, posso scrivere componenti server-side che parlano direttamente con Mexal WebAPI o con il database, senza inventare un layer di API per ogni vista. **Quando uso PHP invece di Node.js?** Quando il bridge deve girare sull'infrastruttura Hostinger del cliente senza chiedere nuove macchine, quando il volume è basso e il provisioning di un container Node è uno spreco, quando un singolo file .php ben scritto risolve in un'ora un problema che con un servizio Node richiederebbe deploy pipeline e supervision. PHP non è morto — è onesto. **Quando FastAPI invece di Node.js?** Quando il backend deve fare AI/ML reali: Anthropic SDK in Python è leggermente più maturo, le librerie di embedding e ricerca vettoriale (sqlite-vec, FAISS) sono native, l'ingest di PDF/IDML/.docx ha tooling Python migliore. Per gli agenti tool-calling complessi e ingest pipeline scelgo FastAPI. **PostgreSQL o SQL Server per dati nuovi?** Se il dato deve convivere con il gestionale, SQL Server (è dove vive Mexal). Se il dato è 'nativo del web' (utenti, sessioni, log applicativi, contenuti dinamici), PostgreSQL via Neon è più ergonomico, ha branching gratuito per ambienti dev, costi prevedibili. Mai mischiare i due ruoli: quel modo di pensare è la radice di metà dei pasticci che ho dovuto sbrogliare. --- ## Scriptcase e gestionali interni stratificati URL: https://andreapellizzari.it/concetti/scriptcase-legacy/ Keywords: Scriptcase, RAD, gestionale interno, PHP legacy, SGI ISO 9001, SGI ISO 14001, sistema di gestione integrato, SQL Server, 2FA, portale B2B, sistema legacy > Scriptcase come piattaforma RAD per gestionali interni di scala in PMI manifatturiera: oltre trecento applicazioni in produzione, SGI ISO 9001+14001 integrato, accesso diretto al database del gestionale Mexal. Scriptcase non è un framework che si trova nei pitch — eppure su gestionali interni di scala fa cose che frameworks più 'moderni' fanno fatica a fare. La piattaforma con cui lavoro ha oltre trecento applicazioni di business in produzione, stratificate negli anni: SGI ISO 9001 e 14001 con audit, qualità, ambiente, valutazione fornitori, configuratori di prodotto, indagini statistiche, HR e timbrature, ticket assistenza, portale B2B admin. Tutto su un singolo database SQL Server condiviso col gestionale Mexal — accesso diretto senza middleware, con Security Module Scriptcase per 2FA e profili. Non è un'app: è un ecosistema. La sfida è mantenerlo, evolverlo, e dove serve farlo dialogare con stack più moderni. Il progetto principale che mostra Scriptcase in azione è la piattaforma gestionale interna con 336 applicazioni — un caso studio da cui emerge come si stratifica un sistema interno serio nel tempo. ### Domande ricorrenti **Cos'è Scriptcase?** Scriptcase è una piattaforma RAD (Rapid Application Development) basata su PHP che permette di costruire interfacce CRUD, report, grafici e dashboard partendo direttamente dallo schema del database. Genera applicazioni PHP standalone deployabili su qualsiasi LAMP. È popolare in contesti aziendali italiani e sudamericani dove la velocità di sviluppo conta più dell'eleganza del framework. **Ha senso costruire nuove app in Scriptcase nel 2026?** Per app interne CRUD su un database esistente, con utenti aziendali, in tempi stretti — sì. Per nuovi prodotti con UX raffinata o requisiti di scalabilità verticale, meglio Next.js o un framework moderno. La regola che applico: Scriptcase è ottimo per vista 'tabellare', meno adatto quando l'interfaccia deve essere ricca o l'app pubblica. **Come gestisco le evoluzioni di un gestionale Scriptcase ricco di applicazioni?** Strato per strato. Le app stabili e funzionanti restano dove sono. Le nuove esigenze vengono valutate: se sono CRUD su entità esistenti, ne nasce un'altra app Scriptcase; se serve qualcosa di più ricco (real-time, grafica, AI, mobile), si fa in Next.js o in PowerApps e si integra via SQL condiviso. La coabitazione è la norma, non l'eccezione. **Cos'è SGI ISO integrato in un gestionale?** SGI = Sistema di Gestione Integrato, in genere Qualità (ISO 9001) + Ambiente (ISO 14001) + Sicurezza (ISO 45001). Implementarlo in un gestionale interno significa avere procedure, istruzioni operative, non conformità, audit, indicatori di performance ambientale tutti dentro lo stesso sistema. È utilissimo perché toglie il PDF/Excel parallelo che la maggior parte delle aziende usa per il SGI. --- ## Sviluppo in-house in PMI manifatturiera italiana URL: https://andreapellizzari.it/concetti/sviluppo-in-house-pmi/ Keywords: sviluppo in-house, sviluppatore aziendale, PMI manifatturiera, software interno, T-shaped developer, generalista pragmatico, AI-assisted development, dominio aziendale > Cosa significa fare sviluppo software interno in una PMI manifatturiera italiana: contesto, vincoli, vantaggi rispetto a una software house, AI come leva nel quotidiano. Il mio mestiere principale non è uno stack — è un contesto. Sviluppo software interno in una PMI manifatturiera italiana del Nordest. Non sono freelance, non sono consulente esterno, non offro servizi commerciali. Sono dipendente, lavoro su quello che serve all'azienda, e ho la fortuna di avere un perimetro abbastanza largo da toccare gestionali, ecommerce, knowledge base AI, automazioni, sistemi di gestione integrato. Il vantaggio dello sviluppo in-house è che vivi il dominio: conosci gli operatori che usano i tuoi software, vedi il problema dieci minuti dopo che è capitato, costruisci sopra cose reali e non boilerplate generato in fretta. Lo svantaggio è che devi essere generalista — il T-shaped developer è la modalità di sopravvivenza. Il riassunto operativo di come lavoro è raccontato in [chi-sono](/chi-sono/) e nelle case study: ogni progetto qui pubblicato è un sistema reale costruito dentro un'azienda, con i vincoli di un'azienda, con la pazienza di un'azienda. ### Domande ricorrenti **Sviluppo in-house o software house: quale modello scegliere come azienda?** Dipende dal volume e dalla criticità. In-house ha senso quando il software è strategico, quando il dominio è specifico (es. produzione manifatturiera), quando si vuole controllo lungo nel tempo. Software house esterna ha senso per progetti puntuali con scope chiuso. Una PMI con 50-200 persone in genere beneficia di almeno uno sviluppatore interno, specie se ha un gestionale customizzato. **Cosa fa uno sviluppatore in-house in una PMI manifatturiera italiana, in concreto?** Tantissime cose diverse, in funzione di cosa serve: estensioni al gestionale (SPRIX su Mexal), automazioni amministrative (Power Automate), interfacce custom (PowerApps o Next.js), siti ecommerce B2B, sistemi di gestione qualità/ambiente, knowledge base AI, integrazioni con fornitori e clienti. Il T-shaped è la regola, non l'eccezione. **Come si integra l'AI nello sviluppo in-house?** Per me l'AI è entrata nel flusso quotidiano via Claude Code e SDK Anthropic: skill custom per i miei stack ricorrenti, generazione di codice supervisionato, refactoring assistito, agenti per l'analisi di documenti aziendali (normative, capitolati, procedure SGI). Il risultato pratico è che produco molto di più senza perdere il controllo qualitativo. **Cosa rende valida la competenza maturata in-house rispetto a quella di un consulente esterno?** Profondità del dominio. Un consulente vede un progetto per qualche mese e va via; chi sta dentro vede il progetto vivere per anni, vede gli edge case che emergono solo dopo dodici mesi di uso reale, capisce perché certe scelte 'sbagliate' sono state fatte e a volte sono giuste nel contesto. Le case study qui raccolte raccontano sistemi in produzione da mesi o anni, non demo. --- # Progetti ## E-commerce B2B/B2C con integrazione Mexal, blog AI-autopilot e schede PDF real-time URL: https://andreapellizzari.it/lavori/ecommerce-b2b/ Cliente: PMI manifatturiera italiana, settore ferramenta Ruolo: Architettura, sviluppo full-stack, integrazione gestionale, automazioni Python Anno: 2026 Durata: 12 mesi di sviluppo, in produzione dal 2026 Stack: Next.js, TypeScript, Firebase, Python, Mexal WebAPI, Docuvision, Brevo, ReportLab, jsPDF/pdf-lib, SQL Server > Ecommerce multilingua in produzione per una PMI del settore ferramenta. Non solo catalogo e carrello: un bot AI che pubblica il blog in autonomia, schede tecniche generate in tempo reale da Mexal con merge degli allegati Docuvision, 180 listini PDF pre-generati in 9 lingue × 5 brand × 4 materiali. ## Contesto Cliente del settore ferramenta con 5 marchi distinti, catalogo di 1.800 articoli attivi, presenza commerciale in 9 mercati linguistici, dealer in tutta Europa. Gestionale interno: Mexal/Passepartout con Docuvision per archivio documenti tecnici. Prima del mio intervento: sito statico aggiornato a mano, copie parallele per le lingue, dealer che chiamavano per i prezzi reali, blog abbandonato da anni, schede tecniche mandate via email PDF per PDF quando un dealer le chiedeva. ## Sfida Costruire un ecommerce B2B+B2C che non fosse "l'ennesimo catalogo online", ma che integrasse nativamente il gestionale, automatizzasse le operazioni editoriali ricorrenti (blog, listini, schede), e sostenesse operativamente dealer, agenti e clienti finali in 9 lingue — senza un team IT interno a mantenerlo. ## Approccio Stack **Next.js + TypeScript + Firebase**, sviluppato con **Claude Code** come acceleratore. Di seguito le feature che fanno davvero la differenza — non le cose standard di ogni ecommerce. ### DidiBot: blog in autopilot su VPS Un agente AI che vive su VPS Hostinger (container **OpenClaw**) e gestisce da solo il ciclo editoriale del blog: - **Lunedì**: analizza Google Search Console (skill `ga4-analytics`) e propone le keyword prioritarie della settimana - **Martedì**: scrive bozze articolo in markdown, genera l'immagine di copertina, carica su `/admin/api/upload-catalog-image.php`, crea il post via `POST /api/blog/posts` con traduzioni in 6 lingue - **Giovedì**: pubblica automaticamente su LinkedIn, Instagram, Facebook via `upload-post`, sottopone l'URL a Google Indexing API - **Comunicazione**: WhatsApp come broker (dmPolicy ristretto a 2 numeri autorizzati), GitHub per commit automatici degli MDX Il blog non è una pagina morta. Pubblica contenuti reali, a ritmo, senza intervento umano dopo il setup iniziale. ### Schede tecniche PDF real-time da Mexal + Docuvision L'endpoint `/api/scheda-tecnica/[codice]/pdf` genera una scheda tecnica A4 completa su richiesta: - Legge in diretta da **Mexal WebAPI** (comando `lista_docdv` su classe Docuvision 1200) l'elenco degli allegati tecnici del prodotto - Scarica i PDF allegati via `get_allegato_archivio` - Compone la scheda con **jsPDF**: badge tipologia, codice + barcode via `bwip-js`, immagine prodotto, attributi tradotti, tabella misure con codici colorati per lettera (A/B/C/Ø) - Con `?includeDrawings=true` fa il merge via **pdf-lib** degli allegati Docuvision, gestendo automaticamente la **rotazione landscape→portrait** per i disegni quotati - Parametri: `?lang=de` per monolingua, `?all=true` per tutte le varianti, default bilingue IT/EN Zero tempo di attesa per dealer e commerciali: cliccano e hanno il PDF. ### 180 listini PDF multilingua pre-generati 5 brand × 9 lingue × 4 materiali base = **180 listini PDF**, tutti pre-generati da uno script Python (`generate_catalog_pdf.py` con **ReportLab**, non viewer esterni). Dizionario di traduzione custom interno per tutti i termini tecnici e di navigazione (9 lingue + pattern matching sulle stringhe composte tipo "Pagina X di Y"). L'area B2B propone il PDF nella lingua del dealer con fallback automatico all'italiano se non esiste. ### scheduled_export.py: l'orchestratore nascosto Uno script Python di ~1100 righe che gira cron-schedulato ogni notte ed è il vero motore dell'ecommerce. Sette sync paralleli non bloccanti: - `V_B2B_EXPORT_CATALOGO_NEW` da SQL Server → MySQL via **pyodbc** (con **safety check**: abort se meno di 100 righe, per non pubblicare un catalogo vuoto dopo un errore ERP) - PDF articoli, prezzi dealer B2B, anagrafica agenti, KPI per nazione, metodi pagamento, inventario - Export `products_python.json` con traduzioni Claude API (cache aggressivo), poi upload dual-target: **FTP a SiteGround** + **push gzip a endpoint Vercel** per bypassare CAPTCHA SiteGround su payload grandi - Generazione feed **Google Merchant Center** XML - Log giornaliero + storico JSON ultimi 30 giorni È l'infrastruttura invisibile che rende tutto il resto "sempre aggiornato" senza intervento umano. ### Email transazionali con tracking dettagliato Invio via **Brevo**, con proxy PHP su SiteGround per aggirare restrizioni IP. Ogni email è loggata in Firestore (`email_logs`) con `brevoMessageId`, status aggiornato in tempo reale da webhook Brevo (`sent` → `delivered` → `opened` → `clicked` → eventuale `bounce`). Dashboard interna nell'admin panel: filtri per tipo, lingua, status, range date; metriche percentuali di consegna e apertura; azioni di resend per le email di benvenuto. ## Risultato Sistema in produzione usato quotidianamente da dealer B2B, clienti B2C e agenti di 5 marchi in 9 mercati linguistici. Il blog si popola da solo, i listini si aggiornano ogni notte, le schede tecniche si generano al volo. Il cliente gestisce l'ecommerce **senza un team IT interno** — la parte operativa è nelle mie mani, ma il 95% del sistema funziona senza mai doverci mettere le mani. Quello che considero il risultato tecnico più interessante è che **ogni automazione nascosta ha un controllo di sanità esplicito** — safety check, fallback, logging, webhook status. Non è AI buttato sopra un ecommerce tradizionale: è un sistema in cui ogni pezzo sa cosa fare quando qualcosa va storto. È la disciplina che distingue un progetto che regge in produzione da uno che crolla al primo errore ERP. --- ## Knowledge base AI con regole di business live-editabili e double-pass ingest URL: https://andreapellizzari.it/lavori/ai-knowledge-base/ Cliente: PMI manifatturiera italiana, settore ferramenta Ruolo: Architettura, pipeline di ingest, agente tool-calling, editor admin, deploy produzione Anno: 2026 Durata: 4 mesi alla prima release, sviluppo attivo in corso Stack: Python, FastAPI, Anthropic Claude Sonnet 4.6, Gemini 2.5 Pro, SQLite + sqlite-vec, PHP 8.3, Docker, Caddy, Mexal WebAPI > Un assistente AI che conosce 3.900 codici articolo e 15 regole di compatibilità tipizzate, risponde citando sempre la fonte, registra tutto in modo immutabile. Costruito con un'ingest pipeline a doppia passata che cattura le allucinazioni silenziose di Gemini, un editor admin live che fa scrivere al tecnico del cliente senza rebuild, e i webhook nativi Mexal per reattività in tempo reale. In produzione HTTPS da aprile 2026. ## Contesto Cliente nel settore ferramenta, catalogo tecnico ampio e complesso: 3.900 codici articolo commerciali (struttura a 12 caratteri con semantica posizionale — famiglia / direzione / serie / finitura), 164 pagine di manuale tecnico con disegni quotati, una quindicina di regole di compatibilità tra componenti (accoppiamenti bandella-cardine con vincoli di diametro, regole su boccole di riduzione con eccezioni per serie, condizioni di applicazione per materiale). Il sapere tecnico dell'azienda viveva nelle teste di poche persone esperte. I dealer chiamavano il commerciale per domande che si ripetevano ogni settimana: *"questa bandella è compatibile con quel cardine?"*, *"per persiane in legno marine cosa mi consigli?"*, *"qual è l'eccezione sulla serie Monza?"*. Un commerciale nuovo impiegava mesi per diventare autonomo. Il manuale tecnico esisteva ma veniva consultato poco. L'idea iniziale del cliente era *"un ChatGPT che sa del nostro catalogo"*. Quello che serviva in realtà era più profondo: un sistema che trasformasse la conoscenza aziendale in un asset vivo, editabile dal tecnico interno, verificabile in audit, e che non allucinasse mai dati tecnici critici. ## Sfida Rispetto a un RAG "fai-da-te" servivano tre garanzie che la maggior parte dei chatbot AI non offre: 1. **Nessuna risposta oracolare**: ogni affermazione deve citare regola applicata + documento sorgente. Se un dealer contesta una risposta, si risale al paragrafo esatto. 2. **Nessuna allucinazione silenziosa** in fase di estrazione dati dal manuale PDF — errori tipo "misura A2 letta come A" che un ingest single-pass lascia passare senza accorgersene. 3. **Autonomia del tecnico interno** sulle regole di business. Quando una nuova eccezione emerge da un caso cliente, deve poterla codificare da solo, senza passare da sviluppatori e senza aspettare rilasci. ## Approccio Architettura a quattro strati — fonti autoritative, knowledge base compilata, indici derivati, agente conversazionale — costruita in AI-assisted development con **Claude Code**. Di seguito le sei cose che fanno davvero la differenza. ```mermaid flowchart TB A[Fonti autoritative
PDF manuale · Mexal SQL · JSON cataloghi
IMMUTABILI] -->|ingest double-pass
Gemini 2.5 Pro + voting| B[Knowledge base compilata
pagine markdown · regole tipizzate R1..R15
LLM scrive, umano legge/edita] B -->|build deterministic| C[Indici derivati
SQLite mirror · sqlite-vec embedding
FAQ persistenti · usage log JSONL] C --> D[Agente conversazionale
Claude Sonnet 4.6 · 12 tool
prompt caching · citation inline] B -. hot-reload :rw .-> E[Editor admin
tecnico interno cliente] E -. edit live .-> B D -->|risponde| U((Dealer / cliente)) style A fill:#fef3c7,stroke:#d97706 style B fill:#ede9fe,stroke:#7c3aed,stroke-width:2px style C fill:#d1fae5,stroke:#10b981 style D fill:#fce7f3,stroke:#db2777 style E fill:#dbeafe,stroke:#2563eb ``` ### Ingest Gemini double-pass con voting e confidence queue Per estrarre dati dal manuale PDF uso **Gemini 2.5 Pro in doppia passata** sullo stesso contenuto (pagina renderizzata come PNG). Confronto gli output, assegno un punteggio di confidenza basato sulle divergenze, e ciò che sta sotto la soglia 0.7 finisce in una cartella `_review/` per validazione manuale del tecnico. Questa disciplina ha catturato errori silenziosi che un estrattore single-pass aveva lasciato passare (caso reale: `A2 letto come A` in layout tabellare spread, 44% di errore su una versione intermedia della pipeline). ### Agente con 12 tool specializzati e prompt caching aggressivo Il runtime è **Claude Sonnet 4.6** con tool calling. 12 funzioni esposte: `sql_query` sul mirror SQLite, `read_wiki` per lettura fresh dal disco, `list_regole` per il catalogo delle regole tipizzate, `calcolo_esposizione` che chiama in diretta le WebAPI Mexal per saldo/fido/insoluti, `get_kit_giotto_plus` per lookup atomico kit, e altri. Il system prompt (~1.500 token con glossario di dominio, regole operative, few-shot) è in **prompt cache ephemeral**: cache-read a €0,30/1M token invece dei €3/1M dell'input full. **Costo reale a regime: €0,04-0,08 per domanda complessa** con 3-8 tool call per query. ### FAQ memoization con similarity vettoriale e anti-fossilization Le domande ben risolte vengono promosse a **FAQ persistenti** in una tabella SQLite con embedding Gemini 3072d (via **sqlite-vec**). Alla query successiva, il tool `search_faq` è la prima mossa dell'agente: se la distanza coseno è sotto 0.55 cita testualmente la FAQ (costo marginale zero, zero latenza di ragionamento); tra 0.55-0.80 la usa come traccia; sopra 0.80 ignora. Il problema noto con le FAQ compilate — che **cristallizzano errori** se non revisionate — è risolto via schema: ogni FAQ ha `last_reviewed_at`, `reviewed_by`, `review_note`. Un endpoint admin `/admin/faq-stale?min_days=90` elenca le FAQ non revisionate, evidenziate in rosso nella dashboard interna. Mitigazione nata dopo aver letto i 632 commenti alla proposta originale del pattern — critica trasformata in schema. ### Editor admin live con volume `:rw` e hot-reload delle regole Il tecnico del cliente modifica le regole tipizzate (R1-R15) da un'interfaccia web PHP+SQLite separata dal backend dell'agente, con layout "foglio manuale" che gli è familiare. Il volume Docker del wiki è montato `:rw`, ogni save scrive direttamente sul filesystem con backup `.bak-{timestamp}` automatico. L'agente legge **fresh dal disco** al tool call successivo — nessun rebuild, nessun restart, nessun deploy richiesto per un cambio di regola. Cambio condizione commerciale speciale per cliente: 30 secondi dalla modifica all'applicazione in produzione. ### Webhook nativi Mexal per reattività event-driven Feature poco nota di Passepartout: il gestionale ha un sistema di **notifiche HTTP native** per eventi sugli archivi (`PUTPC` / `DELPC` per clienti, `PUTAR` / `DELAR` per articoli, `PUTMM` / `DELMM` per movimenti magazzino, `PUTDE` per deleghe). Combinato con le WebAPI in scrittura, il sistema è **completamente bidirezionale**: nuovo movimento magazzino in Mexal → webhook al backend AI → verifica sotto-scorta contro regola `R-MAG-04` → alert automatico al responsabile acquisti. **Niente polling**, niente cron job: il sistema reagisce in tempo reale agli eventi del gestionale. ### Log audit JSONL append-only e analisi costi puntuale Ogni conversazione finisce in `conversations.jsonl` con timestamp, IP, user-agent, session_id, query, risposta, tool chiamati, usage token in/out, durata, eventuali errori. Append-only, immutabile. Materiale per tre scopi contemporanei: candidate FAQ da promuovere, eventuali verifiche di conformità normativa, analisi puntuale del costo (token per query × listino modello = costo reale). ## Risultato In produzione HTTPS dal 19 aprile 2026 — dealer B2B che interrogano il sistema in linguaggio naturale, tecnico interno che mantiene autonomamente le regole, log audit che cresce. Costo operativo a regime: **~€60/mese** con 50 domande/giorno — ordine di grandezza enterprise-ready su budget PMI. Il messaggio tecnico più importante di questo progetto, per chi lo guarda da fuori: **oggi una PMI italiana può costruire un sistema AI di questa complessità senza adottare prodotti enterprise da decine di migliaia di euro all'anno**. Serve disciplina d'architettura (double-pass voting, confidence queue, citation obbligatorie, anti-fossilization delle FAQ, fallback chain espliciti) più gli strumenti giusti. Non AI buttata sopra un catalogo: un sistema in cui ogni pezzo sa cosa fare quando qualcosa va storto, e dove l'errore di produzione più probabile — un'allucinazione silenziosa dell'AI — è intercettato già in fase di ingest. --- ## BlumCat: chatbot tecnico interno su catalogo ferramenta da mobili — gate logic deterministico, eval set come rete di sicurezza, single source of truth MD→Python→prompt URL: https://andreapellizzari.it/lavori/blumcat/ Cliente: PMI manifatturiera italiana, settore ferramenta da mobili Ruolo: Architettura, pipeline OCR doppia, agente tool-calling, gate logic, eval set, deploy produzione Anno: 2026 Durata: Sviluppo continuo da metà 2025, pattern stabilizzato a fine aprile 2026 Stack: Python, FastAPI, Anthropic Claude Haiku 4.5 + Sonnet 4.6, sentence-transformers (mpnet 768d), MinerU, pdfplumber, SQLite + embedding BLOB, NSSM Windows service, prompt caching ephemeral > Un assistente tecnico interno per chi vende e installa ferramenta Blum (cassetti LEGRABOX/MERIVOBOX, cerniere CLIP top, ante a ribalta AVENTOS): copre 45 famiglie, 1.936 codici articolo verificabili e un manuale tecnico di 758 pagine. Costruito su pattern proprietari di context engineering — push deterministico del contesto invece di tool che il modello potrebbe dimenticarsi di chiamare, gate logic server-side con marker espliciti, tool_choice forzato API-side per i punti ad alto rischio, single source of truth con derivazione automatica MD→Python→system prompt — e gated da un eval set deterministico che fa da rete di sicurezza per i refactor. In produzione LAN dal 2026, costo ~€0,05–0,15 per conversazione. ## Contesto Cliente PMI nel settore della ferramenta da mobili, rivenditore tecnico di un grande fornitore austriaco (Blum) con un catalogo molto strutturato: cassetti modulari, cerniere a clip, sistemi a ribalta, suddivisione interna, guide di scorrimento. Il manuale ufficiale è un PDF di 758 pagine fitte di tabelle, formule grafiche e disegni tecnici quotati; le famiglie prodotto sono 45, i codici articolo quasi duemila, e i nomi commerciali (LEGRABOX, MERIVOBOX, AVENTOS HK-S, CLIP top BLUMOTION) si riferiscono a sistemi che si configurano combinando 4–6 componenti. Il sapere viveva nelle teste di pochi tecnici esperti. Le domande che si ripetevano ogni giorno — *"cassetto da 50 cm in cucina, che ferramenta serve?"*, *"questa cerniera mi regge un'anta da 12 kg?"*, *"in TANDEMBOX la sigla D è una spondina o no?"* — passavano per telefonate o email, e un commerciale junior impiegava mesi a diventare autonomo. Il manuale era consultato poco perché lungo da scorrere e organizzato per logica del fornitore, non dell'utente. L'idea iniziale era *"un ChatGPT che sa di Blum"*. Il problema vero era diverso: garantire che il bot **non inventasse mai un codice**, e che le scelte di configurazione passassero per gli stessi vincoli del manuale (portate, NL ammesse, abbinamenti consentiti) — perché un dato sbagliato in distinta significa un ordine sbagliato a fornitore, non un'esperienza utente sub-ottimale. ## Sfida Tre garanzie che un RAG generico su PDF non offre out-of-the-box: 1. **Zero allucinazioni sui codici**: pattern come `770M5002S` o `753.5001S` sono linguistico-statisticamente plausibili anche quando inventati. Servono meccanismi che impediscano al modello di "tirare a indovinare". 2. **Chiusura del flusso conversazionale sui dati chiave**: un cassetto si configura partendo da profondità + portata; senza quei due dati la risposta è inventabile. Il bot non deve mai assumere default silenziosi su parametri che cambiano il codice principale. 3. **Refactor sicuri nel tempo**: il sistema cresce, il manuale cambia ogni anno, le regole vengono raffinate dal tecnico interno del cliente. Serve un'infrastruttura che permetta di mettere mano al codice senza accorgersi mesi dopo che una piccola modifica ha rotto un caso d'uso. ## Approccio Architettura su tre layer — sorgenti immutabili (manuale OCR'd doppio), wiki autoritativo a quattro categorie editabile, agente conversazionale con 17 tool — costruita in AI-assisted development con **Claude Code**. Sei scelte distinguono il progetto da un RAG standard. ```mermaid sequenceDiagram participant U as Utente participant C as Classifier deterministico (Python) participant L as Claude (Haiku/Sonnet) participant T as Tool composito assemble_distinta_cassetto participant DB as SQLite + embedding 768d U->>C: "distinta legrabox 500" C->>C: Intent: famiglia=legrabox, dato chiave mancante=portata C-->>U: GATE inietta direttiva [blumcat-gate:portata]
"chiedi solo la portata, niente codici" L-->>U: "40 kg standard o 70 kg high-load?
[OPZIONI: 40 kg | 70 kg]" U->>C: "70 kg" C->>C: Tutti i dati chiave OK → marker [blumcat-gate:done]
+ inietta distinta-template D001 LEGRABOX C->>L: tool_choice forzato API-side: assemble_distinta_cassetto L->>T: famiglia=legrabox, NL=500, portata=70 T->>DB: lookup pattern + verifica codici reali DB-->>T: 753.5001S, 770M5002S, ZB7M000S, ZF7M70E2, 609.1500 T-->>L: distinta con status=in_db per ogni codice L-->>U: distinta + foto + chip pagina manuale + opzioni varianti Note over C,L: Eval set deterministico verifica:
marker iniettati, tool chiamati, codici reali, blacklist allucinazioni storiche ``` ### Doppio OCR open-source come barriera all'errore di lettura silenzioso Niente single-pass: il manuale passa contemporaneamente attraverso **MinerU** (struttura, tabelle, layout) e **pdfplumber** (raw text — recupera formule grafiche rese come glifi vettoriali che il primo perde). I due output vivono affiancati per famiglia con `fonte` distinto nel DB; il modello vede entrambi via ricerca semantica. Caso reale: l'altezza spondina K = 115,6 mm in TANDEMBOX antaro era invisibile a MinerU perché disegnata graficamente, pdfplumber l'ha catturata come testo flat. È il pattern di affidabilità che mi porto da progetto a progetto: due estrattori complementari sono sempre più solidi di uno solo, anche del migliore. ### Wiki autoritativo a quattro categorie con editor live per il tecnico interno Sopra ai PDF estratti vive un wiki markdown editabile via UI dedicata: **Distinte** (composizione canonica per famiglia: pattern dei codici, varianti, optional), **Guide** (decision tree per scelte: "LEGRABOX o MERIVOBOX?"), **Regole** (vincoli tecnici trasversali con frontmatter YAML — peso anta, numero cerniere, dimensionamento), **Famiglie** (schede narrative "cos'è X, quando sceglierla / NON sceglierla"). Il tecnico interno del cliente modifica un MD da sidebar+textarea+preview live; il salvataggio crea backup `.bak.{ts}` automatico (retention 10), un validator AI controlla pre-save che non ci siano codici inventati o sigle interne. Un reindex selettivo riallinea il DB in 2 minuti. Il cliente è proprietario effettivo della propria knowledge base. ### Single source of truth con derivazione MD→Python→system prompt Per le 4 famiglie di sistemi cassetto (LEGRABOX, MERIVOBOX, TANDEMBOX antaro, METABOX) i parametri tecnici (altezze spondina, NL ammesse, portate, pattern del codice articolo, colori validi) inizialmente vivevano in **3 posti**: i markdown autoritativi, le tabelle inline del system prompt, un dict Python consumato dal tool composito. Ogni cambio = 3 file da toccare = divergenza silenziosa garantita. L'ho ricondotto a **una sola fonte editabile**: il modulo `distinta_parser.py` legge i `D*.md` e produce sia il dict Python (lazy-cached, drop-in del legacy) sia il markdown della sezione del prompt (sostituito al boot via placeholder). Cambiare un valore nel MD si riflette automaticamente al riavvio in entrambi i livelli derivati. È la disciplina che blocca il debt più costoso: il valore che diverge in silenzio. ### Gate logic server-side con marker espliciti + tool_choice forzato API-side Il pattern di context engineering più distintivo del progetto. Un classifier Python deterministico (regex su keyword + scansione di tutto lo storico user, non solo l'ultimo turno) riconosce due stati nei flussi di configurazione cassetto: - **dati chiave mancanti** (manca NL o manca portata) → inietta nel messaggio una direttiva interna `[blumcat-gate:portata]` che impedisce al modello di chiamare il tool, gli vieta di inventare default e gli impone una sola domanda con `[OPZIONI: 40 kg | 70 kg]`; - **dati chiave completi** → inietta `[blumcat-gate:done]` + la distinta-template della famiglia + forza il tool composito tramite il parametro `tool_choice` nativo dell'API ("DEVI chiamare assemble_distinta_cassetto al primo round, no se no ma"). Il principio è che le garanzie hard non passano dal prompt ("DEVI fare X" è un suggerimento, non un contratto) ma da **codice deterministico** + **parametri API**. Il vantaggio: ogni passaggio è ispezionabile, testabile, debuggabile; e funziona con Haiku come con Sonnet. ### Routing modello calibrato sull'eval set Non "Haiku per tutto, Sonnet quando serve" ma **routing per rischio di allucinazione**: lookup secchi (codice articolo, sigla del glossario) vanno su Haiku con cache ben sfruttata; configurazioni, decisioni multi-step, "differenza tra X e Y", calcoli combinati vanno su Sonnet. Le regex del router sono state calibrate **dall'eval set**: passando da 5 a 15 casi sono emersi 4 bug reali in produzione (regex portata che non riconosceva 25/30/65 kg, parsing famiglia che mancava "aventos hk" senza suffisso) — fixati con tre one-liner perché individuati prima che diventassero un report utente. Il routing è codice come ogni altro: si testa. ### Eval set deterministico come gate di refactor e di deploy Una trentina di casi end-to-end (API reale, niente mock) eseguiti via il modulo `evals/` in ~5 minuti per ~€0,30 per run. Ogni caso definisce una conversazione e un set di assertions: tool che il modello DEVE chiamare, tool che NON DEVE chiamare, marker che il classifier DEVE iniettare, codici che la risposta DEVE contenere (verificati in DB), e — fondamentale — **blacklist di codici storicamente allucinati** (`770M500L`, `770M500R`, `378D5002SA`...) che la risposta NON DEVE mai contenere. Ogni bug fissato diventa un nuovo caso (regression test organico). È quello che ha permesso di fare il refactor "single source of truth" descritto sopra senza paura: baseline 28/30 PASS prima del cambio, 28/30 PASS dopo → il refactor non ha rotto nulla. Senza eval set, sarebbe stato un atto di fede. ## Risultato In produzione interna LAN dal 2026 — bot con 17 tool esposti, ricerca semantica su 4.061 chunk con embedding 768d, copertura su 45 famiglie e 1.936 codici, foto e PDF locali per 235 prodotti, citazioni inline con preview cliccabile delle 758 pagine del manuale pre-renderizzate. Il routing tiene il **costo medio nell'ordine dei 5–15 centesimi per conversazione** grazie al prompt caching aggressivo (sconto del 90% sui ~7–8k token "fissi" di catalogo + tools + distinta-template), un ordine di grandezza al di sotto di una conversazione full-Sonnet senza cache. Il messaggio tecnico più trasferibile, per chi guarda da fuori, è che in un dominio dove l'allucinazione **non è un fastidio ma un costo concreto** (un ordine sbagliato a fornitore) il prompt engineering da solo non basta. Servono pattern di **context engineering**: deterministica server-side dove possibile (classifier, gate, push del contesto), parametri API hard dove necessario (`tool_choice`), single source of truth per ogni valore che rischia di duplicarsi, eval set come gate di tutto. È il pattern che ho consolidato qui e che applico sui nuovi progetti di knowledge base AI per PMI manifatturiere — un livello sopra al framework descritto in *architettura-wiki*, con focus sulle garanzie hard che il dominio tecnico richiede. --- ## Architettura wiki: framework interno di knowledge management AI-maintained per applicazioni di assistenza tecnica industriale URL: https://andreapellizzari.it/lavori/architettura-wiki/ Cliente: Framework interno riusabile su progetti di knowledge base AI per PMI industriali Ruolo: Architettura, sviluppo del framework, regole tipizzate, tool contract per agenti Anno: 2026 Durata: In evoluzione continua — stabilizzato sulle prime applicazioni in produzione Stack: Python, FastAPI, Claude SDK, Gemini, SQLite, sqlite-vec, Docker, Caddy, Markdown, YAML, Claude Code Skills > Un framework proprietario a quattro layer — schema di istruzioni tipizzate, wiki markdown accumulato dall'agente, sorgenti autoritative immutabili, indici derivati SQLite — che ho costruito per poter partire più in fretta sui progetti di knowledge base AI in ambito industriale. Double-pass ingest con confidence queue anti-allucinazione, citation per-claim inline, editor live per il cliente proprietario della knowledge base, log delle conversazioni append-only, costi misurabili al centesimo. Applicato in produzione sulla ferramenta (si veda la case study ai-knowledge-base) e in sviluppo su un secondo verticale per la conformità ISO. ## Contesto Dopo aver lavorato su un paio di progetti di assistente AI per PMI manifatturiere, ho capito che il pattern di lavoro era sempre lo stesso: c'è un corpus di conoscenza aziendale stratificata — catalogo prodotti, manuali tecnici, regole di business, FAQ storiche — che vive frammentato tra gestionale, PDF, head di poche persone esperte, fogli Excel. Il lavoro dell'assistente AI non è "rispondere", è **comporre**: leggere da fonti disomogenee, organizzare in uno strato intermedio leggibile, mantenerlo coerente nel tempo, citare sempre la fonte, non allucinare quando un dato non c'è. A forza di incontrare sempre le stesse scelte architetturali ho formalizzato un **framework interno** che uso come punto di partenza su ogni nuovo progetto di questo tipo. ## Sfida Evitare di ricostruire ogni volta da zero l'infrastruttura noiosa (ingest pipeline, tool contract, citation schema, queue di review, FAQ memoization, editor per il cliente, log persistenti), concentrando il lavoro sul **valore verticale** del singolo progetto (lo schema delle regole di dominio, l'adattamento alla fonte cliente). Senza cadere nella trappola opposta: un framework che diventa un mini-prodotto software da manutenere — non è il mio obiettivo. ## Approccio Il framework è documentato in un file `PATTERN.md` di riferimento e in una **skill Claude Code** (`~/.claude/skills/wiki-model/`) che si carica in automatico quando apro un progetto della famiglia. Sei scelte che distinguono questa architettura da un RAG standard. ### Quattro layer persistenti con ruoli chiari Il sistema è organizzato in quattro strati distinti. **Sorgenti autoritative** (PDF, export gestionale, JSON, risposte API): immutabili, mai modificate da nessuno. **Wiki markdown**: pagine `.md` con frontmatter YAML, scritte e ri-scritte dall'agente durante l'uso, lette dall'umano. **Schema di istruzioni** (`CLAUDE.md`, `AGENTS.md`): convenzioni, regole tipizzate R1-Rn, workflow di ingest/query/lint. **Indici derivati** (SQLite + `sqlite-vec`): mirror SQL, embedding vettoriali, FAQ persistenti, viste pre-calcolate. Ogni strato ha un ruolo preciso e uno è rigenerabile dagli altri. ### Ingest a doppia passata con confidence queue L'estrazione dalle fonti non è una chiamata sola: è **double-pass** — due letture indipendenti del medesimo contenuto con un modello di visione (tipicamente Gemini per i manuali PDF con tabelle di misure), confronto dei due output, assegnazione di un punteggio di confidenza da 0 a 1 in base alle divergenze rilevate. Sopra soglia 0.7 il dato entra direttamente in wiki; sotto soglia va in una **cartella `_review/`** per validazione umana prima di diventare conoscenza ufficiale. È la barriera contro l'errore di lettura silenzioso — il dato che sembra giusto, nessuno lo verifica, e salta fuori quando un cliente contesta una misura. Sul primo progetto in produzione, il double-pass ha segnalato una percentuale a doppia cifra di letture problematiche che il single-pass avrebbe lasciato passare. ### Citation per-claim inline + fallback chain esplicito Ogni affermazione nella wiki e ogni risposta dell'agente include la fonte: *"La bandella regge ottanta chili (fonte: raw/manuale_pag47.json)"*. Dove il dato esatto non esiste, la risposta non inventa — segue una **fallback chain** documentata (ID esatto → SKU sibling → variante base → mirror simmetrico → famiglia padre → "dato non disponibile"), con il campo `_fallback_reason` che traccia quale step ha prodotto la risposta. Il tecnico sa sempre quale inferenza l'agente ha fatto e su quale fonte — è la base della verificabilità e, in prospettiva, della compliance. ### FAQ memoization con anti-fossilization schema Le query ben risolte vengono promosse a **FAQ persistenti** in SQLite con embedding: alla query successiva simile, il tool `search_faq` è la prima mossa dell'agente e risponde senza chiamare il modello. Costo marginale zero, latenza bassa. Il rischio noto delle FAQ è che fossilizzino errori — una risposta sbagliata ripetuta all'infinito. Mitigazione: ogni FAQ ha `last_reviewed_at`, `reviewed_by`, `review_note`, un endpoint admin `/admin/faq-stale?min_days=90` elenca quelle non revisionate da troppo tempo, ed evidenzia in rosso quelle scadute. La FAQ diventa così memoria viva con audit trail, non memoria morta. ### Editor live per il cliente con hot-reload delle regole Il tecnico interno dell'azienda cliente ha accesso a un **editor admin** che gli permette di modificare le regole tipizzate della knowledge base (R1-R15 tipicamente — regole come "fallback di simmetria", "priorità di marchio", "soglia minima di catalogo") via interfaccia web, layout "foglio manuale" che gli è familiare. Il volume Docker del wiki è montato `:rw`, ogni save scrive direttamente il `.md` con backup `.bak-{timestamp}` automatico. L'agente legge al prossimo tool call. **Cambio di regola applicato in ~30 secondi, senza deploy, senza intervento mio**. È la scelta che trasforma il cliente da soggetto passivo a proprietario effettivo della propria knowledge base. ### Log conversazioni append-only + costi al centesimo Ogni conversazione con l'agente finisce in un file **`conversations.jsonl` append-only**: timestamp, IP, session id, query, risposta, sequenza dei tool chiamati, token in/out, costo calcolato a tariffa del modello. Tre scopi pratici: candidate FAQ da promuovere, audit di compliance, analisi costo puntuale. Il cliente vede i numeri reali — tipicamente qualche decina di centesimi per conversazione — e sa esattamente quanto gli costa mantenere l'assistente vivo. ## Risultato Il framework ha una prima applicazione matura in produzione — la **ferramenta industriale** raccontata nella case study `ai-knowledge-base` — e un secondo verticale in sviluppo per il supporto alla **conformità ISO 9001/14001**, dove lo stesso pattern serve a gestire procedure, non conformità, evidenze di audit. La parte bella è che ogni nuovo progetto parte già con il 70% dell'infrastruttura pronta, le scelte di affidabilità (double-pass, citation, queue, FAQ anti-fossilization) non vengono rinegoziate ogni volta, e il tempo di discussione col cliente si concentra dove c'è valore — sulle **regole di dominio**, non sulla plumbing. L'altra parte bella è che ogni applicazione concreta restituisce al framework lezioni di campo: il `PATTERN.md` non è congelato, è un documento che cresce con i progetti. --- ## Sistema di assistenza ticket con plugin Outlook VSTO, AI summary con Claude e integrazione Mexal via proxy PHP URL: https://andreapellizzari.it/lavori/ticket-assistance/ Cliente: PMI manifatturiere italiane del Nordest, due istanze gemelle Ruolo: Architettura, sviluppo full-stack, plugin Outlook VSTO, integrazione Claude, proxy Mexal Anno: 2026 Durata: 6 mesi al primo core operativo, sviluppo attivo Stack: Next.js 16, TypeScript, Neon PostgreSQL, Vercel Blob, Outlook VSTO (.NET), WPF Email Uploader, Claude API, Mexal WebAPI, PHP proxy > Piattaforma di gestione ticket per assistenza clienti B2B, in produzione interna su due istanze gemelle (stessa codebase, deploy separati). Plugin Outlook nativo VSTO .NET che allega email al ticket con un click, riassunti AI generati da Claude via tool calling, integrazione Mexal per anagrafiche e storico ordini, access-token pubblici per condivisione sicura con riparatori esterni. ## Contesto Due PMI manifatturiere del Nordest con struttura simile: rete di dealer B2B, assistenza post-vendita in capo a un team ristretto di operatori, la maggior parte delle richieste che arriva via email su caselle generiche e viene lavorata direttamente in Outlook. Canali secondari: telefono ritrascritto a mano, chat saltuaria, form di contatto. Risultato: nessuno aveva la storia completa del cliente, le richieste "cadevano nel vuoto", il cliente ripeteva la stessa cosa a interlocutori diversi. ## Sfida Costruire una **piattaforma di ticketing** che mettesse ordine senza costringere gli operatori a cambiare strumento quotidiano (Outlook), che si integrasse con il gestionale Mexal per non duplicare anagrafiche e storico ordini, e che potesse girare come **due istanze gemelle** — stessa codebase, due deploy separati, due database Neon indipendenti, branding diverso. Niente multi-tenant con tenantId su tabella condivisa: isolamento per deploy, più semplice da auditare e più sicuro tra clienti che non devono vedersi i dati. ## Approccio Stack **Next.js 16 + TypeScript + Neon PostgreSQL + Vercel Blob**, con Claude API per le feature AI-assisted. Di seguito le feature che fanno la differenza — non le cose standard di un sistema di ticketing. ```mermaid flowchart LR subgraph IN[Canali di input] OUT[Plugin Outlook VSTO
.NET desktop] WPF[Email Uploader WPF
drag&drop .msg] WEB[Portale web] end IN --> API[Next.js API routes
JWT operatori interni] API -->|dual persistence| DB[(Neon PostgreSQL
ticket_history append-only)] API -->|blob storage| BLOB[(Vercel Blob
allegati .msg + file)] API -->|POST /summary| CLD[Claude Sonnet
tool calling] CLD -->|digest ragionato| DB API -.->|X-Proxy-Secret| PHPROXY[Proxy PHP
SiteGround] PHPROXY -->|credenziali server-side| MEX[Mexal WebAPI
anagrafiche · ordini · docs] DB -->|access_token 32 byte| EXT((Riparatore esterno
/fornitore)) style API fill:#ede9fe,stroke:#7c3aed,stroke-width:2px style CLD fill:#fce7f3,stroke:#db2777 style PHPROXY fill:#fef3c7,stroke:#d97706 style EXT fill:#d1fae5,stroke:#10b981 ``` ### Plugin Outlook VSTO nativo: allega email al ticket con un click Un add-in VSTO in .NET/C# (non Office.js, scelta deliberata per lavorare direttamente sull'object model di Outlook desktop) che aggiunge un ribbon "ArcoService" in Outlook. L'operatore seleziona l'email, apre il dialog di ricerca ticket, seleziona il ticket corrispondente, e il plugin invia il `.msg` completo al backend via `POST /api/tickets/{id}/attach-email` in multipart form-data con header `x-api-key`. Il backend estrae mittente, destinatari, oggetto, preview corpo, allegati, carica il `.msg` su Vercel Blob e crea un record `TicketEmail` agganciato al ticket. Distribuzione via installer ClickOnce, configurazione UI per API key e URL backend. ### Email Uploader WPF come alternativa drag&drop Per gli operatori che preferiscono lavorare fuori da Outlook — o che usano client di posta diversi — una **seconda applicazione Windows in WPF (.NET 8)** permette di trascinare file `.msg` direttamente sul desktop app e caricarli sul ticket. Usa `MsgReader.dll` per il parsing dei messaggi Outlook, stesso endpoint del plugin. Due strade per lo stesso risultato, perché non tutti lavorano nello stesso modo. ### Proxy PHP lato server per Mexal, credenziali mai nel frontend Le API Next.js non chiamano direttamente Mexal WebAPI: passano da un **micro-proxy PHP** ospitato su un hosting esterno (SiteGround), autenticato con header `X-Proxy-Secret`, che aggiunge le credenziali Mexal server-side e inoltra la richiesta via HTTPS. Significa che il deploy Vercel non conosce le credenziali del gestionale, la superficie d'attacco è ridotta, e se compromesso il frontend non porta a compromissione diretta del gestionale. Dodici endpoint coperti: anagrafiche clienti, storico ordini, articoli venduti a un cliente, movimenti magazzino, documenti DDT/fatture, esposizione crediti-debiti. ### Claude come motore di riassunti strutturati con tool calling Sul ticket è disponibile un'azione "genera riassunto AI": un endpoint `POST /api/tickets/{id}/summary` che raccoglie l'intero thread (email allegate + messaggi operatori + commenti interni), lo passa a **Claude Sonnet** via `callClaudeWithTools()` — wrapper proprio con supporto tool use — e riceve un riassunto in formato strutturato (Riepilogo, Cronologia Interventi datata, Esito, Documenti collegati, Note). Il summary viene persistito in `ticket_summaries` per evitare di pagare di nuovo l'inferenza a ogni apertura ticket. Non è un chatbot, è un digest ragionato. ### access_token pubblici per scambio con fornitori e riparatori esterni Ogni ticket ha un `access_token` generato da un trigger PostgreSQL (32 byte base64) salvato sulla riga. Lo si passa in URL `/ticket/[access_token]` a un riparatore terzo o a un fornitore esterno per dargli accesso al ticket senza aprirgli un account — vede messaggi pubblici, allegati pubblici, può caricare la sua risposta. JWT per gli interni, access_token per gli esterni: due modelli di sicurezza separati per due categorie di utenti. ### Schema ticket-centric con audit trail immutabile e escalation multi-tier La tabella `ticket_history` è append-only (nessun update): ogni cambio stato, assegnazione, escalation finisce lì come riga. `ticket_escalations` permette catene a più livelli (ticket → escalation a riparatore → escalation a produttore) dove ogni livello ha propri commenti, stato e assegnazioni, ma resta legato al ticket padre. Quando un cliente chiede "perché il mio problema è ancora aperto", l'audit risponde con evidenza. ## Stato attuale: core stabile, modulo agenti in rifattorizzazione Il sistema di ticketing è in produzione interna su entrambe le istanze, il plugin Outlook è usato quotidianamente, l'integrazione Mexal gira, l'AI summary è attiva. Sto **rifattorizzando un modulo parallelo** (portale agenti commerciali: catalogo, budget, visite) che vive nella stessa codebase ma tocca un dominio diverso — rewrite degli hook data-fetching, store Zustand più leggero, UI KPI più reattiva. Il ticketing non è toccato da questa rifattorizzazione: è la parte stabile su cui sto costruendo. La case study è qui ora perché il lavoro fatto è significativo — sei mesi di architettura coerente, plugin desktop nativo, tre modelli di sicurezza diversi (JWT interni, access_token esterni, proxy PHP Mexal) — e perché **quando qualcosa non è al 100% lo dico**, invece di vendere come "completo" un prodotto in evoluzione. Quando il rilascio al cliente finale sarà fatto, aggiorno numeri e apprendimenti. --- ## Consuntivazione automatica dei rebate fornitore: dalle fatture elettroniche in ingresso al rendiconto delle note di credito da chiedere URL: https://andreapellizzari.it/lavori/accordi-fornitori/ Cliente: PMI italiana, distribuzione B2B tecnica multi-marchio Ruolo: Architettura, parser FatturaPA, backend PHP, collage SPRIX, integrazione Mexal Anno: 2025 Durata: Evoluzione pluriennale, versione 2.1 a fine 2025, manutenzione attiva Stack: PHP, SPRIX Collage, Mexal WebAPI, CALLWEBSVC, MySQL, XML FatturaPA > Un sistema che chiude il ciclo annuale degli accordi con i fornitori di un'azienda di distribuzione B2B multi-marchio: carica le fatture elettroniche ricevute via SDI, le incrocia con gli accordi in essere (sconti a volume, rebate a scaglioni, premi di categoria, pack a categoria combinata), calcola quanto accredito è effettivamente maturato, e produce il rendiconto con il delta di note di credito ancora da richiedere ai fornitori. Quello che prima si faceva a fine esercizio in Excel, con errori e accrediti dimenticati, ora si legge in un singolo report. Versione corrente 2.1. ## Contesto Un'azienda di distribuzione B2B tecnica multi-marchio compra da **centinaia di fornitori-produttori** e ogni anno negozia con ciascuno un pacchetto di **accordi commerciali**: sconti in fattura, sconti a volume, rebate sul fatturato annuo, premi di categoria, pack promozionali stagionali. Tutta questa parte è operativa (il buyer compra, le fatture arrivano) — ma c'è un secondo tempo, la **consuntivazione** a fine periodo, dove bisogna calcolare, fornitore per fornitore, quanto rebate è maturato in base a quello che si è effettivamente comprato, e chiedere al fornitore le **note di credito** corrispondenti. Se questo pezzo è approssimativo si perdono soldi veri — accrediti dimenticati, rebate calcolati male, NC mai richieste. In molte PMI italiane questa parte vive in Excel e su controlli manuali di migliaia di righe fattura. ## Sfida Chiudere il ciclo end-to-end, in modo automatico e verificabile: **dalla fattura elettronica ricevuta via SDI, al calcolo degli accrediti maturati per ogni accordo attivo, fino al rendiconto che l'amministrazione usa per chiedere le NC ai fornitori**. Senza sostituire il gestionale (Mexal resta il sistema di verità di ordini e fatture), senza duplicare le anagrafiche, e senza costringere buyer e amministrazione a cambiare strumenti. Un layer che sta accanto al gestionale e fa quello che il gestionale non fa. ## Approccio Stack **PHP + SPRIX Collage + MySQL**, con Mexal come fonte di ordini/fatture/anagrafiche. Il sistema si compone di sei meccanismi che, messi in fila, coprono l'intero ciclo. ```mermaid flowchart LR SDI[Fatture elettroniche
FatturaPA via SDI] -->|parser XML| IMP[(imp_xml_fatture
MySQL)] IMP -->|mapping
codice→marchio| CALC{Calcolo accrediti
per accordo} subgraph ACC[Accordi attivi] STD[Accordi standard
sconti · rebate · scaglioni] PACK[Pack a categoria combinata
template + istanze ordini] end MEX[Ordine fornitore
in Mexal] -.->|collage SPRIX
CALLWEBSVC + JSON| PACK ACC --> CALC CALC -->|accredito maturato| REND[Rendiconto annuale
delta da chiedere] REND -->|note di credito da emettere| NC[Fornitori produttori] style IMP fill:#fef3c7,stroke:#d97706 style ACC fill:#ede9fe,stroke:#7c3aed style CALC fill:#d1fae5,stroke:#10b981,stroke-width:2px style REND fill:#fce7f3,stroke:#db2777 ``` ### 1. Ingestione delle fatture elettroniche in arrivo Le FatturaPA dei fornitori entrano in SDI e da lì arrivano nel gestionale. Un parser PHP legge il loro XML (DOM con gestione namespaces) e riversa ogni riga in `imp_xml_fatture`: numero, data, codice articolo fornitore, descrizione, quantità, importo, mittente. Una view `v_imp_xml_qualifica_prodotti` fa il **mapping tra codice fornitore e marchio merceologico interno** — è il passaggio che trasforma il dato grezzo della fattura in dato consuntivabile, perché gli accordi sono su marchio, non sul codice articolo originale. Oltre cinquecento file XML processati in archivio, con storicità pluriennale. ### 2. Anagrafica accordi standard con calcolo multi-modalità Ogni fornitore ha uno o più accordi attivi configurati nel sistema. Ciascun accordo specifica **come si calcola** l'accredito maturato — quattro modalità combinabili: per articolo specifico (tot euro a pezzo), per percentuale sul totale acquistato, per valore fisso al raggiungimento di soglia, per **scaglioni a soglia** (tipico: 10% fino a 50.000 euro di acquistato nel periodo, 15% oltre). Ogni accordo può escludere marchi merceologici specifici (classico: separare il canale e-commerce) e ha una periodicità di liquidazione variabile (1, 2, 3, 6, 12 mesi). Il modulo `api_v2/calcoli.php` (~800 righe) è quello che trasforma "fatture ricevute nel periodo + accordo" in "euro di accredito maturato". ### 3. Accordi pack a categoria combinata (la feature meno banale) I pack sono un tipo di accordo più sofisticato: non "sconto sul marchio X", ma "**se nell'ordine sono presenti tutte queste categorie merceologiche contemporaneamente** (es. frigo + lavastoviglie + forno dello stesso brand), allora maturi questo accredito". Un pack è un template riutilizzabile che, quando il buyer apre un ordine fornitore che lo matcha, genera una **istanza reale** (tabella `for_accordi_pack_ordini`) con il dettaglio righe salvato come JSON. La consuntivazione è per istanza, non per accordo astratto — ogni volta che il pack viene "usato" su un ordine reale, si accumula un'istanza calcolabile. ### 4. Collage SPRIX bidirezionale sugli eventi Mexal Il buyer non apre un tool parallelo, lavora dentro Mexal come sempre. Un **collage SPRIX** di circa 650 righe (`collage_pack_ordini.spr`) si attiva sull'apertura/salvataggio di un ordine fornitore Mexal: via `CALLWEBSVC` chiede al backend PHP quali pack sono attivi per quel fornitore in quel momento, li propone al buyer. Alla conferma, SPRIX invia un POST con JSON nativo al backend che crea l'istanza pack collegata all'ordine. Se l'ordine viene cancellato in Mexal, un evento DELETE richiama l'API e ripulisce l'istanza. La sincronizzazione è bidirezionale e autopulente — zero istanze orfane. ### 5. Stato a due livelli: acquisti e amministrazione Ogni accordo e ogni pack hanno **due coppie di flag indipendenti**, gestite da ruoli diversi: il flag `attivo` lato acquisti (l'accordo è valido o sospeso commercialmente) e i flag `in_gestione` + `chiuso` lato amministrazione (la nota di credito è stata chiesta, abbinata, contabilizzata). Un accordo può essere "commercialmente valido" ma "consuntivazione non ancora chiusa". È la distinzione che evita la confusione tipica dei sistemi a singolo campo "stato" dove non si capisce mai se la pratica è aperta per acquisti o per amministrazione — e permette alle due funzioni di procedere in parallelo senza stepparsi i piedi. ### 6. Rendiconto di controllo: il punto di verità per l'esercizio Il report `rendiconto_controllo.php` (circa 1.400 righe) compone a fine periodo il quadro fornitore per fornitore: accordi standard attivi, pack attivi, fatture collegate nel periodo, **accredito maturato totale**, **note di credito già ricevute**, **delta residuo da chiedere**. È il documento operativo con cui il controllo di gestione chiude l'esercizio e con cui l'amministrazione bussa al fornitore per le NC mancanti. Prima dell'esistenza di questo report, il quadro si ricostruiva a mano in Excel — con il risultato che una percentuale non nulla di accrediti maturati finiva per non essere mai richiesta. ## Risultato Il sistema è in produzione alla versione 2.1 e gira in modo silente sotto al gestionale. Il buyer lavora come sempre dentro Mexal; il collage SPRIX gli propone i pack quando servono; il backend PHP accumula le istanze; le fatture elettroniche entrano, vengono qualificate per marchio, e incrociate con gli accordi. A fine periodo — trimestre, semestre, anno — il rendiconto è già lì. La consuntivazione che prima costava giornate di Excel oggi si legge in un report, e il delta da richiedere ai fornitori è tracciato senza ambiguità. Il mix di competenze che il progetto richiede — **PHP + SPRIX Collage + DOM FatturaPA + logica rebate a scaglioni + gestionale Mexal** — è quello che mi permette di lavorare a problemi che un consulente puramente moderno non saprebbe affrontare, e che un consulente puramente legacy non saprebbe modernizzare. È anche il tipo di problema che a mio avviso ha un mercato più ampio del singolo cliente: tutte le distribuzioni B2B multi-marchio italiane hanno la stessa catena business, e gran parte la gestisce ancora a mano. --- ## PowerApps operative con integrazione Mexal, Power Automate come orchestratore e migrazione selettiva a Next.js URL: https://andreapellizzari.it/lavori/power-platform/ Cliente: PMI manifatturiera italiana, settore distribuzione tecnica B2B Ruolo: Progettazione, sviluppo PowerApps, flow Power Automate, dashboard Power BI, migrazione selettiva Anno: 2024 Durata: Attività pluriennale, in evoluzione Stack: PowerApps, Power Automate, Power BI, SharePoint, Dataverse, SQL Server, Mexal WebAPI, Cloudmersive, Bing Maps, Next.js > Sei PowerApps multi-screen e oltre venti flow Power Automate costruiti per una PMI manifatturiera italiana del settore distribuzione tecnica: integrazione nativa con Mexal WebAPI, validazione dati real-time, routing SQL multi-backup, generazione preventivi on-demand, dashboard Power BI embedded. Oggi in migrazione selettiva verso Next.js dove la complessità lo giustifica. ## Contesto PMI del settore distribuzione tecnica B2B, rete di agenti commerciali sul territorio, magazzino centrale con logistica propria, gestionale interno Mexal/Passepartout con archivio Docuvision. Licenze Microsoft 365 già presenti — Power Platform incluso ma sotto-utilizzato. Nel tempo ho costruito un ecosistema di **sei PowerApps multi-screen** e **oltre venti flow Power Automate** che copre l'operatività quotidiana di agenti, magazzinieri e direzione. ## Sfida Dare a commerciali, capireparto e magazzinieri strumenti che girano su telefono e tablet, collegati in tempo reale al gestionale, senza aspettare mesi di sviluppo custom e senza duplicare anagrafiche. Il tema ricorrente: **Power Platform è comodo per l'UI mobile, ma da solo non parla con Mexal** — e senza Mexal un'app commerciale non ha prezzi, disponibilità, documenti reali. Il trucco è stato mettere Power Automate in mezzo come orchestratore verso il gestionale. ## Approccio Cinque feature che fanno la differenza rispetto a un'installazione Power Platform "standard". ### Mexal WebAPI dentro Power Automate con retry circuitato Il flow che genera preventivi cliente chiama direttamente **Mexal WebAPI** (comando `esec_collage_server_remoto`, codice app dedicato) passando righe articoli in JSON, RAEE, calcoli ricarico marchio e modalità di pagamento. In uscita: numero documento Mexal, PDF e HTML generati on-demand da un endpoint interno. Il flow implementa un **Do-Until con retry fino a tre volte** (timeout PT60S) per assorbire i fallimenti transitori dell'endpoint — gestione esplicita dell'errore, non "speriamo che funzioni". È la feature che fa saltare agli occhi il gap: un'app PowerApps che invece di scriversi in SharePoint e sincronizzare la notte, registra il preventivo sul gestionale nel momento in cui l'agente preme "conferma". ### Validazione dati real-time con Cloudmersive Durante il data entry di contatti e anagrafiche, email, numeri di telefono e URL sito web vengono validati **al volo** via Cloudmersive (Data Validation API), con connessioni dedicate per ogni tipo (`controlla_mail`, `controlla_numero_telefono`, `controlla_sito`). Niente dato sporco che entra e viene scoperto mesi dopo in Power BI — il sistema rifiuta il campo non valido prima del submit. Disciplina di data quality spostata al bordo. ### Routing SQL multi-backup per resilienza Le app più critiche (gestione giri di consegna, aggiornamento consegne) hanno **tre connessioni SQL Server differenti** configurate come backup. I flow Power Automate sanno instradare verso l'istanza secondaria se la primaria non risponde, con pattern Do-Until + retry per garantire la scrittura delle tabelle `T_CONSEGNE_ELENCO_OC` anche durante finestre di manutenzione del database principale. Un magazziniere che chiude la bolla in banchina non sa — e non deve sapere — che dietro c'è un failover. ### Generazione preventivi e recupero documenti Docuvision Un secondo gruppo di flow si occupa della parte documentale: generazione HTML dinamica del preventivo via endpoint del portale B2B (parametri in query string, nessun template SharePoint), conversione in PDF via **Cloudmersive Document Conversion**, salvataggio su OneDrive, invio email al cliente. Parallelamente, quando l'agente deve rivedere un preventivo vecchio, un flow recupera il PDF direttamente da **Docuvision** via Mexal WebAPI. Niente archivio parallelo, niente sync notturna: la fonte di verità resta il gestionale. ### Dashboard Power BI embedded nelle PowerApps Le dashboard più usate (marginalità agenti, budget, evasione ordini, giacenze critiche) non sono report mensili in PDF ma **visualizzazioni Power BI embedded dentro la PowerApp di monitoraggio**, con refresh frequente dal dataset cloud. Il responsabile commerciale apre l'app dal telefono e vede lo stato oggi, non il mese scorso. Teams come canale di notifica per gli avvisi SLA-critical (consegne in ritardo, giro in uscita). ### Migrazione selettiva a Next.js Dove la logica di business è cresciuta oltre quello che la UI PowerApps regge bene, ho migrato il modulo verso **Next.js**. Esempio concreto: il calcolo di ricarico markup e il filtraggio RAE/PROMO in import da documento vivono ora in una web app Next.js (`ListiniPanel.tsx`, endpoint `/api/agent/commerce/cart/from-document`), con accesso diretto a Mexal WebAPI e SQL Server senza passare da Power Automate. La regola che seguo: si migra solo quando la complessità supera il valore della rapidità Power Platform — non prima. ## Risultato Un ecosistema Power Platform in produzione quotidiana su decine di operatori tra agenti, magazzino e direzione, con integrazione gestionale reale (non finta tramite export notturni) e pattern di resilienza espliciti. Il valore non è aver "costruito app su Microsoft 365" — tante aziende lo fanno — ma aver saputo **decidere caso per caso dove Power Platform regge e dove conviene salire di stack**. Competenza trasversale rara: la maggior parte delle implementazioni Power Platform che vedo in giro sono o "SharePoint + form" basic, o costruzioni complicatissime che stanno in piedi per miracolo. Nessuna delle due è la risposta giusta. --- ## Porting PowerApps → web con sql-proxy Node.js: middleware singleton veloce e affidabile sulla intranet aziendale URL: https://andreapellizzari.it/lavori/porting-powerapps-nodejs/ Cliente: PMI manifatturiera italiana, settore distribuzione tecnica B2B Ruolo: Architettura, sviluppo del proxy, migrazione delle app HTML, integrazione multi-sistema Anno: 2026 Durata: Migrazione in corso, sei app completate, cinque in pipeline Stack: Node.js, Express, SQL Server, MS Dataverse, Mexal WebAPI, Brevo, HTML/JS vanilla, Windows Service, PostgreSQL Neon, MySQL > Sei PowerApps operative già in pensione, cinque in pipeline di migrazione. Un servizio Node.js di circa 5.700 righe che gira come Windows Service sulla intranet e funge da middleware unico verso SQL Server del gestionale, Dataverse, Mexal WebAPI, Brevo email/SMS e MySQL. Frontend in HTML vanilla single-page, deploy versioning dentro il proxy stesso con rollback istantaneo, dual-write SQL+Dataverse per coesistere con le PowerApps ancora vive. ## Contesto La stessa azienda ha un ecosistema **PowerApps maturo** — sei applicazioni in produzione quotidiana coprono l'operatività commerciale e logistica: dashboard provvigioni agenti, creazione preventivi, picking di magazzino con barcode scanner, gestione giri di consegna con mappe, monitoraggio consegne real-time. Hanno funzionato per anni e ancora funzionano. Ma a un certo punto i limiti sono diventati evidenti: **latenza di caricamento** di qualche secondo su ogni transizione, UX rigida e faticosa da evolvere, dipendenza dal runtime PowerApps e dalla rete cloud, e un debug che richiede aprire l'IDE Power Platform invece del browser. ## Sfida Spostare le PowerApps critiche in **web app che girano sulla intranet aziendale**, con prestazioni migliori e piena controllabilità, **senza perdere** le integrazioni Dataverse (dove vivono dati strutturali come coordinate GPS destinazioni, scale premio, eventi), senza rompere i flussi Mexal (stampa etichette, modifica scadenze, aggiornamento contatti), e soprattutto **senza chiedere agli operatori di cambiare abitudine** — il capo magazzino che prima apriva una PowerApp deve aprire una pagina web che fa esattamente la stessa cosa, solo meglio. ## Approccio L'architettura che ho scelto ha un pezzo centrale forte — il **sql-proxy** — e un perimetro deliberatamente semplice attorno. Sei feature che fanno la differenza. ```mermaid flowchart LR A[App HTML vanilla
intranet] -->|fetch JSON| P(sql-proxy
Node.js + Express
Windows Service) P -->|pool mssql| S[(SQL Server
gestionale)] P -->|OAuth2 + cache token| D[(Dataverse)] P -->|HTTPS| M[Mexal WebAPI
via proxy PHP] P -->|SMTP| B[Brevo
email / SMS] P -->|pg| N[(PostgreSQL
Neon)] P -->|mysql| Y[(MySQL
SiteGround)] style P fill:#ede9fe,stroke:#7c3aed,stroke-width:2px ``` ### sql-proxy come singleton middleware Node.js Un servizio Node.js + Express di circa **5.700 righe** che gira come Windows Service sulla intranet aziendale (IP interno dedicato, porta 3100 HTTP e 3443 HTTPS) e parla con **tutto** quello che serve alle app: pool su SQL Server del gestionale, client Dataverse con OAuth2 Azure AD, routing verso Mexal WebAPI, invio email e SMS via Brevo, lettura/scrittura su PostgreSQL Neon e MySQL SiteGround. Oltre settanta endpoint REST esposti. L'idea è che tutte le integrazioni stiano in un unico posto, con un'unica API key, un unico log, un unico servizio da monitorare — invece di N client sparsi nelle N app. ### App HTML single-page leggerissime come frontend Invece di salire di stack con un framework pieno, ogni app migrata è un **singolo file HTML** con JavaScript vanilla che fa `fetch()` al proxy sulla rete locale. Zero bundler, zero build step, zero framework update quarterly. La più complessa — quella che ha sostituito `Gestione_Giro_Clienti_R2` — è un HTML da quattromila righe con sei pagine interne (indice, email, piani di carico, calendario, liste di magazzino, analisi fabbisogno), e carica istantaneamente perché servita dal proxy stesso sulla intranet. Le sei app già in pensione: **monitor consegne, picking primo livello, picking secondo livello, launcher con PIN, launcher ufficio, gestione giro**. ### Dual-write SQL + Dataverse per coesistere con le PowerApps sopravvissute Durante la migrazione le due generazioni devono vedere gli stessi dati. Il proxy lo garantisce con **dual-write esplicito**: quando una app web salva un collo o una nota operativa, il proxy scrive prima su SQL Server (fonte di verità operativa) e poi su Dataverse (fonte condivisa con le PowerApps ancora attive). Se una delle due scritture fallisce, il proxy logga la discrepanza e la seconda diventa recuperabile. Nessuno fra gli utenti si accorge che sta usando un sistema di transizione. ### Pool, cache e retry intelligente: ogni scelta elimina un modo di fallire Il proxy non è una semplice pass-through. Pool **mssql con connessioni riutilizzabili**, **cache dei token OAuth Dataverse** con refresh automatico 60 secondi prima della scadenza (elimina il giro di login ripetuti), **batch delay di 500 ms** tra chiamate Dataverse per non incappare nel throttling del tenant, **fetch con retry** automatico fino a tre tentativi su HTTP 429, **timeout dedicati** per ogni upstream (Mexal 30 s, Dataverse 15 s, proxy PHP 10 s). Ogni dettaglio è lì perché in PowerApps quel modo di fallire esisteva e faceva perdere tempo. ### Routing Mexal integrato per le operazioni gestionale Quattro endpoint dedicati inoltrano a Mexal le azioni operative più usate: stampa etichetta spedizione in formato ZPL (collage remoto), elenco e download dei documenti da archivio Docuvision, modifica della data di scadenza di un ordine cliente, aggiornamento dei contatti di spedizione. Dietro c'è il **proxy Mexal PHP su hosting esterno** che già gestisce credenziali e certificati self-signed — il sql-proxy lo chiama come client e centralizza così anche l'accesso al gestionale. ### Deploy versioning delle app HTML dentro il proxy stesso La cosa che preferisco di questa architettura. Il proxy espone endpoint che **deployano le app che ha sopra**: `POST /api/apps/:app/:file/deploy` pubblica una nuova versione di un file HTML con backup automatico, `GET /api/apps/:app/:file/history` lista tutte le revisioni, `POST /rollback/:revision` torna istantaneamente a una versione precedente, `GET /api/apps/status` dà lo stato di tutte le app. Niente git push, niente pipeline CI/CD, niente FTP: modifico un HTML, lo posto al proxy, in meno di un secondo è live — e se ho rotto qualcosa, torno indietro con una singola chiamata. È il deploy più pragmatico che abbia mai messo in piedi. ## Risultato Sei PowerApps operative già sostituite da app web più reattive, cinque ancora in pipeline (dashboard provvigioni, monitoraggio ordini, scale premio, gestione eventi, viaggi premio). L'operatore di magazzino che prima aspettava tre-quattro secondi di caricamento ora apre una pagina web che risponde in millisecondi. L'agente che consulta la dashboard delle sue consegne vede lo stato aggiornato senza refresh manuale. Il vero oggetto di valore **non è il frontend** — le app HTML sono volutamente semplici — **è il proxy**: è dove vivono pool, cache, retry, dual-write, deploy versioning, cioè tutte le garanzie di prestazione e affidabilità che PowerApps non riusciva a dare per design. È anche il tipo di migrazione che conferma una convinzione che porto avanti da anni: **scegliere lo stack giusto conta più della modernità nominale**. Un servizio Node.js scritto bene con HTML vanilla sopra sta battendo un ambiente low-code moderno, e lo sta facendo sullo stesso hardware. --- ## DDT digitale web chiamabile dal gestionale Mexal: documento dinamico via WebAPI, con bypass DNS e debug live URL: https://andreapellizzari.it/lavori/ddt-digitale/ Cliente: PMI manifatturiera italiana, settore ferramenta Ruolo: Architettura, sviluppo PHP, integrazione Mexal WebAPI, deploy Anno: 2026 Durata: Progetto in produzione con manutenzione attiva Stack: PHP, Mexal WebAPI, cURL, HTML5, file_get_contents context options > Un bridge PHP di circa 3.300 righe che rende in tempo reale la versione web di un documento di trasporto, richiamabile con un click dal gestionale Mexal. Integra sette endpoint WebAPI per ricomporre il documento da fonti multiple, bypassa il DNS intranet con IP hardcoded quando serve, e ha un sistema di debug live HTML+file che ho costruito ad hoc per troubleshooting in produzione. Piccolo in righe, distintivo nel pattern. ## Contesto Nel gestionale Mexal l'utente apre un documento di trasporto — un DDT, un ordine cliente, un preventivo — e lo vede nella sua forma "tabulato DOS-like" tipica del gestionale. Questa rappresentazione va benissimo in ufficio ma non è la cosa migliore da spedire a un cliente o mostrare a un commerciale in visita: serve una versione **più pulita, web, condivisibile via link**, che mantenga però tutti i dati reali del gestionale senza duplicarli. ## Sfida Dare al gestionale un pulsante che, dal singolo documento, aprisse in un browser la sua versione web pronta per essere mostrata o inviata — **sempre aggiornata** rispetto a Mexal, **senza database locale** che duplicasse qualcosa, e che funzionasse anche dall'interno dell'infrastruttura PMI reale, con tutti i suoi limiti (DNS intranet a volte lento, certificati self-signed, IP filtrati). Un piccolo ponte, ma preciso. ## Approccio Stack **PHP puro**, senza framework, senza Scriptcase, senza Laravel. Tre file principali (circa 3.300 righe di codice core), una chiamata cURL verso Mexal WebAPI, composizione dinamica dell'HTML. Cinque feature che rendono il progetto non banale. ### Un click dal gestionale, un URL web che si apre Dentro Mexal è configurato un comando personalizzato che, sul documento selezionato, invoca l'URL della web app passando sigla, serie e numero documento come parametri. L'operatore clicca da Mexal e si apre il browser con la versione web. Nessuna esportazione, nessuna copia, nessun rischio di sfasamento dei dati: la pagina viene generata al momento leggendo dal gestionale stesso via WebAPI. ### Sette endpoint WebAPI compongono il documento Per ricostruire un DDT completo servono fonti diverse: movimenti di magazzino per le righe articolo, ordini-matrici per risalire all'ordine cliente d'origine, anagrafica cliente, anagrafica fornitore quando serve, indirizzi di spedizione alternativi, preventivi correlati. Il bridge orchestra **sette endpoint Mexal WebAPI** (movimenti-magazzino, ordini-matrici, ordini-clienti, preventivi, fornitori, indirizzi-spedizione, clienti) e assembla il documento in memoria, riga per riga, con totali, sconti, modalità di pagamento, dati del trasportatore e indirizzo di destinazione. ### Bypass DNS intranet con IP hardcoded nel context cURL Un dettaglio che un consulente junior non metterebbe: in alcuni momenti il DNS della rete interna PMI impiega fino a diciassette secondi a risolvere l'host di Mexal, rendendo l'apertura del documento inaccettabile. Ho **hardcoded l'IP di Mexal** nei `file_get_contents` context options con `stream_context_create` — quando il DNS non collabora, cURL va dritto all'IP e apre la connessione in un istante. Con fallback intelligente tra `file_exists()` per asset locali e `get_headers()` per URL remoti. È la classica micro-ottimizzazione che in un ambiente enterprise teorico non avrebbe senso, e che invece nella realtà delle PMI italiane cambia la differenza tra "va" e "non va". ### Parsing robusto del codice documento L'utente passa un identificativo tipo `OC3/1234` o `FT2/567`: il parser estrae sigla (due caratteri), serie (un carattere), numero, e compone la tupla `SIG+SERIE+NUM` che serve a indirizzare la chiamata WebAPI al tipo documento giusto. Gestisce DDT, ordini cliente, fatture, preventivi con lo stesso codice di parsing, senza duplicarlo per ogni tipo. ### Debug system su due canali, live durante l'esecuzione Debuggare integrazioni con ERP legacy in produzione è notoriamente scomodo — tipicamente scrivi su un log e lo leggi dopo. Ho costruito un sistema di debug a **due canali simultanei**: un file (`ddt_digitale_debug.log`, con `fflush()` forzato dopo ogni write per vedere gli eventi man mano che accadono) e una **visualizzazione HTML colorata** in pagina con box blu per info e rossi per errori, timestamp al millisecondo, scritta **durante** l'esecuzione e non alla fine. Quando qualcosa va storto lo vedo in diretta sul browser, non dopo. Il file di log cresciuto durante una sessione intensa di troubleshooting è diventato di alcuni megabyte — lo tengo come testimonianza della disciplina con cui il progetto è stato messo in produzione. ## Risultato Piccolo progetto — circa 3.300 righe di PHP — ma quotidianamente usato: ogni volta che qualcuno in azienda ha bisogno di mandare un documento al cliente in forma decente, clicca il bottone dal gestionale e apre il web. Nessuna duplicazione di dati, sempre aggiornato, nessuna finestra di disallineamento. È l'esempio del pattern che mi piace di più portare in PMI: **non ricostruire il gestionale, aggiungergli piccoli ponti che lo rendano utile in scenari moderni**. Il DDT non è diventato "il nuovo software": è rimasto quello che era in Mexal, e intorno è cresciuta una pagina web che lo porta dove serve. --- ## Gestionale interno Scriptcase con 336 applicazioni, SGI ISO integrato e accesso diretto al database Mexal URL: https://andreapellizzari.it/lavori/gestionale-scriptcase/ Cliente: PMI manifatturiera italiana, settore ferramenta Ruolo: Architettura, sviluppo e manutenzione pluriennale della piattaforma, modellazione viste SQL Server, modulo SGI ISO Anno: 2025 Durata: Evoluzione pluriennale, in produzione continua Stack: Scriptcase, PHP, SQL Server, MySQL, Mexal Docuvision, Scriptcase Security Module 2FA, Power BI, PowerApps, SiteGround > Una piattaforma gestionale interna costruita in Scriptcase con 336 applicazioni in produzione, stratificata negli anni su un unico database SQL Server condiviso con il gestionale Mexal. Copre Sistema di Gestione Integrato Qualità-Ambiente ISO 9001+14001, valutazione fornitori, approvvigionamento con controllo sottoscorta ed esplosione produzione, configuratori di prodotto verticali, indagini statistiche (storici vendite/acquisti, analisi tempi produzione per operatore, forecast), HR/timbrature, ticket assistenza, portale B2B admin. Security Module Scriptcase con 2FA, dashboard Power BI embedded, integrazione Docuvision, perfino moduli AI. Non una web app: un ecosistema. ## Contesto PMI del settore ferramenta, stesso cliente dell'ecommerce B2B ma sul lato back-office. Gestionale principale Mexal/Passepartout con database SQL Server, archivio documentale Docuvision, infrastruttura Microsoft 365. Il tema ricorrente delle PMI italiane: ci sono decine di esigenze operative che il gestionale non copre nativamente — audit qualità, valutazione fornitori, pianificazione audit, gestione reclami, carbon footprint imballi, controlli statistici, dashboard operative — e non ha senso comprare un software verticale per ciascuna. ## Sfida Costruire un **gestionale interno extensibile** che crescesse negli anni, coprisse tutti i processi laterali del business sopra Mexal senza duplicare il dato, restasse mantenibile da un singolo sviluppatore, e che gli operatori potessero aprire nel browser come qualsiasi strumento interno. Vincolo di costo tipico PMI: zero budget per software verticali multipli. ## Approccio **Scriptcase** come fabbrica di applicazioni PHP — un RAD maturo che genera CRUD web-based partendo da tabelle e viste SQL. Non è un framework moderno, ma nel contesto giusto è una leva produttiva enorme: una nuova maschera o un nuovo report operativo nasce in poche ore e va subito in mano agli operatori. Sei feature che rendono questa piattaforma diversa dalle tipiche installazioni Scriptcase che si vedono in giro. ```mermaid flowchart TB subgraph MEX[Mexal / Passepartout - gestionale core] SQL[(SQL Server
V_QUALITA_* · V_MECPLAST_*
V_B2B_* · V_PIANIF_*)] DOCU[Docuvision
archivio documentale] end subgraph SC[Scriptcase layer - 336 applicazioni] SGI[Modulo SGI ISO
audit · non conformità · CFP
~30 app] OFP[Ordini fornitori
sottoscorta · DIBA esplosa] HR[HR / Timbrature
banca ore · ferie] QC[Certificati PDF QC
bilingue · portale cliente] OTHER[...altri moduli] end subgraph MOB[PowerApp mobile] PPF[Pianifica Prodotti Finiti R2
monitor lotti · task · bindelli] end SQL -->|viste V_*
lettura/scrittura diretta| SC SC -->|webservice custom
CALLWEBSVC| DOCU SQL <-.->|stesse viste
strato dati condiviso| PPF SGI -.->|Power BI embedded| DASH[Dashboard Maico BI] SC -.->|Security Module
2FA + gruppi/permessi| USERS((Operatori
reparti)) style SQL fill:#fef3c7,stroke:#d97706 style DOCU fill:#fef3c7,stroke:#d97706 style SC fill:#ede9fe,stroke:#7c3aed,stroke-width:2px style PPF fill:#d1fae5,stroke:#10b981 ``` ### 336 applicazioni in produzione su un'unica piattaforma Il numero non è di per sé una feature, ma dice cosa significa concretamente: **74 `grid_*`** (elenchi tabellari navigabili), **73 `form_*`** (schede di editing con validazioni), più menu, dashboard, report PDF, webservice interni, gestione immagini, stampe di controllo qualità, calendari, questionari. Circa 40 applicazioni sono Security Module (login, 2FA, gestione gruppi/utenti/permessi), ~296 sono business pure. La piattaforma è stratificata su anni di lavoro incrementale: quando serve un nuovo processo, invece di comprare un software dedicato, nasce una nuova app. ### Sistema di Gestione Integrato qualità + ambiente ISO 9001/14001 Il modulo **SGI** è la parte più particolare: circa 30 applicazioni che implementano gli obblighi ISO 9001 (qualità) e ISO 14001 (ambiente) in modo strutturato — non in Excel, non in fogli condivisi, ma in un'area specifica del gestionale, organizzata in sei sezioni: Pianificazione, Supporto, Attività Operative, Valutazione delle Prestazioni, Miglioramento, Ambiente. Ci sono "Obiettivi Economici" e "Obiettivi Strategici" con vista anno corrente e anno prossimo per la pianificazione pluriennale; "Elenco Strumenti di Misura" per le calibrazioni, "Calendario Attività Formative" ed "Elenco Attività Formative" per la formazione del personale, "Istruzioni Operative", "Procedure" e "Normative" come corpus documentale; "Calendario Audit (Dettaglio)" annuale con "Gestione Audit" per l'esecuzione auditata; "Gestione Anomalie" e "Azioni Corr.-Piani di Miglioram." collegate alle non conformità, con "Totale Costi Non conformità" e "Totale Non Conformità per Fornitore" per il controllo di gestione; "Valutazione Fornitori" e "Sintesi Valutazione Fornitori"; "Gestione Invio e Monitoraggio Questionario Clienti" con "Sintesi Questionario Valutazione" e "Customer Satisfaction Score". Tutto audit-ready. ### Indicatori ambientali e Carbon Footprint Parte **Ambiente** del SGI, non banale in contesto PMI: "CFP - Indirizzi di spedizione clienti con peso" e "CFP - Indirizzi di spedizione fornitori con peso" per il tracciamento del peso imballi lungo la catena logistica, "Indicatori Impiego Imballi in Plastica" per la rendicontazione del packaging, più "Tabelle e Grafici di sintesi Generale" per la direzione. Coerente con ISO 14001 clause 9.1 — e preparatorio per la direttiva CSRD che inizia a chiedere anche alle PMI rendicontazione ambientale strutturata. ### Connessione diretta al database SQL Server di Mexal Scriptcase gira tipicamente su MySQL, qui invece è configurato per leggere e scrivere direttamente le **viste SQL Server** del gestionale Mexal via connector dedicato: `V_B2B_EXPORT_CATALOGO_NEW`, `V_QUALITA_ELENCO_TICKET`, `V_MECPLAST_CONSUNTIVO_*`, `V_PIANIF_OF_SERIE_1_*`, `V_DDFB2B_ORDINI_VENDITE`. È il pattern che evita la "piattaforma parallela con dati doppi": l'anagrafica clienti è quella di Mexal, gli ordini sono quelli di Mexal, i consuntivi di produzione sono quelli di Mexal. Scriptcase aggiunge UI e logica di processo, non una seconda verità. ### Integrazione Docuvision dal gestionale Mexal Una famiglia di webservice custom dentro Scriptcase — "Download Docuvision Ordine", "Restituisci PDF Disegno Articolo", "PDF Disegno Ordine di Produzione" — che attingono all'archivio **Docuvision**, il modulo documentale di Mexal dove finiscono scansioni di ordini, disegni tecnici, DDT, certificati. L'operatore qualità che apre una non conformità su un articolo allega il disegno tecnico direttamente da Docuvision, il commerciale che verifica un ordine recupera il PDF originale senza aprire Mexal. Zero archivio parallelo. ### Configuratori di prodotto verticali basati su geometria reale Dentro la piattaforma vivono **configuratori di prodotto custom** tutt'altro che generici. Il più rappresentativo è "Ferramenta per Superamento Spalletta" per cardini di porta: l'utente inserisce **sette quote dimensionali** — misura spalletta (S), spessore anta (A), spessore battuta (B), asse di rotazione anta aperta (S1), quota di regolazione del cardine (H1), quota di regolazione della bandella (H2), aria tra anta aperta e muro (D) — e il configuratore calcola il **prodotto consigliato** restituendo anche il **disegno tecnico dinamico** dell'installazione, con zoom regolabile e pulsante di stampa. Non è un "configuratore ecommerce" che fa varianti di taglia e colore: è uno strumento di progettazione meccanica verticale sulla categoria cardini, con una versione R2 in test che itera l'algoritmo di selezione. ### Indagini statistiche e forecast Un modulo dedicato **Indagini Statistiche** con una decina di report analitici: storico vendite e acquisti multi-anno, statistica vendite per categoria, **analisi tempi di produzione** sia per prodotto sia per singolo operatore, monitoraggio costi lavorazioni esterne (subfornitura), progressivi clienti, ordini ultimo anno per account, **Forecast Maico** per la linea cardini (previsione di consumo basata su storico). Report operativi tipo viste SQL + grid Scriptcase con drill-down, non dashboard glossy — ma quello che serve a chi deve prendere decisioni di acquisto o di pianificazione produzione. ### PowerApp di pianificazione produzione come estensione mobile del gestionale Il modulo produzione del gestionale ha una parte ufficio in Scriptcase e una **parte operativa in PowerApps** che vive sullo stesso strato dati — "Pianifica Prodotti Finiti" (arrivata oggi alla seconda iterazione R2). È la dashboard con cui il responsabile produzione mette in lavorazione gli ordini cliente: ricerca per OC, liste dei lotti da incaricare o tassativi, monitor attivi su **Lotti**, **Task**, **Gestione Attrezzature**, **Agg.FAST**, **Monitor Robot**. Per ogni prodotto finito in lavorazione si vede scadenza lotto, quantità ordinata cliente, quantità ordine di fornitura, quantità assegnata, quantità incaricata, stato dell'ultima lavorazione interna e dell'eventuale **lavorazione esterna** (subfornitura stampaggio plastica con i suoi numeri d'ordine di conto lavoro). Due colonne separate per i semilavorati già pianificati e quelli da pianificare, con pulsante "Bindelli" per la **raccolta tempi della pressa** durante lo stampaggio. A margine destro lo storico vendite dell'articolo degli ultimi sei mesi — così il responsabile decide la priorità con contesto commerciale. È il tipo di PowerApp che uno Scriptcase desktop non può replicare per UX: qui conta il touch, la reattività, il funzionamento in reparto. ### Approvvigionamento con sottoscorta ed esplosione produzione Il modulo **Ordini e Preventivi Fornitori** include un controllo sottoscorta non banale (circa 12.000 righe PHP su due file principali): non si limita a segnalare gli articoli sotto il minimo, ma propone lo **sviluppo produzione** necessario — se il componente sotto minimo è un semilavorato interno, esplode la distinta base via vista `T_PIANIF_SVILUPPO_OF_BASE_1` e genera la richiesta di produzione. Il calcolo del fabbisogno unisce più fonti: consumo trimestrale da vendite, ordini cliente aperti, sviluppo produzione pianificato, lead time medio di approvvigionamento. La colonna "DA ORDINARE" è calcolata come `max(0, Scorta_Rif − Esistenza − Ordinato)` con evidenza rossa. Le varianti di uno stesso codice base (primi 11 caratteri) sono raggruppate in righe collassabili, con filtri client-side e cache locale per velocità di consultazione su cataloghi grandi. Accanto: Monitor Ordini Fornitori real-time e Gestione Preventivi Fornitori con confronto multiplo per articolo. ### Modulo HR: timbrature, banca ore, ferie/permessi con approvazione a due livelli Un modulo interno completo per la gestione del personale, con circa 8.000 righe PHP strutturate in 14 file. I dipendenti timbrano da browser mobile con **geolocalizzazione GPS obbligatoria** (HTML5 + reverse geocoding via BigDataCloud) e protezione anti-doppio-clic (blocco di timbrature identiche nei due minuti successivi). Le richieste di ferie e permessi passano attraverso un'**approvazione gerarchica a due livelli indipendenti**: due responsabili diversi autorizzano o rifiutano separatamente, ciascuno con note e timestamp proprio. La **banca ore** ha una tabella movimenti dedicata che registra ogni caricamento, consumo di ferie, consumo di permessi e rettifica con saldo prima/dopo, responsabile autore del movimento e link alla richiesta che l'ha generato — audit trail completo. Viste SQL aggregate per disponibilità per dipendente e reparto. ### Generazione PDF certificati qualità multilingue con portale cliente Un altro modulo da ~3.600 righe che genera **PDF certificati di controllo qualità bilingue IT/EN** pronti per essere spediti al cliente, con tabulati di test distruttivi e non distruttivi, celle colorate per evidenziare i valori critici, firma digitale raster integrata, numero di pagina e intestazioni ripetute. Sviluppato con **FPDF 1.85** (scelta deliberata: output PDF piccolo e prevedibile rispetto a TCPDF/mPDF). A corredo c'è un **portale cliente esterno** dove il cliente finale accede ai propri certificati senza login, filtrando per matricola articolo o lotto e per intervallo di date — il link glielo mandi in email e se lo recupera da solo quando serve. Chiusura elegante del ciclo qualità. ### Dashboard Power BI embedded e AI applicata al legacy La piattaforma non è congelata nel passato: ospita una **Dashboard Maico BI** in Power BI embedded per la linea cardini, e contiene moduli di **prova AI** — una demo interattiva, un'estrazione automatica di dati da documenti, un generatore di descrizioni prodotto — per automazione editoriale e data entry ripetitivo. Anche Scriptcase ha avuto la sua fase di AI-assisted. ## Risultato Trecento applicazioni in produzione, un Security Module con 2FA che gestisce decine di utenti interni con permessi per gruppo e per singola app, un SGI ISO completo che ha retto audit esterni senza problemi, integrazione reale con il gestionale Mexal sottostante. Scriptcase non è lo stack che sceglierei oggi per un progetto nuovo — e infatti i moduli più complessi (ecommerce B2B, ticket assistance, knowledge base AI) sono stati gradualmente riscritti in Next.js moderno. Ma quello che gira in Scriptcase **continua a girare benissimo**, viene aggiornato quando serve, e ha permesso a una singola persona di coprire decenni-uomo di sviluppo di moduli operativi. È l'esempio concreto del perché **legacy non è un termine peggiorativo**: è tecnologia che ha pagato il suo ROI venti volte. --- ## Guida SPRIX e skill Claude Code auto-aggiornata per sviluppo su Mexal/Passepartout URL: https://andreapellizzari.it/lavori/documentazione-sprix/ Cliente: Asset pubblico e personale — knowledge base per sviluppatori Mexal italiani Ruolo: Autore, architetto della skill, sviluppo dei tool di auto-discovery Anno: 2025 Durata: Progetto sempre aperto, versione corrente v1.2 (ottobre 2025), skill v2.0 Stack: Markdown, Python, PDF parsing, Claude Code Skills, SPRIX, Mexal WebAPI v2.32 > Un sistema di conoscenza vivo sul linguaggio SPRIX e sulle WebAPI Mexal/Passepartout: guida markdown pragmatica + skill Claude Code richiamabile che si carica d'autorità su ogni progetto Mexal + due comandi custom (/mexal-discover e /mexal-changelog) che mantengono il catalogo endpoint e campi sincronizzato interrogando le API live e processando i PDF di changelog ufficiali. Non documentazione statica: un asset che invecchia più lentamente del manuale del vendor. ## Contesto SPRIX è il linguaggio di scripting proprietario di **Mexal/Passepartout**, gestionale diffusissimo nelle PMI italiane. Due dialetti da padroneggiare — **SPRIX "puro"** per generare report, procedure batch e automazioni stand-alone, e **SPRIX Collage** per eventi dentro Mexal (apertura documento, salvataggio, navigazione tra anagrafiche) con accesso all'archivio aperto nella sessione — più le **WebAPI** per integrazioni esterne. Chi ci sviluppa ha tre scomode realtà con cui confrontarsi: manuali ufficiali enormi (683 pagine solo per il Manuale SPRIX, 123 per le WebAPI), campi che cambiano nome tra una release e l'altra senza che la documentazione si aggiorni subito, e una community italiana molto ristretta che condivide poco. Il risultato è che ogni volta che si entra in un progetto Mexal — dopo mesi passati su altro — si perde una giornata a riorientarsi. **Nota di posizionamento**: questa guida nasce come appunti personali dal campo per il mio uso quotidiano come **sviluppatore interno** su Mexal. Non sono un partner certificato Passepartout né un implementor commerciale — quello che trovate qui e nella skill Claude Code associata è competenza maturata sviluppando sul gestionale in licenza delle aziende per cui lavoro. ## Sfida Costruire un riferimento che fosse **consultabile in minuti, non in ore**, scritto dal campo (con le trappole reali del linguaggio e delle API), e che **non invecchiasse** a ogni release Passepartout. Ma soprattutto: che fosse **integrato nel mio flusso di lavoro quotidiano** con Claude Code, non un PDF da aprire a parte o un sito esterno da consultare. ## Approccio Il progetto ha due facce complementari: la **guida markdown** (`guida_sprix/`) e la **skill Claude Code** (`mexal-webapi/`) che la rende richiamabile in automatico. Cinque feature che fanno di questo un asset diverso dal classico "blog tecnico". ### Skill Claude Code auto-caricata su ogni progetto Mexal Ho installato la skill in `~/.claude/skills/mexal-webapi/`, con un `SKILL.md` che dichiara scopo, trigger e istruzioni, e una cartella `references/` di circa 1.400 righe su sei file (`endpoints.md`, `filtri-e-campi.md`, `errori-comuni.md`, `logica-gestionale.md`, `sprix-callwebsvc.md`, `changelog-skill.md`). Il mio `CLAUDE.md` globale ha una regola esplicita: quando Claude Code riconosce un progetto che tocca Mexal (WebAPI, SPRIX, CALLWEBSVC), **deve leggere `SKILL.md` prima di rispondere**. Significa che ogni volta che apro un progetto gestionale — mio, di un cliente, o un nuovo — Claude ha in contesto il catalogo endpoint, i pattern, gli errori noti e le configurazioni aziendali, senza che io debba incollare nulla. La conoscenza è già nel sedile del conducente. ### `/mexal-discover`: discovery live delle WebAPI che scrive sulla skill Uno **slash command** (`/mexal-discover [azienda] [--endpoint risorse/clienti] [--all]`) che si connette alle WebAPI Mexal dell'installazione reale (via `curl -k` per gestire il certificato self-signed), chiama `/risorse/help` per elencare gli endpoint disponibili, poi interroga ciascuno con `?info=true` per estrarne lo schema dei campi. I file `references/filtri-e-campi.md`, `references/endpoints.md` e `references/discovery-log.md` vengono **aggiornati automaticamente**, con segnalazione esplicita di novità e rimozioni rispetto allo stato precedente. Il comando riconcilia la documentazione con la realtà: dove il manuale ufficiale e il comportamento effettivo divergono, vince il live. ### `/mexal-changelog`: parsing dei PDF di release Passepartout Secondo slash command (`/mexal-changelog [path/al/ManWebAPI_vX.XX.pdf]`) che legge il PDF di changelog ufficiale in blocchi da 20 pagine, estrae nuovi endpoint, campi aggiunti, servizi introdotti, rinominazioni e deprecazioni, e aggiorna `SKILL.md`, `endpoints.md`, `filtri-e-campi.md`, `errori-comuni.md`, più un `changelog-skill.md` che tiene lo storico delle versioni della skill agganciato alle versioni del manuale. Quando Passepartout rilascia la v2.33, non devo rileggermi 60 pagine di novità: lancio il comando e la skill si allinea. ### Mappa relazioni campi-tabelle costruita a mano Nel file `webapi/mexal_api_helper.py` ho codificato una **mappa manuale** dei campi foreign-key dentro Mexal — quale campo di quale endpoint punta a quale altro endpoint (es. `cod_conto` su un documento → `/risorse/clienti`). È informazione che **le API live non espongono**: va ricavata leggendo il gestionale in produzione e annotando i collegamenti. Questa mappa è la parte che un altro consulente non può estrarre da nessuna documentazione — è esperienza compressa. ### Trappole SPRIX documentate come prima cosa La guida privilegia i **bug-trap reali** del linguaggio rispetto alla teoria: il limite di 12 caratteri sui nomi variabile, l'assenza del costrutto `WHILE` (serve `FOR` con uscita esplicita), la differenza sostanziale tra **SPRIX** e **COLLAGE** (due dialetti con semantica diversa), le condizioni case-sensitive nei filtri WebAPI (`contiene` sì, `Contiene` no), i campi rinominati tra versioni (`nota` vs `note`). Dodici errori documentati in `06_errori_comuni.md` sono quelli in cui ho visto perdere ore a me stesso e ad altri. ### Copertura quantificata Trentadue endpoint WebAPI documentati in cinque categorie (risorse anagrafiche, documenti, servizi, dati generali, DocuVision). Ventotto servizi speciali invocabili via `POST /webapi/servizi`. Diciotto esempi SPRIX completi copiabili. I 683 PDF del manuale SPRIX convertiti in testo e indicizzati. Quattro linguaggi di esempio nelle chiamate: SPRIX nativo, TypeScript, PHP, Python. ## Risultato La guida è alla versione 1.2 (ottobre 2025), la skill alla 2.0 — generate in parte dai miei stessi comandi `/mexal-changelog` e `/mexal-discover`. Uso questo sistema ogni giorno: ogni volta che riprendo un progetto Mexal, Claude Code ha in contesto le informazioni giuste **prima di scrivere la prima riga**, e quando Passepartout aggiorna il gestionale lancio un comando e mi allineo. Il valore non è tecnico, è **di posizionamento**. Nella nicchia italiana dello sviluppo su Mexal la documentazione condivisa è scarsa, in inglese ancora meno. Avere un asset vivo, pubblicamente leggibile, agganciato al workflow di Claude Code, è il tipo di prova tangibile di competenza che fa la differenza quando un potenziale cliente cerca "qualcuno che capisce SPRIX e AI-assisted development" — intersezione dove in Italia siamo davvero in pochi. --- ## Mestiere SPRIX: competenza tradizionale ma rara per integrare Mexal/Passepartout nel mondo moderno URL: https://andreapellizzari.it/lavori/mestiere-sprix/ Cliente: Sviluppo interno aziendale su gestionale Mexal in licenza del datore di lavoro Ruolo: Sviluppatore SPRIX interno, integrazioni e automazioni sul gestionale aziendale Anno: 2026 Durata: Competenza stratificata su più anni di lavoro in-house Stack: SPRIX, SPRIX Collage, Mexal WebAPI, CALLWEBSVC > Il lato pratico della mia competenza SPRIX — cosa significa davvero essere uno sviluppatore SPRIX sul gestionale Mexal/Passepartout, perché questa competenza è rara in Italia, a quali problemi reali risponde nelle PMI manifatturiere, e perché il mix con le competenze moderne (Next.js, Python, AI) è quello che rende possibili integrazioni serie tra gestionale tradizionale e web contemporaneo. Competenza maturata come sviluppatore interno, non come partner certificato. ## Contesto Questa case study è un po' diversa dalle altre: non racconta un progetto specifico, racconta una **competenza trasversale** — SPRIX, il linguaggio di sviluppo di Mexal/Passepartout. È la parte tradizionale del mio profilo, quella meno "alla moda" rispetto ad AI, Next.js o Power Platform — ma è anche una delle più concrete e rare in Italia. Nelle PMI manifatturiere italiane, Mexal è ovunque; gli sviluppatori che lo sanno davvero personalizzare sono pochi. **Nota di posizionamento**: lo sviluppo SPRIX che racconto qui — e che trovate nei progetti concreti come [accordi-fornitori](/lavori/accordi-fornitori/) e [gestionale-scriptcase](/lavori/gestionale-scriptcase/) — è stato fatto come **sviluppatore interno** delle aziende in cui ho lavorato, sotto la licenza Mexal dei rispettivi datori di lavoro. **Non sono un partner certificato Passepartout** né offro implementazione SPRIX come servizio a clienti terzi. ## A cosa serve saper fare SPRIX SPRIX è la leva ufficiale per personalizzare Mexal. Senza SPRIX, un'azienda che usa il gestionale resta confinata a quello che il software standard copre; con SPRIX può farlo fare praticamente quello che vuole. Nella mia esperienza, i problemi che SPRIX risolve quotidianamente rientrano in quattro famiglie ricorrenti: - **Integrazioni tra il gestionale e il web**: sincronizzare l'anagrafica clienti con un ecommerce, mandare ordini in tempo reale a un portale B2B, ricevere preventivi da una web app e scriverli come documenti Mexal. - **Automazioni batch pianificate**: generazione notturna di report su misura, export controllati verso partner esterni, aggiornamenti di massa su archivi, invio di email transazionali agganciate a eventi contabili. - **Personalizzazione dei flussi documentali**: logiche custom sull'apertura o sul salvataggio di un ordine, calcoli di sconti/ricarichi/RAEE fuori dagli standard, validazioni specifiche del settore manifatturiero. - **Ponti verso l'archivio documentale Docuvision**: recuperare al volo disegni tecnici di articolo, PDF di ordini fornitore, certificati di qualità — e renderli accessibili dove servono fuori dal gestionale. Ognuno di questi scenari è un'area dove le PMI manifatturiere italiane hanno bisogni concreti e ricorrenti — e dove senza SPRIX restano incastrate in processi manuali. ## Perché è una competenza rara Mexal è un gestionale italiano, venduto quasi esclusivamente nel mercato italiano. SPRIX è un linguaggio proprietario che esiste solo dentro questo ecosistema: non ce ne sono equivalenti generici, non c'è community pubblica su Stack Overflow o GitHub, non c'è documentazione che non sia quella ufficiale del vendor. Il canale formativo è la scuola certificata Passepartout per partner — chiusa, verticale. Chi impara SPRIX raramente condivide pubblicamente, perché il lavoro finisce nelle cartelle cliente e il bacino dei dev veramente attivi è piccolo. Risultato: ogni volta che una PMI ha bisogno di un'integrazione SPRIX, farla diventa difficile — ci sono pochi professionisti che la sanno fare davvero. ## Dove sta il valore del mix La mia differenza non è "so fare SPRIX". La differenza è **saper fare SPRIX e insieme Next.js moderno, Python, API REST, AI applicata**. Il valore emerge quando un'azienda ha bisogno di un'integrazione *vera* tra gestionale tradizionale e strato web contemporaneo — una web app che riceve ordini da clienti, li validaa, li spedisce al gestionale senza duplicazioni, aggiorna lo stato in tempo reale, gestisce gli errori da entrambi i lati. Questo mix di competenze — il dentro e il fuori del gestionale, parlanti nelle lingue giuste — è quello che nelle PMI italiane fa la differenza tra "progetto ambizioso che non parte mai" e "sistema integrato che funziona". ## Dove vederlo in pratica Gli esempi concreti di SPRIX applicato sono nelle altre case study del portfolio: il [collage che aggancia gli ordini fornitore agli accordi pack](/lavori/accordi-fornitori/) (sincronizzazione bidirezionale gestionale ↔ portale web), i [webservice che attingono a Docuvision dentro Scriptcase](/lavori/gestionale-scriptcase/), le [chiamate WebAPI che il DDT digitale fa per ricomporre un documento web](/lavori/ddt-digitale/). Se ti interessa anche il lato didattico — cosa significa scrivere SPRIX quotidianamente, come lo ho documentato, come Claude Code lo usa via skill auto-caricata — c'è la case study [documentazione SPRIX e skill Claude Code](/lavori/documentazione-sprix/). --- ## Chatbot "Rolando": assistente di vendita multilingua con product extraction inline, prompt caching Claude e contesto dinamico di pagina URL: https://andreapellizzari.it/lavori/chatbot-rolando/ Cliente: PMI manifatturiera italiana, settore ferramenta Ruolo: Architettura, sviluppo frontend e backend, prompt engineering, integrazione con catalogo Anno: 2026 Durata: Sviluppato in continuità con l'ecommerce B2B/B2C, iterazione continua Stack: Next.js, TypeScript, Claude Haiku 4.5, Anthropic SDK, Server-Sent Events, React, Framer Motion, sessionStorage > Un chatbot conversazionale live dentro l'ecommerce B2B/B2C della PMI ferramenta, con la voce e il tono del fondatore storico dell'azienda. Widget React che parla in nove lingue, estrae in real-time i codici prodotto dalle risposte e genera schede card inline con immagine e prezzo, segue l'utente tra brand diversi e tra catalogo shop e flipbook PDF, ottimizzato via prompt caching Claude per tenere i costi sotto controllo su volume alto. Non un chatbot generico — un commesso AI che conosce il catalogo, il contesto della pagina e cosa l'utente ha nel carrello. ## Contesto L'ecommerce B2B/B2C di una PMI del settore ferramenta ha catalogo molto tecnico: migliaia di articoli divisi su cinque brand, varianti per materiale, dimensione, colore, ambito d'uso, applicazioni specifiche (cardini, cerniere, chiusure per serramenti tecnici). Un dealer professionale che cerca un prodotto specifico ci arriva; un utente B2C che esplora fa fatica — troppi filtri, troppi brand, non sa da dove partire. E spesso quello che manca non è la feature nel sito, è **qualcuno con cui parlare** — il commesso esperto che in negozio ti dice "per il tuo caso prova questo, non l'altro". ## Sfida Mettere accanto al catalogo un **chatbot conversazionale** che giocasse quel ruolo di commesso — con tono riconoscibile, voce coerente con il brand, accesso in tempo reale al catalogo, consapevole del contesto della pagina (prodotto corrente, carrello, brand filtrato, lingua) — e che restasse economicamente sostenibile su volume alto: qualche centinaio di conversazioni al giorno non devono costare come un consulente umano. ## Approccio Stack **Next.js + Anthropic SDK + Claude Haiku 4.5 + Server-Sent Events**. Widget React flottante a destra, backend in App Route che parla direttamente con l'API di Claude in streaming. Sei scelte che fanno la differenza rispetto a un "chatbot OpenAI sul sito". ### Persona definita: "Rolando", voce del fondatore Il chatbot non è generico. Ha nome proprio — **Rolando** — e prompt di sistema calibrato sulla voce e sul tono del fondatore storico dell'azienda: cordiale, competente, appassionato, orgoglioso del Made in Italy. Quando l'utente scrive in italiano riceve la risposta di Rolando in italiano con un tono specifico; quando scrive in inglese o in una delle altre otto lingue supportate, Rolando risponde lì con lo stesso registro. Dare un nome proprio e una voce riconoscibile trasforma l'interazione — l'utente non parla con "l'intelligenza artificiale del sito", parla con una persona. ### Product extraction in tempo reale con schede card inline Quando Claude nomina un prodotto nella risposta (es. "ti consiglio il cardino FBC00084 se hai un'anta standard"), un regex estrae i codici dal testo e li incrocia con una **mappa prodotti in memoria** caricata dal catalogo compatto (`products_ai.json`, circa 50 KB). Per ogni codice matchato, sotto il messaggio compare una **card prodotto inline** con immagine, nome, prezzo (netto B2B o IVA inclusa B2C in base al profilo utente), link al dettaglio shop. Quando la risposta cita prodotti di brand diversi, le card sono organizzate in un **carosello orizzontale "anche negli altri brand"**. Niente di hardcoded: l'LLM parla naturale, il parser estrae, il rendering costruisce la visualizzazione ricca. ### Contesto dinamico di pagina + memoria conversazionale Il backend riceve con ogni messaggio un **pageContext** che dichiara: URL corrente, prodotto eventualmente in visualizzazione, brand filtrato, contenuto del carrello, posizione nel flipbook catalogo PDF. Rolando adatta la risposta: se sei in una pagina prodotto, si riferisce a quel prodotto ("visto che stai guardando questo…"); se hai già due articoli nel carrello, suggerisce il complementare; se sei nel catalogo PDF anziché nello shop online, propone pagine del flipbook invece dei link al carrello. La conversazione in sé è persistita in `sessionStorage` per la durata della visita. ### Prompt caching per rendere sostenibile il volume Il prompt di sistema (istruzioni di Rolando + catalogo compatto) è di circa 15 mila token — su una conversazione libera sarebbe un costo che si moltiplica a ogni messaggio. Ho strutturato la chiamata con **ephemeral blocks Anthropic** separati: le istruzioni di Rolando come blocco cacheable statico, il catalogo come blocco cacheable con refresh orario, la conversazione corrente come contenuto non cached. Risultato: **cache hit sopra il 90% dei messaggi**, costo effettivo per conversazione contenuto nell'ordine di pochi centesimi anche a regime di centinaia di interazioni al giorno. ### Streaming Server-Sent Events + follow-up chip + verbose toggle La risposta di Rolando **scorre parola per parola** nel messaggio (SSE dal backend, parser incrementale nel widget) — percezione di risposta istantanea. In fondo a ogni risposta, estratti dal testo con sintassi `[FOLLOWUP:testo]`, appaiono **chip di follow-up cliccabili** con domande successive suggerite da Rolando stesso ("Vuoi sapere se c'è la versione in inox?", "Cerchi lo stesso prodotto in un altro colore?"). Un toggle "dettagliato / conciso" in testa al widget modula la lunghezza della risposta (cinquanta vs cento-cinquanta parole) a preferenza dell'utente, con la scelta salvata in localStorage. ### Link shop con filtri preformulati e salto dentro al flipbook Rolando può rispondere con una sintassi speciale `[SHOP:Vedi tutti i cardini nero opaco | f_brand=x&f_Colore=nero]` che viene resa come **pulsante-pillola con icona lente** che apre il catalogo filtrato esattamente con i parametri giusti. Nel flipbook catalogo PDF fa l'equivalente con un "salta a pagina 47" che posiziona direttamente dentro il visualizzatore. L'utente non deve imparare i filtri, li riceve già applicati. ## Risultato Il chatbot è in produzione e gira sul traffico reale dell'ecommerce, con **nove lingue attive** e rate limit di sessanta richieste al minuto per IP. La combinazione di persona chiara, contesto dinamico, product extraction e prompt caching è quello che lo fa funzionare come commesso AI sostenibile economicamente su qualunque volume. La cosa di cui vado più fiero è che **non fa finta di essere un umano** — dice chiaramente di essere un assistente digitale — ma riesce comunque a dare la sensazione di parlare con qualcuno che conosce davvero il catalogo, perché quel qualcuno esiste (il fondatore) e Rolando ne è la declinazione AI coerente. --- # Diario tecnico ## LangExtract: estrazione strutturata da testo, con la prova di dove viene URL: https://andreapellizzari.it/diario/2026-06-04-langextract-estrazione-verificabile/ Data: 2026-06-04 Tag: ai, estrazione-dati, integrazione, python > Un esempio, non un modello addestrato: LangExtract estrae dati strutturati da testo non strutturato e ancora ogni valore all'offset esatto nel documento. L'ho provato sui cataloghi prodotto, ma penso gia' ai DDT di carico. C'e' un problema che torna in forme diverse da anni: prendere un testo non strutturato (la riga di un catalogo, un documento di trasporto, una scheda tecnica) e tirarne fuori dati ordinati. La risposta classica e' una regex scritta a mano per ogni caso, fragile, che si rompe appena cambia il formato. Funziona, ma ogni nuovo fornitore vuole la sua. LangExtract, libreria open source di Google (Apache 2.0), prova un'altra strada. Due ingredienti: gli insegni il pattern con uno o due esempi (niente addestramento, niente dataset etichettato), e ogni valore estratto resta ancorato all'offset di carattere nel documento originale. Questo secondo punto e' quello che mi interessa: non e' solo "il modello ha detto 90 cm", e' "il modello ha trovato 90 cm in questa posizione precisa del testo". Verificabile, e per costruzione non puo' allucinare un valore che nel testo non c'e'. L'ho messo alla prova sull'agente conversazionale su catalogo che sto costruendo, nella fase di inserimento di un nuovo brand. Oggi ogni serie di prodotti ha la sua regex cucita a mano. Ho dato a LangExtract un solo esempio e l'ho fatto girare su cinque serie diverse di cappe dello stesso costruttore: ha eguagliato le regex su tutto cio' che era effettivamente nel testo. Il dettaglio che mi ha convinto: dove il mio valore "vero" veniva da un default dichiarativo (conoscenza di dominio, non scritta nella descrizione), LangExtract si e' correttamente rifiutato di inventarlo. Ha lasciato il campo vuoto, che e' la cosa onesta. ```mermaid flowchart LR A[Testo grezzo] --> B[1-2 esempi few-shot] B --> C{Estrazione} C --> D[Valore strutturato] C --> E[Offset nel testo] D --> F[Dato verificabile] E --> F ``` Lo segno qui soprattutto per il futuro. Il caso ovvio e' l'elaborazione dei DDT di carico, ma vale per qualsiasi documento gestionale dove servirebbe poter dire da dove arriva ogni numero: bolle, schede, output WebAPI da digerire. In un contesto gestionale la tracciabilita' del dato non e' un vezzo, e' la differenza tra un'automazione di cui ti fidi e una che devi ricontrollare a mano. Due cautele, per onesta'. Lavora sul testo, non sul layout: l'estrazione dal PDF complicato resta un passo a monte. E una chiamata a un modello costa, mentre una regex e' gratis. Per questo lo vedo come motore di voto e disambiguazione (gira a fianco delle regex, alza la mano quando dissente), non come estrattore primario da rilanciare a ogni aggiornamento. Provider-agnostico, fra l'altro: gira anche via OpenRouter, non serve legarsi a un fornitore. --- ## Il coseno manca i codici esatti: retrieval ibrido su cataloghi PDF URL: https://andreapellizzari.it/diario/2026-05-27-retrieval-ibrido-cataloghi-pdf/ Data: 2026-05-27 Tag: ai, rag, python, claude > Un agente di vendita cerca dentro i cataloghi PDF dei produttori per similarità semantica. Ma su contenuto pieno di codici e misure, il coseno sbaglia proprio sui termini esatti. La cura: una gamba lessicale fusa con RRF. Sto costruendo un agente di vendita virtuale per un catalogo multi-marca di cucine ed elettrodomestici: il modello ragiona, e una serie di strumenti gli danno accesso ai dati. Uno di questi strumenti cerca dentro i cataloghi PDF dei produttori, migliaia di pagine indicizzate come testo. Funzionava per similarità semantica pura: la query e ogni pagina diventano vettori (embedding BGE-M3), e si recuperano le pagine più vicine. Il problema l'ho visto solo guardando i casi giusti. La similarità semantica è ottima per i concetti ("schemi di montaggio", "manutenzione filtri"), ma sui codici articolo e le misure sbaglia dove dovrebbe essere più precisa. Un codice come `RT.4150` per il coseno è quasi rumore: non c'è semantica in una sigla. E in un catalogo tecnico, spesso il codice esatto è la ricerca. La cura non era esotica. Lo stesso sistema aveva già, su un altro strato, un retrieval ibrido collaudato: una gamba lessicale (BM25 su indice FTS5, che pesa la corrispondenza letterale dei termini) accanto a quella vettoriale, e le due liste fuse con Reciprocal Rank Fusion (un metodo che combina i due ordinamenti premiando chi sta in alto in entrambi). Uno strumento solo era rimasto indietro. Ho copiato il pattern già maturo invece di inventarne uno nuovo. ```mermaid flowchart LR Q[Query utente] --> V["Gamba vettoriale
(embedding + KNN)"] Q --> B["Gamba lessicale
(BM25 / FTS5)"] V --> F[Fusione RRF] B --> F F --> O[Risultati ordinati] ``` La parte che mi ha insegnato qualcosa è la verifica. Misurare la precisione aggregata avrebbe mentito: la media sarebbe sembrata buona, nascondendo che le query per codice esatto fallivano del tutto. Ho segmentato i test in tre categorie (codice esatto, misura o sigla, concetto) e confrontato la versione vettoriale pura con l'ibrido. Sui codici esatti il risultato è netto: | Query per codice esatto | Coseno puro | Ibrido | |---|---|---| | sigla puntata (`RT.4150`) | fuori dai primi 5 | 3ª posizione | | codice EAN a 13 cifre | fuori dai primi 5 | 2ª posizione | | seconda sigla puntata | fuori dai primi 5 | 3ª posizione | Tre casi su tre in cui prima la pagina corretta non usciva nemmeno tra le prime cinque, e ora esce in cima. Sui concetti: pari, nessuna regressione. È esattamente la diagnosi che la metrica aggregata avrebbe sepolto. Un dettaglio di igiene operativa che rifarei sempre: l'indice lessicale è un file separato, derivato, rigenerabile in meno di un decimo di secondo. Non lo verso nel repository e non lo "migro": un controllo all'avvio del server confronta numero di righe e timestamp dell'ultima ingestione, e se qualcosa è cambiato lo ricostruisce da solo. Se per qualsiasi motivo manca, lo strumento torna al comportamento vecchio (solo vettoriale) invece di rompersi. In produzione il primo avvio l'ha rigenerato in mezzo secondo, e una query reale ha confermato la fusione attiva. L'ibrido non è una novità. La disciplina di non fidarsi della media, e di trattare gli artefatti derivati come usa-e-getta, quella sì che paga. --- ## Microsoft 365 sotto sorveglianza: un monitor fatto in casa dopo un phishing URL: https://andreapellizzari.it/diario/2026-05-26-monitor-microsoft-365-fatto-in-casa/ Data: 2026-05-26 Tag: microsoft-365, security, python, automazione, incident-response > Dopo un phishing arrivato da un fornitore compromesso ho costruito un monitor di sicurezza per il tenant Microsoft 365: headless, sola lettura, e con un trucco per leggere i log di accesso senza pagare la licenza Entra P1. L'innesco è stato un phishing che non sembrava un phishing: arrivava da un fornitore vero, il cui account era stato compromesso, con un link a un documento "condiviso" ospitato su un servizio Microsoft legittimo usato come civetta. Classico attacco AiTM, dove la pagina-esca intercetta credenziali e token di sessione. Dopo averlo contenuto mi sono accorto del problema vero: non avevo nessuna visibilità continua su cosa succedeva nel tenant. Così ho costruito un monitor che gira ogni sei ore, headless e in sola lettura, con un token applicativo. Controlla, su tutte le caselle, i segnali che contano davvero per una compromissione: nuovi consensi OAuth, regole di posta con inoltro esterno, credenziali aggiunte di nascosto a un'app (la backdoor silenziosa che quasi nessuno guarda), nuovi amministratori, nuovi metodi di autenticazione, invii anomali. In più un controllo locale sul server. Il pezzo di cui vado più fiero è come leggo gli accessi senza spendere. I sign-in ricchi di Graph richiedono la licenza Entra P1, che qui non c'è: ```python # GET https://graph.microsoft.com/v1.0/auditLogs/signIns -> 403 senza Entra P1 # Stessi eventi di login, gratis, dall'Unified Audit Log: # POST https://manage.office.com/.../subscriptions/start?contentType=Audit.AzureActiveDirectory # GET .../subscriptions/content?contentType=Audit.AzureActiveDirectory ``` L'Office 365 Management Activity API espone gli stessi eventi, gratis e in modo headless. Il "no" della licenza diventa un "sì" cambiando porta d'ingresso. ```mermaid flowchart TD T[Task ogni 6h] --> S[Sweep tenant
Graph, sola lettura] T --> A[Audit log
Management Activity API] T --> L[Threat-hunting locale] S --> F{Rilievi} A --> F L --> F F --> K[Filtro acknowledged
per fingerprint] K --> E[Email OK / WARN / FAIL] G[GUI di revisione] -.->|marca i noti, contieni| K ``` Per i falsi positivi ho evitato di hardcodare filtri, che fanno perdere segnale. Ogni rilievo ha un'impronta: marco una volta il rumore noto come "a posto", e solo le novità vere fanno scattare l'allarme. La rilevazione resta larga, il rumore sparisce. Due scelte di metodo che rifarei. I segreti non li ho messi dentro l'eseguibile (sarebbe sicurezza finta: bastano `strings` o un decompilatore, e un binario rubato se li porta dietro); stanno cifrati con DPAPI, legati a quella macchina. E ogni azione distruttiva resta manuale e con conferma: si revoca una sessione, che è reversibile, non si disabilita un account al buio. La parte più istruttiva è stata tarare il rumore: la mia stessa attività di amministrazione veniva segnalata come sospetta. Il valore non era nel codice, ma nel decidere cosa lasciar fare in automatico e cosa no. --- ## Arcocat, due settimane di hardening: domain contracts, knowledge graph, catalog RAG URL: https://andreapellizzari.it/diario/2026-05-14-arcocat-due-settimane-hardening/ Data: 2026-05-14 Tag: ai, claude, agente, prompt-caching, knowledge-graph, rag, runtime-contracts > Diario in presa diretta delle ultime due settimane su arcocat. Quattro ondate di lavoro che spostano peso dal prompt al runtime: guardrail deterministici, discovery enforcement, routing per brand, streaming, knowledge graph indicizzato, catalog RAG, e otto policy YAML come fonte unica. Le ultime due settimane su arcocat (il sistema CPQ multi-brand su cinque cataloghi reali che e' il successore di [BlumCat](/lavori/blumcat/)) sono state inusuali: niente feature nuove visibili in chat, molto lavoro di hardening sotto la linea di galleggiamento. A posteriori si vede un filo conduttore: la riga di codice che dice "il modello DEVE fare X prima di Y" e' migrata, una alla volta, dal prompt al runtime. Provo a raccontarla per ondate. ## Ondata 1: strumenti per non rompere niente Per primo ho costruito quello che mi era mancato fino a quel momento: una sicurezza per cambiare cose senza paura. Hot-reload dei metadati di categoria con TTL 5 secondi (basta toccare un file YAML, niente restart del server). Trace persistente in JSONL per ogni turn di conversazione (debug post-mortem leggibile a freddo). Ventisette golden queries pytest end-to-end su tre brand, con assertion non solo sul "tool e' stato chiamato" ma sull'**effettivo match degli attributi** sui prodotti ritornati. Un health-check L15 wiki vs PIM che mi avrebbe catturato il bug "Lorenzo non e' a Monza" delle settimane precedenti. Modello di default cambiato da Sonnet a Haiku 4.5: ventuno golden passano in tre minuti e venti contro i tre e cinquanta di Sonnet, costo per query circa quattro volte piu' basso, regressione zero. Cost middleware leggero che logga in `cost.jsonl` per ogni turn, con alert console se la sessione supera mezzo dollaro. Niente di sofisticato; funziona. ## Ondata 2: determinismo dove la verita' e' finita Il pattern di bug che mi tormentava era questo: query "forni pirolitici", il modello sceglie autonomamente l'attributo `pirolitico: true` (che non esiste nel PIM, e' `autopulizia: pirolitica`), il database ritorna ventuno prodotti ma di questi solo undici sono effettivamente pirolitici. Silent wrong. Soluzione strutturale: spostare l'enforcement fuori dal prompt e dentro a un validator runtime. Un classificatore deterministico `_classify_filter_key(cat, key, val)` con quattro stati (native, known_json, unknown_attribute, unknown_value), un pre-dispatch hook che blocca i filtri JSON su attributi non verificati e ritorna un payload `UNVERIFIED_ATTRIBUTE` con un sample dei valori realmente disponibili. Il modello riceve un errore strutturato, chiama `list_distinct_values`, ritenta. Sui ventisette golden: `unknown_attribute` crolla da 10% a 0%, `unknown_value` da 14% a 7%, e nel 19% dei tentativi bloccati l'auto-recovery funziona senza intervento umano. Sulla stessa scia, ho costruito un piccolo motore di guardrail con quattro regole YAML attive (distanza cappa piano cottura per la norma EN 60335, peso anta vs meccanismo di sollevamento, numero cerniere per spessore anta, dimensioni vano incasso EU). AST visitor manuale per la sandbox aritmetica, senza `eval`. Se la regola finisce in `BLOCK`, re-prompt al modello con il verdict strutturato, max un retry. Il tool LLM-callable `check_safety` che avevo in precedenza l'ho deprecato: il validator runtime e' deterministico, citabile e testabile, l'LLM li non aggiungeva valore. ## Ondata 3: routing e streaming Il prompt era diventato troppo lungo: diciotto kilochar, con istruzioni branch-specifiche per cinque brand caricate sempre. Ho aggiunto un classificatore regex deterministico in `brand_router.py` che gira come step zero di ogni conversazione: se la query e' chiaramente brand-specifica, espongo al modello solo i 7-9 tool relativi a quel brand invece dei 16 totali. Prompt char crolla da 18247 a 7916 (meno cinquantasette percento), avg token in per turn da 17000 a 9214 (meno quarantasei percento), latency dei golden cala del tredici-diciotto percento. Tool `expand_brand_scope` come escape hatch se il modello scopre durante la conversazione che la query e' in realta' cross-brand. In parallelo, streaming end-to-end via OpenRouter (passthrough trasparente del `cache_control` Anthropic). TTFT da otto secondi a un secondo e mezzo percepiti, cache HIT confermato in JSONL con 96% di reduction sul prefisso `system + tools` di 9582 token. Costo prefisso da circa novantasei millesimi di dollaro a circa otto millesimi per call. ```mermaid flowchart LR Q[Query utente] --> R{Routing classifier
regex deterministic} R -->|brand match| T1[Tool set ridotto
7-9 tool] R -->|no match| T2[Tool set completo
16 tool] T1 --> D{Discovery
Enforcement} T2 --> D D -->|attr verificato| S[search_products] D -->|UNVERIFIED| L[list_distinct_values
full-scan] L --> S S --> G{Guardrail engine
4 regole YAML} G -->|PASS| OUT[Streaming
cache HIT 96%] G -->|BLOCK| RP[Re-prompt LLM
max 1 retry] RP --> OUT ``` Telemetry hardening, sopra a tutto: log di `model_observed` e `cache_creation/read_input_tokens` per ogni risposta. La scoperta del log non l'avevo prevista: chiediamo a OpenRouter `claude-haiku-4-5`, OpenRouter ci routa a `anthropic/claude-4.5-haiku-20251001`. Behavioral drift invisibile senza la riga di trace. ## Ondata 4: dal wiki narrativo al knowledge graph I [wiki narrativi per brand](/studio/wiki-narrativo-ai-maintained/) erano un grafo *implicito* nei link in prosa ("vedi R001", "(F005)", `[F012](../path/...)`). Un indexer offline Python di circa duecentottanta righe ora lo rende esplicito: scrive in `pim.db` due tabelle additive (`wiki_entities` con SHA-256 per change detection, `wiki_edges` con tipo inferito dal verso del link). Risultato su cinque brand: 166 entita' e 557 edges. Tre nuovi tool LLM-callable per percorrerlo (`find_related`, `find_citing`, `get_entity_details`). Sopra al wiki, un registry SSoT a parte: `canonical_aliases.yaml` con quindici concetti commerciali ambigui e i loro override per brand. L'esperto interno edita il file, l'agente impara. Niente codice Python toccato. ## Ondata 5: catalog RAG e fonte unica Quando il catalogo di un brand non e' un singolo datasheet ma un PDF da centinaia di pagine, il wiki narrativo non scala. Ho aggiunto un secondo binario di retrieval indipendente: PyMuPDF per il parsing, BGE-M3 per gli embedding, sqlite-vec per la similarity search, in un DB separato `document_vectors.db` da venti megabyte. Cinque brand indicizzati: 1973 entita' + 2295 chunks + 7550 mentions edges su 1980 PDF totali. Tre tool LLM-callable con schema dual-purpose (`source_type` distingue datasheet da catalog_page). L'osservazione che mi tengo da questa fase: **SKU recall basso non e' un problema di embedding, e' un problema di layout extraction**. Embedding migliori non risolvono i codici in tabelle a due livelli. Il prossimo step strutturale sara' entity-centric extraction offline (regex SKU + table + page anchor + adjacency + inverted index), non embedding piu' grossi. Per chiudere il giro, ho consolidato tutte le policy in una directory `domain_contracts/` con otto file YAML (`valid_brands`, `brand_routing_patterns`, `sku_regex`, `native_columns`, `canonical_aliases`, `guardrail_rules`, `sku_invariants`, `kg_edge_types`), un loader Pydantic v2 con TTL 5 secondi, un cross-validator `check_contracts.py` che esegue 14 check su DB reale e ritorna exit 0/1/2. Cinque engine separati nel codice ora leggono da questa fonte unica via try-import + fallback hardcoded (backward-compat zero-rischio). Un orchestrator `refresh_arcocat.py` lega sei passi (wiki indexer, document ingester, SKU audit, drift wiki-vs-pim, alignment, check_contracts, regression 27 golden) in single entry-point. Severity aggregata, exit code, skill `arcocat-refresh` user-invocable. ## Cosa mi tengo Tre lezioni, in ordine di durabilita'. La prima: **il prompt si accorcia mano a mano che la macchina assorbe le clausole "il modello DEVE..." in contratti runtime**. Meno cinquantasette percento di prompt char in due settimane senza perdita di capacita'. Il prompt vuole intent, non enforcement. La seconda: **i nodi della constellation crescono per sedimentazione di layer separati, non per ingrossamento di un layer unico**. Il wiki narrativo per l'umano, l'index strutturale per la macchina (knowledge graph), i contratti dichiarativi per il runtime (domain contracts). Sono tre fonti diverse di verita' che si annotano a vicenda, non tre fasi della stessa cosa. La terza, di metodo: **prima costruisci come misurare, poi cambi**. I ventisette golden queries pytest e il L15 drift mi hanno permesso di toccare cinque punti del sistema senza paura. Ogni intervento e' chiuso con una run di regression, non con un "secondo me funziona". Per il dettaglio architetturale aggiornato vedi lo [studio sullo stack agentico per PMI](/studio/stack-agentico-pmi/) bumpato a v2.4 e il [playbook di onboarding catalogo](/studio/playbook-onboarding-catalogo-pdf/) a v1.3. --- ## Cambiare backend al giudice LLM: da Anthropic SDK a OpenRouter URL: https://andreapellizzari.it/diario/2026-05-11-openrouter-refactor-brand-mcp/ Data: 2026-05-11 Tag: ai, openrouter, claude, integrazione > Ho riscritto un sistema di validazione LLM da SDK Anthropic a SDK OpenAI puntato a OpenRouter. Dieci volte meno costo, qualche gotcha Windows, un path di rollback semplice. Ho passato la settimana a riscrivere il giudice LLM di un sistema di catalogazione interno. Quattro moduli `brand-knowledge-mcp` distinti (uno per ogni catalogo di componenti gestito) usavano l'SDK ufficiale Anthropic per chiamare Haiku 4.5 con `tool_choice` forzato sullo schema di validazione. Funzionava bene, ma la fattura saliva e cominciava a pesare anche sullo sviluppo locale. Il refactor è stato sostituire l'SDK Anthropic con l'SDK OpenAI puntato a OpenRouter (`base_url=https://openrouter.ai/api/v1`), modello default `openai/gpt-4.1-mini`. Il differenziale di costo è circa dieci volte (input 0.40 contro 1.00 dollari per milione di token, con caching automatico OpenAI sopra i 1024 token). Ma il vero motivo per accettare il pattern non è il prezzo: è la possibilità di switchare modello via variabile d'ambiente senza toccare codice, che vuol dire poter confrontare GPT, Claude e DeepSeek nello stesso giro. ```mermaid flowchart LR subgraph Prima A1[brand-mcp] --> A2[Anthropic SDK] A2 --> A3[Haiku 4.5
tool_choice forzato] end subgraph Dopo B1[brand-mcp] --> B2[OpenAI SDK] B2 -- base_url --> B3[OpenRouter] B3 --> B4[gpt-4.1-mini
response_format json_schema] end ``` Tre cose tecniche che vale la pena annotare. **Schema enforcement diverso.** OpenAI non supporta `tool_choice="required"` sull'output JSON come fa Anthropic in modo pulito. La sostituta è `response_format={"type": "json_schema", ...}` con `extra_body={"provider": {"require_parameters": True}}` per OpenRouter. Output identico (un dizionario validato), strada diversa. **Path di rollback preservato.** Le settings hanno un campo `llm_backend` con due valori: `openai_compat` (default) e `anthropic_native` (legacy). Il giorno in cui un brand peggiora qualitativamente con gpt-4.1-mini, basta una variabile d'ambiente per tornare al giudice Haiku 4.5 solo per quel modulo. Niente flag globali, niente codice morto. **Il gotcha Windows.** L'orchestratore di test cross-brand lanciava i quattro moduli in subprocess separati (per liberare la memoria di SBERT fra brand) e leggeva stdout via `PIPE`. Su Windows un subprocess che fa molto output e un parent che fa anche `sys.stdout.write` per echo crea un deadlock silenzioso: il PIPE si riempie, write bloccante, child fermo, parent fermo. La soluzione è stata far scrivere il subprocess direttamente sul file di log (`stdout=open(log, "w")`) e tenere il monitoring esterno via tail. Niente PIPE intermedio, niente blocco. Lo smoke test cross-brand finale: 4 brand su 4 verdi, costo per chiamata 0.0014 dollari, latenza media 22 secondi (in linea con Haiku 4.5). --- ## La differenza fra una skill che dice cosa è rotto e una che lo aggiusta URL: https://andreapellizzari.it/diario/2026-05-11-skill-self-healing-prescriber-fixer/ Data: 2026-05-11 Tag: ai, automazione, claude, skill, knowledge-base > Una skill che rileva problemi è utile. Una che classifica le issue per priorità, propone i comandi e li esegue automaticamente cambia il modo in cui lavoro. La differenza non è cosmetica, è strutturale. Sto costruendo un sistema di knowledge base AI multi-brand: cinque fornitori, sette categorie merceologiche, regole tecniche per ogni dominio, eval RAGAS per misurare la qualità delle risposte. Il punto di partenza era una skill di manutenzione che eseguisse audit sistemici cross-brand prima di onboardare un nuovo fornitore. La prima versione era un classico detector: dieci layer di verifica (coverage estrazione, integrità schema, populator, routing), un report markdown con tabelle di ✅/⚠️/❌. Funzionava: al primo run ha scovato tre bug invisibili che il sistema portava silenziosamente da settimane. Però il flusso reale era: leggi report, capisci la priorità, vai a leggere la documentazione del pattern, esegui il comando di fix manualmente. Bottleneck umano in mezzo. Il salto qualitativo è stato passare a `detector → prescriber → fixer`. Per ogni codice di finding (es. `L9-LOADER-DROP`) ho aggiunto un `ActionSpec` strutturato: ```python ActionSpec( code="L9-LOADER-DROP", priority=1, effort="low", auto_fixable=True, fix_command="python -m health_check.fixers.fix_loader_drop --apply", fix_fn=lazy_import, reference="anti-pattern bool/boolean (3 settimane silenti)", ) ``` Il report ora ha una sezione "Prescription" ordinata per priorità, separa auto-fixable da manual review, e un flag `--fix all-auto` applica tutto in pipeline. Il primo test reale: trenta regole prive di pagine PDF di riferimento (blocker per il context_precision RAGAS) sistemate in cinque secondi. Stima manuale dello stesso lavoro: un'ora e mezza. ```mermaid flowchart LR A[Run health-check] --> B{Findings} B -->|OK| Z[Sistema healthy] B -->|warn / error| C[Prescriber] C --> D[Knowledge base
code, ActionSpec] D --> E{Auto-fixable?} E -->|Si| F[Auto-fix
idempotente] E -->|No| G[Manual review
priority ordered] F --> H[Re-run verify] G --> H H --> Z ``` La lezione sui bug invisibili è separata e merita una nota. Il pattern era questo: il loader Python accettava i tipi schema solo se in una whitelist (`measurement`, `select`, `boolean`, `freeform`), ma i file MD nuovi usavano `bool`, poi `multiselect`, poi `text`. Il loader scartava silenziosamente. Risultato: attributi dichiarati nello schema, mai popolati nel database, mai riempiti nelle risposte. Il bug è rimasto invisibile tre settimane perché nessun test cross-layer collegava il dichiarato al popolato. Il cross-check sistematico l'ha catturato in trenta secondi al primo run. L'ultima osservazione riguarda la skill come asset. Sono passato da `v1.0` a `v1.6` in pochi giorni: ogni caso reale onboardato ha aggiunto due o tre anti-pattern documentati, oggi sono trentuno. La stima di effort per il prossimo brand è scesa del 33% rispetto al precedente, non perché il sistema sia più semplice ma perché la skill cattura preventivamente quello che prima richiedeva debug a posteriori. La conoscenza accumulata si chiude in artefatti eseguibili, non in note sparse. --- ## Filtrare grossolanamente, validare in dettaglio: il consiglio emerge da solo URL: https://andreapellizzari.it/diario/2026-05-06-filter-then-validate-consiglio-comparativo/ Data: 2026-05-06 Tag: architettura, cpq, ai-applicata, pattern > Combinando un filtro grossolano (PIM con attributi tipizzati) e una validazione chirurgica (Knowledge Tool con LLM-judge), la shortlist diventa comparativa senza che tu lo chieda. Il consiglio emerge dalla composizione. Sto costruendo un sistema di configurazione prodotto cross-brand per la PMI dove lavoro: l'utente descrive un caso d'uso in prosa libera ("guida cassetto da 500 millimetri, portata 40 chili, peso anta 60 chili") e il sistema deve proporre i prodotti compatibili dei vari fornitori, con motivazione tecnica. L'architettura e' a due livelli. Un PIM cross-brand con attributi tipizzati per categoria (profondita', portata, altezza, attacco): filtra in modo deterministico ma grossolano, ottimizzato per **recall** alto. Un Knowledge Tool per ogni fornitore, ognuno con il suo wiki di regole tecniche curato dall'esperto interno e validate via LLM-judge: ottimizzato per **precisione** alta. Il supervisore orchestra: filtro grossolano in PIM, poi validazione chirurgica per ogni candidato, poi sintesi in tre stati (consigliato | compatibile | sconsigliato). Il pattern in se' e' canonico nel mondo CPQ industriale (Constructor.com lo cita, Akeneo PIM lo formalizza). Non l'ho inventato io. Quello che ho osservato testandolo end-to-end e' una proprieta' emergente che non avevo pianificato. ```mermaid sequenceDiagram participant U as Utente participant S as Supervisore participant P as PIM (cross-brand) participant K as Knowledge Tool brand U->>S: Query in prosa libera S->>P: filtra(attributi tipizzati) P-->>S: shortlist N candidati par Validazione parallela S->>K: valida_compatibilita(codice_1, contesto) S->>K: valida_compatibilita(codice_2, contesto) S->>K: valida_compatibilita(codice_N, contesto) end K-->>S: status + citazioni regole per ogni candidato S-->>U: shortlist sintetizzata, 3 stati ``` Test reale: query "guida cassetto profondita' 500 mm, portata 40 kg, peso anta 60 kg". Il filtro PIM ha tornato due candidati: una guida da 40 kg di portata e una da 70 kg, entrambe da 500 mm di profondita'. La validazione del Knowledge Tool ha valutato il contesto "peso anta 60 kg" sui due candidati separatamente: la guida da 40 kg e' stata bloccata (peso eccede portata), la guida da 70 kg approvata. La shortlist finale propone entrambe con motivazioni opposte: "ti propongo la 70 kg, mentre la 40 kg non regge il tuo peso anta". Questo output comparativo e' il valore consulenziale del CPQ industriale, ed emerge **dalla composizione** delle due fonti. Non l'ho chiesto. Non c'e' un agente LLM che decide "fammi un confronto"; il confronto nasce perche' il filtro restituisce candidati alternativi (recall) e la validazione li discrimina sul contesto specifico (precisione). La conseguenza pratica e' che lo strato di sintesi del supervisore puo' essere una pura funzione deterministica di una decina di righe: aggrega le validazioni per brand, applica regole conservative ("in conflitto vince chi blocca"), ritorna i tre stati. Niente LLM ricorsivo, niente relax automatico del filtro, niente prompt elaborati. La parte intelligente e' nei due strati a monte; lo strato di sintesi e' un commodity. Caveat onesto: ho scoperto questa proprieta' anche grazie a un bug del mio parser deterministico (un'espressione regolare che mancava di catturare un attributo, allargando di fatto il filtro). Se il parser fosse stato perfetto, il filtro avrebbe ritornato un solo candidato e la shortlist comparativa non sarebbe emersa **per quella query specifica**. Pero' la stessa query riformulata diversamente, oppure altre query in cui un attributo non e' stato menzionato, producono la stessa proprieta'. Il bug ha solo reso visibile prima cio' che esiste comunque. Metto in nota: per chi costruisce sistemi simili, vale la pena testare con query parziali o ambigue prima di rifinire il parser. Le proprieta' emergenti del pattern si vedono meglio quando gli stadi a monte sono "gentilmente imprecisi". Riferimenti: il case study sull'[architettura wiki](/lavori/architettura-wiki/) e quello sul [knowledge base AI-maintained](/lavori/ai-knowledge-base/) raccontano il livello sotto, dove vivono le regole tecniche. --- ## Roll-out multi-brand di landing page in PMI: il valore del pre-check URL: https://andreapellizzari.it/diario/2026-05-04-porting-multibrand-landing-pmi/ Data: 2026-05-04 Tag: multi-brand, next.js, ai-workflow, porting > Sto portando cinque landing brand di un'azienda manifatturiera. Dopo le prime fasi, le lezioni più utili non sono di codice ma di workflow: pre-check prima di partire, branch+tag per fase, smoke test SEO automatico, decisioni utente cristallizzate prima del codice. Sto portando cinque landing brand di un'azienda manifatturiera italiana verso un nuovo design system unificato. Multi-dominio (un sito Vercel, sei domini diversi), nove lingue, theming brand-aware via CSS custom properties scritte SSR sull'attributo `data-brand` dell'``. Dopo le prime due fasi (foundation theming + landing brand 1 + correzioni successive), le lezioni più utili non sono di codice. Sono di workflow. **1. Pre-check readiness prima di partire una fase** Lo studio architetturale generale aveva detto "due sessioni per brand". In realtà la prima è andata in due sessioni di sviluppo + cinque hotfix iterativi: link al catalog rotti perché il pattern URL non era documentato, listini PDF che cercavano nella cartella `public/` locale ma vivevano sul CMS, traduzioni mancanti, banner incoerenti col brand. Ogni hotfix scopriva un'altra incognita. Per la fase successiva ho lanciato una sessione AI dedicata di **pre-check readiness**: niente codice, solo audit (asset disponibili, listini esistenti, attributi prodotto, decisioni di tono, rischi). Output: un documento markdown di tre pagine con sei decisioni utente da prendere e cinque rischi specifici con mitigazione. Costo: una sessione. Risparmio atteso: tre o quattro sessioni di hotfix. **2. Decisioni utente cristallizzate prima del codice** Il pre-check produce sei domande secche. L'utente risponde sei volte (anche solo "a, b, a, c, ok, ok"). Quelle risposte diventano vincoli scritti in memoria, e la sessione di sviluppo successiva non ha più nulla da decidere — solo da eseguire. **3. Smoke test SEO automatico** Trenta URL × sei domini × campi `` (canonical, hreflang, `data-brand`), tutto in trenta secondi. Snapshot baseline, diff vs nuovo run dopo ogni merge. Se cambia qualcosa di non-atteso, fail. È servito a beccare regression che a occhio non si vedono. **4. Errori ortografici nascosti nei dati** Il bug più sottile della fase: un attributo prodotto si chiamava `Applicazione su Allumino` — senza la "i" finale. Refuso storico mai corretto. Tre filtri funzionavano, uno no. Non è una correzione del codice, è una documentazione: il pattern URL accettato dal catalog è viscoso, e il fix giusto è descriverlo nella memoria condivisa AI per le fasi successive, non sistemare i dati e rischiare di rompere altri filtri esistenti. ```mermaid flowchart LR A[Sessione pre-check
audit + decisioni] --> B[Documento markdown
vincoli scritti] B --> C[Sessione sviluppo
branch + commit] C --> D[Smoke test SEO] D -->|OK| E[Merge + tag fase] D -->|FAIL| F[Rollback] E --> G[Pre-check fase successiva] F --> C ``` Il roll-out non è finito. Ma il workflow regge: ogni fase produce un tag git, uno snapshot SEO, un documento di lessons learned. Le sessioni AI lavorano in parallelo o in cascata, io coordino. Il valore non è in quanto produce un'AI a singola query — è in **quanto produce una serie di sessioni AI specializzate** quando un umano fa da arbitro alle decisioni. --- ## Da default Vercel a manuale d'officina: redesign del portfolio in 8 wave URL: https://andreapellizzari.it/diario/2026-05-02-redesign-portfolio-skill-impeccable/ Data: 2026-05-02 Tag: design, skill, impeccable, ai-assisted-dev, tipografia > Il sito era caduto nel default Vercel-cream. La skill impeccable forza scelte di brand, register e gate sequenziali (teach, shape, craft) che salvano dall'AI slop. Otto commit per arrivare a 'Manuale d'officina'. Il punto di partenza era riconoscibile a tre metri: violet `#7c3aed`, font Geist, scala zinc, card rounded-2xl, hero "Ciao sono Andrea + 2 bottoni", radial gradient mesh sullo sfondo. Default Vercel/Next.js 2024-2026, indistinguibile da centinaia di portfolio dev. L'AI slop test della skill `impeccable` lo bocciava al primo livello: chiunque guardando la home avrebbe indovinato la categoria dal solo aspetto. Ho applicato `impeccable`, una skill Claude Code derivata dal pattern frontend-design di Anthropic, che impone tre gate sequenziali prima di toccare codice. Il senso è proprio impedire la convergenza estetica. ```mermaid flowchart LR T[teach] --> S[shape] S --> C[craft] T -.PRODUCT.md + DESIGN.md.-> R[(repo)] S -.brief confermato.-> R C -.implementazione.-> R ``` Il gate più importante è il primo. **Teach** chiede di scrivere `PRODUCT.md` (register brand-vs-product, lettore primario, anti-references esplicite, principles strategici) e `DESIGN.md` (palette OKLCH, scala tipografica, motion). Senza l'output approvato dall'utente, gli altri comandi non procedono. L'effetto è stato chirurgico. La domanda "chi è il lettore primario?" mi ha forzato a scegliere: imprenditore o direttore di PMI manifatturiera italiana, 40-65 anni, non sviluppatore. Cambia tutto rispetto a "developer peer". Niente più hero techbro, niente CTA aggressivi: la home doveva sembrare un frontespizio di volume, non una landing. Da qui la direzione: **Manuale d'officina, edizione personale**. Riferimenti positivi Olivetti, Vignelli, Pirelli storico, Stripe Press. Palette `Committed`: panna calda (`oklch(96.5% 0.012 78)`), oltremare profondo (`oklch(35% 0.10 252)`), ottone (`oklch(62% 0.13 65)`) come accent rare. Fraunces variable con axes `wonk` e `SOFT` per il display, Inter per body, JetBrains Mono per metadata. Numerazione progressiva visibile su tutto: lavori `#01-#13`, concetti romani `I-VI`. Il craft è diventato meccanico. Otto wave incrementali, una alla volta: 1. **Wave 0**: token CSS, fonts, header, footer. 2. **Wave 1**: frontespizio (home come copertina di volume). 3. **Wave 2**: indice lavori numerato e scheda tecnica. 4. **Wave 3a-d**: concetti come voci di enciclopedia, diario come registro datato, pagine personali, pulizia componenti residui (CommandPalette, MermaidBlock, ReadingProgress). Otto commit puliti su un branch dedicato `redesign-officina`, con `main` congelato per rollback istantaneo. Build statico verificato a ogni wave, deploy via FTP a fine ciclo. La cosa che mi sono portato a casa è che la skill non è un generatore di estetica: è un protocollo che impedisce le scelte di reflex. Senza teach, sarei rimbalzato su un palette swap superficiale (dark mode con accent diverso) e il risultato sarebbe stato un altro portfolio dev distinguibile dagli altri solo per la mano di vernice. Con teach, è la voce del lettore a guidare persino la composizione del frontespizio. Il sito è visibile in produzione: il [colophon](/colophon/) racconta lo stack, le [voci concetti](/concetti/) usano lo stesso pattern di sezione, l'[indice diario](/diario/) è adesso un registro datato con la data come anchor laterale. Un [post recente](/diario/2026-04-28-aprire-il-sito-agli-agenti-ai/) si occupa invece dell'altra metà del progetto, l'AI-discoverability via `llms.txt`. --- ## Da Q&A bot a piattaforma multi-catalogo: il design dei tre strati URL: https://andreapellizzari.it/diario/2026-05-01-bot-10-percento-del-problema/ Data: 2026-05-01 Tag: architettura, ai, claude, agente, multi-tenant > Sessione di design partita per disegnare il flusso di un chatbot tecnico interno. Finita con il design di una piattaforma multi-catalogo a tre strati. Il bot copriva il 10% del problema reale. Avevo in produzione un Q&A bot interno per il catalogo di un grosso brand di ferramenta — uno strumento che tecnici e venditori interrogavano per cercare codici, leggere distinte, capire compatibilità. Funzionava bene: ~4000 chunk indicizzati, prompt caching, router Haiku/Sonnet, costo medio per conversazione sotto i dieci centesimi. La sessione doveva essere una mezza giornata — solo "fare un diagramma di flusso del funzionamento attuale". È durata otto ore, è finita con tre design doc (~6500 righe), due poster A3 stampabili e una rivelazione poco confortante: **il bot era il 10% del problema reale**. ## I tre strati Disegnando i flussi è emerso che il bisogno non è "consultare un catalogo" — è **assistere un agente commerciale a costruire una proposta** (per esempio una cucina completa) che attraversa una trentina di cataloghi eterogenei, verificando vincoli e compatibilità tra prodotti di brand diversi. ```mermaid flowchart TB USER[Agente commerciale] DISP{Dispatcher
routing per catalogo} USER --> DISP DISP --> C subgraph A["Strato A — Knowledge"] KB[Q&A per singolo catalogo
~30 cataloghi] end subgraph B["Strato B — Constraint"] F[filter_products] X[check_compatibility] end subgraph C["Strato C — Commercial"] O[Orchestratore
vincoli + config in build] end C --> A C --> B ``` Lo strato A è quello che avevo già. Lo strato B esisteva a frammenti, sparso in regole markdown. Lo strato C — l'orchestratore commerciale che accumula vincoli del cliente e propone due-tre configurazioni — non esisteva proprio. ## La pipeline di auto-curation Onboardare trenta cataloghi a mano è infattibile. La proposta è una pipeline a sette step con un AI **curator-as-reviewer**: un modello propone, un altro modello rivede, e solo quello che passa entrambi entra nel ramo `wiki/`; il resto resta in `draft/` finché un umano non firma. Tre modi operativi: **Bootstrap** (catalogo nuovo da zero), **Refresh** (nuova edizione manuale dello stesso brand), **Reactive cascade** (un editor cambia una regola — il sistema propaga in cascata sulle distinte e schede impattate, in modalità draft). ## La parte più scomoda A metà sessione mi sono fermato a fare una review architetturale comparativa: per ogni componente che stavo per scrivere custom, esiste già qualcosa di SOTA? Risultato: - **[Promptfoo](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=framework-promptfoo.md)** per gli eval (niente runner custom) - **[DoIt](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=framework-doit.md)** per il cascade engine - **[LangGraph](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=framework-langgraph.md)** per l'orchestratore di Strato C Cinque altri framework parcheggiati con trigger di re-evaluation. Il principio è banale ma facile da dimenticare quando il prototipo funziona: **scrivere meno codice possibile, soprattutto quando esistono primitive collaudate**. ## I documenti Per fissare il pattern in un colpo d'occhio ho prodotto due poster A3 stampabili (HTML standalone, CSS print-ready, niente dipendenze): - [Poster — Catalog · Agent design pattern](/diario-assets/2026-05-01-bot-10-percento-del-problema/poster.html) — la versione *generica*, riutilizzabile - [Poster — la piattaforma concreta](/diario-assets/2026-05-01-bot-10-percento-del-problema/poster-arcocat.html) — la versione *istanziata* sull'azienda per cui lavoro Sotto ai poster ci sono i tre design doc che hanno guidato la sessione, più l'handoff aggiornato a fine giornata: - [Design Doc Piattaforma](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=design-doc-platform.md) (~1600 righe) — il "perché" dei tre strati, dei vincoli operativi e dell'integrazione gestionale progressiva - [Playbook P-1 — Refactor side-by-side](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=playbook-p1-refactor.md) (~620 righe) — il "come" del cutover senza downtime - [Playbook — Onboarding catalogo](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=playbook-onboard-catalogo.md) (~420 righe) — i cinque stage per portare un brand nuovo dentro la piattaforma - [Handoff di fine sessione](/diario-assets/2026-05-01-bot-10-percento-del-problema/viewer.html?d=handoff.md) (~600 righe) — stato puntuale, decisioni aperte, prossimi step Sono pubblicati così come sono usciti, ad eccezione di un'email di un rivenditore che ho rimosso. Tutto il resto è il working draft reale: contiene tabelle di decisioni con `default suggerito` da confermare, sezioni `da chiudere`, gate per stage. È un esempio onesto di quanto un design doc utile assomigli più a una checklist di domande aperte che a una specifica chiusa. ## La cosa che mi porto a casa Il diagramma di flusso fatto bene non documenta — interroga. Costringe a chiedere *chi* fa *cosa*, *quando*, e ti restituisce in cambio l'onestà brutale di vedere quanto del problema il prototipo non stava risolvendo. In questo caso, il 90%. Niente di tutto questo è in produzione: il bot esistente continua a girare invariato. Il design vive su un branch separato in attesa di sign-off. Ma adesso so cosa sto costruendo, e non è quello che pensavo di star costruendo stamattina. --- ## Da una distinta sbagliata a un eval set: misurare invece di sperare URL: https://andreapellizzari.it/diario/2026-04-30-eval-set-misurare-invece-di-sperare/ Data: 2026-04-30 Tag: ai, claude, tool-calling, eval, context-engineering > Un chatbot tecnico interno restituisce cinque codici inventati su sei. Il fix funziona, ma il vero salto è quando smetti di sperare che i fix reggano e inizi a misurare con un eval set. Sto lavorando a un chatbot tecnico interno per la PMI dove sono dipendente: assiste i nostri tecnici nella scelta di componenti di catalogo del fornitore principale. Funziona — finché non funziona. Settimana scorsa una distinta articoli è uscita con cinque codici inventati su sei. Solo il primo era reale; gli altri avevano i pattern giusti della nomenclatura del fornitore ma erano puro frutto di hallucination del modello. ## La diagnosi non era ovvia Ispezionando la sessione: zero blocchi `tool_use` nella risposta finale. Il bot, pur avendo a disposizione un tool dedicato che restituisce i codici verificati dal database interno, ha generato la distinta a memoria. Il system prompt aveva una direttiva testuale ("DEVI chiamare il tool"), ma il modello fast (Haiku 4.5) l'ha ignorata. In più, due bug concorrenti nel "gate" che decide quando iniettare il template canonico nel contesto. Quando l'utente forniva tutti i dati chiave, il sistema saltava l'iniezione del template. Il modello si trovava senza i pattern dei codici e senza un vincolo hard sul tool. Il classico "sbaglia perché l'abbiamo lasciato senza armi". ## Le pezze Quattro fix in sequenza, tutti deployati: ```mermaid sequenceDiagram participant U as Utente participant G as Gate participant L as LLM participant T as Tool DB U->>G: query + dati chiave Note over G: classifier intent G->>L: prompt + template canonico G->>L: tool_choice forzato + smart model L->>T: chiamata tool obbligatoria T->>L: dati verificati L->>U: risposta basata su dati reali ``` Iniezione del template anche con dati chiave completi. `tool_choice` forzato API-side (da invito testuale a costrizione). Forzatura del modello smart quando un template è in contesto. Alias del normalizer per coprire varianti che sfuggivano al regex. Test manuale: i codici sono giusti. Tutto bene. ## Il vero salto A questo punto la persona con cui lavoravo ha detto la frase giusta: *"ma stiamo procedendo per tentativi, mettendo pezze caso per caso"*. Aveva ragione. Senza una rete di sicurezza, ogni modifica futura è un atto di fede. Cambio di marcia: scriviamo un eval set. Ventitre casi di test ripetibili, distribuiti su sette categorie (lookup, distinte per varie famiglie, regole tecniche, gate logic, edge case). Runner Python, ~5 minuti per il run completo, ~$0.30 di API. Il primo run ha sorpreso: espandendo da 5 a 15 casi sono emersi **quattro bug reali mai visti prima**: - una regex hardcoded su due valori specifici che bloccava silenziosamente tre famiglie prodotto su quattro - un fallback del normalizer che mappava una variante alla famiglia sbagliata - un termine letterale nel router invece della radice (matchava "calcolo" ma non "calcola") - un'assertion mia troppo stretta Fix: tre one-liner. Workflow: *test fail → fix → test pass → deploy*. ## Cosa ho imparato Tre cose che voglio ricordare: 1. **Il vero context engineering non è scrivere prompt lunghi**, è decidere *quali risorse iniettare quando*: push vs pull, gate logic, model routing. Il modello sbagliato per il task sbagliato è una causa concomitante con un contesto incompleto. 2. **La pezza non è una parolaccia se è blindata da un test**. Diventa tossica quando aggiunge dati senza una single source of truth — esempio: ho ancora le altezze degli stessi componenti scritte a mano in tre file diversi, e prima o poi divergeranno. Quello sì che è debt. 3. **Senza eval set, ogni modifica è speranza**. Un'ora investita per costruire 23 casi ha trovato bug che mesi di sessioni manuali non avevano rilevato. Il prossimo passo è il refactor "single source of truth". Ma adesso ho la rete di sicurezza per farlo senza paura. --- ## Il giorno in cui il chatbot ha imparato a correggersi URL: https://andreapellizzari.it/diario/2026-04-29-chatbot-feedback-loop-autofix/ Data: 2026-04-29 Tag: ai, claude, agente, knowledge-base, feedback-loop > Un chatbot tecnico interno è un asset statico finché non costruisci il loop che lo fa evolvere. In una sessione ho aggiunto il sistema feedback con AI auto-fix: l'utente segnala un errore, l'editor scrive la correzione, l'AI applica il diff sul file MD del wiki. Sto sviluppando un chatbot tecnico interno per la PMI dove lavoro: 758 pagine di catalogo prodotti di un fornitore, 1900 codici articolo, 14 regole di calcolo, distinte e guide decisionali. La parte difficile non è far rispondere il bot — quella la fa Claude. La parte difficile è che il bot a volte **sbaglia in modo plausibile**: inventa un codice colore che non esiste, tratta un componente "singolo pezzo" come "set", cita una sigla interna nella risposta all'utente. Oggi il primo utente reale ha trovato due bug in cinque conversazioni. Errori onesti, dati sottili: `SW-N` invece di `CS-M` per il nero, quantità raddoppiate per un codice che il bot pensava fosse una coppia. Si correggono in trenta secondi a mano nel wiki. Ma chi li corregge davvero, e quante volte tocca farlo prima che diventi un peso? La risposta che ho costruito oggi è un **loop semi-automatico**: l'utente clicca "segnala errore", scrive cosa è andato storto, e parte una pipeline: ```mermaid flowchart LR U[Utente segnala] --> A[Claude Haiku
categorizza] A --> E[Editor riceve
note pre-compilate] E -->|conferma e scrive
la correzione canonica| F[Claude Sonnet
genera diff su file MD] F --> V[Validator AI
controlla codici] V --> R[Editor approva
con un click] R --> S[File aggiornato
backup automatico
feedback marcato fixed] ``` L'editor non apre più il file a mano. Scrive in italiano semplice cosa va corretto ("il codice X è singolo pezzo, non set; per N elementi servono 2N pezzi"), e Claude Sonnet legge la nota più il file MD attuale e produce il **diff minimale** che applica la correzione preservando struttura e tono. Un altro modello (Haiku, più economico) valida l'output cercando codici inventati, sintassi rotta, contraddizioni interne. L'editor vede side-by-side il "prima" e il "dopo" e conferma. Per testare end-to-end ho creato un feedback dummy con un codice attacco frontale inesistente. L'AI ha generato un diff impeccabile: tre righe in più nel file MD, una nota di avvertimento esplicita ("non esistono altri suffissi oltre a questi tre"). Ho confermato. File salvato, backup creato, feedback marcato `fixed`. Trenta secondi. Il salto qualitativo non è il singolo bottone "segnala". È che il sistema ha cambiato natura: da **asset statico** (wiki che qualcuno ogni tanto manutiene) a **organismo che evolve** (ogni interazione difettosa è una potenziale lezione). I 14 errori che troverò nei prossimi mesi non saranno più 14 task di manutenzione miei — saranno 14 segnalazioni che l'editor di dominio gestisce in pochi minuti, con l'AI che fa il lavoro pesante di trovare il punto giusto del file e formulare la modifica nel formato corretto. Il pattern è generale: ogni knowledge base AI-assisted dovrebbe avere un loop di feedback che converte "il modello ha sbagliato qui" in una modifica permanente del corpus, con il minimo attrito possibile. --- ## Far leggere un decreto legislativo all'AI: pipeline da .docx a Word URL: https://andreapellizzari.it/diario/2026-04-28-analisi-normativa-strutturata-con-ai/ Data: 2026-04-28 Tag: ai, automazione, powershell > Mi è arrivato un decreto legislativo da 270 KB in formato .docx con 26 articoli e centinaia di reati richiamati, da mappare sui processi aziendali. Ho costruito una pipeline che lo macina dall'estrazione XML al documento Word finale. In una PMI manifatturiera del Nordest il management mi gira un .docx da 270 KB: l'elenco aggiornato dei reati previsti dal D.Lgs. 231/01, ventisei articoli del decreto e dentro ciascuno da uno a quindici reati presupposto richiamati. La domanda è semplice e fastidiosa: per ogni reato, in quali processi aziendali si potrebbe configurare, e abbiamo già procedure interne che lo coprono? Una mappatura manuale onesta sono due giorni di lavoro. Il punto è che il primo strato — struttura, ipotesi di rischio, riconciliazione con le procedure esistenti — è esattamente il tipo di lavoro in cui l'AI è utile, purché le si dia il contesto giusto. **Step uno, estrazione del testo**. Un .docx è uno zip con XML dentro, non serve Word per leggerlo: ```powershell [System.IO.Compression.ZipFile]::ExtractToDirectory($docxPath, $tempDir) $xml = [xml](Get-Content "$tempDir\word\document.xml" -Raw) $ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable) $ns.AddNamespace("w", "http://schemas.openxmlformats.org/wordprocessingml/2006/main") $xml.SelectNodes("//w:p", $ns) | ForEach-Object { ($_.SelectNodes(".//w:t", $ns) | % { $_.InnerText }) -join "" } | Where-Object { $_.Trim() } | Out-File $txtPath -Encoding utf8 ``` In tre comandi ho 1.900 righe di testo lavorabile. **Step due, indice navigabile**. Un grep mirato sui pattern degli articoli mi dà la mappa con i numeri di riga: ```bash grep -nE "^\d+\.?\s*Art\.\s*\d+" elenco_reati.txt ``` Questo sommario è l'ancoraggio che do all'AI per evitare che si perda nel documento. **Step tre, contesto reale dall'azienda**. È il dettaglio che fa la differenza. Il sistema documentale interno ha due tabelle (`documenti_procedure` e `documenti_istruzioni_operative`) con i flag dei reparti coinvolti — acquisti, produzione, qualità, verniciatura, magazzino. Una query mi dice quali procedure esistono *davvero*. Senza questo passo l'AI inventa procedure plausibili ma fantasma: un Modello 231 popolato da `PR-IT-XX` mai scritte serve a poco. **Step quattro, generazione strutturata**. Contesto aziendale (settore, posizionamento B2B, organigramma reale) + testo normativo + indice articoli → output Markdown gerarchico: per ogni articolo del decreto, livello di rischio motivato, fattispecie concreta calata sul business, procedure esistenti vs da redigere. Per le aree sensibili (sicurezza sul lavoro, ambiente, reati tributari) allegati di dettaglio reato-per-reato. Per le aree non significative (terrorismo, beni culturali, mutilazioni, frode sportiva) giudizio sintetico con motivazione di esclusione. **Step cinque, output consegnabile**. Il consulente esterno vuole un Word, non un Markdown. PowerShell 7 ha `ConvertFrom-Markdown` nativo, Word ha un'interfaccia COM: ```powershell $html = (ConvertFrom-Markdown -Path $mdPath).Html # wrap in con CSS inline per tabelle e heading $word = New-Object -ComObject Word.Application $doc = $word.Documents.Open($htmlPath) $doc.SaveAs([ref]$docxPath, [ref]16) # wdFormatDocumentDefault = .docx ``` Output: 79 KB di Word pulito, tabelle formattate, gerarchia titoli, zero artefatti HTML. ```mermaid flowchart LR A[.docx normativo] --> B[Estrazione XML] B --> C[Testo + indice articoli] D[(DB procedure interne)] --> E[Contesto aziendale reale] C --> F[AI: mappatura strutturata] E --> F F --> G[Markdown gerarchico] G --> H[Word COM] H --> I[.docx consegnabile] ``` Due ore al posto di due giorni. L'AI non sostituisce il consulente 231 — il giudizio sui rischi specifici, l'applicazione della giurisprudenza recente, la difesa nelle ispezioni restano lavoro umano. Ma le consegno un punto di partenza già allineato all'azienda reale, non un boilerplate generico. Il pattern è replicabile per qualunque normativa lunga: decreti legislativi, capitolati, norme tecniche UNI/ISO. Quando il documento è strutturato, l'AI è bravissima a strutturarlo ancora meglio. --- ## Aprire il sito agli agenti AI: llms.txt, JSON-LD esteso e versioni raw dei post URL: https://andreapellizzari.it/diario/2026-04-28-aprire-il-sito-agli-agenti-ai/ Data: 2026-04-28 Tag: ai, seo, discoverability > Metà del traffico di ricerca tecnica oggi passa per LLM, non per Google. Sei modifiche concrete — llms.txt, JSON-LD esteso, versioni raw .md dei post — per farsi trovare da ChatGPT, Claude e Perplexity. Per anni la regola era semplice: scrivi contenuti buoni, mettici i meta tag giusti, e Google ti trova. Funzionava perché chi cercava qualcosa apriva una pagina di risultati e cliccava un link. Oggi una fetta crescente di quel traffico non passa più dalla pagina dei risultati: passa direttamente da una conversazione con ChatGPT, Claude o Perplexity. Quando qualcuno chiede "come integrare Mexal WebAPI con Next.js", il modello compone una risposta — e cita le fonti che ha trovato citabili. Se il tuo sito non è citabile, sei invisibile. Il mio sito esisteva già con tutte le cose che la SEO classica chiede: sitemap, OpenGraph, JSON-LD `Person` minimo, canonical URL, lingua dichiarata. Mi mancava un livello sopra: rendere espliciti gli stessi contenuti in formati che gli LLM ingeriscono volentieri. Ho fatto sei modifiche concrete. Tre sui metadati, tre sulla forma dei contenuti. Sui metadati: ho aggiunto `sameAs` allo schema `Person` con i miei profili LinkedIn e GitHub, perché senza quei link i modelli non riescono a triangolare che l'Andrea del sito è la stessa persona del profilo professionale altrove. Ho aggiunto JSON-LD `CreativeWork` alle case study (prima ce l'aveva solo il diario come `BlogPosting`). Ho messo `BreadcrumbList` su tutte le pagine annidate, così la gerarchia è esplicita anche per chi non legge il menu visuale. Sulla forma dei contenuti: ho creato `llms.txt` e `llms-full.txt`, lo standard emergente che è una specie di carta d'identità per modelli linguistici — un markdown pulito che dice chi sono, quali sono i contenuti chiave, dove sono. Ho elencato esplicitamente nel `robots.txt` GPTBot, ClaudeBot, PerplexityBot, Google-Extended e una decina di altri bot AI (tecnicamente il wildcard basterebbe, ma esplicitarli è un segnale di benvenuto). E soprattutto ho esposto per ogni post del diario una versione raw `/diario//index.md`: gli scraper AI estraggono molto meglio da markdown grezzo che da HTML stilizzato pieno di div e classi Tailwind. Tutto è automatico via postbuild script: ogni `npm run build-and-deploy` rigenera le tre rappresentazioni (HTML per umani, markdown raw per modelli, aggregato globale in `llms-full.txt`) a partire dagli stessi file MDX sorgente. ```mermaid flowchart LR A[content/diario/*.mdx] --> B[next build] B --> C[HTML statico] B --> D[postbuild] D --> E[llms.txt
llms-full.txt] D --> F[diario/slug/index.md
raw markdown] ``` Quello che mi piace di questo cambio è che il sito non è cambiato: contiene esattamente le stesse parole di ieri. Ha solo aperto i microfoni. La discoverabilità per gli agenti AI non è un livello in più di marketing — è un livello in più di onestà: dire chiaramente, in formati che le macchine leggono bene, quello che già stai dicendo agli umani. Tra qualche settimana verifico se Perplexity sa rispondere a "chi è Andrea Pellizzari?" senza inventarsi le competenze. --- ## Quando il preventivo arriva con il prototipo allegato URL: https://andreapellizzari.it/diario/2026-04-28-prototipo-allegato-al-preventivo/ Data: 2026-04-28 Tag: ai, claude-code, preventivi, produttività, mexal > Un contatto, software house partner Mexal, mi chiede aiuto per un preventivo che deve fare a un suo cliente: dashboard agenti su un ERP custom. In quindici minuti gli ho preparato proposta tecnica, demo HTML e PDF. Non era la dashboard — era il fatto che si presentasse da sola. Un mio contatto gestisce una piccola software house italiana: vende soluzioni Mexal alle PMI manifatturiere, ha qualche cliente storico, conosce il mestiere. Ieri mi scrive: un suo cliente — non su Mexal, su un ERP italiano diverso che gira su MSSQL — gli ha chiesto un preventivo per un sistema di reporting BI per la direzione vendite e la rete agenti. Mail settimanali con i KPI, area web riservata per agente, cruscotto per il direttore. Niente di esotico, ma serio. Il punto del messaggio era questo: *"non sono ancora abituato a usare Claude Code, quanto tempo ci metto a impostare un preventivo decente?"*. Per uno sviluppatore che lavora ancora con il flusso tradizionale, sono due giorni: leggere i materiali, capire i dati che il cliente esporta dal gestionale, abbozzare un'architettura, scrivere la proposta in Word, eventualmente fare un mockup statico. Gli ho detto: dammi mezz'ora. Ho aperto Claude Code, gli ho passato la documentazione che il cliente aveva inviato (un docx con i requisiti, due Excel con dati reali esportati dall'ERP, un facsimile di un report di un loro fornitore preesistente). Brief: *"costruisci una proposta tecnica completa con prototipo HTML basato sui dati reali, niente boilerplate generico, basa l'architettura su pattern che ho già in produzione altrove"*. Quindici minuti dopo avevo: - Una proposta tecnica strutturata in markdown e PDF (30 pagine, con architettura, stack motivato, milestone, importi) - Quattro deliverable HTML interattivi costruiti sui dati reali del cliente — mail per agente, area agente passwordless, cruscotto direzione, dettaglio cliente - Una build script Python per generare il PDF dalla sorgente markdown Tutto coerente con pattern che avevo già messo in produzione su altri progetti — niente di sperimentale, niente di inventato. Il mio contatto ha inoltrato la cosa al cliente lo stesso pomeriggio. Mi ha scritto stamattina: il cliente ha detto sì. Quello che mi colpisce non è la dashboard — di dashboard se ne fanno ogni giorno. Quello che mi colpisce è che il vantaggio competitivo non sta più nella *capacità di fare* la dashboard. Sta nella **velocità con cui un preventivo si presenta da solo**. Se il preventivo arriva con un prototipo cliccabile costruito sui dati reali del cliente, il cliente smette di valutare la proposta e inizia a immaginare il sistema in produzione. È un altro mestiere. Per i partner Mexal tradizionali questa è una linea di confine che non hanno ancora attraversato. Per chi lavora in-house e ha integrato l'AI nel flusso quotidiano, è già normale. Lo strano interessante è che il rapporto non è competitivo: il mio contatto sa fare il suo cliente — relazione, requisiti, gestionale specifico — meglio di me. Quello che ha guadagnato è una marcia in più sulla parte che gli costava di più. Non è che gli ho rubato il cliente: gli ho fatto risparmiare un week-end di scrittura di proposta, e gli ho regalato la possibilità di rispondere prima dei concorrenti. L'AI non sostituisce il rapporto col cliente. Ma cambia il ritmo a cui quel rapporto si muove. --- ## Quando il bug si vede al primo click: audit UX automatizzato con Playwright URL: https://andreapellizzari.it/diario/2026-04-27-audit-ux-playwright-headless/ Data: 2026-04-27 Tag: playwright, ai, ux > Ho usato Playwright in modalità headless per simulare un agente che apre la nostra PWA su tablet e clicca tutto: in trenta minuti sono usciti tredici bug oggettivi che gli utenti veri avrebbero visto al primo uso. Volevo un primo filtro di test sulla PWA che stiamo per dare in mano agli agenti commerciali. Non avevo voglia di chiamare due colleghi e farli testare a freddo: volevo capire prima quanti bug **oggettivi** ci fossero — quelli che chiunque vede al primo click. Il setup è stato veloce. La PWA usa un cookie JWT per l'auth (non-HttpOnly per design, perché va settato lato client al login). Ho scaricato il segreto dall'ambiente di produzione con `vercel env pull`, scritto un piccolo script Node che firma un token "agente puro" — senza bypass admin, perché volevo simulare un agente reale, non un superuser. Cookie iniettato via `document.cookie` dentro Playwright, viewport a 1024×1366 (tablet portrait), login Microsoft saltato a piè pari. ```mermaid sequenceDiagram participant Me as Sessione participant Token as Script gen-token participant Browser as Playwright participant App as PWA in prod participant DB as Postgres Me->>Token: vercel env pull (JWT_SECRET) Token->>DB: SELECT user + agent Token-->>Me: JWT 24h "agente puro" Me->>Browser: setViewport 1024x1366 Me->>Browser: document.cookie = agent_token Browser->>App: navigate /agent-v3 App-->>Browser: tablet UI renderizzata Browser->>App: 10 step di scenario App->>DB: SELECT / UPDATE ``` Lo scenario era una giornata tipica: home → cliente a rischio → catalogo → carrello → checkout (solo preventivi, mai ordini reali) → proposta confronto → giro pianificato → wizard verbale visita → statistiche → export CSV → budget → promozioni → logout. **Tredici bug oggettivi in una sessione**. Tre meritano di essere raccontati. **"Ultimo ordine -119 giorni fa"**. La lista clienti mostrava giorni *negativi* per molti record. La query calcolava `CURRENT_DATE - MAX(MAKE_DATE(anno, mese, 1))` su tutti i movimenti, inclusi gli ordini con consegna *futura* — perché in azienda si inseriscono ordini con consegna anche mesi avanti. Risultato: data futura → sottrazione negativa → "ultimo ordine fra 119 giorni". Fix di una riga: `AND MAKE_DATE(...) <= CURRENT_DATE`. **Add-to-cart silenzioso a 0 €**. Articoli senza listino popolato finivano nel carrello a `0,00 €`, l'API rispondeva 200, niente warning. L'agente avrebbe potuto inviare un preventivo con righe a zero senza accorgersene. Fix: badge "Prezzo da verificare" sulle righe interessate nello step di riepilogo del checkout. **185 clienti su 185 con badge "Rischio"**. Il classico `null` vs zero. Il campo `health_score` `NULL` veniva castato a `0`, soglia `< 35` → tutti high-risk. Il fix è un confronto in più: distinguere `null` da `0`, mostrare il badge solo quando il punteggio è effettivamente calcolato e basso. Una lezione metodologica che mi tengo: prima di fixare codice emerso da un audit UX, far girare il sync dei dati. Su cinque bug iniziali, uno si è auto-risolto col solo sync notturno — i dati erano vecchi, non il codice. Avrei sprecato tempo a investigare logica per qualcosa che non era un problema. Questo metodo non sostituisce un agente vero che usa la PWA otto ore al giorno. Coglie i bug *oggettivi* — segno meno hardcoded, calcoli rotti, label fuorvianti, dead-end di navigazione. Non coglie quelli *soggettivi*: "questa lista è ordinata male per il mio workflow", "questa parola in questo contesto suona ambigua", "qui mi aspettavo un'azione e invece ne è partita un'altra". Però come **filtro pre-rilascio** è sproporzionatamente efficace rispetto al costo. Trenta minuti di Playwright per il primo passaggio, più qualche ora per i fix, fanno una giornata in meno di feedback raccolto via messaggi dopo il rilascio. --- ## Benvenuto nel diario URL: https://andreapellizzari.it/diario/2026-04-24-benvenuto-nel-diario/ Data: 2026-04-24 Tag: meta > Apro questa sezione del sito come quaderno pubblico: appunti brevi dalle sessioni di lavoro quotidiane su gestionali, integrazioni, AI applicata. Meno strutturato delle case study, più vicino a come succedono davvero le cose. Le case study che trovi su [/lavori](/lavori/) sono pezzi strutturati, scritti dopo — quando un progetto è maturo abbastanza per essere raccontato per intero. Ma nel mezzo, giorno per giorno, capitano decine di cose tecnicamente interessanti che non meritano una case study intera ma **meritano di essere annotate**: un pattern scoperto, un bug risolto in un modo elegante, una scelta di stack presa in cinque minuti ma che vale una discussione, un'integrazione arrivata in fondo dopo due giorni di "perché non funziona". Questo diario è quello: un **quaderno pubblico** dove deposito appunti brevi, dalle sessioni di lavoro. Non ha frequenza fissa — pubblico quando c'è qualcosa di rilevante, non per fare rumore. I post sono corti, concreti, e — quando ha senso — accompagnati da uno snippet di codice o un dettaglio di architettura. Temi che probabilmente emergeranno: - Integrazioni con Mexal/Passepartout (WebAPI, SPRIX Collage, CALLWEBSVC) - AI-assisted development — prompt caching, tool calling, agenti - Stack moderni vs legacy — quando e perché scegliere l'uno o l'altro - Power Platform — dove regge, dove conviene salire di stack - Dettagli di front-end, deploy, operations quando sono fuori dall'ovvio Se quello che leggi ti fa venire una domanda o un commento, [scrivimi](/contatti/) — rispondo volentieri. --- ## Diagnostica 'perché questo codice non è pubblicato': una query grossa batte dieci query puntuali URL: https://andreapellizzari.it/diario/2026-04-24-diagnostica-codice-non-pubblicato/ Data: 2026-04-24 Tag: python, sql-server, diagnostica > Un articolo sparito dal sito è la domanda settimanale in ufficio. Ho scritto un tool Python+tkinter che risponde in due secondi con una query SQL e due chiamate FTP — e mi ha costretto a leggere come si deve le viste dell'ERP aziendale. Un dialogo settimanale in ufficio: "questo articolo non è sul sito, perché?". Prima di questo mese rispondevo aprendo cinque pagine del gestionale, un client SQL, un FTP client e un browser. Venti minuti di archeologia per scoprire che mancava un flag in una scheda di caratteristiche, o che l'immagine sul server aveva un underscore di troppo. Ho scritto un tool che lo fa in due secondi. ## Nove check in una singola query La tentazione iniziale era ovvia: per ogni controllo una query. Un'interrogazione sull'anagrafica, una sul listino, una sulla scheda posizionamento, una sulle caratteristiche, e così via. Undici controlli → undici round-trip al database. Per un codice. La cosa utile è che tutte le informazioni che mi servivano stavano in quattro tabelle collegate da una chiave articolo. Un `LEFT JOIN` fatto bene le tira fuori in una sola chiamata: ```python sql = """ SELECT a.codice, a.flag_cancellato, a.flag_annullato, l.prg_listini, p.flag_annullato AS posiz_annullato, c.flag_a_catalogo, c.brand_a, c.brand_b FROM articoli a LEFT JOIN listini l ON a.codice = l.codice LEFT JOIN posizionamento p ON a.codice = p.codice LEFT JOIN caratteristiche c ON a.codice = c.codice WHERE a.codice LIKE ? """ ``` Con il `LIKE` accetta anche un prefisso: cerco `ABC00245` e mi tira fuori tutti i figli in un colpo. Dieci codici da diagnosticare diventano una query, non sessanta. ## LEFT JOIN non esclude, RIGHT JOIN sì Il giorno prima avevo dedicato un'ora a una ricognizione delle viste SQL del catalogo. Sono viste scritte negli anni da persone diverse, una sopra l'altra — `v_dati_catalogo` che chiama `v_dati_normalizzati` che chiama `v_export_finale`. Un albero che una GUI diagnostica può anche ignorare, ma per capirne i filtri occorre leggerle. La lezione più utile di quella ricognizione è banale ma sistematica: **un `LEFT JOIN` non esclude righe, un `RIGHT JOIN` o un `INNER JOIN` sì**. In mezzo alle viste c'era un `RIGHT JOIN` sulla scheda caratteristiche che era la vera spina dorsale del catalogo: se un articolo non aveva un record in quella tabella, spariva silenziosamente tre livelli di vista più sopra. Il diagnostico deve elencare quella join come check dedicato, non come dettaglio tecnico. ## FTP per bypassare la cache HTTP Per capire se un articolo è stato realmente pubblicato, il tool scarica il manifesto JSON del sito. Il file è servito via HTTPS ma l'hosting ha una cache aggressiva: a volte risponde con una copia di dieci minuti prima, falsando il verdetto. Lo scarico via FTP: niente cache, niente CAPTCHA, e come bonus ottengo anche la lista delle immagini nella directory del catalogo per l'ultimo check. Il risultato finale è una GUI tkinter con un campo di input e quattro possibili verdetti: `PUBBLICATO`, `NON_PUBBLICABILE`, `IN_ATTESA_EXPORT`, `FILIERA_OK_SENZA_IMMAGINE`. Distribuita come EXE da 17 MB, gira sui PC dell'ufficio con un doppio click. Il tool non è elegante. Ma ha trasformato una domanda da venti minuti di lavoro in un doppio click. --- ## L'editor come fonte di verità: normalizzare a valle, non a monte URL: https://andreapellizzari.it/diario/2026-04-24-editor-normalizzazione-post-ingest/ Data: 2026-04-24 Tag: architettura, knowledge-base, ingest > Ingest massivo da PDF, IDML, schede tecniche e gestionale produce dati affidabili al 60%. Invece di pulirli alla fonte, ho scelto di normalizzarli a valle con un editor editoriale che diventa la fonte di verità canonica. Il catalogo di prodotti dell'azienda per cui lavoro nasce da fonti eterogenee: PDF di manuali cartacei, file InDesign (IDML) con testi e disegni, schede tecniche per singolo prodotto, anagrafica del gestionale, export dell'ecommerce. Ogni fonte ha il suo formato, il suo lessico, i suoi errori. L'approccio ingenuo sarebbe: pulisco alla fonte. In pratica non funziona. Alcune fonti non sono editabili (il PDF del manuale è un file pubblicato, non una base dati). Altre sono di sistemi terzi che non gestisco. La manutenzione alla fonte costa più di quanto valga, e comunque non coprirebbe mai il 100%. Ho scelto l'approccio opposto: l'ingest è rumoroso, la normalizzazione avviene a valle. Ho costruito un editor editoriale in PHP + SQLite che prende i dati dai vari ingest e diventa la fonte di verità canonica per la generazione degli output — schede tecniche PDF, chatbot AI di supporto tecnico, export verso altri sistemi. Il modello dati ha un punto non ovvio: la **struttura per-sezione**. Una pagina di manuale cartaceo contiene spesso più famiglie distinte (es. sulla stessa pagina: gambo a vite, gambo sagomato, cardine su lama, estrattore per manutenzione). Ciascuna ha misure, disegni tecnici, descrizioni applicative diverse. Nel modello iniziale avevo una sola tabella legata alla pagina — ma collassava tutto su un unico livello, e l'editor finiva per mostrare colonne misura inutili per metà dei prodotti. Il refactor ha aggiunto una tabella `sezione_gruppo` tra pagina e codice prodotto. Ora ogni sezione ha le sue colonne misura, la sua descrizione, il suo flag di validazione editoriale indipendente. Prima di mettere in produzione ho scritto un audit strutturale — una ventina di query che verificano invariants: zero incoerenze di foreign key, zero colonne con scope ambiguo (globali + per-sezione nello stesso gruppo), zero prodotti senza sezione, nessun duplicato. L'audit è passato pulito su tutto il DB (circa 3900 SKU, 164 pagine di manuale, quasi 500 sezioni). Il che significa due cose: (1) il refactor non ha introdotto regressioni, (2) il modello regge per tutti i casi reali, non solo per quelli che avevo in mente quando l'ho disegnato. Il pattern che sto consolidando: gli ingest non devono essere puliti, devono essere **completi**. La pulizia è un lavoro editoriale umano che avviene dopo, su un modello dati disegnato per supportarlo. La fonte di verità non è mai l'ingest — è il risultato del lavoro editoriale sull'ingest. --- ## Mermaid nei miei MDX: diagrammi architetturali che vivono nel sorgente URL: https://andreapellizzari.it/diario/2026-04-24-mermaid-diagrammi-mdx/ Data: 2026-04-24 Tag: mermaid, mdx, tooling > Ho cominciato a usare Mermaid per i diagrammi tecnici del portfolio. Il vantaggio vero non è graphical — è che il diagramma è testo, versionabile, editabile in 10 secondi, e spiega da solo cose che la prosa fatica a trasmettere. Finché i diagrammi architetturali del mio sito sono stati screenshot di Lucidchart o draw.io, li ho aggiornati esattamente **mai**. Ogni modifica significava riaprire uno strumento esterno, ritoccare le box, riesportare PNG, ricaricare nell'asset. Per una persona che cambia idea spesso sull'architettura, questo lavoro si traduce in **diagrammi che si staccano dalla realtà** nel giro di un mese. Ho provato **Mermaid** come alternativa. In pratica è una libreria JavaScript che trasforma testo in SVG: scrivi qualcosa come ```mermaid flowchart LR A[Fatture XML] --> B[Parser] B --> C[(DB)] C --> D[Rendiconto] ``` e il browser lo renderizza. Non è nuova (esiste dal 2014), non è esclusiva, ma nei miei flussi MDX ha un effetto potente per tre motivi. **Primo: il diagramma vive nel sorgente del post.** Quando rileggo una case study sei mesi dopo e capisco che ho bisogno di cambiare un box, apro il file MDX, ritocco due righe di testo, committo, deploy. Il PNG esterno non esiste più — niente sync da mantenere. **Secondo: il diff è leggibile.** Se cambio un diagramma su git diff vedo `- A --> B` / `+ A --> C[nuovo nodo]`. Con un'immagine il diff sarebbe una marea di bytes opachi. Per collaborare su un diagramma (anche solo "con me stesso sei mesi dopo") questo conta. **Terzo: stile automatico e tema-aware.** Ho configurato il componente che intercetta i blocchi mermaid nei MDX per renderizzare con la palette violet/zinc del sito, e per ri-renderizzare quando cambia il tema dark/light. Non devo pensarci ogni volta — la consistenza è automatica. Il trade-off è che **Mermaid non produce diagrammi bellissimi**. Se l'obiettivo è una slide da pitch con effetti grafici curati, ci sono tool migliori. Ma se l'obiettivo è **documentare un'architettura tecnica in modo che resti aggiornata**, vince tutto: il costo di modificare un diagramma è il costo di modificare tre righe di testo, e quando lo è, lo fai davvero. Oggi ci sono cinque case study del portfolio con diagrammi Mermaid — [ai-knowledge-base](/lavori/ai-knowledge-base/) per l'architettura a quattro layer, [accordi-fornitori](/lavori/accordi-fornitori/) per il ciclo business dei rebate, [ticket-assistance](/lavori/ticket-assistance/) per le identità di sicurezza, [gestionale-scriptcase](/lavori/gestionale-scriptcase/) per il triangolo Mexal-Scriptcase-PowerApp, [porting-powerapps-nodejs](/lavori/porting-powerapps-nodejs/) per il sql-proxy al centro con i suoi upstream. Ogni diagramma è sei-dodici righe di testo dentro l'MDX. Il pattern generale che ricavo: **più un asset è testo, più rimane vero nel tempo**. Le architetture documentate in PNG muoiono. Quelle in testo versionato hanno una chance. --- ## Quando il pull non basta: products.json in push + Vercel Blob URL: https://andreapellizzari.it/diario/2026-04-24-products-json-vercel-blob-push/ Data: 2026-04-24 Tag: nextjs, vercel, architettura > L'hosting blocca Vercel con un captcha anti-bot e lo shop si svuota a campione. La fix non è un retry più furbo: è invertire il flusso da pull a push e usare Vercel Blob come unico strato coerente tra container. Ho un [e-commerce B2B](/lavori/ecommerce-b2b/) su Next.js che legge un `products.json` da ventisei megabyte servito da un hosting PHP condiviso. Per mesi l'architettura è stata banale: Python esporta il JSON ogni notte via FTP, Vercel lo fetcha a runtime. Poi ho iniziato a vedere lo shop vuoto a campione — pagine prodotto "non trovato", sitemap vuota, intere sessioni senza catalogo. Tre guasti si erano stratificati in silenzio. Il primo è l'abitudine di Next.js di cachare anche le risposte sbagliate. Il Data Cache con `revalidate: 900` memorizzava per quindici minuti anche le risposte a zero prodotti, così un singolo fetch fallito sporcava la cache per un quarto d'ora buono. Il secondo è il captcha anti-bot dell'hosting. Sopra una certa soglia di richieste rispondeva con HTML di challenge al posto del JSON — Vercel riceveva `text/html`, il parsing saltava, e il container serviva un array vuoto. Il terzo è la natura effimera dei container Vercel. Memoria e `/tmp` sono per-istanza: ogni nuovo container parte azzerato. Se in quel momento il captcha era attivo, quella sessione vedeva zero prodotti finché l'istanza non moriva. La soluzione non è stata un retry più lungo o uno user-agent più furbo. Ho **invertito il flusso**. Invece di Vercel che tenta il pull da un upstream instabile, è il Python dell'export che fa **push** a un endpoint Next.js con il JSON compresso gzip. L'endpoint valida che l'array prodotti non sia vuoto (safety check contro wipe accidentali) e scrive in tre layer. Il layer che rende il sistema coerente **tra sessioni diverse** è Vercel Blob. Memory e `/tmp` sopravvivono solo finché vive il container; Vercel Blob è storage condiviso — tra container, tra deploy, tra regioni. Un utente su un container appena creato vede lo stesso catalogo di uno su un'istanza calda. A runtime la lettura è una cascata a cinque livelli: memory cache con TTL un'ora, poi Vercel Blob con timeout a venti secondi per non hangare fino al limite di trecento, poi endpoint PHP dedicato meno esposto al captcha del file statico, poi `/tmp` senza TTL come safety net (meglio stale che vuoto), infine struttura vuota come disastro totale. Ogni strato tollera il fallimento di quello sotto, e nessuno scrive mai cache vuote. La morale che mi porto a casa è che quando l'upstream è instabile, aggiungere resilienza al client serve poco. Conviene spostare il lavoro di garanzia sulla parte che puoi controllare: il producer. Il pull è elegante, il push è onesto. --- ## Script custom per backup: ho impiegato due ore a capire che era la scelta sbagliata URL: https://andreapellizzari.it/diario/2026-04-24-script-vs-tool-maturo/ Data: 2026-04-24 Tag: backup, tooling, lesson-learned > Avevo iniziato a scriverci sopra uno script Python: zip AES, upload FTP, rotation, una password aneddotica come domanda di sicurezza. Due ore dopo ho capito che Duplicati open-source fa già la stessa cosa, meglio. Piccola riflessione sulla pigrizia virtuosa. Dovevo fare backup di una quindicina di GB di progetti di lavoro. Il primo riflesso è stato istintivo: *scrivi uno script Python*. Lo sviluppatore medio ha questa reazione pavloviana — problema tecnico → apri editor → iniziamo a scriverci sopra. E l'ho fatto. Script pulito: legge una lista di path, filtra `node_modules` e cache, crea uno zip cifrato AES-256 con `pyzipper`, lo carica via FTP su uno spazio hosting che avevo inutilizzato. Ho aggiunto una domanda di sicurezza mnemonic (una data di nascita famigliare) per evitare esecuzioni accidentali, rotation dei backup vecchi, log strutturato, `.env` per le credenziali. Duecento righe di Python pulito, pronto a girare in Windows Task Scheduler. Poi ho fatto il primo dry-run e ho iniziato a guardare i numeri. Quindici GB di dati. Dieci GB di spazio disponibile sull'hosting. Un singolo backup non ci stava con rotation sensata. Ho cominciato a stringere le esclusioni, a togliere cartelle di asset binari, a tagliare media non essenziali. Ogni regola che aggiungevo era un pezzo di comportamento che dovevo mantenere nel tempo. A quel punto mi sono fermato a pensare. Stavo costruendo, piano piano, **un software di backup**. Fatto male, con meno funzionalità, con più codice da mantenere di qualunque tool maturo già esistente. Ho cambiato rotta. Sono andato su **OneDrive personale** (un terabyte di spazio che usavo a quaranta percento) e ho installato **Duplicati**: open-source, backup incrementali, cifratura AES built-in, retention policy con granularità fine, web UI decente, integrazione con decine di storage target. Venti minuti di wizard, setup fatto. Il primo backup full è partito subito, quelli successivi saranno incrementali — solo i delta modificati, niente più zip ricreato ogni volta. La lezione non è nuova ma vale la pena ripeterla a sé stessi ogni tanto. Quando il problema che stai risolvendo è **un problema classico, già risolto da tool maturi da anni** (backup, monitoring, logging, scheduling, parsing di formati standard), scrivere uno script custom è quasi sempre la scelta sbagliata. Non perché non funzioni, ma perché finisci a riprodurre male le stesse feature che trenta persone hanno già implementato meglio, e ti impegni a manutenerle. Lo script ha senso **quando il problema ha un twist specifico del tuo caso** che nessun tool pubblico risolve. Per tutto il resto, un'ora spesa a valutare i tool esistenti ti fa risparmiare dieci ore di scrittura e cento di manutenzione. Oggi ho cancellato il mio script di backup Python senza rimpianti. I dati sono più al sicuro adesso, con meno codice mio in giro. --- # Studi ## Arcocat in sintesi: sette scelte tecniche e perche' contano URL: https://andreapellizzari.it/studio/arcocat-in-sintesi/ Versione: 1.1 Aggiornato: 2026-05-26 Keywords: arcocat, scelte tecniche, chatbot CPQ, guardrail, discovery enforcement, domain contracts, executive summary > Pagina di sintesi per chi conosce un chatbot AI ma non l'ha mai costruito. Le sette scelte tecniche distintive di arcocat in tabella sinottica e in sette brevi sezioni problema-scelta-beneficio. Riferimento a quattro studi tecnici per approfondire. Questa pagina e' pensata per chi conosce abbastanza un chatbot AI da apprezzarne le scelte, ma non l'ha mai costruito da zero. L'audience tipica sono un amministratore, un capo, un cliente esterno: gente sveglia tecnicamente che vuole capire **perche'** un sistema e' costruito in un certo modo, senza dover leggere il codice. Arcocat e' un chatbot tecnico interno per la consultazione di sei cataloghi prodotto di brand reali, sviluppato dentro una PMI manifatturiera italiana del Nordest, successore diretto di [BlumCat](/lavori/blumcat/) (che restava su un singolo brand). La sua audience sono i commerciali, gli addetti al magazzino e i tecnici di showroom: persone che tollerano un'interfaccia testuale ma non tollerano codici articolo inventati, regole fisiche sbagliate o tempi di risposta lunghi. Il punto centrale di questa pagina e' uno: **un modello AI generico, da solo, non basta**. Per costruire un chatbot tecnico affidabile su un dominio reale serve incollare sette mattoncini specifici sopra al modello. Ogni mattoncino risolve un problema concreto che, se ignorato, manda in errore silenzioso il sistema. Da maggio 2026 arcocat ha fatto un salto di framing: non e' piu' "il chatbot", e' una piattaforma server agentica con client separati. Per il quadro infrastrutturale completo (cervello e tool-loop, layer dati, confine client/server, traiettoria di assorbimento dell'app gestionale) vedi [Arcocat come piattaforma agentica](/studio/arcocat-infrastruttura/). ## In un colpo d'occhio ```mermaid flowchart LR Q[Query utente
linguaggio naturale] --> R{Capisce
il brand} R --> T[Strumenti ridotti
al brand] T --> D{Controlla
il filtro} D --> S[Cerca nel catalogo
+ wiki narrativo
+ PDF voluminoso] S --> G{Controlla
le regole fisiche} G --> OUT[Risponde citando
la pagina del catalogo] ``` | # | Cosa farebbe un chatbot AI normale | Scelta di arcocat | Perche' importa | |---|---|---|---| | 1 | Inventa codici prodotto sui cataloghi grezzi | Wiki narrativo curato + grafo di link offline | 2107 SKU verificati, zero codici inventati | | 2 | Vede tutti gli strumenti sempre, e' lento e sbaglia | Routing per brand davanti al modello | -57% lunghezza prompt, -46% costo per turno | | 3 | Cerca filtri che non esistono e ritorna risultati sbagliati | Discovery enforcement con suggerimento dei valori reali | Filtri errati silenziosi dimezzati (da 14% a 7%) | | 4 | Puo' suggerire cose fisicamente impossibili | Quattro regole fisiche eseguite fuori dal prompt | Sicurezza non delegata all'AI, sempre verificata | | 5 | L'utente aspetta 8 secondi prima della prima parola | Streaming + cache del prompt riusabile | Tempo alla prima parola 8s a 1.5s, -90% costo prefisso | | 6 | Aggiungere un brand richiede toccare 10 file di codice | Otto file YAML come fonte unica delle policy | Onboarding di un nuovo brand = 5 file YAML + 1 check automatico | | 7 | Cambi una cosa, non sai se ne rompi dieci | 27 conversazioni di test + 6 health check in un comando | Ogni intervento chiude con regression OK/WARN/FAIL | ## 1. Wiki narrativo curato + grafo di link Un chatbot AI generico, se gli dai un PDF di catalogo come e', inventa i codici articolo che non esistono o li sbaglia di una cifra. Non per cattiveria del modello: per come funzionano i modelli, "indovinano" la cifra mancante con la stessa naturalezza con cui completano una frase. Per evitarlo, arcocat **non** legge il PDF al volo quando l'utente chiede qualcosa. Prima costruisce un "manuale di consultazione" interno: file di testo organizzati per brand con regole (R), famiglie di prodotto (F), distinte (D), schede di categoria (C), curati a mano da un esperto interno con l'aiuto del modello stesso. Ogni regola e ogni famiglia ha un identificatore (R001, F005) e i link tra parti sono espliciti. Da questi file di testo, un programma offline costruisce un piccolo grafo (166 entita' e oltre 557 link, in crescita coi brand). Quando l'utente chiede qualcosa, il modello consulta il grafo, non il PDF grezzo. Il risultato e' che i codici articolo che cita sono sempre verificabili: 2107 SKU controllati, zero codici inventati. Il "manuale" e' editabile dall'esperto interno senza toccare codice. Per cataloghi voluminosi (sopra le mille pagine) si aggiunge un secondo indice parallelo che permette al modello di citare la pagina esatta del PDF nella risposta. ## 2. Routing per brand davanti al modello Quando il modello AI ha 16 strumenti da scegliere ad ogni turno, e' piu' lento, costa di piu' (perche' deve "leggere" le descrizioni di tutti) e sbaglia piu' spesso quale chiamare. Tipico problema dei chatbot generici che vogliono fare tutto in un unico flusso. La scelta di arcocat e' netta: prima che il modello veda la query, un piccolo programma deterministico (un classificatore basato su espressioni regolari, niente AI) legge la query e capisce di che brand parla. Solo allora il modello viene attivato, con esposti solo i 7-9 strumenti relativi a quel brand invece dei 16 totali. Se la query non e' brand-specifica, il modello vede tutto. Risultato misurato: lunghezza del prompt iniziale crollata del 57%, costo per turno meno 46%, tempo di risposta meno 13-18%. Un piccolo gesto deterministico davanti al modello vale molto piu' di una "ottimizzazione" interna al modello. ## 3. Discovery enforcement Questo e' il problema piu' insidioso che ho incontrato in due anni di chatbot tecnici. L'utente chiede "forni pirolitici", il modello sceglie autonomamente di filtrare nel database per l'attributo "pirolitico=true". Pero' nel database l'attributo si chiama "autopulizia" con valori "pirolitica" o "vapore". Il filtro "pirolitico=true" e' tecnicamente valido (non da' errore), ritorna 21 prodotti, ma di questi solo 11 sono effettivamente pirolitici. **Silent wrong**: l'utente non vede l'errore. La scelta di arcocat: un piccolo guardiano deterministico tra il modello e il database. Quando il modello sta per filtrare su un attributo, il guardiano controlla che l'attributo esista davvero nel database. Se non esiste, blocca la richiesta e ritorna al modello un errore strutturato con il sample dei valori realmente disponibili. Il modello impara nel turno successivo e ritenta. Misurato sui 27 test reali: errori "silenziosi" sui filtri scesi dal 14% al 7%, attributi inventati a 0%, recupero automatico nel 19% dei casi bloccati. Senza che l'utente debba accorgersene. ## 4. Quattro regole fisiche fuori dal prompt Un modello AI non conosce, in modo affidabile, la norma EN 60335 (distanza minima cappa-piano cottura). Non sa che un meccanismo di sollevamento ha un range di peso ammesso per anta. Puo' "dirti" le regole se gliele scrivi nel prompt, ma puo' anche "dimenticarle" su una conversazione complessa. In contesti dove la regola serve davvero (sicurezza, regolamento, garanzia), il prompt non basta. Arcocat ha quattro regole fisiche codificate in file di testo come **regole eseguibili**. Dopo che il modello ha formulato una risposta, un piccolo motore controlla la risposta contro le regole. Se viola, il modello viene chiamato a riformulare con scuse e suggerimento alternativo. La regola e' deterministica, citabile (porta il numero della norma), e non aggirabile. Esempio reale: il modello suggerisce un meccanismo di sollevamento per un'anta da 14 kg, ma quel modello ha range fino a 12 kg. La regola fisica blocca, il modello riformula: "il pezzo che ti avevo proposto non regge 14 kg, ecco i due adatti". La responsabilita' fisica diventa deterministica, non delegata alla "memoria" dell'AI. Per un'azienda che si gioca la reputazione su consigli tecnici, e' la differenza tra fidarsi e non fidarsi. ## 5. Streaming + cache del prompt riusabile Prima dell'intervento, l'utente aspettava otto secondi prima di vedere la prima parola della risposta. Inaccettabile per una chat. Due interventi insieme. Primo, il prompt iniziale (le istruzioni di sistema, le descrizioni degli strumenti, le regole base: circa 9600 token) viene marcato come "riusabile" verso il provider AI. Dal secondo turno in poi, il sistema riceve uno sconto del 96% sul costo di rilettura di quel pezzo. Secondo, la risposta arriva parola per parola (streaming) come in ChatGPT, non in un colpo solo. Risultato: tempo alla prima parola sceso da 8 secondi a 1.5 secondi percepiti, costo per call sulla parte iniziale crollato da circa 96 millesimi di dollaro a circa 8 millesimi. La differenza in bolletta si vede, ma soprattutto cambia la percezione: la chat sembra una chat, non un form. ## 6. Otto file di policy come fonte unica Un sistema che cresce per brand finisce sempre nello stesso modo: regole sparse in dieci file di codice diversi. Aggiungere un brand nuovo diventa un esercizio di archeologia. Arcocat ha una cartella con otto file di testo (formato YAML, leggibile a occhio): la lista dei brand, le regole del classificatore di routing, le espressioni per riconoscere i codici articolo di ogni brand, i sinonimi commerciali (esempio: "pirolitico" e "Autopulizia pirolitica" sono la stessa cosa), le quattro regole fisiche, i range di peso e dimensioni, gli attributi nativi del database, i tipi di legame nel grafo. Sopra a questa cartella un controllo automatico esegue 14 verifiche su database reale: la regex SKU di ogni brand becca davvero codici esistenti? Gli alias rimandano davvero ad attributi che esistono? Le regole fisiche fanno riferimento ad attributi presenti? Onboardare un nuovo brand passa da "dieci file di codice da modificare" a "cinque file YAML da editare + un comando di check che ti dice OK". Editabile da un amministratore non sviluppatore. ## 7. Refresh orchestrator + 27 test reali L'ultima scelta e' di metodo, non di architettura: come faccio a sapere che un cambiamento non rompe qualcos'altro? Esiste un singolo comando che lancia in sequenza sei controlli: l'indicizzatore del wiki narrativo, l'ingestore dei PDF voluminosi, l'audit dei codici articolo contro i range fisici, il check di coerenza wiki vs database, il check di allineamento dei sinonimi, il cross-validator delle otto policy. Sopra a questi sei, una **suite di 27 conversazioni di test** che simula query reali con assertion non solo sul "tool e' stato chiamato" ma sull'**effettivo match degli attributi** sui prodotti ritornati. Severity aggregata, exit code 0 (tutto OK) / 1 (warning) / 2 (errore). Ogni intervento sul sistema e' chiuso con una run di questo refresh, non con un "secondo me funziona". E' la differenza che mi permette di toccare cinque punti del sistema in due settimane senza paura. ## Le scelte che ho NON fatto, e perche' Tre esempi. **Non ho lasciato la safety check al modello AI**. Avevo inizialmente uno strumento che l'AI poteva chiamare per chiedere conferma su un consiglio. L'ho deprecato: il motore di guardrail deterministico copre l'intera classe di problemi senza dipendere dalla scelta del modello di "chiamare lo strumento al momento giusto". **Non ho centralizzato i dati in un database di scala industriale da subito**. Il design teorico lo suggerirebbe per scalabilita'. La realta' operativa: SQLite e' sufficiente per il primo brand e per i successivi finche' restano sotto i 100.000 chunks. Il database industriale entra solo quando arriva un PIM cross-brand reale, non al day 1. **Non ho adottato un framework agent "completo"** (LangGraph, Microsoft Agent Framework, DSPy). Per un singolo sviluppatore in-house in una PMI, l'overhead di astrazione di questi framework e' superiore al beneficio. Ho costruito i pezzi minimi necessari (strumenti standalone, supervisor leggero, contratti tipizzati) sapendo che un giorno potrei migrare, ma senza pagarne il prezzo oggi. ## Per chi vuole approfondire Le sette scelte sopra hanno ognuna uno studio tecnico dedicato. In ordine di profondita': - [Agentizzare una PMI: stack, architettura, strumenti (v2.4)](/studio/stack-agentico-pmi/): la mappa architetturale completa, 40 minuti di lettura. Per capire come si combinano i sette mattoncini. - [Wiki narrativo AI-maintained (v1.2)](/studio/wiki-narrativo-ai-maintained/): il pattern del wiki + grafo, 27 minuti. Per capire come si costruisce il "manuale di consultazione". - [Agentizzare un'azienda: timeline e flussi reali (v1.4)](/studio/flussi-agentici-pmi/): cinque flussi end-to-end, 20 minuti. Per vedere il sistema in azione. - [Onboarding catalogo PDF: playbook 7 fasi (v1.3)](/studio/playbook-onboarding-catalogo-pdf/): come si onboarda un nuovo brand, 25 minuti. Per capire l'effort di manutenzione. E nel diario tecnico, il post [Arcocat, due settimane di hardening](/diario/2026-05-14-arcocat-due-settimane-hardening/) racconta in presa diretta le ultime due settimane di lavoro. - Un modello AI generico da solo non basta per un chatbot tecnico affidabile: serve incollare sette mattoncini specifici sopra. - I sette mattoncini risolvono sette problemi concreti, ognuno con un beneficio misurato. - La filosofia di base: **determinismo dove la verita' e' finita (fisica, schema, regole), AI dove c'e' ambiguita' (intent, prosa, scoping)**. - Ogni scelta puo' essere fatta o non fatta: questa pagina rende esplicite le scelte e le scelte alternative scartate. - Sistema in produzione con regression continua su 27 conversazioni di test. Aggiornato il brand count (sei cataloghi) e aggiunto il rimando al nuovo studio infrastrutturale "Arcocat come piattaforma agentica", che racconta la svolta a piattaforma e il confine client/server. Prima stesura. Pagina di sintesi per audience non sviluppatori (amministratori, capi, clienti esterni). Sette scelte tecniche distintive di arcocat in tabella sinottica + sette sezioni narrative. Tre scelte alternative scartate. Cross-link ai quattro studi tecnici di approfondimento. --- ## Arcocat come piattaforma agentica: come funziona l'infrastruttura URL: https://andreapellizzari.it/studio/arcocat-infrastruttura/ Versione: 1.0 Creato: 2026-05-26 Keywords: arcocat, piattaforma agentica, architettura client server, infrastruttura AI, knowledge graph, guardrail, strangler-fig, agentizzare app esistente > Il funzionamento logico di arcocat dall'alto: il cervello e il tool-loop, i mattoncini deterministici, il layer dati come fonte di verita', la svolta a piattaforma con client separati, e la traiettoria di assorbimento agentico di un'app gestionale esistente. Questo studio spiega come funziona arcocat dall'alto, in modo logico: cosa fa ogni pezzo, perche' sta dove sta, e come il sistema e' cambiato nell'ultima fase. E' la pagina che vorrei avere davanti quando riapro il progetto dopo mesi e devo ricostruire il quadro senza rileggere il codice. Il punto di partenza e' un cambio di prospettiva. Arcocat non e' piu' "il chatbot del catalogo": e' diventato una **piattaforma server agentica**, e l'interfaccia con cui ci si parla e' solo la sua prima faccia. Le sezioni che seguono raccontano l'infrastruttura attuale e la direzione. ## 1. Cos'e' arcocat oggi Arcocat e' un assistente tecnico interno per la consultazione dei cataloghi prodotto, sviluppato dentro una PMI manifatturiera italiana del Nordest. Nasce come successore di un chatbot mono-brand precedente (che restava su un solo marchio) e oggi copre sei brand reali, con alcune migliaia di codici prodotto in un database strutturato. Il backend e' un servizio Python (FastAPI) servito in un container Docker su un server di LAN interno. Il salto di framing e' il cuore di questo studio: ho smesso di pensare ad arcocat come "un assistente conversazionale" e ho iniziato a pensarlo come **una piattaforma server** sopra cui girano uno o piu' client. Oggi il client e' una console agentica web. Domani potranno essercene altri (per esempio un'app verticale che indaga gli ordini di un cliente). Tutti parlano alla stessa parte server, nessuno duplica la logica. La logica di dominio (modello, strumenti, conoscenza, regole, dati, integrazioni) vive nel server. I client ereditano le evoluzioni del server e adattano solo l'interfaccia. Un client non reimplementa mai la logica server. Questo principio e' la spina dorsale dell'intera infrastruttura. ## 2. Il cervello e il loop Il cervello e' un modello Claude raggiunto via OpenRouter. Non uso un framework agent (LangChain, LangGraph, Microsoft Agent Framework): per un singolo sviluppatore in-house l'overhead di astrazione supera il beneficio. Ho scritto un **tool-loop custom**: il modello decide a runtime quali strumenti chiamare, il loop intercetta le chiamate, esegue, restituisce i risultati, finche' il modello non produce la risposta o chiede una scelta all'utente. Il prompt di sistema non vive nel codice: arriva da un gestore di prompt versionati (Langfuse). Posso cambiarlo senza ridistribuire il container. {` query utente | v [routing brand] (regex deterministico, niente AI) | espone solo gli strumenti del brand giusto v [modello tool-loop] <--> strumenti (discovery, grafo, RAG, validate) | ogni filtro passa dal guardiano discovery v [guardrail deterministici] (regole fisiche, policy) | v risposta in streaming + cita la pagina del catalogo `} ## 3. I mattoncini deterministici L'idea di fondo: **determinismo dove la verita' e' finita (fisica, schema, regole), AI dove c'e' ambiguita' (intent, prosa, scoping)**. I mattoncini che metto attorno al modello: - **Routing per brand**: un classificatore a espressioni regolari legge la query prima del modello e capisce di che brand si parla. Cosi' il modello vede 7-9 strumenti invece di tutti. Misurato: prompt iniziale meno 57%, costo per turno meno 46%. - **Discovery enforcement**: un guardiano tra modello e database. Se il modello prova a filtrare su un attributo che non esiste (il classico "pirolitico uguale vero" quando nel database l'attributo si chiama "autopulizia" con valore "pirolitica"), blocca e restituisce i valori reali disponibili. Il modello ritenta nel turno dopo. I filtri sbagliati silenziosi sono scesi dal 14% al 7%, gli attributi inventati a zero. - **Guardrail e regole fisiche**: una cinquantina di regole eseguite fuori dal prompt, piu' alcune regole fisiche (per esempio la norma EN 60335 sulla distanza cappa-piano, i range di peso ammessi per i meccanismi di sollevamento anta). Dopo che il modello risponde, un motore verifica e, se viola, lo fa riformulare. La responsabilita' fisica e' deterministica, non delegata alla memoria dell'AI. - **Policy come fonte unica**: una cartella di file YAML tiene brand, regole di routing, espressioni per riconoscere i codici di ogni brand, alias e sinonimi, regole fisiche, range, attributi nativi, tipi di legame del grafo. Onboardare un brand nuovo e' editare pochi file YAML piu' un check automatico, non toccare dieci file di codice. ## 4. Il layer dati: la verita' Qui vive la fonte di verita', ed e' la parte che mi sta piu' a cuore tenere pulita. - **Database prodotti (SQLite)**: alcune migliaia di codici su sei brand. E' la verita' strutturata sui prodotti (codice, brand, categoria, attributi tipizzati). - **Wiki narrativo**: file di testo curati a mano per brand, organizzati in regole, famiglie, distinte, schede di categoria. E' il "manuale di consultazione" editabile dall'esperto senza toccare codice. Da qui un programma offline costruisce un **knowledge graph**: quando l'utente chiede qualcosa il modello consulta il grafo, non il PDF grezzo. Risultato: codici sempre verificabili, zero codici inventati. - **RAG sui PDF voluminosi**: per i cataloghi sopra le mille pagine un secondo indice permette al modello di citare la pagina esatta. I PDF sono serviti staticamente. - **Media prodotto**: immagini servite staticamente, alimentate dai feed strutturati dei brand. - **Policy deterministiche**: alias, invarianti, regole guardrail (la cartella YAML gia' citata). Il database possiede i dati strutturati. Il wiki possiede la conoscenza narrativa e i link. Le policy possiedono le regole. I client non possiedono niente di tutto questo: leggono il risultato via contratto. Questa separazione e' cio' che mi permette di rigenerare il grafo o cambiare uno schema senza rompere le interfacce. ## 5. Prestazioni e osservabilita' - **Streaming e prompt caching**: la risposta arriva parola per parola (stream NDJSON: evento di avvio, delta di testo, evento di chiusura). Il prefisso del prompt (sistema piu' descrizioni strumenti) e' marcato come riusabile verso il provider, e c'e' un secondo punto di cache sulla conversazione per il tool-loop a piu' iterazioni. Tempo alla prima parola sceso da 8 secondi a circa 1.5; costo del prefisso in rilettura crollato (lo sconto sulla cache arriva al 96%). - **Prompt versionati e telemetria**: costo e latenza per turno tracciati. - **Osservabilita' di servizio**: un endpoint di salute (con modalita' profonda che esegue anche uno smoke del provider) e un endpoint di stato per il monitoraggio. Le trace dei turni vengono conservate. Esempio concreto: una query "forni pirolitici" passata dal client produce uno stream completo, il loop chiama in sequenza gli strumenti di sinonimi, attributi, valori distinti e ricerca prodotti, trova i modelli pertinenti, risponde raggruppando per linea e chiede quale serie interessa. Costo dell'ordine del centesimo di dollaro, latenza di alcune decine di secondi per il giro discovery completo. Il path completo e' vivo end-to-end, non solo la configurazione. ## 6. Sicurezza dei rilasci La domanda operativa: come faccio a sapere che un cambiamento non rompe altro? Risposta: un orchestratore di refresh piu' una suite di regressione. - **Refresh orchestrator**: un comando che rilancia in sequenza indicizzatore del wiki, ingest dei PDF, audit dei codici contro i range, coerenza wiki contro database, allineamento dei sinonimi, cross-validate delle policy. - **Golden queries**: circa 27 conversazioni di test con assertion non solo sul "strumento chiamato" ma sull'effettivo match degli attributi sui prodotti ritornati. - **Health check** su drift degli attributi e freshness dei sample. Severity aggregata, exit code 0/1/2. Ogni intervento si chiude con una run, non con un "secondo me funziona". E' cio' che mi permette di toccare piu' punti del sistema in due settimane senza paura. ## 7. La svolta a piattaforma: server e client Fino a poco fa la UI era una pagina tecnica monolitica. Ora una console agentica (sviluppata in SvelteKit) la sostituisce come interfaccia di produzione: chat-first, canvas adattivo, voce, mobile-first. Il punto chiave non e' "ho fatto una UI nuova", e' aver formalizzato il **confine client/server**: - Il client tocca solo se stesso: componenti, UX, stato UI, rendering, voce, chiamate HTTP. - Il server tocca tutto il resto: modello, prompt, strumenti, grafo, database, guardrail, dati, integrazioni. - L'unico accoppiamento e' il **contratto HTTP**: un endpoint di streaming (NDJSON), piu' gli endpoint per il turno sincrono e il reset; un payload prodotto tipizzato; uno snapshot del preventivo passato a ogni turno; i mount statici di PDF e immagini. Gli endpoint di salute e stato sono osservabilita', non li consuma il client. - Il backend non ha CORS abilitato: in sviluppo il client passa da un reverse proxy che inoltra le chiamate al backend stesso-origin; in produzione si mette un proxy davanti o si serve same-origin. Domani un frontend verticale (per esempio una vista dedicata agli ordini di un cliente) sara' un nuovo client che si collega alla stessa parte server. Non duplica nulla. Il loop e' sempre lo stesso: il server evolve, i client ereditano e adattano solo l'interfaccia. ## 8. La traiettoria: assorbire l'app gestionale agenticamente Qui sta la parte ambiziosa. L'azienda ha un'app gestionale tradizionale (Next.js, database PostgreSQL sincronizzato dall'ERP, integrazione con l'ERP, invio email, un assistente per le statistiche, e i concetti centrali del commerce: clienti, carrelli, ordini, accordi, listini personalizzati). La decisione: **non riscrivere l'app gestionale dentro il client**. Le sue procedure si ripensano una alla volta come agenti, e la logica nuova si costruisce lato piattaforma (il server), non nel client. E' uno strangler-fig: l'app tradizionale resta in produzione in parallelo e si spegne modulo per modulo man mano che gli agenti la sostituiscono. Dove serve solo data-entry puro, l'app tradizionale puo' restare. Conseguenza importante: assorbendo le sue procedure, la piattaforma **eredita anche il ruolo di integrazione dati** che oggi ha l'app gestionale. Le entita' (clienti, carrelli, ordini) restano la verita' dell'ERP: la piattaforma le espone agli agenti, non le riscrive da zero. Come le raggiunge (riuso delle API dell'app esistente oppure accesso diretto al database) e' un dettaglio implementativo lato server, ancora da decidere. ## 9. Cosa NON ho fatto e perche' - **Niente framework agent pesante**: pezzi minimi necessari (strumenti standalone, supervisor leggero, contratti tipizzati), con la consapevolezza che un giorno potrei migrare senza pagarne il prezzo oggi. - **Niente database industriale al day-1**: SQLite basta finche' resto sotto soglie ragionevoli. Il database di scala entra quando arriva un PIM cross-brand reale. - **Safety non delegata al modello**: il guardrail deterministico copre l'intera classe di problemi, non dipende dal fatto che l'AI "si ricordi" di chiamare lo strumento giusto. - **Niente riscrittura uno-a-uno dell'app gestionale**: le procedure si ripensano come agenti, non si ricopiano come schermate CRUD. - **Niente logica di dominio nei client**: se una feature richiede logica server, si fa nella piattaforma e il client la consuma. - Arcocat non e' piu' un chatbot: e' una piattaforma server agentica. La console con cui ci si parla e' il suo primo client, ne potranno seguire altri. - La spina dorsale: la logica vive nel server, i client ereditano e non duplicano mai. - L'infrastruttura logica: cervello LLM piu' tool-loop custom, mattoncini deterministici (routing, discovery enforcement, guardrail, policy YAML), layer dati come verita' (database prodotti, wiki narrativo piu' grafo, policy, RAG sui PDF), streaming piu' cache, osservabilita', regressione su golden queries. - La traiettoria: assorbire l'app gestionale esistente agenticamente con uno strangler-fig, facendo crescere il server, non i client. Per le sette scelte tecniche distintive raccontate per un pubblico non sviluppatore, vedi [Arcocat in sintesi](/studio/arcocat-in-sintesi/). Per la mappa architetturale completa vedi [Agentizzare una PMI](/studio/stack-agentico-pmi/), e per il pattern del wiki curato [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/). Prima stesura. Quadro infrastrutturale di arcocat dopo la svolta a piattaforma: cervello e tool-loop, mattoncini deterministici, layer dati, prestazioni e osservabilita', sicurezza dei rilasci, confine client/server, traiettoria di assorbimento agentico dell'app gestionale esistente. --- ## Agentizzare un'azienda: timeline e flussi reali URL: https://andreapellizzari.it/studio/flussi-agentici-pmi/ Versione: 1.4 Aggiornato: 2026-05-14 Keywords: flussi agentici, Inngest, MCP, workflow durabili, timeline strumenti, filter-then-validate, PIM, knowledge tools, configuration context > Volume operativo dello studio sull'agentizzazione PMI. Timeline grafica che mostra quando entra ogni strumento; cinque flussi reali end-to-end (onboarding, chat tecnica, query cross-brand strutturata, generazione offerta, ETL notturno) con strumenti specifici per ogni passo; diagramma complessivo del sistema allineato all'architettura v2.0 a 5 livelli. Questo studio e' il volume operativo di [Agentizzare una PMI: stack, architettura, strumenti](/studio/stack-agentico-pmi/). Lo stack per i miei progetti l'ho descritto la'. Qui mostro come si applica nella pratica: in che ordine entrano gli strumenti nel tempo, e cinque flussi reali end-to-end con tutti i passi e gli strumenti coinvolti. E' la differenza tra una mappa e un percorso effettivamente camminato. La v1.2 di questo studio e' allineata al modello bi-dimensionale a 5 livelli introdotto nello studio principale v2.0 (Typed Query Layer, Supervisor Filter-then-Validate, Knowledge Tools + PIM + Rule Engine + Mexal/Promo MCP, Configuration Context, Infrastruttura). Il **nuovo Flusso 5** (query strutturata cross-brand "lavastoviglie 60 classe A") mostra il pattern Filter-then-Validate end-to-end. Il **Flusso 2** (chat tecnica) e' stato riformulato per usare i Knowledge Tools brand-specifici. Per il modello completo vedi [Pattern dati: bi-dimensionale](/studio/stack-agentico-pmi/#pattern-dati-il-modello-bi-dimensionale) nello studio principale. Tutti i flussi descritti sotto rispettano lo stesso **modello di esecuzione** (asincrono, event-driven, durable) dichiarato nello studio principale. Per i guardrail permanenti (4 trigger types ammessi, retry policy, anti-pattern come polling o HTTP blocking, eccezione sync per chat HTTP), vedi la sezione [Modello di esecuzione](/studio/stack-agentico-pmi/#modello-di-esecuzione-tutto-e-asincrono-ed-event-driven) prima di costruire un nuovo flusso. ## Timeline grafica fase / strumento Vista cronologica di quando ogni strumento entra nello stack. Ogni colonna e' una fase temporale; ogni riga e' un layer architetturale. La scelta consapevole di un colore indica priorita' e livello di urgenza. Il grosso dell'investimento e' nei mesi 1-3 (P1 + P2). Dal mese 4 in poi sono aggiunte mirate per dolori specifici, non un nuovo "big bang". La disciplina di crescita: aggiungere uno strumento solo quando si sente il dolore concreto che risolve. ## Quattro flussi concreti Quattro casi d'uso che potrei realisticamente avere. Per ognuno: trigger, step-by-step, chi chiama chi, cosa produce, quali strumenti. La somma di questi flussi da' l'immagine reale di come lo stack respira giorno per giorno. ### Flusso 1: onboarding nuovo cliente Gestionale ERP · Caddy · Inngest · Cloudflare WAF}> Il gestionale invia webhook `POST /webhook/erp` al VPS quando un nuovo cliente viene salvato. Inngest riceve l'evento `erp/cliente.creato` con payload (cliente_id, ragione_sociale). Genera `trace_id = uuid()`. Inngest · Claude Agent SDK · gestionale-mcp · Langfuse · prompt caching}> L'agente principale (Claude Sonnet) chiama `gestionale-mcp.cerca_cliente(cliente_id)`. L'MCP server fa GET, restituisce JSON con: ragione sociale, P.IVA, indirizzo, settore merceologico, contatto referente. Claude Sonnet · catalogo-mcp · pgvector}> L'agente interpreta il settore merceologico. Cerca articoli pertinenti via `catalogo-mcp.suggerisci_per_categoria`. Restituisce 5 articoli piu' venduti per categoria. Claude Sonnet · prompt caching (system prompt 2k token cached, -90% costo) · Langfuse}> L'agente compone email di benvenuto in italiano: presentazione azienda, richiamo a 3 articoli rilevanti per il settore, call-to-action soft. Output: bozza markdown + oggetto. Claude Haiku · regole hardcoded (Layer C) · Langfuse}> Un secondo agente (Claude Haiku, modello piu' economico) riceve bozza + regole hardcoded: tono brand, no promesse di sconto, no claim non documentati, max 3 link. Output: `{ok: bool, issues: [...]}`. Se KO, lo step 3 ricomincia con feedback. Microsoft Presidio · Langfuse}> Prima di scrivere il log finale su Langfuse, Presidio scansiona la bozza e maschera P.IVA, email del cliente, numero di telefono. La bozza completa va al cliente, ma il trace contiene `[EMAIL_REDACTED]`. Inngest waitForEvent · Apprise · Telegram · Approval inbox custom (Next.js)}> `step.waitForEvent("onboarding.approvato", timeout: "48h")`. Apprise notifica il commerciale via Telegram con link all'approval inbox. Lui apre, vede la bozza, clicca "Approva" o "Modifica e approva". Se nessuna risposta in 48h, escalation a responsabile commerciale. SMTP · Postgres audit_log · gestionale-mcp}> Invio via SMTP. Append su `audit_log` Postgres: `(audit_id, trace_id, cliente_id, decisore_umano, timestamp, hash_email)`. Aggiorna campo `cliente.onboarding_audit_id = trace_id` tramite `gestionale-mcp`. Apprise · Telegram}> Apprise notifica al commerciale "Email inviata a [Cliente]" con link al trace Langfuse per audit immediato. ### Flusso 2: domanda tecnica consulenziale via chat Supervisor (intent=consult) -> Knowledge Tool brand" }, { label: "Trigger", value: "Messaggio Telegram" }, { label: "Durata", value: "5-12 secondi" }, { label: "Costo stimato", value: "~0.02 EUR/domanda" } ]} > Telegram Bot API · Hermes Agent (o webhook custom) · Caddy · Postgres conversation_history}> Telegram Bot API invia webhook al VPS. Il runtime conversazionale identifica l'utente via memoria persistente. Genera `trace_id`. Parser Python: regex + dizionari + spaCy IT · fallback Claude Haiku · Langfuse}> Parsing slot deterministico pre-LLM. Estrae: `{intent: "consult", categoria: "cerniera", filtri: {peso_anta_kg: 18, tipo_frontale: "vetro"}, testo_libero: "come scegliere", brand_hint: "blum"}`. Latenza ~50ms a costo zero se i pattern matchano (caso comune); fallback a Haiku con `tool_use` forzato per i casi residui. Claude Sonnet (Supervisor) · system prompt cached · routing deterministico · Langfuse}> Il Supervisor riceve il TypedQuery. Intent="consult" + categoria="cerniera" + brand_hint="blum" -> routing diretto al `BlumKnowledge` MCP. Niente PIM in questo caso (non e' filtro strutturato). Niente Mexal (non e' richiesta commerciale). BlumKnowledge MCP · SQLite + FTS5 + cosine NumPy · RRF (k=60) · prompt caching}> `blum-knowledge.cerca_knowledge("come scelgo cerniera per anta vetro pesante", top_k=5, alpha=0.5)`. Hybrid retrieval: BM25 FTS5 nativo SQLite + cosine semantica via Reciprocal Rank Fusion. Ritorna chunks da G004 (decision tree cerniere), R007 (cerniere per ante in vetro), R009 (cerniere per ante pesanti). Match esatto su sigle KH/FH preservato dal BM25. Rule Engine Python · R*.md frontmatter eseguibile · Langfuse}> `rule_engine.evaluate(brand="blum", prodotto={categoria: "cerniera", peso_anta_kg: 18, tipo_frontale: "vetro"})`. Trigger di R009 (peso > 15 kg richiede cerniera high-load) e R007 (vetro richiede piastrina dedicata). Output strutturato con messaggi + fonte_pagina_fis. Claude Sonnet · Langfuse}> Supervisor compone risposta italiana combinando: chunks narrativi del Knowledge Tool + vincoli tecnici dal Rule Engine + citazioni pagina manuale. Decision tree esplicito: "anta 18kg vetro -> serie 71B con piastrina vetro -> codice consigliato X". Niente "compatibile" generico, ragionamento consulenziale. Claude Haiku · check codice articolo via blum-knowledge.dettaglio_codice · Langfuse}> Il guardian verifica: codici articolo citati esistono davvero nel catalogo Blum (lookup esatto via `dettaglio_codice`), regole citate hanno fonte_pagina valida, no consigli di montaggio non documentati, disclaimer "in caso di dubbio consultare rivenditore" presente. Postgres `config_context` · Langfuse}> Se l'utente ha una sessione progetto attiva (es. configurazione cucina in corso), aggiorna ConfigurationContext aggiungendo il modulo cerniera con attributi e vincoli emessi. fonte_per_campo annotata: "moduli.cerniera.codice = BlumKnowledge.cerca_knowledge". Presidio · Langfuse · Telegram}> Presidio scansiona la conversazione e maschera PII nel trace. Conversazione salvata in `conversation_history` Postgres (scadenza 30 giorni). Risposta inviata via Telegram con citazioni manuale + disclaimer. ### Flusso 3: generazione offerta commerciale Appsmith · Inngest · Caddy · API key auth}> Il commerciale apre l'internal tool, seleziona cliente, inserisce articoli desiderati e quantita', clicca "Genera offerta". Appsmith chiama webhook Inngest con evento `commerciale/offerta.richiesta`. gestionale-mcp · pybreaker (circuit breaker)}> Workflow chiama in sequenza: `gestionale-mcp.cerca_cliente(...)` → `gestionale-mcp.storico_acquisti(cliente_id, mesi=24)`. Pattern visto: ordini medi, sconto medio storicamente applicato. gestionale-mcp · regola if/else Python (Layer C) · Apprise}> **Hard rule in codice**: `gestionale-mcp.verifica_credito(cliente_id)`. **Fail safe**: se esposizione superiore al 90% del fido, workflow si ferma e notifica responsabile credito. Nessun agente LLM puo' aggirare questa regola. Claude Sonnet · prompt caching · gestionale-mcp (3-5 chiamate) · Langfuse}> L'agente compone offerta. Tool calling iterativo: lookup prezzi listino, calcolo sconto raccomandato basato su storico, calcolo totale, termini pagamento standard. Claude Haiku · regole hardcoded · Langfuse}> Il guardian verifica: sconto applicato dentro range storico (entro 5% del medio), termini pagamento standard, nessun articolo "da non vendere" (allowlist), totale corretto. Output: `{ok, sconto_attuale, sconto_max_auto: 15%, richiede_approval: bool}`. Inngest waitForEvent · Apprise · Approval inbox custom}> **Branching deterministico**: se sconto calcolato superiore al 15%, `step.waitForEvent("offerta.approvata", timeout: "24h")`. Apprise notifica responsabile commerciale via Telegram. Altrimenti auto-approve e prosegue. gestionale-mcp · WeasyPrint · MinIO · Postgres}> `gestionale-mcp.genera_pdf_offerta(dati)`. Genera PDF con template aziendale (WeasyPrint). Upload su MinIO bucket `offerte/` con nome `offerta_{trace_id}.pdf`. Restituisce URL signed valido 30 giorni. gestionale-mcp · gestionale ERP · Postgres audit_log}> `gestionale-mcp.salva_offerta(payload)` crea record offerta nel gestionale con `audit_id = trace_id`. Append immutabile su `audit_log`: `(audit_id, trace_id, decisore_umano, commerciale, sconto_applicato, totale)`. Appsmith · Langfuse}> Appsmith mostra "Offerta generata" con: link PDF su MinIO, link al trace Langfuse per audit, button "Invia al cliente" (che triggera flusso comm separato). ### Flusso 4: ETL notturno catalogo prodotti Inngest cron}> Inngest scheduler fa partire il workflow alle 03:00 ogni notte. `trace_id` generato, `batch_id` univoco per il run. Python (requests) · Infisical (secret) · Apprise · pybreaker}> Script Python scarica il feed dal Productdata Service del produttore. Auth via API key in Infisical. Se 401/403/timeout, notifica admin via Apprise ed esce con stato `failed`. Se OK, file XML salvato temporaneamente. Python (lxml) · jsonschema}> Parser BMEcat estrae record articoli: codice, descrizione IT, dimensioni, immagine URL, scheda PDF URL, gerarchia categoria. Validazione schema. Statistiche del run. Python · Postgres}> Confronto sintetico hash-based con catalogo Postgres esistente. Output: `(nuovi, modificati, rimossi)`. Voyage AI (o Ollama + nomic-embed) · pgvector}> Per articoli nuovi e modificati: chiamata batch a Voyage AI (o Ollama+nomic-embed locale per privacy). Output: vettori 1024-dim. Upsert in `articoli` Postgres + `articoli_vec` pgvector. Claude Haiku · Anthropic Batch API (50% sconto) · pgvector}> Re-indexing del wiki narrativo: per ogni articolo modificato, rigenera la "scheda narrativa" tramite agente (Claude Haiku, batch). Salva in `kb` con embedding aggiornato. Promptfoo (o eval custom) · Langfuse · Apprise}> L'eval-agent campiona 10 articoli random tra quelli modificati. Per ognuno simula query tipica utente, verifica che l'articolo target sia nei top 5 risultati. Se score sotto soglia, notifica e apre issue automatico. WAL-G · Restic · MinIO (offsite copy)}> WAL-G backup incrementale Postgres. Restic backup file su MinIO con retention policy 30gg locale + 90gg remoto. Apprise · Telegram}> Apprise notifica via Telegram al magazziniere e responsabile prodotto: "Catalogo aggiornato. +N articoli, ~M modifiche, -K deprecati. Eval retrieval: X%. Tempo totale: Y min." ### Flusso 5: query strutturata cross-brand (Filter-then-Validate) Web app Next.js · Caddy · Postgres sessions · ConfigurationContext esistente}> La chat web invia POST con `cliente_id="rossi_srl"` (gia' selezionato in sessione precedente). Genera `trace_id` propagato a tutti i livelli. Carica ConfigurationContext esistente se presente (sessione progetto cucina aperta). Parser Python: regex + dizionari + spaCy IT · Langfuse}> Parsing slot pre-LLM. La query "lavastoviglie 60 classe A" matcha pattern noti (categoria + larghezza_cm + classe_energetica). Output tipizzato: ```json { "intent": "search", "categoria": "lavastoviglie", "filtri": {"larghezza_cm": 60, "classe_energetica": "A"}, "contesto_progetto_ref": "sess_xyz789", "cliente_id": "rossi_srl" } ``` Latenza ~30ms a costo zero (no LLM chiamato). Critico: "60" e' parsato come `larghezza_cm`, non puo' confondersi con "60 watt" o "60 db" altrove. Claude Sonnet (Supervisor) · routing deterministico · Langfuse}> Supervisor riceve TypedQuery. Intent="search" + filtri tipizzati su categoria nota -> attiva pattern **Filter-then-Validate cross-brand**. Pianifica: PIM (filter) -> Knowledge Tools brand (validate, parallelo) -> Mexal (prezzo cliente) -> Promo (verifica). Niente sintesi LLM "intelligente": flusso deterministico. PIM MCP · Postgres JSONB · indici GIN · CategorySchema C001_lavastoviglie.md}> `pim.filtra_prodotti(categoria="lavastoviglie", attributi={larghezza_cm: 60, classe_energetica: "A"}, top_k=20)`. Query Postgres su tabella `prodotti` con filtro JSONB indicizzato. Schema validato contro C001 (larghezza_cm e' measurement; classe_energetica e' select con valori ammessi A-G). Output: 12 codici candidati cross-brand (5 Bosch, 4 Whirlpool, 3 BSH). BoschKnowledge MCP · WhirlpoolKnowledge MCP · BSHKnowledge MCP · Rule Engine · ConfigurationContext lookup · Inngest step.run paralleli}> Per ogni candidato (top 5 dopo ranking PIM), il Knowledge Tool brand corrispondente fa `valida_compatibilita(prodotto, contesto)` in parallelo. Il contesto include il ConfigurationContext (cucina con cassetti retrostanti). Trigger di vincolo cross-modulo X-COMPAT-001 ("lavastoviglie 60 con cassetti retrostanti -> verifica nicchia 560mm"). Output strutturato per candidato: ```json [ {"codice": "SMV68N20EU", "brand": "bosch", "validation": {"status": "warn", "messaggio": "Richiede nicchia 560mm. Cassetti retrostanti 540mm: rischio."}}, {"codice": "WTC36HK02EU", "brand": "whirlpool", "validation": {"status": "ok", "messaggio": "Profondita' 545mm, compatibile con cassetti retrostanti 540mm."}}, ... ] ``` Mexal MCP · pybreaker circuit breaker · classe sconto cliente}> Per ogni candidato sopravvissuto: `mexal.prezzo_per_cliente(codice, "rossi_srl")` + `mexal.disponibilita(codice)`. Ritorna prezzo netto cliente (con classe sconto applicata) + disponibilita' magazzino + lead time. Concurrency limitata (`{ limit: 3 }` per Mexal). Circuit breaker davanti. Promo MCP · DB promo Arco · `progresso_target_sconto`}> `promo.promo_attive(codici=[...], cliente_id="rossi_srl")` -> ritorna eventuali campagne attive sui codici candidati. `promo.progresso_target_sconto("rossi_srl", codice)` per ogni codice -> ritorna "il cliente Rossi ha fatturato Bosch 18.500 EUR sull'anno; prossimo target sconto a 20.000 EUR; aggiungere SMV68N20EU per 1.800 EUR lo porterebbe oltre soglia attivando +3% retroattivo". Claude Sonnet · template Markdown · Langfuse}> Il Supervisor compone la risposta combinando: filter PIM + validate Knowledge Tools + prezzo Mexal + promo. Pattern: tre stati distinti, mai mescolati: - **Consigliato**: WhirlpoolKnowledge approva attivamente + prezzo cliente competitivo + nessun warning. Mostrato in primo piano. - **Compatibile**: PIM ok + Knowledge non ha warn ma non ha approvato attivamente. Secondario. - **Sconsigliato**: PIM ok ma Knowledge segnala problema cross-modulo. Mostrato comunque con motivazione esplicita: "compatibile per filtro, ma sconsigliato perche'...". Plus suggerimento commerciale dal Promo: "se aggiungi SMV68N20EU sblocchi target sconto +3%". Claude Haiku · check codici via PIM.attributi_per_codice · Langfuse}> Il guardian verifica: codici citati esistono nel PIM (lookup esatto), vincoli citati hanno rule_id e fonte valida, distinzione tre stati rispettata, nessun prezzo inventato (tutti vengono da Mexal lookup), suggerimento promo cita rule_id originale. Se KO, rigenerazione. Postgres `config_context` · audit log immutabile}> Se l'utente seleziona uno dei prodotti (turn successivo), il modulo si aggiunge al ConfigurationContext con tracciabilita' completa (fonte_per_campo: brand=PIM, codice=user_input, prezzo=Mexal, vincoli=RuleEngine). Audit log append-only. Presidio · Langfuse · Web app}> Presidio maschera nome cliente Rossi (PII) nei log Langfuse. Risposta strutturata inviata alla chat web con cards per ognuno dei tre stati + pulsante "Aggiungi al progetto cucina" sul consigliato. Il Flusso 5 NON chiama tutti i brand Knowledge Tools in parallelo dall'inizio (federazione naive del v1.x). Chiama PRIMA il PIM (deterministico, 12 candidati), POI solo i Knowledge Tools dei brand effettivamente presenti tra i candidati (max 3-5 chiamate, non 30). Filter-then-Validate riduce drasticamente latenza, costo, e complessita' di sintesi rispetto a federazione naive. Tutti e cinque condividono lo stesso stack di base: Inngest, Langfuse, Postgres+pgvector, Caddy, Cloudflare, MCP server, Claude API. Aggiungere il sesto, decimo, ventesimo workflow non aggiunge infrastruttura, riusa quella esistente. **L'investimento dei mesi 1-3 si ammortizza ad ogni nuovo flusso aggiunto.** ## Diagramma end-to-end completo Vista panoramica di come tutti i pezzi si parlano. Un singolo schema mostra i flussi possibili, gli strumenti per layer, le frecce di dipendenza, le sezioni cross-cutting (osservabilita' + safety + human). {` ================================================================= INPUT (canali utente) ================================================================= [Telegram] [WhatsApp] [Slack] [Web Next.js] [Webhook ERP] [Cron] | | | | | | +----------+--------+----------+-------------+----------+ | ================================================================= EDGE / NETWORK / SECURITY ================================================================= Cloudflare DDoS, WAF, rate limiting front | v Caddy (VPS) TLS automatico, reverse proxy, routing | v Auth layer API key validation, JWT, multi-tenant header | ================================================================= LIVELLO 1 — TYPED QUERY LAYER (slot filling pre-LLM) ================================================================= Parser deterministico: regex + dizionari + spaCy IT Fallback Haiku con tool_use forzato per casi residui Output: TypedQuery {intent, categoria, filtri tipizzati, ...} | ================================================================= LIVELLO 2 — SUPERVISOR (Filter-then-Validate orchestrator) ================================================================= Claude Sonnet con routing deterministico per intent Pattern: PIM filtra -> Knowledge Tool valida -> sintesi 3 stati distinti: compatibile / consigliato / sconsigliato | ================================================================= ORCHESTRATOR — workflow durabili (Inngest) ================================================================= step.run("filter-pim") step.run("validate-knowledge") ogni step ATOMICO, step.run("mexal-prezzo") resilient, retry, step.run("rule-engine-eval") loggato, ispezionabile step.run("synth-supervisor") step.run("guardian-valida") step.waitForEvent("approvazione") (per flussi commerciali) cron schedules · event listeners · approval gates · retry policy | ================================================================= LIVELLO 3 — FONTI ETEROGENEE (federate selettivamente) ================================================================= 3A. KNOWLEDGE TOOLS 3B. PIM lite 3C. RULE 3D. MEXAL + per brand singleton ENGINE PROMO MCP (constellation) Postgres singleton singleton JSONB evaluator BlumKnowledge schema in Python prezzo, BoschKnowledge MD-Karpathy legge da disponib, WhirlpoolKnowledge C*.md R*.md promo, BSHKnowledge frontmatter bundle, eseguibile target sconto cerca_knowledge filtra( valuta dettaglio_codice categoria, condizione valida_compatib attributi) DSL + tool dominio SQLite + FTS5 + cosine + RRF | ================================================================= LIVELLO 4 — CONFIGURATION CONTEXT (stato persistito tipizzato) ================================================================= {progetto, moduli, vincoli_attivi, fonte_per_campo, ...} Postgres JSONB. Cresce ad ogni step. fonte_per_campo annota chi ha messo cosa: user_input | PIM | BrandKnowledge | Mexal | RuleEngine | ================================================================= LIVELLO 5 — INFRASTRUTTURA / BACKEND SYSTEMS ================================================================= [ ERP Mexal ] [ Postgres + pgvector ] [ MinIO (S3) ] un solo DB: - dati Langfuse - state Inngest - pgvector (Configuration Context, audit log) - PIM lite (JSONB) - conversation history ================================================================= CROSS-CUTTING — osservabilita' (sempre attivo, ovunque) ================================================================= OpenTelemetry GenAI (tessuto connettivo trace cross-process) | v Langfuse · Sentry · UptimeRobot · Loki (P3) · Metabase (P2) trace_id propagato in OGNI livello trace_id = inngest_run_id = audit_id = configuration_context.id -> un click ti porta dal record di business al log completo ================================================================= CROSS-CUTTING — safety (sempre attivo, ovunque) ================================================================= Layer A: Orchestrator strutturale (Inngest + Supervisor deterministico) Layer B: Critic / Guardian agent (Claude Haiku) Layer C: Hard rules in codice (if/else Python) budget cap · allowlist · approval gate forzato rate limiting · circuit breaker (pybreaker) Layer D: Rule Engine deterministico (vincoli tecnici prodotto) PII redaction (Microsoft Presidio) prima di ogni log Secret management (Infisical) per credenziali Audit log immutabile (Postgres append-only) Kill switch (Unleash feature flag) ================================================================= CROSS-CUTTING — human in the loop ================================================================= Apprise (Telegram/email/Teams) Approval inbox custom (Next.js) Appsmith (internal tools) Metabase (dashboard manageriali) Outline (wiki interna runbook + ADR) Editor wiki Karpathy (MD curato per Knowledge / Schema PIM / Rule) `} Tutto passa attraverso i tre layer cross-cutting (osservabilita', safety, human-in-the-loop). Non c'e' scorciatoia: ogni richiesta e' tracciata, validata, controllabile da un umano se serve. E' questo che rende il sistema *aziendale* e non un giocattolo. ## Come leggere e applicare I cinque flussi sono esempi rappresentativi. La realta' sara' 10-20 flussi entro il primo anno, ognuno una variazione di questi pattern. Non implementarli "uno alla volta come da copione", implementare i flussi che risolvono i dolori reali, in qualunque ordine. Tra i cinque mostrati, il **Flusso 4 (ETL notturno)** e' il piu' semplice da costruire per primo: deterministico, senza utenti, basso rischio, alto valore percepito. E' un ottimo "primo workflow Inngest" della propria storia. Il **Flusso 5 (cross-brand strutturato)** e' invece il piu' rappresentativo del **valore aggiunto del modello v2.0**: senza Filter-then-Validate (PIM filtra + Knowledge Tools validano + Mexal arricchisce + Promo suggerisce), una query come "lavastoviglie 60 classe A" cadrebbe in pattern allucinatori (vector search confonde "60 cm" con "60 watt"; LLM inventa modelli plausibili; nessun vincolo cross-modulo emerge). Costruire il Flusso 5 e' la prova che il sistema non e' "un chatbot search", ma un CPQ con consulenza tecnica. I cinque flussi condividono il 70% dell'infrastruttura: stesso Inngest, stesso Postgres+pgvector, stessi MCP server, stesso Langfuse. Aggiungere il sesto flusso costa molto meno del primo. E' l'effetto compounding dell'investimento iniziale. "Mese 2", "Mese 4-6" sono orizzonti di pianificazione, non scadenze. Se al mese 1 si sente il dolore della mancata PII redaction, si anticipa Presidio. Se al mese 6 non si ha ancora bisogno di Authelia, si rimanda. **Il vero principio e': aggiungere quando si sente il dolore, non quando il piano dice di farlo.** - I cinque flussi (onboarding, chat tecnica consulenziale, ETL notturno, generazione offerta, query cross-brand strutturata) coprono i pattern principali: pipeline lineare, knowledge consulenziale single-brand, cron deterministico, conditional approval commerciale, Filter-then-Validate cross-brand. - Il **Flusso 5** e' il piu' rappresentativo del valore v2.0: PIM filtra (recall alto) -> Knowledge Tools brand validano (precisione alta) -> Mexal arricchisce (prezzo cliente) -> Promo suggerisce (target sconto) -> Supervisor sintetizza con tre stati (compatibile / consigliato / sconsigliato). - Tutti riusano lo stesso stack di base: Inngest, Langfuse, Postgres+pgvector, MCP server, Anthropic API. Ogni nuovo workflow aggiunge configurazione, non infrastruttura. - La timeline dei mesi 1 - 6+ e' una mappa di pianificazione, non una checklist. La regola operativa resta: aggiungere uno strumento quando si sente il dolore concreto che risolve, non prima. - Il primo workflow da costruire e' quello deterministico senza utenti (Flusso 4 ETL notturno). Bassi rischi, alto valore visibile, ottimo ambiente per imparare Inngest e instrumentare Langfuse. Per la mappa architetturale completa, i 4 contratti tipizzati, i nove punti aperti, vedi [Agentizzare una PMI: stack, architettura, strumenti v2.0](/studio/stack-agentico-pmi/). Per il pattern Karpathy del livello knowledge editabile (wiki narrativi brand + R*.md regole + C*.md schema PIM), vedi [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/). Allineamento ai nuovi step deterministici introdotti nel runtime dello stack v2.4. I flussi descritti non cambiano nelle loro tappe maggiori, ma si sono "ispessiti" di hook deterministici prima e dopo i tool LLM-callable, secondo il principio "determinismo dove la verita' e' finita". (1) **Routing brand classifier come step 0 di ogni flusso**: classifier regex deterministic (5 brand pattern, 10/10 smoke) gira prima del primo tool call del Supervisor; il set di tool esposto al Supervisor crolla da 16 a 7-9 quando la query e' brand-specifica, prompt char -57%, latency -13-18%. Tool `expand_brand_scope` come escape hatch in-session per query cross-brand identificate solo a runtime. Inserito nel Flusso 2 (chat tecnica) e nel Flusso 5 (query cross-brand strutturata) come prima riga della pipeline. (2) **Discovery Enforcement come gate pre-filter**: il Flusso 5 ora ha un pre-dispatch validator `_classify_filter_key(cat, key, val)` che blocca filtri JSON su attributi/valori non verificati e ritorna payload `UNVERIFIED_ATTRIBUTE` + sample dei valori disponibili; il Supervisor recupera automaticamente via `list_distinct_values` / `list_attributes` (full-scan, non sample) e ritenta. Sui 27 golden: unknown_attribute 10%->0%, unknown_value 14%->7%, 19% dei tentativi bloccati con auto-recovery LLM. Sostituisce l'approccio prompt-based ("il modello DEVE chiamare list_distinct_values prima di filtrare") con un contratto runtime testabile. (3) **Guardrail BLOCK come step post-recommendation**: nel Flusso 5 (cross-brand) e nel Flusso 4 (generazione offerta), dopo la sintesi del Supervisor parte un hook deterministico che esegue 4 regole YAML (GR001 cappa-piano cottura EN 60335, GR002 peso anta vs meccanismo AVENTOS, GR003 cerniere CLIP top, GR004 dim vano incasso EU) via AST sandbox visitor manuale; se BLOCK, re-prompt LLM con verdict structured + max 1 retry. Esempio: AVENTOS HK top + peso anta 14 kg -> BLOCK -> il modello riformula con scuse e suggerisce HF/HS top. (4) **Streaming end-to-end via OpenRouter**: TTFT 8s -> 1.5s percepito (cache HIT 96% reduction sul prefisso system+tools 9582 token, costo prefisso da ~$0.0096 a ~$0.0008 per call). Il Flusso 2 e' il primo che si percepisce realmente come "chat": l'utente vede testo apparire, non spinner. Endpoint `/chat/stream` NDJSON, frontend con ReadableStream + TextDecoder line-by-line. (5) **Refresh orchestrator come gate periodico cross-flusso**: `refresh_arcocat.py` (~290 LOC) lega 6 step in single entry-point (wiki indexer, document ingester opt-in, SKU audit, wiki vs pim drift L15, Fase 1 alignment L16, check_contracts, regression 27 golden), severity aggregata OK/WARN/FAIL exit 0/1/2. Diventa il "tick di salute" che chiude il giro: ogni edit a wiki o canonical_aliases attiva un refresh leggero (~2s no-regression); ogni onboarding nuovo brand o batch PDF attiva un refresh pieno (~4 min). Diagramma end-to-end aggiornato implicitamente con 3 nuove fasce cross-cutting: pre-LLM (Routing + Discovery) come "filtro deterministico", post-LLM (Guardrail) come "safety net runtime", periodico (refresh orchestrator + check_contracts) come "tick di coerenza". Lezione operativa nuova: **i flussi non si arricchiscono di nuovi tool, si arricchiscono di hook deterministici intorno agli stessi tool**, e il prompt diventa piu' corto man mano che la macchina assorbe le clausole "il modello DEVE..." in contratti runtime. Allineamento di coerenza tooling con master v2.1. Audit interno ha identificato strumenti effettivamente in uso in BlumCat reale (sentence-transformers, pdfplumber, PyMuPDF, SQLite per Knowledge Tools small) che mancavano dalla timeline del Data layer (cella Mese 1). Aggiunti come P1/P2 espliciti. Postgres+pgvector spostato a Mese 2 (introduzione nel migration path v2.0, non al day 1 di BlumCat). Voyage AI e Ollama riclassificati come "alternative" per i Mesi 4-6 (opzionali se serve). La timeline ora riflette correttamente l'evoluzione: BlumCat oggi usa SQLite + sentence-transformers locale, l'arcocat futuro introduce Postgres+pgvector quando arriva il PIM cross-brand. Allineamento allo stack v2.0. Aggiunto **Flusso 5: query strutturata cross-brand** ("lavastoviglie 60 classe A per cliente Rossi") che mostra end-to-end il pattern Filter-then-Validate: Typed Query Layer slot filling -> Supervisor routing -> PIM filtra candidati cross-brand -> Knowledge Tools brand validano in parallelo (con vincoli cross-modulo via Rule Engine) -> Mexal MCP arricchisce con prezzo cliente -> Promo MCP suggerisce target sconto -> sintesi Supervisor con tre stati distinti (compatibile / consigliato / sconsigliato). **Flusso 2** (chat tecnica) riformulato per usare Knowledge Tools brand-specifici (Typed Query Layer, BlumKnowledge MCP con hybrid retrieval BM25 FTS5 + cosine + RRF, Rule Engine per regole tecniche, ConfigurationContext aggiornato). **Diagramma end-to-end** rivisto con i 5 livelli v2.0: Typed Query Layer + Supervisor + Inngest orchestrator + Fonti eterogenee (Knowledge / PIM / Rule Engine / Mexal+Promo) + Configuration Context + Backend systems. Aggiornato cross-cutting "safety" con quarto layer (Rule Engine deterministico per vincoli tecnici prodotto, separato da safety LLM). Aggiunto Callout in apertura che cita esplicitamente il "Modello di esecuzione" introdotto in v1.2 dello studio principale. I quattro flussi descritti rispettavano gia' implicitamente quel modello: il cross-link rende esplicito il legame e fa da promemoria a chi costruisca un nuovo flusso in futuro. Prima stesura. Timeline grafica con 9 layer × 5 fasi temporali, quattro flussi reali end-to-end (onboarding cliente, chat tecnica, generazione offerta, ETL notturno catalogo), diagramma complessivo con cross-cutting su osservabilita', safety e human-in-the-loop. --- ## Onboarding catalogo PDF in CPQ multi-brand: playbook 7 fasi URL: https://andreapellizzari.it/studio/playbook-onboarding-catalogo-pdf/ Versione: 1.3 Aggiornato: 2026-05-14 Keywords: onboarding catalogo, PDF estrazione, MinerU, wiki narrativo, Knowledge Tool, PIM, CategorySchema, Rule Engine, playbook operativo, Claude Code prompt > Playbook operativo per chi vuole iniziare domani ad aggiungere un nuovo catalogo brand (PDF voluminoso) a un sistema CPQ multi-brand seguendo lo stack v2.1. Sette fasi con strumenti, prompt template per Claude Code, anti-pattern, stima tempi realistica per i primi N brand. Versione astratta del playbook usato in azienda; i passi sono trasferibili a ogni dominio con catalogo tecnico denso. Tre studi ([stack v2.3](/studio/stack-agentico-pmi/), [flussi v1.3](/studio/flussi-agentici-pmi/), [wiki narrativo](/studio/wiki-narrativo-ai-maintained/)) descrivono **cosa** costruire e **perche'**. Questo descrive **come iniziare domani**: il playbook concreto per onboardare un nuovo catalogo brand (PDF voluminoso) come Knowledge Tool dentro lo stack CPQ multi-brand v2.3. Tutte le fasi sotto sono **Claude-assisted**: il developer lavora in sessione iterativa con Claude Code per costruire gli script Python di parsing/extraction/curation. **NON e' lavoro manuale a mano dell'esperto**. Il 95% della struttura (PIM, wiki narrative, regole frontmatter) viene derivata dai dati stessi via questi script. L'esperto interno **non edita YAML/JSON**: corregge body narrativo markdown + aggiunge le nozioni mentali (regole non scritte). Vedi [Meta-pattern Claude-assisted ingest](/studio/stack-agentico-pmi/#il-meta-pattern-claude-assisted-ingest) nel master per il razionale strutturale. Sette fasi sequenziali, ognuna con: **cosa fare**, **strumenti**, **deliverable**, **prompt template per Claude Code**, **errori comuni**. Si applica se: - Lo stack v2.1 e' **gia' impostato** (almeno: PIM lite con primo schema categoria, Supervisor minimale, primo Knowledge Tool brand di riferimento). Se non c'e' nulla, vedi prima la sezione [Migration path da BlumCat](/studio/stack-agentico-pmi/#stack-minimo-per-partire) dello studio principale. - Stai aggiungendo un brand **nuovo** alla constellation di Knowledge Tools (secondo, terzo, ennesimo brand). - La fonte autoritativa di conoscenza e' un **manuale PDF voluminoso** (centinaia di pagine, tabelle, formule, codici verificabili). Non si applica bene se: - Il brand ha gia' un'API strutturata (BMEcat, schema standard): parti dall'API, non dal PDF. - Il catalogo e' piccolo (sotto qualche centinaia di prodotti) o senza nomenclatura verificabile: per quello basta un RAG semplice + FAQ. - Sei al primo brand assoluto: vedi piuttosto la [Migration path BlumCat -> stack v2.1](/studio/stack-agentico-pmi/#stack-minimo-per-partire) (5 step di setup di base, poi questo playbook al sesto). ## Pre-requisiti (cosa deve esistere PRIMA di iniziare) Checklist da verificare prima della Fase 1. Se manca qualcosa, fermati e completalo. | # | Pre-requisito | Verifica | |---|---|---| | 1 | Repo git con accesso scrittura | `git status` funziona nel progetto | | 2 | VPS/server con Docker Compose attivo | `docker compose ps` mostra Postgres + Inngest + Langfuse + MCP server esistenti | | 3 | Postgres con estensione pgvector + JSONB | `\dx` in psql mostra pgvector | | 4 | PIM lite installato con almeno uno schema categoria definito | tabella `prodotti` esiste, almeno un `wiki_/categorie/C001_*.md` | | 5 | Supervisor minimale (Python `if/elif` su intent o LangGraph) | endpoint `/supervisor/query` risponde | | 6 | Almeno un Knowledge Tool brand esistente come template | es. `-knowledge-mcp` deployato | | 7 | Anthropic API key valida | `ANTHROPIC_API_KEY` in `.env.local`, test con `curl` | | 8 | Account/credenziali al portale del fornitore | accesso al portale brand per scaricare PDF + schede tecniche | | 9 | Spazio disco: ~5-10 GB liberi sul server | `df -h` | | 10 | Claude Code installato sul PC dev | `claude --version` | Non procedere. Risolvere prima il pre-requisito mancante. Tentare il playbook senza pre-requisiti porta a setup half-baked che si bloccano a meta'. ## Fase 1: scelta del brand pilota Se hai N brand candidati per essere "il prossimo", non sceglierne uno a caso. Quattro criteri pesati. | Criterio | Peso | Domanda | |---|---|---| | Profondita' catalogo | 20% | Il brand ha 100, 500 o 5000 codici? Sotto 200 il valore aggiunto e' marginale. | | Qualita' PDF fornitore | 30% | Il PDF e' nativo (testo selezionabile)? Ha tabelle ben strutturate? Le formule sono testuali o grafiche? | | Frequenza aggiornamenti | 15% | Annuale (raro) vs trimestrale (oneroso)? Aggiornamenti rari = ROI maggiore. | | Fatturato del brand sul tuo business | 35% | Brand top 3 vs longtail? Top brand giustificano l'investimento. | **Approccio operativo**: assegna 1-5 a ciascun criterio per ogni candidato, calcola pesato, scegli il top. Se un brand e' usato da clienti grossi che notano subito gli errori, parti da li': la pressione qualita' guida il rigore della cura. Brand "di nicchia" si possono fare dopo, con curve di apprendimento gia' assestate. ### Prompt template per Claude Code (Fase 1) ``` Sto valutando di aggiungere il brand "{NOME_BRAND}" come Knowledge Tool al nostro stack CPQ multi-brand v2.1. Riferimento canonico: https://andreapellizzari.it/studio/stack-agentico-pmi/ Aiutami a fare scoring secondo i 4 criteri del playbook (/studio/playbook-onboarding-catalogo-pdf/#fase-1-scelta-del-brand-pilota): 1. Profondita' catalogo (numero codici stimato, ottieni da pagina prodotti) 2. Qualita' PDF fornitore (allegato/link al PDF master se l'ho) 3. Frequenza aggiornamenti (cerca informazioni sull'ultimo update) 4. Fatturato sul nostro business (te lo dico io) Output: tabella scoring con somma pesata + raccomandazione (pilota / rinviare / scartare). ``` ### Deliverable Fase 1 - Documento `decisioni_arch/-scelta-brand-.md` con scoring + decisione finale + razionale + rischi noti. --- ## Fase 2: estrazione PDF (MinerU + pdfplumber) Dual extractor obbligatorio (vedi [pattern in dettaglio](/studio/wiki-narrativo-ai-maintained/#1-pipeline-ocr-ingest-dal-pdf-ai-chunk-searchable)). Nessun OCR singolo copre il 100%. ### Strumenti - MinerU (CLI Python) per markdown strutturato + tabelle - pdfplumber (libreria Python) per raw text supplementare - PyMuPDF (`fitz`) per pre-render JPG di tutte le pagine (citazioni inline) ### Steps 1. **Crea cartella destinazione**: `families//` (es. `families//`). 2. **Copia il PDF master**: `families//source.pdf`. 3. **Lancia MinerU**: produce `extracted.md` + immagini segmentate. 4. **Lancia pdfplumber** con script ad-hoc: produce `extracted_supplementary.md` raw text. 5. **Pre-render pagine in JPG** a 150 DPI: `manuale_pages/pag-NNN.jpg` (per chip citazione inline). ~5 min per 800 pagine, ~130 MB totali. 6. **Costruisci breadcrumb pagine**: parser su `extracted.md` per estrarre titoli H1-H4 -> CSV `pagine_breadcrumb.csv`. 7. **Costruisci albero del manuale**: `albero_manuale.md` con struttura gerarchica (utile per diff fra edizioni). 8. **`_status.json` per famiglia**: marker che la pipeline ha completato (verde/rosso). ### Tempi attesi | Pagine PDF | MinerU | pdfplumber | PyMuPDF render | Totale | |---|---|---|---|---| | 200 | 15-30 min | 1-2 min | 1-2 min | 20-35 min | | 500 | 40-60 min | 3-5 min | 3-5 min | 50-70 min | | 1000 | 1.5-2.5 h | 6-10 min | 6-10 min | 2-3 h | ### Errori comuni - **PDF scansionato non nativo**: MinerU restituisce poco. Soluzione: passare per OCR pre-process (ocrmypdf) prima. - **Tabelle complesse multi-pagina**: MinerU le spezza male. Soluzione: vision Claude in-session sulle pagine specifiche dopo l'estrazione (vedi sezione 8 di [wiki narrativo](/studio/wiki-narrativo-ai-maintained/#8-ciclo-di-vita-annuale)). - **Formule grafiche perse**: cattura via pdfplumber (raw text recupera glifi che MinerU smaltisce). ### Prompt template per Claude Code (Fase 2) ``` Sto estraendo il PDF del brand "{NOME_BRAND}" (file in families/{brand_id}/source.pdf, {NUM_PAGINE} pagine). Riferimento metodologico: /studio/wiki-narrativo-ai-maintained/#1-pipeline-ocr-ingest-dal-pdf-ai-chunk-searchable Spawna un agente Explore che: 1. Verifica che source.pdf esiste e e' apribile 2. Lancia MinerU CLI con output in families/{brand_id}/mineru_out/ 3. Lancia uno script pdfplumber che produce extracted_supplementary.md 4. Lancia pdftoppm o PyMuPDF per pre-render JPG 150 DPI in blum_data/manuale_pages/pag-NNN.jpg (sostituisci 'blum_data' col path del tuo media folder) 5. Costruisce pagine_breadcrumb.csv parsando i titoli di extracted.md 6. Scrive families/{brand_id}/_status.json con esito + statistiche Riporta dimensioni output, eventuali errori, suggerimenti di pagine critiche per le quali probabilmente serve vision in-session. ``` ### Deliverable Fase 2 - `families//source.pdf` - `families//extracted.md` (MinerU) - `families//extracted_supplementary.md` (pdfplumber) - `media/manuale_pages/pag-NNN.jpg` (per citazioni inline) - `families//pagine_breadcrumb.csv` - `families//albero_manuale.md` - `families//_status.json` --- ## Fase 3: definizione CategorySchema (PIM lite) Prima di scrivere il wiki narrativo, **definisci o aggiorna lo schema attributi** per le categorie del brand. Questo decide come il PIM filtrera' i prodotti cross-brand. ### Decisioni da prendere 1. **Le categorie del brand sono nuove o gia' esistenti** nel PIM? - Esistenti (es. il PIM ha gia' `C001_lavastoviglie.md`): aggiungi solo il brand al `valori` dell'attributo `brand` di quella categoria. - Nuove (es. mai entrato il segmento "ferramenta cerniere"): definisci da zero `C00N_.md` con tutti gli attributi tipizzati. 2. **Schema categoria condiviso o brand-specifico?** - Condiviso (raccomandato): `C001_lavastoviglie.md` ha gli stessi attributi per Bosch / Whirlpool / BSH. La domanda "lavastoviglie 60 classe A" funziona cross-brand. - Brand-specifico (sconsigliato): `C001_lavastoviglie_bosch.md` separato. Si perde la query cross-brand. Solo se gli attributi sono **veramente** diversi per natura. ### Pattern Karpathy esteso (vedi [wiki narrativo](/studio/wiki-narrativo-ai-maintained/#frontmatter-eseguibile-estensione-v2-0-per-rule-engine)) Lo schema vive in MD-Karpathy editabile da UI. Esempio: ```markdown --- id: C001 categoria: lavastoviglie versione: 1.1 # bump quando aggiungi nuovo brand o nuovo attributo ereditato_da: C000_elettrodomestico_incasso attributi: - { nome: larghezza_cm, tipo: measurement, unita: cm, valori_tipici: [45, 60] } - { nome: classe_energetica, tipo: select, valori: [A, B, C, D, E, F, G] } - { nome: capacita_coperti, tipo: measurement, unita: coperti } - { nome: tipo_incasso, tipo: select, valori: [totale, scomparsa_parziale, libero_installazione] } - { nome: brand, tipo: select, valori: [bosch, whirlpool, bsh, NEW_BRAND] } # AGGIUNGI QUI - { nome: modello, tipo: text } - { nome: codice, tipo: text, identifier: true } --- # C001 - Lavastoviglie (narrativa) ``` ### Disciplina di versioning - Bump **patch** (1.1.1) per fix typo o riformulazione narrativa. - Bump **minor** (1.2) per nuovo attributo o nuovo brand aggiunto. - Bump **major** (2.0) per ristrutturazione (es. attributi rinominati: serve migration dei dati prodotto esistenti). ### Errori comuni - **Aggiungere attributi "ad-hoc per il nuovo brand"**: significa inventare attributi che il brand precedente non ha mai avuto. Rompe la query cross-brand. Soluzione: o l'attributo e' valido per **tutti** i brand della categoria (e va nello schema condiviso), o vive nel **wiki narrativo del brand specifico** (non nel PIM). - **Confondere `valori_tipici` con `valori`**: `valori` (per `select`) e' la lista chiusa ammessa (validata). `valori_tipici` (per `measurement`) e' indicativa per UI/suggerimenti, ma il valore reale puo' essere qualsiasi numero nell'unita'. - **Saltare la versione**: senza bump, il PIM non sa quale schema applicare alla categoria. La validazione fallisce silenziosamente. ### Prompt template per Claude Code (Fase 3) ``` Devo aggiungere il brand "{NOME_BRAND}" alle categorie esistenti del PIM del nostro stack CPQ v2.1. Categorie del brand (da decidere insieme): {ELENCO_CATEGORIE_BRAND} es: ["lavastoviglie", "frigorifero_incasso", "forno_incasso"] Riferimenti: - /studio/stack-agentico-pmi/#3b-pim-lite-per-attributi-cross-brand - /studio/playbook-onboarding-catalogo-pdf/#fase-3-definizione-categoryschema-pim-lite Per ogni categoria del brand: 1. Leggi wiki_/categorie/C*.md esistenti 2. Se la categoria esiste gia' (es. C001_lavastoviglie.md), proponi un diff minimale: aggiungere il brand al valori di 'brand', bump versione 3. Se la categoria e' nuova, proponi un nuovo C00N_.md scaffold guardando il manuale del brand per estrarre attributi tipizzati comuni (larghezza, classe, capacita, ecc.) Output: - diff per ogni file MD esistente - bozza completa per ogni file MD nuovo - domande aperte (es: "questo attributo del brand X non esiste in Y, ti conviene metterlo nel wiki narrativo invece che nello schema?") Non scrivere ancora i file: voglio review prima. ``` ### Deliverable Fase 3 - `wiki_/categorie/C*.md` aggiornati o nuovi. - Bump versione di ogni categoria modificata. - Test al PIM (al boot deve ricaricare lo schema senza errori). --- ## Fase 4: cura wiki narrativo del brand (D*/G*/R*/F*) Adesso scrivi il wiki Karpathy del brand. Quattro categorie di file (vedi [wiki narrativo cap. 2](/studio/wiki-narrativo-ai-maintained/#2-wiki-autoritativo-a-4-categorie)): | Categoria | File | Cosa contiene | Quando crearne uno | |---|---|---|---| | **Distinte** (`D*.md`) | `wiki_/distinte/D001_.md` | Composizione canonica per famiglia (lista componenti, pattern codici, optional, errori da non fare) | Una per famiglia "configurabile" | | **Guide** (`G*.md`) | `wiki_/guide/G002_.md` | Decision tree narrativo per scelte ("scegli X vs Y vs Z") | Una per asse decisionale ricorrente | | **Regole** (`R*.md`) | `wiki_/regole/R002_.md` | Regola tecnica con frontmatter eseguibile (vedi Fase 5) | Una per regola/formula trasversale | | **Famiglie** (`F*.md`) | `wiki_/famiglie/F001_.md` | Scheda narrativa "cos'e' X, quando sceglierla, quando NO" | Una per famiglia user-facing | ### Ordine consigliato di scrittura 1. **F* (famiglie) prima**: sono il piu' veloce da scrivere e danno una "anagrafica narrativa" di base. 1 famiglia = 1 ora circa. 2. **G* (guide) seconde**: danno il decision tree che il bot useremo per i casi "come scelgo". 3. **D* (distinte) terze**: piu' complesse, richiedono di mettere insieme codici reali. 1 distinta = 2-3 ore. 4. **R* (regole) per ultime**: vedi Fase 5 dedicata, con frontmatter eseguibile. ### Strumenti operativi - **Editor wiki UI** (esempio gia' presente nel template di riferimento): browser su `http:///editor` con whitelist email. Scrittura assistita + AI auto-fix sui feedback. - **Validator AI pre-save** (vedi [v2.0 sezione 3A](/studio/stack-agentico-pmi/#3a-knowledge-tools-per-brand-constellation-di-mcp)): blocca syntax MD rotta, codici Blum inventati (cit), sigle interne user-facing. - **Lint workflow mensile**: orphan pages, stale claims, concept menzionati senza scheda. Task DoIt schedulato. ### Prompt template per Claude Code (Fase 4) ``` Devo scrivere il wiki narrativo per il brand "{NOME_BRAND}", categoria "{CATEGORIA}" (es. lavastoviglie). Riferimenti: - /studio/wiki-narrativo-ai-maintained/#2-wiki-autoritativo-a-4-categorie - /studio/playbook-onboarding-catalogo-pdf/#fase-4-cura-wiki-narrativo-del-brand - pattern di esempio: wiki_/famiglie/F001_.md (usa il primo Knowledge Tool brand esistente come modello) Spawna un sub-agente Explore (max 1, NO parallelo per evitare rate limit) che: 1. Legge families//extracted.md per la sezione della categoria scelta 2. Identifica le famiglie principali (es. lavastoviglie scomparsa totale, parziale, libero installazione) 3. Per la famiglia che ti indico, scrive bozza F001_.md seguendo il template di riferimento: - frontmatter YAML (id, brand, categoria, pag_start, pag_end) - sezione "Cos'e'": narrativa user-facing - sezione "Quando sceglierla / Quando NO": consulenza - sezione "Codici tipici": SOLO codici verificati nell'estratto - sezione "Vedi anche": cross-link [[D001 ...]] [[G001 ...]] [[R001 ...]] CRITICO: NON inventare codici articolo. Se non vedi il codice nell'extracted.md, NON metterlo. Se hai dubbi, scrivi `# TODO verificare manualmente` e segnalalo. Output: bozza MD completa, lista codici usati con riferimento a pagina manuale, lista TODO da verificare manualmente. ``` ### Errori comuni - **Inventare codici**: il LLM tende a "completare" codici plausibili. Soluzione: validator AI pre-save che cerca pattern di codici e li verifica contro extracted.md / DB. - **Cross-link rotti** (`[[D001 ...]]` che non esiste): lint workflow li trova. Fix: rinominare correttamente o rimuovere il link. - **Sigle interne user-facing**: l'esperto del brand usa sigle (KH, FH, NL, LF nel caso ferramenta) che il bot rivolto al cliente non deve usare senza spiegare. Validator AI le segnala. - **Spawn troppi sub-agenti in parallelo**: rate limit Anthropic. Massimo 2 sub-agenti contemporaneamente, meglio 1. ### Deliverable Fase 4 - `wiki_/famiglie/F*.md` (una per famiglia user-facing, ~5-15 file) - `wiki_/guide/G*.md` (5-10 file decision tree) - `wiki_/distinte/D*.md` (per famiglie configurabili, 5-15 file) - 0 broken link interni (verifica con script `audit_wiki.py`) - 0 codici inventati (verifica con `audit_wiki.py` cross-ref con extracted.md) --- ## Fase 5: definizione Rule (R*.md frontmatter eseguibile) Le regole tecniche del brand, scritte come MD-Karpathy con frontmatter eseguibile per Rule Engine deterministico (vedi [wiki narrativo: frontmatter eseguibile](/studio/wiki-narrativo-ai-maintained/#frontmatter-eseguibile-estensione-v2-0-per-rule-engine)). ### Cosa cattura una regola Vincoli tecnici espliciti, non interpretazioni discorsive. Esempi: - "Se anta > 18 kg, usa cerniera high-load" -> regola con condizione su peso_anta_kg. - "Per lavastoviglie 60 cm e cassetto retrostante < 560 mm, segnala rischio nicchia" -> regola cross-modulo. - "Per cucina 60 con sistema cassetti X, profondita' minima nicchia elettrodomestico Y mm" -> regola cross-categoria. ### Schema del frontmatter ```yaml id: R005 nome: "Cassetto: set guida X vs Y per portate alte" ambito: . # es. blum.legrabox, bosch.lavastoviglie, cross.compat versione: 1.0 fonte_pagine_fis: [248] # pagine manuale per citazione condizione: all_of: - { fatto: famiglia, op: "=", valore: legrabox } - { fatto: portata_kg, op: ">", valore: 40 } - { fatto: NL_mm, op: ">", valore: 500 } azione: warn # allow | warn | suggest | block messaggio_template: "Per portata {{portata_kg}}kg e NL {{NL_mm}}mm usa Y, non X." test_cases: - { in: {famiglia: legrabox, portata_kg: 70, NL_mm: 600}, expect: warn } - { in: {famiglia: legrabox, portata_kg: 40, NL_mm: 400}, expect: pass } ``` ### Da dove arrivano le regole Tre fonti pratiche, in ordine di valore: 1. **Manuale del fornitore** (sezioni "informazioni applicative", "regole di montaggio", "vincoli tecnici"). E' il giacimento principale. 2. **Esperti interni** che lavorano col brand da anni (commerciali tecnici, tecnici showroom). Conoscono i casi limite e gli errori frequenti. 3. **Feedback degli utenti** del bot (segnalazioni di errori): quando un utente segnala "il bot mi ha consigliato X ma non funziona", spesso emerge una regola che mancava. ### Strumento: editor regole L'editor wiki UI espone i `R*.md` come gli altri file. L'esperto puo' modificare body narrativo + tabella decisione + (con minimo training) il blocco `condizione` del frontmatter. Il **validator AI pre-save** verifica: - Syntax YAML del frontmatter. - `condizione` ben formata (combinatori validi, fatti referenziati). - `test_cases` passano (ogni test in -> azione attesa). ### Prompt template per Claude Code (Fase 5) ``` Devo estrarre le regole tecniche per il brand "{NOME_BRAND}", categoria "{CATEGORIA}" dal manuale (families//extracted.md). Riferimenti: - /studio/wiki-narrativo-ai-maintained/#frontmatter-eseguibile-estensione-v2-0-per-rule-engine - /studio/stack-agentico-pmi/#3c-rule-engine-deterministico - /studio/playbook-onboarding-catalogo-pdf/#fase-5-definizione-rule Spawna un sub-agente che: 1. Legge la sezione "informazioni applicative" / "regole tecniche" / "tabelle vincoli" del manuale (~10-30 pagine tipiche) 2. Identifica regole esplicite: vincoli numerici, soglie, decision tree 3. Per ogni regola identificata, propone un R*.md con: - frontmatter standard (id, nome, ambito, versione, fonte_pagine_fis) - condizione formalizzata in DSL JSON (all_of/any_of, fatto/op/valore) - azione (allow/warn/suggest/block) - messaggio_template (con {{var}} per substitution) - test_cases (almeno 2: uno che triggera, uno che non triggera) 4. Per ogni regola, body narrativo con: contesto, tabella decisione, note, vedi-anche (cross-link) CRITICO: se la regola non e' esplicita nel manuale ma "dedotta", segnala 'inferenza-non-verificata' e NON la aggiungere automaticamente. Le regole inventate dal LLM sono pericolose. Output: lista R*.md proposti, lista regole "inferite" da verificare con esperto, eventuali pagine vision-needed (se la regola sta in un disegno quotato che MinerU non ha catturato bene). ``` ### Errori comuni - **Regole inferite ma non documentate**: il LLM "estrapola" da poche evidenze. Soluzione: marker esplicito "inferenza non verificata", validare con esperto prima di committare. - **Test cases che non riflettono la regola**: il test deve essere il caso piu' rappresentativo della condizione, non un edge case strano. - **Ambito troppo largo o troppo stretto**: `ambito: bosch` (troppo largo) vs `ambito: bosch.lavastoviglie.modello_X` (troppo stretto). Tipicamente: `.` o `cross.compat` per cross-modulo. ### Deliverable Fase 5 - `wiki_/regole/R*.md` (5-15 file tipici per brand maturo) - Tutti i `test_cases` passano (pre-commit hook) - 0 regole "inferite-non-verificate" mergiate nel main --- ## Fase 6: build Knowledge Tool MCP Adesso impacchetti il wiki + dataset come MCP server. Il template e' il primo Knowledge Tool brand esistente. ### Componenti del Knowledge Tool ``` -knowledge-mcp/ ├── data/ │ ├── chunks.db # SQLite con chunks + embeddings │ ├── codici.db # SQLite con anagrafica codici (da extracted.md) │ └── manuale_pages/ # JPG citazioni inline ├── wiki_/ # MD-Karpathy editabile (D*/G*/R*/F*) ├── server.py # FastMCP entry point ├── tools/ │ ├── cerca_knowledge.py # hybrid retrieval BM25 FTS5 + cosine + RRF │ ├── dettaglio_codice.py # lookup esatto │ ├── valida_compatibilita.py # validazione consulenziale (chiama Rule Engine) │ └── ... (tool dominio-specifici) ├── pipeline/ │ ├── chunk_wiki.py # chunking dei MD │ ├── embed_chunks.py # sentence-transformers 768d │ └── reindex.py # rebuild DB ├── Dockerfile └── requirements.txt ``` ### Tre tool minimi di contratto comune (obbligatori) Ogni Knowledge Tool brand deve esporre **questi tre tool** per essere consumabile dal Supervisor: ```python @mcp.tool() def cerca_knowledge(query: str, top_k: int = 5, alpha: float = 0.5) -> list[dict]: """Hybrid retrieval BM25 + cosine + RRF nel wiki di questo brand.""" # vedi /studio/stack-agentico-pmi/#3a-knowledge-tools-per-brand-constellation-di-mcp @mcp.tool() def dettaglio_codice(codice: str) -> dict: """Lookup esatto di un codice nel catalogo di questo brand.""" @mcp.tool() def valida_compatibilita(prodotto: dict, contesto: dict) -> dict: """Validazione consulenziale. Applica regole tecniche del brand (chiama Rule Engine). Ritorna {ok, status, motivazione, alternative}.""" ``` Plus tool dominio-specifici (assemble_distinta, get_regola, get_media, ecc.) come per il primo Knowledge Tool. ### Strumenti - FastMCP Python - SQLite con FTS5 nativo (per BM25 hybrid) - sentence-transformers (mpnet 768d) per embedding - Docker Compose per deploy ### Prompt template per Claude Code (Fase 6) ``` Devo costruire il Knowledge Tool MCP per il brand "{NOME_BRAND}". Template di riferimento: il primo Knowledge Tool brand esistente (es. -knowledge-mcp). Riferimenti: - /studio/stack-agentico-pmi/#3a-knowledge-tools-per-brand-constellation-di-mcp - /studio/playbook-onboarding-catalogo-pdf/#fase-6-build-knowledge-tool-mcp Spawna un sub-agente che: 1. Clona la struttura del template -knowledge-mcp in -knowledge-mcp 2. Sostituisce wiki_/ con wiki_/ (i file scritti in Fase 4-5) 3. Lancia pipeline/chunk_wiki.py per chunkare D*/G*/R*/F* 4. Lancia pipeline/embed_chunks.py per generare embeddings 768d 5. Verifica che i 3 tool minimi di contratto comune siano implementati e funzionanti (smoke test su query di esempio) 6. Aggiunge tool dominio-specifici se utili (es. assemble_distinta per famiglie configurabili) 7. Build Docker, deploy in docker-compose.yml accanto agli altri MCP server 8. Smoke test: chiama cerca_knowledge("query test"), verifica risposta Output: report di build + log dei test + URL endpoint MCP per registrazione nel Supervisor. ``` ### Smoke test obbligatori prima di andare alla Fase 7 - `cerca_knowledge("una query rappresentativa")` ritorna almeno 1 chunk con score > 0.5. - `dettaglio_codice("")` ritorna l'anagrafica completa. - `valida_compatibilita({...}, {...})` ritorna struttura `{ok, status, motivazione}` per un caso di test. ### Deliverable Fase 6 - Repo `-knowledge-mcp/` deployato e in esecuzione. - 3 smoke test passati. - Documentato in `decisioni_arch/-deploy--knowledge.md`. --- ## Fase 7: integrazione con Supervisor (Filter-then-Validate) Adesso registri il nuovo Knowledge Tool nel Supervisor e attivi i flussi cross-brand che includono il brand. ### Step 1. **Aggiungi mapping `brand -> knowledge_tool`** nel Supervisor: ```python knowledge_tools = { "blum": BlumKnowledgeMCP("http://blum-knowledge:8000"), "bosch": BoschKnowledgeMCP("http://bosch-knowledge:8000"), "": NewBrandKnowledgeMCP("http://-knowledge:8000"), # AGGIUNGI } ``` 2. **Verifica che il PIM ha il brand nei `valori`** della categoria (Fase 3). 3. **Smoke test Filter-then-Validate cross-brand**: lancia una query come quelle del [Flusso 5](/studio/flussi-agentici-pmi/#flusso-5-query-strutturata-cross-brand-filter-then-validate). Verifica che: - PIM include il nuovo brand nei candidati; - Knowledge Tool del nuovo brand viene chiamato con `valida_compatibilita`; - Sintesi mostra il nuovo brand nelle 3 colonne (compatibile / consigliato / sconsigliato). 4. **Aggiorna eval set**: aggiungi 5-10 casi di test che includono il nuovo brand. Lancia regression: tutti devono passare. 5. **Deploy in produzione**: rolling con osservabilita' attiva (Langfuse trace di ogni query nei primi giorni). 6. **Monitor 1 settimana**: occhio ai trace Langfuse per casi anomali. Tipico: regole troppo strette o troppo lasche, codici inventati nel wiki, vincoli cross-modulo non triggerati. ### Prompt template per Claude Code (Fase 7) ``` Devo integrare il nuovo brand "{NOME_BRAND}" nel Supervisor v2.1. Riferimenti: - /studio/stack-agentico-pmi/#livello-2-supervisor-pattern-filter-then-validate - /studio/flussi-agentici-pmi/#flusso-5-query-strutturata-cross-brand-filter-then-validate - /studio/playbook-onboarding-catalogo-pdf/#fase-7-integrazione-con-supervisor-filter-then-validate Spawna un sub-agente che: 1. Aggiorna il mapping knowledge_tools nel Supervisor aggiungendo il nuovo brand 2. Verifica nel PIM che il valore "{nome_brand_kebab}" sia nei `valori` della categoria (vedi Fase 3) 3. Aggiunge 5-10 casi di test all'eval set, includendo: - 1 lookup esatto codice del nuovo brand - 2 query consult single-brand (consulenza tecnica) - 2 query search cross-brand (Filter-then-Validate) - 1 query con vincolo cross-modulo che triggera Rule Engine - 1 edge case noto del brand 4. Lancia regression eval: tutti i casi (vecchi + nuovi) devono passare 5. Smoke test live di una query Flusso 5 cross-brand Output: log eval, log smoke test, eventuali fail con root cause analysis, checklist deploy production. ``` ### Errori comuni - **Knowledge Tool non risponde**: rete Docker, MCP transport, env vars. Verifica con `curl` diretto al tool. - **Eval regression**: nuovi test passano ma quelli vecchi fail -> il nuovo Knowledge Tool ha "rubato" candidati ad altri brand (es. PIM ranking sballato). Soluzione: rivedere il PIM ranking o tarare alpha del hybrid retrieval. - **Vincoli cross-modulo non triggerati**: il Rule Engine non vede il nuovo brand nelle regole `cross.compat`. Soluzione: aggiornare le regole cross.compat per includere il nuovo brand dove rilevante. ### Deliverable Fase 7 - Supervisor riconosce il nuovo brand. - Eval set esteso (target: 5-10 casi nuovi, tutti passano). - Smoke test live con Flusso 5 cross-brand riuscito. - Deploy production con osservabilita' attiva. --- ## Stima tempi realistica | Brand # | Tempo totale | Note | |---|---|---| | 1° (setup base) | ~10-13 settimane | Vedi [Migration path BlumCat -> v2.1](/studio/stack-agentico-pmi/#stack-minimo-per-partire). Include impostazione Supervisor, PIM, Rule Engine. | | 2° brand | 2-3 settimane | Primo onboarding "vero" col playbook. La maggior parte del tempo va in Fase 4 (wiki narrativo) e Fase 5 (regole). | | 3° brand | 1-2 settimane | Hai gia' il template, sai dove sono le insidie. | | 4°-5° brand | 5-7 giorni | Pipeline rodata. Bottleneck: cura wiki narrativo (non automatizzabile, richiede esperto). | | 6°+ brand | 3-5 giorni | Bottleneck: solo cura wiki. Tutto il resto e' "premere play". | | 30° brand (orizzonte 2-3 anni) | 2-3 giorni | A regime: pipeline industriale. | La pipeline tecnica (estrazione, build MCP, integrazione Supervisor) si automatizza progressivamente. La cura del **wiki narrativo brand-specifico** (D*/G*/R*/F* curati da un esperto del brand) NON si automatizza. E' il vero collo di bottiglia. Pianifica risorsa esperto interno fin dal primo brand: senza, scali al 5° brand e poi ti blocchi. ## Anti-pattern e errori comuni Sintesi dei rischi visti finora, da tenere come checklist mentale prima di ogni fase. ### Errori di processo - **Saltare i pre-requisiti** ("tanto sistemo dopo"): porta a setup half-baked che si bloccano a meta'. La checklist e' un gate, non una formalita'. - **Brand pilota scelto a caso**: porta a costi/risultato squilibrati. Il primo brand "vero" e' quello che insegna; sceglierlo male significa imparare le lezioni sbagliate. - **Cura wiki delegata all'AI senza supervisione esperto**: il LLM scrive narrativa plausibile ma con codici inventati e regole inferite. Il validator AI cattura il 70%, ma il restante 30% richiede esperto umano. ### Errori tecnici - **Schema PIM "ad-hoc per il nuovo brand"**: rompe la query cross-brand. Schema condiviso per categoria, brand come attributo. - **Inventare codici**: il LLM tende a "completare". Validator AI + audit script cross-ref con extracted.md. - **Cross-link rotti**: lint workflow mensile. Pre-commit hook se possibile. - **Spawn sub-agenti in parallelo > 2**: rate limit Anthropic. Massimo 2, meglio 1. - **Saltare smoke test**: deploy senza smoke test = deploy con bug nascosti che emergono in produzione. Mai. ### Errori conversazionali (col bot in produzione) - **Knowledge Tool che inventa codici**: 1 segnalazione utente -> apri il wiki, cerca il codice, se non c'e' aggiungi nota "codice X non disponibile nel catalogo Y" come F*. - **Filter-then-Validate che non triggera vincoli cross-modulo**: il Rule Engine non vede il nuovo brand. Aggiornare regole `cross.compat`. - **Three-state UI confusa** (compatibile / consigliato / sconsigliato): se l'utente non capisce la distinzione, rivedere il template di sintesi del Supervisor. ## Prompt template generale per Claude Code Quando inizi una sessione Claude Code per qualsiasi onboarding nuovo brand, **comincia sempre con questo prompt** (assicura coerenza con stack v2.1): ``` Sto onboardando il brand "{NOME_BRAND}" come Knowledge Tool nel nostro stack CPQ multi-brand v2.1. Contesto canonico (riferimenti autoritativi): - Stack v2.1: https://andreapellizzari.it/studio/stack-agentico-pmi/ - Flussi v1.3: https://andreapellizzari.it/studio/flussi-agentici-pmi/ - Wiki narrativo Karpathy: https://andreapellizzari.it/studio/wiki-narrativo-ai-maintained/ - Playbook 7 fasi: https://andreapellizzari.it/studio/playbook-onboarding-catalogo-pdf/ Vincoli: - Modello bi-dimensionale (knowledge verticale per brand + cross-cutting orizzontale per dato strutturato) - Pattern Filter-then-Validate per query cross-brand (PIM filtra, Knowledge valida, tre stati: compatibile/consigliato/sconsigliato) - Karpathy come fondazione di rappresentazione del knowledge editabile (NON come architettura completa) - Hybrid retrieval BM25 FTS5 + cosine + RRF (k=60) come default in cerca_knowledge - Niente codici inventati: sempre verificati contro extracted.md o DB - Sub-agenti spawn max 2 in parallelo per evitare rate limit Anthropic Sono alla Fase {N} del playbook. Aiutami con: {DESCRIZIONE_TASK}. Prima di scrivere codice o file, conferma che hai letto i 4 riferimenti sopra e dimmi che approccio segui. ``` Questo prompt forza il LLM a: - Caricare il contesto dei 4 documenti canonici (via web fetch). - Esplicitare quali vincoli sta rispettando. - Allinearsi alla terminologia v2.1 (Knowledge Tool, Filter-then-Validate, Karpathy, hybrid retrieval). - Chiedere conferma di approccio prima di partire (riduce drift architetturale). - Onboarding di un nuovo brand a uno stack CPQ multi-brand v2.1 si fa in **7 fasi sequenziali**: scelta pilota -> estrazione PDF -> CategorySchema PIM -> wiki narrativo brand -> Rule frontmatter eseguibile -> build Knowledge Tool MCP -> integrazione Supervisor. - Il **bottleneck strutturale** e' la cura del wiki narrativo (D*/G*/R*/F*): non si automatizza, richiede esperto interno. Pianifica risorsa fin dal primo brand. - I **prompt template per Claude Code** in ogni fase forzano coerenza con lo stack v2.1 (richiamando i 4 documenti canonici come contesto). - Stima tempi realistica: 2-3 settimane il secondo brand, 3-5 giorni il decimo, 2-3 giorni a regime. - Errore piu' frequente: **inventare codici**. Validator AI + audit script cross-ref con extracted.md sono il guard. Per il modello architetturale completo entro cui questo playbook vive, vedi [Agentizzare una PMI: stack v2.1](/studio/stack-agentico-pmi/). Per il pattern del livello knowledge editabile (wiki Karpathy), vedi [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/). Per esempi end-to-end di flussi che includono il nuovo brand una volta integrato, vedi [Agentizzare un'azienda: timeline e flussi reali](/studio/flussi-agentici-pmi/). Due nuovi check-point operativi assorbiti dal playbook (la skill `brand-onboarding` interna e' passata da v2.1 a v2.3 in due settimane), e un nuovo gate di chiusura. (1) **CP10.5 Knowledge Graph indexing**: dopo la cura del wiki narrativo (CP7-CP10), lancia `python chatbot/scripts/wiki_indexer.py [--rebuild]` per popolare in pim.db le tabelle additive wiki_entities + wiki_edges. L'indexer e' incrementale via content_hash SHA-256 (~50ms tipici) e usa lo stesso pattern `_extract_wiki_refs` (3 regex: prose "vedi R001", paren "(R001)", MD link, bullet "- R001 peso...") gia' presente nei Knowledge Tool brand-specifici. Baseline 5 brand: 166 wiki_entities + 557 wiki_edges. Se l'indexer mostra meno di 50 edges totali per il brand, qualcosa nel wiki e' rotto (prosa che non cita identificatori R/F/D/C, o slug non-ID tipo "aventos_hk_top" che l'indexer skippa). Verifica anti-pattern: famiglie con slug non-ID vanno rinominate o accettate come gap noto. (2) **CP11 Catalog RAG split + ingest** (nuovo, quando il PDF brand e' un catalogo voluminoso, non un singolo datasheet): usa `split_brand_pdf.py` (generico) per dividere il PDF in chunks anchor-aware; lancia `document_ingester.py` per estrarre entities + chunks BGE-M3 + mentions edges (lookup-driven SKU detection per codici numerici, EAN, codici con spazio tipo Foster "8100 615") e popolare `document_vectors.db` con sqlite-vec. Baseline 5 brand: 1973 entities + 2295 chunks + 7550 mentions edges su 1980 PDF totali (20MB DB). I tre tool LLM-callable risultanti (`search_documents`, `get_document_section`, `find_documents_by_sku`) hanno schema dual-purpose `source_type` (datasheet vs catalog_page). Gotcha critico: il server cache pim.db stale dopo `wiki_indexer.py --rebuild` -> HTTP 500 -> restart obbligatorio. (3) **Gate post-onboarding: refresh orchestrator + check_contracts**: lancia `python chatbot/scripts/refresh_arcocat.py --rag-refresh` come hard gate prima di considerare il brand "onboardato". Il refresh lega 6 step (wiki indexer, document ingester, SKU audit invariants, wiki vs pim drift L15, Fase 1 alignment L16, check_contracts cross-validator 8 policy, regression 27 golden) con severity aggregata OK/WARN/FAIL exit 0/1/2. Aggiungere il brand a `domain_contracts/valid_brands.yaml` + `brand_routing_patterns.yaml` + `sku_regex.yaml` + (se serve invariants categoria) `sku_invariants.yaml`: `check_contracts` valida regex SKU contro DB reale (5/5 brand devono passare). Skill brand-onboarding interna versionata v2.3 con 39 anti-pattern accumulati (vs ~30 al momento della prima stesura del playbook); i 3 nuovi anti-pattern significativi sono: wiki convention con slug-ID obbligatorio (#37), alias registry come "data not code" obbligatorio per ogni concept ambiguo (#38), validate_sku via SKUInvariants invece che check inline nel populator (#39). Tempi onboarding aggiornati come **forchetta misurata**, non come "tot": primo brand 5-8 giorni (PDF voluminoso con catalog RAG attivo), secondo brand 3-5 giorni (skill ormai stabile), brand 3-5 in fascia 1-3 giorni se il dominio e' coerente con i precedenti (es. tutti elettrodomestici flat) e 3-5 giorni se introduce categorie nuove (es. ferramenta vs elettrodomestici, stressa compatibilita' meccaniche). Anti-pattern di processo nuovo (#3-bis): **non onboardare due brand "simili" di fila come stress test**; il vero stress per la maturita' dello stack e' un brand strutturalmente diverso (Hafele/Hettich come 6o brand se sei partito da Blum/Falmec, NON Smeg che e' elettrodomestici flat e ti dice solo che la pipeline gira). Studio strutturalmente coerente con v2.4 dello stack; le 7 fasi originali restano valide, sono solo arricchite di 2 check-point nel finale e di un gate di chiusura. Esplicitato il meta-pattern **"Claude-assisted ingest"** in cima al playbook. Tutte le fasi (estrazione PDF, definizione CategorySchema, cura wiki narrativo, definizione Rule, build Knowledge Tool MCP) sono Claude-assisted: developer + Claude Code in sessione iterativa scrivono gli script Python che fanno il lavoro pesante. L'esperto interno **non edita YAML/JSON**, corregge body narrativo + aggiunge nozioni mentali. Cross-link al master v2.3 dove il razionale strutturale e' formalizzato (riduzione effort 5-10x rispetto a pattern data engineering tradizionale). Tempi di onboarding nelle tabelle restano validi: questa e' chiarificazione di metodo, non revisione tempi. Allineamento versioni dei riferimenti documentali: bumpati i riferimenti "stack v2.0" → "stack v2.1" e "flussi v1.2" → "flussi v1.3" per riflettere le versioni correnti dei documenti citati nei prompt template per Claude Code. I prompt ora puntano alle versioni piu' aggiornate degli studi correlati. Il design architetturale resta v2.0 (la versione 2.1 e' un allineamento tooling, non un nuovo rework). Prima stesura. Playbook operativo a 7 fasi per onboarding di un nuovo catalogo brand (PDF voluminoso) come Knowledge Tool dentro lo stack CPQ multi-brand v2.1. Per ogni fase: cosa fare, strumenti, deliverable, prompt template per Claude Code, errori comuni. Stima tempi realistica per il primo, secondo, decimo brand. Anti-pattern di processo e tecnici. Prompt template generale per Claude Code che forza coerenza con i 4 documenti canonici dello stack v2.1 (stack, flussi, wiki narrativo, questo playbook). --- ## Agentizzare una PMI: stack, architettura, strumenti URL: https://andreapellizzari.it/studio/stack-agentico-pmi/ Versione: 2.4 Aggiornato: 2026-05-14 Keywords: agenti AI, MCP server, Inngest, Langfuse, PMI, architettura, memoria agentica, execution model, event-driven, PIM, CPQ, supervisor pattern, filter-then-validate, slot filling, rule engine, configuration context, knowledge tools, Karpathy, RRF > Mappa architetturale per costruire sistemi agentici dentro una PMI manifatturiera. Modello bi-dimensionale (knowledge verticale per brand + cross-cutting orizzontale per dato strutturato), cinque livelli (typed query, supervisor filter-then-validate, fonti eterogenee, configuration context, infrastruttura), framing CPQ, sette principi guida, modello di esecuzione asincrono, memoria a tre livelli, quattro contratti tipizzati, strumenti open source, dimensionamento VPS. Nelle ultime settimane mi sono trovato a disegnare lo stack tecnico per "agentizzare" alcune funzioni di una PMI manifatturiera italiana del Nordest, l'azienda per cui lavoro come sviluppatore in-house. Non un esperimento, non un POC: un sistema che dovra' reggere mesi di esercizio, costare poco, restare manutenibile da una persona sola. Questo studio raccoglie le scelte fatte e i ragionamenti dietro. Vale per chi ha la mia stessa scala (PMI con qualche decina di persone, gestionale Passepartout, sviluppo interno) e vuole introdurre agenti AI senza inseguire il framework di moda. La versione 2.0 di questo studio (maggio 2026) e' una riscrittura strutturale. Il design v1.x modellava il problema su un solo asse (constellation di agenti per brand). Un caso d'uso concreto ("lavastoviglie 60 classe A" cross-brand) ha fatto emergere la sua incompletezza, e una review architetturale comparativa (PIM, federated search, multi-agent supervisor pattern Anthropic, CPQ classico) ha fatto emergere il modello bi-dimensionale che oggi propongo. La sezione [Pickup selettivi da letteratura industriale](#pickup-selettivi-da-letteratura-industriale) traccia il percorso di triangolazione che ha portato qui. Questo studio fa parte di una serie di 4 documenti tecnici interconnessi + 2 presentazioni a slide. Per orientarvi nel quadro completo (ordine di lettura consigliato, cronologia evoluzione, glossario, fonti di triangolazione) vedete la **[Mappa dello studio](/presentazioni/mappa-studio.html)**: indice navigabile con cards per ogni documento, sintesi 1-minuto del modello, cheat sheet dei concetti fondamentali. Aggiornata di pari passo a ogni bump versione. Questo studio resta la mappa architetturale di riferimento. L'evoluzione successiva (arcocat come piattaforma server con client separati, e l'assorbimento agentico progressivo dell'app gestionale tradizionale) e' raccontata in [Arcocat come piattaforma agentica](/studio/arcocat-infrastruttura/). ## Sette principi guida Prima dello stack tecnico, sette regole che reggono l'architettura. Sono la cosa che separa un giocattolo demo da un sistema aziendale. **1. Determinismo dove possibile, LLM dove serve giudizio.** Un workflow agentico aziendale e' fatto in massima parte da codice deterministico. Gli agenti LLM sono nodi dentro questo codice, non il codice stesso. **2. Gli agenti non si auto-orchestrano.** Mai. Anche quando sembra di si' (un agente che chiama altri agenti), sotto deve esserci un layer deterministico che gestisce schedule, dipendenze, retry, persistenza, approval, audit. **3. Stesso core, facciate multiple.** La logica di business (es. "interagire col gestionale") vive in un solo posto. Sopra ci si mettono facciate diverse a seconda del consumatore: libreria Python per app deterministiche, REST per il web, MCP server per agenti LLM. **4. Lego, non monoliti.** Componenti specializzati, ognuno fa una cosa bene, ognuno e' sostituibile. Piu' sforzo iniziale del framework "all-in-one", ma in due anni e' la differenza tra "scelgo quello che cambia" e "sono incastrato in un framework abbandonato". **5. Observability dal giorno zero.** Instrumentare un sistema "dopo" e' sempre dolore. Una riga di codice in piu' al primo deploy evita refactor a sei mesi. **6. Hard rules nel codice, non nei prompt.** Cose critiche (limiti spesa, allowlist azioni, approval gate) vivono in `if/else` Python o TypeScript, non nel system prompt. Un prompt si aggira con prompt injection, una logica deterministica no. **7. Auditabilita' e' un requisito, non un'aggiunta.** Ogni decisione di un agente deve essere ricostruibile a posteriori. AI Act EU 2026, GDPR, e semplice buon senso commerciale lo richiedono. **8. Cost variability esplicito (aggiunto in v2.2).** Il costo agentico NON e' flat per conversazione: puo' variare 5-50x tra una query semplice (lookup, ~0.01 EUR) e una multi-step (configurazione cucina + Mexal + promo + Knowledge Tools, ~0.30-0.50 EUR). Pricing flat per cliente e' insostenibile. Hard rule: budget cap esplicito per session in codice (Layer C). Lezione presa dal cambio retroattivo di GitHub Copilot agentic billing (aprile 2026): chi non lo prevede paga il doppio. ## Framing: stiamo costruendo un CPQ, non un chatbot Punto di metodo che la v1.x non aveva esplicitato. Il sistema che descrivo non e' "un chatbot intelligente sul catalogo": e' un **CPQ** (Configure, Price, Quote) con interfaccia conversazionale. CPQ e' una categoria di software industriale documentata da almeno trent'anni. La differenza tra "chatbot prodotto" e CPQ: | | Chatbot prodotto | CPQ | |---|---|---| | Scope tipico | Find product | Configure + Price + Quote | | Stato di sessione | Storia conversazione | Configurazione strutturata | | Vincoli | Documentati a parole | Costraint Satisfaction Problem | | Output | Risposta testuale | Configurazione valida + offerta | | Validazione | LLM-based | Rule engine deterministico | | Failure mode | Allucina dato plausibile | Refuso esplicito ("non e' compatibile, perche'") | Riconoscere il framing CPQ ha tre conseguenze immediate sul design: 1. Non si modella il problema come "retrieval di prodotti". Si modella come "navigazione vincolata in uno spazio di configurazioni valide". 2. La valutazione dei vincoli e' un **Constraint Satisfaction Problem**, e l'LLM e' lo strumento sbagliato. Per i vincoli serve un evaluator deterministico (Rule Engine). 3. Lo stato della sessione non e' "history conversazione": e' una **Configurazione** strutturata che cresce in modo incrementale. Va persistita come oggetto tipizzato, non come testo. Questo non vuol dire copiare un CPQ enterprise (SAP, Salesforce, Oracle). Significa adottare il **modello mentale CPQ** e implementarlo con strumenti leggeri adatti alla scala PMI: Postgres + JSON Schema + Python evaluator + LLM per la parte conversazionale. Se in qualunque momento il design "scivola" verso "chatbot che cerca prodotti", abbiamo perso il filo. Il valore aggiunto verso i clienti non e' nel "trovare prodotti" (per quello esistono Amazon, marketplace B2B, configuratori brand). E' nella **consulenza tecnica vincolata**: "data l'anta da 18 kg con frontale in vetro, quale cerniera, quante, con che piastrina, perche'". E' ragionamento basato su regole, non similarita' semantica. ## Pattern dati: il modello bi-dimensionale Il design v1.x modellava il problema su un solo asse, **constellation per brand**: un mini-agente per brand, ognuno autonomo. Funziona se il dominio e' stretto e single-brand. Crolla appena entrano: - query strutturate cross-brand ("lavastoviglie 60 classe A" che attraversa Bosch + Whirlpool + BSH); - promo o bundle cross-categoria ("bundle che includono cerniere + lavastoviglie"); - offerte composite ("cucina 60 con cassetti LEGRABOX + lavastoviglie 60 classe A per cliente Rossi con sconti applicabili"). Il problema e' di modellazione: **brand verticale** e **categoria orizzontale** sono due dimensioni indipendenti. Modellarne solo una porta a federazione naive (un agente per brand chiamato in parallelo) che e' subottimale per i dati strutturati. ### I due assi **Asse verticale (per brand): conoscenza consulenziale** Regole tecniche, sigle interne (KH/FH/NL/LF), decision tree, distinte canoniche, formule. Questa conoscenza vive bene **per brand**: le regole Blum sono diverse dalle regole Bosch, la formula portata-cassetto Blum non si applica a Whirlpool. Modellata come **constellation di Knowledge Tools** (uno per brand), ognuno con il proprio wiki narrativo curato umanamente (pattern Karpathy, vedi [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/)). **Asse orizzontale (cross-brand): dato strutturato e business** Attributi tipizzati (lavastoviglie ha sempre `larghezza_cm`, `classe_energetica`, indipendentemente dal brand), prezzo, disponibilita', sconti cliente, promo, bundle. Questa parte vive bene **centralizzata**: avere uno schema "Lavastoviglie" duplicato in BoschCat e WhirlpoolCat e' la garanzia statistica di divergenza silenziosa. Modellata come **singleton MCP centralizzati**: PIM lite (attributi), Mexal MCP (business), Promo MCP (offerte), Rule Engine (vincoli cross-brand). ### Il pattern industriale di riferimento Akeneo PIM (open source, 20 anni di tradizione Product Information Management) modella esattamente cosi': **Family** (schema attributi per categoria prodotto) + **Category** (albero merceologico) + **Attribute** (tipizzato), con il **brand come attributo del prodotto**, non come entita' separata. Una rule reale di Akeneo concatena `brand + model -> product_title`. Questo modello industriale e' lo schema mentale che il design v2.0 adotta. Constellation per brand resta valido per la dimensione **knowledge consulenziale**. Si aggiunge il **PIM cross-brand** per la dimensione **dato strutturato**. I due non sono alternativi, sono **complementari** e operano su query diverse. ### Quando una query usa quale dimensione | Tipo query | Esempio | Dimensione attivata | |---|---|---| | Consulenza narrativa | "come scelgo cerniera per anta da 18kg?" | Knowledge tool brand (Blum) | | Filtro strutturato single-brand | "cerniera Blum 110° spessore 19mm" | Knowledge tool brand + suoi attributi | | Filtro strutturato cross-brand | "lavastoviglie 60 classe A" | PIM (singleton) | | Lookup esatto | "cosa e' 750.5001S?" | Knowledge tool brand | | Disponibilita' / prezzo | "ho la SMV68N20EU? prezzo per cliente Rossi?" | Mexal MCP | | Promo | "promo attive su lavastoviglie?" | Promo MCP | | Bundle cross-categoria | "bundle Blum + lavastoviglie?" | Promo MCP + Mexal | | Composizione complessa | "offerta cucina 60 cliente Rossi" | Tutti, con stato in Configuration Context | | Comparativo | "Bosch SMV68 vs Whirlpool W7" | PIM + 2 knowledge tools brand + Mexal | Il **Supervisor** (vedi [Livello 2](#livello-2-supervisor-pattern-filter-then-validate)) e' il routing intelligence che decide quali fonti chiamare per ogni query, in che ordine, con che pattern. Ogni Knowledge Tool brand espone, oltre ai tool specifici di dominio, **tre tool minimi di contratto comune**: cerca_knowledge(query, top_k) con hybrid retrieval (BM25 FTS5 + vector + Reciprocal Rank Fusion), dettaglio_codice(codice) per lookup esatto, valida_compatibilita(prodotto, contesto) per validazione consulenziale. Questi tre contratti permettono al Supervisor di federare i Knowledge Tools senza accoppiamento al dominio specifico. ## La mappa architetturale a cinque livelli Vista d'insieme. Ogni livello ha responsabilita' chiare e contratti tipizzati con i confinanti. {` [ CHANNELS — input ] Web · Telegram · WhatsApp · Slack · Email · API · Cron | v [ EDGE / NETWORK ] Cloudflare (DDoS+WAF) -> Caddy (TLS+routing) | v == LIVELLO 1: TYPED QUERY LAYER (slot filling pre-LLM, deterministico) == "lavastoviglie 60 classe A" -> {intent, categoria, filtri tipizzati} | v == LIVELLO 2: SUPERVISOR (Filter-then-Validate orchestrator) == LangGraph minimale o Python deterministico. NON arbitra opinioni: orchestra flussi tipizzati. | +-----------------+-----------------+----------+ v v v v == LIVELLO 3: FONTI ETEROGENEE (federate selettivamente) == 3A. KNOWLEDGE TOOLS 3B. PIM lite 3C. RULE 3D. MEXAL + per brand singleton ENGINE PROMO MCP (constellation) Postgres singleton singleton JSONB evaluator BlumKnowledge schema in Python prezzo, BoschKnowledge MD-Karpathy legge da disponib, WhirlpoolKnowledge R*.md promo, frontmatter bundle cerca_knowledge filtra( eseguibile dettaglio_codice categoria, valida_compatib attributi) + tool dominio | v == LIVELLO 4: CONFIGURATION CONTEXT (stato persistito tipizzato) == {progetto: "cucina_60", moduli: [{...}, {...}], vincoli_attivi: [...]} Cresce ad ogni step. Letto/aggiornato da tutti i livelli. | v == LIVELLO 5: INFRASTRUTTURA == Inngest (workflow durabili) · Postgres + pgvector (data layer) Anthropic API (LLM) · Langfuse (observability) · MCP runtime ------- cross-cutting trasversali (sempre attivi) ------- Sicurezza (3 layer) · Observability (Langfuse + OTel) · Auditabilita' (trace_id) `} Il flusso di una query passa **sempre** attraverso i 5 livelli, con il Supervisor (Livello 2) che decide quali fonti del Livello 3 attivare in base al typed query del Livello 1, alimentando/leggendo il Configuration Context (Livello 4), tutto sopra l'infrastruttura del Livello 5. ## Modello di esecuzione: tutto e' asincrono ed event-driven Sezione fissata in v1.2 e mantenuta integralmente in v2.0: e' la fondazione di esecuzione su cui si regge tutto il resto. Senza un modello esplicito, le decisioni si prendono caso per caso e nel tempo divergono. ### Principio guida **Tutto il sistema gira in modalita' asincrona ed event-driven. La sola eccezione legittima e' la chiamata HTTP iniziale del client (utente che digita su un'interfaccia web e aspetta una risposta in pochi secondi)**. Conseguenze pratiche: - Niente polling. Niente cron che ogni 30 secondi controlla "ci sono novita'?". - Niente HTTP long-running blocking. Se un'integrazione esterna risponde in 30 secondi, va orchestrata async, mai messa dietro a una request che lascia il client appeso. - Niente `time.sleep()` Python in un workflow: tiene il processo vivo e si perde tutto al primo restart. ### Inngest come substrato unico di esecuzione Inngest non e' "il workflow engine": e' **il substrato di esecuzione asincrona** dell'intero sistema. Fa tre cose contemporaneamente, con un solo strumento: | Ruolo | Cosa fa | |---|---| | **Event bus** | Pub/sub di eventi strutturati. Pubblichi con `inngest.send({ name: "x", data: {...} })`, le funzioni in ascolto reagiscono. | | **Queue durabile** | I lavori sono persistiti in Postgres. Sopravvivono ai restart, ai crash, ai deploy. Niente perdita di stato. | | **Workflow engine** | Orchestra step in sequenza con `step.run`, `step.sleep`, `step.waitForEvent`. Retry automatici. | Il framing unitario evita di pensare a tre componenti separati: e' un solo layer, una sola dashboard, un solo database, una sola API mentale. ### I quattro tipi di trigger Tutto cio' che accade nel sistema parte da uno di questi quattro trigger. Sono **i soli ammessi**: ogni nuovo workflow deve appartenere a una di queste categorie. ```typescript // 1. WEBHOOK in ingresso da sistemi esterni (gestionale, fornitori, ecc.) inngest.createFunction( { id: "onboarding-cliente" }, { event: "erp/cliente.creato" }, // listener su evento async ({ event, step }) => { ... } ); inngest.send({ name: "erp/cliente.creato", data: {...} }); // 2. CRON schedulato (lavori periodici) inngest.createFunction( { id: "etl-catalogo-notturno" }, { cron: "0 3 * * *" }, // ogni notte alle 03:00 async ({ step }) => { ... } ); // 3. CHAIN interna (workflow che spawna un altro workflow) await step.sendEvent("notifica-completamento", { name: "comm/onboarding.completato", data: { cliente_id } }); // 4. MANUAL dashboard (replay, debug, recovery) // Lanciato manualmente dalla dashboard Inngest, utile per: // - replay di un workflow fallito // - test di un nuovo agente in staging // - recovery dopo un disastro ``` Criteri di scelta: - **Webhook**: quando un sistema esterno deve **notificarci** che qualcosa e' successo. Sempre preferibile a polling. - **Cron**: lavori legittimamente periodici (ETL notturni, report giornalieri, pulizie). Mai per "controllare se ci sono novita'": quello e' un anti-pattern (vedi sotto). - **Chain**: per disaccoppiare flussi che logicamente appartengono a momenti diversi. Es. "onboarding completato" emette un evento che fa partire "invio benvenuto", invece di tutto in un unico mega-workflow. - **Manual**: solo per intervento umano consapevole, mai come sostituto di trigger automatici. ### Retry policy esplicita Inngest fa retry automatici di default. **Le scelte specifiche sono queste, e si applicano a tutti i workflow**: | Categoria errore | Policy | Tentativi | |---|---|---| | Errore transitorio (rete, timeout, 5xx) | Exponential backoff Inngest default | 4 (default) | | Chiamata gestionale (Mexal): timeout / busy | Retry rapido + circuit breaker `pybreaker` davanti | 3 retry, poi breaker apre per 1 min | | Errore client (4xx, validazione fallita) | NESSUN retry: errore deterministico, ritentare e' inutile | 0 | | Fallimento permanente dopo retry | Va in **dead letter queue** (tabella `failed_workflows` Postgres) | 0 | | LLM provider down (Anthropic 5xx) | Fallback su provider secondario via LiteLLM | 1 fallback | Per gli errori in DLQ: alert immediato via Apprise al canale operativo, replay manuale dalla dashboard quando l'incidente e' risolto. **Mai ignorare la DLQ**: e' il segnale che qualcosa di non transitorio e' rotto. ### Long-running: `step.sleep` vs `step.waitForEvent` Due strumenti diversi per "aspettare", da non confondere: - **`step.sleep("3 days")`**: attesa di tempo passivo. Il workflow riprende dopo 3 giorni, indipendentemente da cosa succeda. Costo: zero (Inngest non tiene il processo vivo, lo riprende quando serve). - **`step.waitForEvent("approval", { timeout: "24h", match: "data.cliente_id" })`**: attesa di un evento specifico (con timeout). Il workflow riprende quando l'evento arriva, oppure va in timeout. Costo: zero finche' non si sblocca. Durata massima: in pratica illimitata (giorni, settimane). Costo durante l'attesa: zero. **Nessun processo vivo, nessuna risorsa allocata**. E' la magia del durable execution. ### Backpressure e concurrency Quando un picco di eventi arriva (es. import massivo di 500 clienti, errore di un sistema esterno che genera 1000 webhook duplicati), serve un limite per non saturare downstream o l'API LLM: ```typescript inngest.createFunction( { id: "onboarding-cliente", concurrency: { limit: 5 }, // max 5 esecuzioni concorrenti rateLimit: { limit: 100, period: "1m" } // max 100 lanci al minuto }, { event: "erp/cliente.creato" }, async ({ event, step }) => { ... } ); ``` Default consigliato per workflow che chiamano il gestionale (Mexal e' a connessioni limitate): `concurrency: { limit: 3 }`. Per workflow che chiamano solo Anthropic API: `concurrency: { limit: 10 }`. Tarare in base ai dolori reali, dopo 4-6 settimane di dati Langfuse. ### L'unica eccezione sincrona ammessa Quando un utente digita in chat web e aspetta una risposta in 3-5 secondi, la chiamata HTTP iniziale e' sincrona. **E' l'unica eccezione legittima**. Regole per non degenerare: - L'HTTP handler non fa lavoro pesante: prepara il prompt, chiama Anthropic, ritorna la risposta. Nient'altro. - Se serve un'azione collaterale (es. salvare conversation history, generare un PDF), parte un evento Inngest in background (`inngest.send`). L'utente vede subito la risposta, il lavoro continua async. - Timeout HTTP duro: 30 secondi. Oltre, errore restituito al client. Mai lasciar cuocere una request. Questi sono i comportamenti che, se introdotti anche in un solo workflow, **rompono il modello di esecuzione** e generano bug silenti che si manifestano mesi dopo. Mai aggirare: - **Polling**: cron che ogni N secondi/minuti controlla "ci sono novita'?". Sempre rimpiazzabile con webhook in arrivo. Se il sistema sorgente non ha webhook, fallback su cron lungo (orario, non al minuto), mai polling stretto. - **HTTP long-running blocking**: dietro a un endpoint REST, lasciar partire un workflow da 30 secondi tenendo il client appeso. Sempre: ritorna subito un `task_id`, il client polla `/status/{task_id}` o riceve webhook quando finisce. - **`time.sleep()` Python in un workflow**: tiene il processo vivo, perde lo stato al primo restart, occupa risorse. Sempre `step.sleep` durabile. - **Mega-workflow monolitici di 20 step**: difficile da debuggare, da replay parziale, da modificare. Sempre split via chain (eventi tra workflow). - **Workflow sync nel chat HTTP**: l'utente in chat aspetta, ma dentro l'handler giri 8 secondi di tool calling sequenziale. Spostare async tutto cio' che non e' la singola risposta LLM. - **Bypass della DLQ**: ignorare i `failed_workflows` perche' "tanto si riprova al prossimo cron". Mai. Ogni fallimento permanente va analizzato. ## Livello 1: Typed Query Layer (slot filling pre-LLM) Il primo livello dello stack v2.0 e' **deterministico e sta prima dell'LLM**. La query naturale dell'utente viene parsata in una struttura tipizzata che il resto del sistema consuma. Senza questo livello, il rischio "60 cm vs 60 watt vs 60 db" e' permanente. ### Cosa fa Trasforma una query in linguaggio naturale in un oggetto `TypedQuery` strutturato: ```typescript // Input "lavastoviglie da 60 classe A" // Output { intent: "search", categoria: "lavastoviglie", filtri: { larghezza_cm: 60, classe_energetica: "A" }, testo_libero: null, contesto_progetto_ref: null } ``` ```typescript // Input "come scelgo una cerniera Blum per anta da 18 kg con frontale in vetro?" // Output { intent: "consult", categoria: "cerniera", filtri: { peso_anta_kg: 18, tipo_frontale: "vetro" }, testo_libero: "come scegliere", contesto_progetto_ref: null } ``` ```typescript // Input "aggiungi al progetto cucina la lavastoviglie SMV68N20EU" // Output { intent: "configure", categoria: "lavastoviglie", filtri: { codice: "SMV68N20EU" }, testo_libero: null, contesto_progetto_ref: "cucina_" } ``` ### Come si implementa Tre tecniche compongono il parser, in ordine di affidabilita' decrescente: 1. **Regex e dizionari deterministici** per categorie note, unita' di misura, valori categorici fissi (classi energetiche, colori standard, sigle interne KH/FH/NL). Veloce, zero costo, debug semplice. 2. **NER (Named Entity Recognition) leggero** con spaCy italiano per estrazione di entita' generiche (numeri con unita', nomi di marchi, codici articolo). Funziona offline, ~50ms. 3. **Slot filling LLM-based** (Claude Haiku con prompt ristretto e schema JSON forzato via `tool_use`) per i casi residui. ~200ms, ~$0.0002 per query. Il parser tenta in ordine: se 1+2 estraggono tutto, l'LLM non viene chiamato. Su query semplici la latenza e' sotto 50ms a costo zero. Su query ambigue il fallback LLM produce comunque output tipizzato (lo schema e' forzato dal `tool_use` API-side). ### Perche' non e' un dettaglio tecnico Senza Typed Query Layer, il sistema cade nei seguenti pattern fallaci: - "60" finisce in vector search e matcha "60 watt" su un altro prodotto. - "classe A" non viene normalizzato (l'utente puo' scrivere "A", "A+", "A++++", "Classe A", "energy A"): query divergenti danno risultati divergenti. - L'intent ("search" vs "consult" vs "configure" vs "compare") non viene riconosciuto, e il Supervisor non sa quale flusso attivare. - Non c'e' un punto dove validare che la query e' ben formata prima di consumare token LLM. Questo e' lo "Slot Filling" classico dei Task-Oriented Dialog Systems, documentato dagli anni '90. Risolto bene da almeno tre generazioni di architetture (rule-based, statistical, neural). Vale per la PMI tanto quanto per Google Assistant, solo con strumenti piu' leggeri. ### Validazione empirica Step 4 TQL formale (v2.3.6) Sezione aggiunta in v2.3.6 dopo l'implementazione formale del TQL in arcocat (vedi `arcocat/REPORT_STEP4_TQL.md`). Il design teorico v2.3 prevedeva 3 stadi (regex → NER spaCy → LLM fallback). La realta' implementativa ha mostrato che lo stadio LLM **non e' necessario** per il dominio Blum sistemi_box: regex raffinato + sinonimi tabulati in MD-Karpathy cattura il 100% degli edge case (15/15 casi mirati a separatori virgola, lowercase, sinonimi italiani, ordine variabile, conversione cm→mm). **Decisione empirica**: scelta Opzione C ibrido (regex + sinonimi MD), LLM fallback **non implementato** nel default. File `tql_llm.py` non creato. Se in produzione emergono casi reali non coperti, lo si aggiunge come sub-iterazione minore con scope chiaro. Coerente con principio "semplicita' = vincere". **Pattern Karpathy esteso ai sinonimi**: i sinonimi `tipo_componente` (8 enum, 30+ varianti italiane: "guida"/"guide", "spondina"/"spondine", "frontale", "monoblocco", "scrigno", "fianchino"/"fiancata"/"slitte"/"scorrevoli"/"gallery", ecc.) e `famiglia_box` (7 enum, 13 varianti) vivono in `wiki_arcocat/sinonimi/TQL_SINONIMI.md` con frontmatter YAML. L'esperto Arco aggiunge un sinonimo nuovo via edit MD, **non release di codice**. Stesso pattern del CategorySchema PIM C001. Plus fallback hard-coded subset L3 se MD assente: il TQL degrada gracefully, non si rompe. **Lezione architetturale primaria di Step 4**: il TQL **non deve essere perfetto**. Slot filling parziale + filter PIM ampio + Knowledge Tool brand intelligente = **output consulenziale** (pattern "shortlist comparativa di default" gia' validato in L3 Q5-bis). Investire ~5-10h per sostituire regex con LLM puro ha ROI marginale; investire le stesse ore in **R-rule formali nel KB brand** (alza grounding rate da 11% a >50%) ha ROI molto piu' alto. Conferma metodologica della lezione iter2 ("variance vs gap separabili"): il valore consulenziale del CPQ nasce dal grounding regolatorio, non dalla precisione del parser di input. **Numeri pre/post Step 4**: | Metrica | L3 (regex L3-OP1 fragile) | Step 4 (regex raffinato + MD) | Note | |---|---|---|---| | Edge case L3-OP1 risolti | 0/10 | 15/15 | separatori virgola, lowercase altezza, sinonimi nuovi | | Test supervisor-mcp | 20/20 verdi | 56/56 verdi | +36 nuovi (21 unit slot_filling + 15 edge case) | | Cross-validate strict trasparenza Q1 | strict 3/3 (Iter2) | strict 3/3 | baseline mantenuto, nessuna regressione | | Grounding rate 5 query L3 baseline | 11% | 12% | invariato (atteso, TQL non aggiunge regole) | | Grounding rate 5 query EDGE nuove | n/a | 22% | TQL piu' completo → candidati piu' rilevanti al LLM judge | | Cost per query (cache HIT) | $0.0035 | $0.0035 | invariato (LLM judge unico stage costoso) | | Latency aggiuntiva TQL | ~1ms regex | ~1ms regex + cached MD load | invariata operativamente | **Implicazione operativa per onboarding 2°brand**: il TQL e' ora robusto su Blum. Per Bosch/Hettich/altro, l'onboarding consiste nell'aggiungere sinonimi al MD-Karpathy (es. tipo_componente Hettich avra' "guide push-to-open", "Quadro", "Easy-Move", varianti tedesche/inglesi) e schema CategorySchema PIM (es. C002_lavastoviglie con attributi `larghezza_cm`, `classe_energetica`). Niente codice Python da toccare per il TQL. ## Livello 2: Supervisor pattern - Filter-then-Validate Il secondo livello e' il **router intelligente** che riceve `TypedQuery`, decide quali fonti del Livello 3 chiamare, in che ordine, e sintetizza la risposta. Non e' un agente "intelligente" nel senso tradizionale (non improvvisa): e' un **orchestratore deterministico** che esegue un flusso prevedibile. ### Il pattern: Filter-then-Validate L'ordine di chiamata delle fonti **non e' parallelo**. E' sequenziale e gerarchico: 1. **Filter** (PIM, recall alto, precisione bassa): per query con filtri strutturati, il PIM produce candidate set deterministico. "Lavastoviglie 60 classe A" -> 12 codici candidati. 2. **Validate** (Knowledge Tools brand, precisione alta, recall basso): per ogni candidato del set, il Knowledge Tool del brand applica regole tecniche e ragionamento consulenziale. "Modello SMV68N20EU richiede nicchia 560mm: nel tuo progetto cucina i cassetti retrostanti lasciano solo 540mm". Esito: scartato. 3. **Synthesize** (Supervisor LLM): combina filter + validate, espone risultati con tre stati distinti (compatibile / consigliato / sconsigliato), eventualmente con motivazione. ``` [TypedQuery] | v [Supervisor classifica intent] | +-- intent="search" + filtri tipizzati --> Filter PIM (Livello 3B) | | | v | candidates: [c1, c2, c3...] | | | v | for each c: Knowledge Tool brand (Livello 3A) | .valida_compatibilita(c, configuration_context) | | | v | scored_candidates: [{c1, ok, ...}, {c2, warn, ...}] | | +-- intent="consult" + categoria ----> Knowledge Tool brand direttamente | .cerca_knowledge(query) | +-- intent="quote" + contesto -------> Mexal MCP (Livello 3D) | .prezzo_per_cliente(codici, cliente) | .verifica_credito(cliente) | + Promo MCP | .promo_attive(codici, cliente) | +-- intent="compare" + N codici -----> per ogni codice: PIM.attributi + Knowledge Tool brand | v tabella comparativa strutturata | v [Sintesi italiana con conflitti espliciti] ``` ### Implementazione LangGraph Supervisor (`langgraph-supervisor-py`, libreria ufficiale di LangChain) e' lo strumento pubblicato per questo pattern. Implementa `create_supervisor([agents], model, prompt)` con handoff tools custom. Per la PMI e' anche **eccessivo**: una versione minimale in Python con `if/elif` sui `TypedQuery.intent` e tool calling Anthropic nativo basta per le prime decine di flussi. ```python def supervise(typed_query: TypedQuery, config_context: ConfigurationContext) -> Response: if typed_query.intent == "search" and typed_query.filtri: candidates = pim.filtra(typed_query.categoria, typed_query.filtri) scored = [] for cand in candidates: brand_tool = knowledge_tools[cand.brand] validation = brand_tool.valida_compatibilita(cand, config_context) scored.append({"candidato": cand, "validation": validation}) return synthesize_search(scored, typed_query) elif typed_query.intent == "consult": brand_tool = pick_brand_tool(typed_query) return brand_tool.cerca_knowledge(typed_query.testo_libero, top_k=5) elif typed_query.intent == "quote": # ... elif typed_query.intent == "configure": # aggiorna config_context, valida vincoli via Rule Engine # ... ``` ### Conflict resolution: tre stati distinti Quando filter (PIM) e validate (Knowledge Tool) divergono, la risposta deve esporre **entrambi** i punti di vista, non scegliere uno dei due nascondendo l'altro. Tre stati: | Stato | Significato | Esempio | |---|---|---| | **Compatibile** | Filtro PIM ok, ma Knowledge non ha valutato | "Modello X soddisfa larghezza 60 e classe A" | | **Consigliato** | Filtro PIM ok + Knowledge approva attivamente | "Modello X soddisfa filtri e si adatta al tuo progetto cucina" | | **Sconsigliato** | Filtro PIM ok ma Knowledge segnala problema | "Modello X soddisfa filtri MA richiede nicchia 560mm, nel tuo progetto solo 540mm disponibili" | In caso di **sconsigliato**, vince il Knowledge Tool. Mostrare comunque il prodotto come "compatibile per filtro" senza menzionare la criticita' rinuncia esattamente al differenziatore consulenziale che e' il valore aggiunto del sistema. Il Supervisor NON chiama tutti i brand tools in parallelo come "pari grado" sperando che la sintesi LLM scelga il giusto. Quel pattern (federazione naive del v1.x) genera 30 latenze, 30 formati diversi, e un Supervisor che impazzisce in sintesi. Filter-then-Validate e' deterministico: PIM filtra (con FTS5 + cosine + RRF se serve hybrid retrieval), Knowledge Tool valida solo i candidati ridotti. ### Pattern documentato da Anthropic Il pattern Supervisor con worker e' documentato esplicitamente nel paper [How we built our multi-agent research system](https://www.anthropic.com/engineering/multi-agent-research-system) (Anthropic, 2025). Lead agent (Sonnet/Opus) coordina, sub-agents specializzati operano in parallelo. Misurato +90.2% performance vs single-agent setup. Il Filter-then-Validate del v2.0 e' una specializzazione del pattern per il caso CPQ multi-brand. ### Proprieta' emergente: shortlist comparativa di default (validato empiricamente in v2.3.4) Sezione aggiunta in v2.3.4 dopo l'implementazione L3 (Supervisor minimale). Vedi `arcocat/REPORT_L3.md`. Il pattern Filter-then-Validate **produce shortlist comparativa di default**, senza richiederlo a design. La proprieta' emerge dalla composizione di filter recall-alto + validate precision-alta: - Il PIM applica filtri **necessari ma non sufficienti** (recall alto): per "guida cassetto LEGRABOX 500mm + peso anta 60kg", PIM ritorna sia 750.5001S (portata 40kg) sia 753.5001S (portata 70kg). Entrambi soddisfano `NL_mm=500 + tipo_componente=set_guide + LEGRABOX`. Il PIM non decide quale e' meglio per il contesto utente: filtra strutturale, non valida consulenziale. - Il Knowledge Tool brand discrimina con **contesto utente specifico** (precisione alta): valuta R001 LEGRABOX (peso anta vs portata frontale) sui 2 candidati. Output: 750.5001S `blocked` (portata 40 < peso anta 60), 753.5001S `compatibile` (portata 70 sufficient). - Il Supervisor sintetizza in **shortlist comparativa**: "ti propongo entrambe queste opzioni, ma 753 e' adatta al tuo carico, 750 no". E' il valore consulenziale del CPQ industriale (Constructor.com pattern), che il search "puro" non produce. **Implicazione architetturale**: il pattern non richiede LLM judge sul synthesize ne' relax automatico nel filter. Synthesize resta pure function deterministica `(filter_match, validate_status, citazioni) -> stato canonico in 3 valori`. Il valore comparativo emerge dalla pipeline, non da intelligenza addizionale. **Caveat onesto** (validazione L3): in CP2 il valore comparativo e' emerso anche grazie a un bug regex slot filling (portata_kg non estratto), che ha allargato il filter PIM. Anche con slot filling perfetto la shortlist `[750.5001S, 753.5001S]` resterebbe (filter recall alto by design). Il bug ha solo reso piu' visibile la proprieta' che esiste comunque. **Generalizzabile a multi-brand**: il pattern scala. Con 2 brand (Blum + Hettich), una query "set guide cassetto NL=500 portata=70kg" produce candidati cross-brand (es. 753.5001S Blum + KA-3000-EB Hettich), ognuno validato dal proprio Knowledge Tool brand con contesto utente. Il Supervisor presenta confronto consulenziale "per il tuo progetto cucina, Blum 753 ha vantaggi X, Hettich KA-3000 ha vantaggi Y, suggerisco Blum perche'...". E' il differenziatore CPQ rispetto a search/configurator generici. ## Livello 3: Fonti eterogenee Il livello 3 contiene **quattro tipi di fonti**, ognuna con responsabilita' chiare e contratti tipizzati. Il Supervisor le chiama selettivamente in base al typed query. ### 3A. Knowledge Tools per brand (constellation di MCP) **Cosa sono**: server MCP, uno per brand, ognuno con il proprio wiki narrativo curato umanamente (pattern Karpathy, vedi [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/)). Espongono tool dominio-specifici (regole, distinte, sigle) **piu' tre tool minimi di contratto comune** federabili dal Supervisor. **Disclaimer importante**: NON sono "agenti autonomi". Sono MCP server con tool. La distinzione e' sostanziale: - Un **agente autonomo** ha sua chat, sua memoria, sua identita', decide da solo cosa fare. - Un **MCP server** e' un endpoint con funzioni tipizzate. Non decide nulla, esegue chiamate di tool. Il Supervisor (Livello 2) e' l'**unico vero agente** del sistema. I Knowledge Tools sono fonti chiamate dal Supervisor. Una chat-per-brand (es. la chat BlumCat che oggi i clienti Arco usano per fare domande solo su Blum) puo' restare come **UI specializzata** sopra al Knowledge Tool, ma e' una scelta di interfaccia, non di architettura. **Tre tool minimi per contratto comune**: ```python @mcp.tool() def cerca_knowledge(query: str, top_k: int = 5, alpha: float = 0.5) -> list[dict]: """Hybrid retrieval nel wiki di questo brand. Combina BM25 (FTS5 nativo SQLite) e cosine semantica via RRF (k=60). alpha: 0=solo BM25, 1=solo vector. Default 0.5 per dominio tecnico misto. """ bm25_hits = fts5_search(query, k=60) vec_hits = cosine_search(embed(query), k=60) return rrf_merge(bm25_hits, vec_hits, k_rrf=60)[:top_k] @mcp.tool() def dettaglio_codice(codice: str) -> dict: """Lookup esatto di un codice nel catalogo di questo brand.""" return db.execute("SELECT * FROM codici WHERE codice = ?", (codice,)).fetchone() @mcp.tool() def valida_compatibilita(prodotto: dict, contesto: dict) -> dict: """Validazione consulenziale: questo prodotto e' adeguato per il contesto? Applica regole tecniche del brand. Ritorna `{ok, status, motivazione, alternative}`. Status puo' essere: ok, warn, blocked. """ # Esempio: applica regole brand-specifiche da R*.md (vedi Rule Engine) return rule_engine.evaluate(brand=BRAND_ID, prodotto=prodotto, contesto=contesto) ``` **Tool dominio-specifici (esempi BlumKnowledge)**: ```python @mcp.tool() def get_regola(rid: str) -> dict: """Ritorna la regola R (frontmatter parsato + body markdown).""" @mcp.tool() def assemble_distinta_cassetto(famiglia: str, NL_mm: int, portata_kg: int, ...) -> dict: """Compone distinta canonica con codici reali.""" @mcp.tool() def get_media(codice: str) -> dict: """Ritorna {foto_url, pdf_url, manuale_pag_url} per un codice.""" ``` **Storage**: SQLite per agente. ~4-5k chunks tipici per brand, cosine in-process con NumPy ~50ms su CPU. Zero servizi esterni, zero overhead operativo. Sotto i 100k chunks e' la scelta giusta. **Aggiornamento hybrid retrieval rispetto a v1.x**: nella v1.x l'unica retrieval era cosine semantica. La v2.0 introduce hybrid (BM25 FTS5 nativo SQLite + cosine + Reciprocal Rank Fusion con k=60). RRF e' standard industriale per merge cross-source di score incompatibili: documentato da Algolia/Elastic/OpenSearch da anni, formula `1/(k+rank)`, zero calibrazione richiesta. Per dominio tecnico (codici come "750.5001S", sigle KH/FH) il match esatto BM25 vale quanto la similarita' semantica, e RRF li combina senza sintonizzare alpha. ### Pattern definitivo per `valida_compatibilita` — validato empiricamente in v2.3.2 Sezione aggiunta in v2.3.2 dopo l'iterazione 1 di validazione empirica (vedi `arcocat/REPORT_ITERAZIONE_1.md`). Il pattern di costruzione del prompt per `valida_compatibilita` non e' libero: l'iterazione 1 ha dimostrato che esiste UN pattern ottimale per Knowledge Tool brand con rule set piccolo-medio (sotto ~50 regole), e che la sua alternativa apparente (retrieval-only del rule set) produce regression empirica. **Pattern raccomandato: rule completo cached + hint dinamico** ``` system_block (cache_control:ephemeral, ~16k token cached): - rule set INTERO del brand (frontmatter eseguibile + body completi delle R*.md) - istruzioni di citazione (cita rule_id + fonte_pagine_fis solo per regole davvero applicabili) user_block (NOT cached, ~500-1000 token): - PRODOTTO: codice o descrizione - CONTESTO: prosa libera o JSON serializzato - REGOLE PRIORITARIE: [Rxxx, Ryyy, Rzzz] <- hint da retrieval RRF mirato fonte='regola' - CHUNKS DI SUPPORTO: top 5 chunks fonte != 'regola' da hybrid retrieval safety net Opzione C: ultima difesa lato Python (status=ok + citazioni=[] -> warn). ``` Cache HIT atteso: 90-95% dalla 2a call (TTL 5 min Anthropic). Hint pesa ~50-100 token marginale, NON rompe il caching. Il LLM ha tutto il rule set + signal di rilevanza retrieval, e puo' citare regole hint OR altre del rule set se piu' pertinenti. **Anti-pattern empirico (DA NON RIPETERE)**: retrieval mirato chunk-level esclusivo come **gating** del rule set (es. "carica solo top 3-5 regole emerse dal retrieval"). In iterazione 1 BlumCat misurato: safety net 2/4 → 3/4 (peggio), citazioni 1 → 0 (peggio), costo +55%. Cause: 1. Cache miss strutturale: le regole selezionate variano per call → `cache_control:ephemeral` non aggancia. 2. Chunk-level retrieval su rule set astratto e' rumoroso: query lunghe con token specifici producono regole off-topic, nascondendo regole pertinenti. 3. Top-k=3-5 esclude regole pertinenti per query arricchite di token specifici. Conclusione: **retrieval mirato chunk-level e' utile come HINT (priorita' nel user_block), NON come gating esclusivo del rule set**. **Quando passare a retrieval-only**: solo se rule set > ~200 regole (~200k token, fuori dalla finestra cache pratica). Per Blum (14 regole), Bosch atteso (~30-50), Whirlpool atteso (~20-40), il pattern "tutto cached + hint" resta ottimale. Per dominii regolatori densi (farmaceutico, finanziario) dove le regole possono superare i 200, retrieval-only o tiering diventa necessita' tecnica, ma e' caso edge. **Validazione numerica iterazione 1 BlumCat (2026-05-05)**: | Metrica | L1 (rule completo, no hint) | Iter1 (rule completo + hint) | |---|---|---| | Safety net trigger | 2/4 (50%) | 1/4 (25%) | | Citazioni totali (4 casi) | 1 | 3 | | Casi grounded | 1 | 2 | | Cache HIT | 100% | 95.2% | | Costo per call | $0.003 | $0.0028 | Pattern Iter1 quindi: stesse caratteristiche L1 (cache HIT, costo) + miglioramento netto su grounding (citazioni 3x, safety net dimezzato). Da adottare come default in tutti i Knowledge Tool brand v2.x. **Nota in v2.3.4 sul non-determinismo del judge** (validato in `arcocat/REPORT_L3.md` sez 4.2 + confermato in `arcocat/REPORT_ITER2_BLUMKNOWLEDGE.md` v2.3.5): con `temperature` default Anthropic = 1, il LLM judge produce variance fra chiamate consecutive sullo stesso prompt. In L3 cross-validate Q1 misurato: stesso codice, stessa pipeline, esiti `consigliato` vs `compatibile` su due run a 5s di distanza. **La trasparenza strutturale del Supervisor tiene** (codici + bucket usable/blocked sono invariant deterministici), il **status fine no**. Mitigation raccomandata: passare `temperature=0` a `valida_compatibilita` (modifica chirurgica di 1 riga, validata empiricamente in iter2 minore BlumKnowledge: cross-validate strict equality regge 3/3 run consecutivi). Il giudizio tecnico diventa riproducibile, l'output testuale resta naturale (system prompt + few-shot guidano lo stile, non la creativita' su `temperature`). Compatibile con `cache_control:ephemeral` (cache HIT 100% post-fix confermato). Tradeoff netto a favore di `temperature=0` per use case validation. **Caveat empirico** (iter2): `temperature=0` riduce variance al ~95-100% sui casi non-borderline e al ~80-90% sui casi borderline (logit vicini fra opzioni alternative). Sui casi borderline il residuo si manifesta come oscillazione `compatibile↔sconsigliato`, mitigabile a livello di **regola formale nel KB** (preferibile a `top_k=1` o judge piu' grande). Pattern: la stabilita' definitiva nasce da grounding regolatorio piu' forte, non da sampling LLM piu' aggressivo. **Lezione metodologica "variance vs gap coverage" (lezione primaria iter2)**: per Knowledge Tool brand con LLM judge, due sintomi sono diagnosticamente confusi finche' non si separa `temperature`. Sintomi: (a) variance status fine fra chiamate consecutive sullo stesso prompt, (b) grounding rate basso (poche citazioni esplicite). Diagnosi corretta: (a) e' rumore di sampling LLM, (b) e' assenza di regole formali nel KB. Verifica empirica iter2 BlumKnowledge: post-fix `temperature=0`, cross-validate diventa deterministico (a risolto) ma grounding rate resta invariato a 11% (b inalterato). **L'invarianza di (b) post-fix di (a) e' essa stessa la prova diagnostica** che (b) e' gap reale, non artefatto di sampling. Implicazione operativa: prima fix variance, poi misura gap; senza fix variance la misura del gap e' rumorosa. Generalizzabile a ogni nuovo brand del constellation. ### 3B. PIM lite per attributi cross-brand **Cosa e'**: il **Product Information Management** lite. Un singleton che tiene gli attributi tipizzati per categoria prodotto, **brand-as-attribute**, schema definito in MD-Karpathy. Permette query strutturate cross-brand del tipo "lavastoviglie 60 classe A". **Modello dati ispirato Akeneo**: - **Categoria** (es. `lavastoviglie`, `cerniera`, `cassetto`): gerarchia merceologica. - **Schema attributi per categoria** (es. lavastoviglie ha sempre `larghezza_cm`, `classe_energetica`, `capacita_coperti`, `tipo_incasso`, `brand`, `modello`). - **Attribute tipizzato**: `measurement` (con unita'), `select` (con valori ammessi), `boolean`, `text`. - **Brand** e' un attributo del prodotto, non un'entita' separata. - **Channel / locale** come "viste" diverse (b2b, b2c, ecommerce, print): se servono. **Schema in MD-Karpathy**: invece di definire lo schema in Python o JSON Schema rigido, lo schema vive in `wiki_arcocat/categorie/C*.md` con frontmatter YAML eseguibile + body markdown narrativo. Stesso pattern del wiki narrativo dei Knowledge Tools. Editabile da UI da un esperto di dominio senza toccare codice. ```markdown --- id: C001 categoria: lavastoviglie versione: 1.0 ereditato_da: C000_elettrodomestico_incasso attributi: - { nome: larghezza_cm, tipo: measurement, unita: cm, valori_tipici: [45, 60] } - { nome: classe_energetica, tipo: select, valori: [A, B, C, D, E, F, G] } - { nome: capacita_coperti, tipo: measurement, unita: coperti } - { nome: tipo_incasso, tipo: select, valori: [totale, scomparsa_parziale, libero_installazione] } - { nome: brand, tipo: select, valori: [bosch, whirlpool, bsh] } - { nome: modello, tipo: text } - { nome: codice, tipo: text, identifier: true } --- # C001 - Lavastoviglie Categoria di elettrodomestici per il lavaggio stoviglie. Distribuita in 3 brand principali (Bosch, Whirlpool, BSH). ## Discriminanti chiave per la consulenza - **Larghezza**: 60 cm (standard cucine moderne) vs 45 cm (compatte / monolocali) - **Tipo incasso**: scomparsa totale (frontale incollato a misura) vs parziale vs libera ## Cross-categoria: vincoli con altri prodotti Una lavastoviglie da 60 in cucina a parete con cassetti retrostanti necessita di valutare profondita' nicchia (vedi vincolo X-COMPAT-001). ``` **Storage**: Postgres con colonna `attributi JSONB` per ciascun prodotto, indici GIN per filtri rapidi. Schema dinamico per categoria letto da C*.md al boot. Niente Akeneo full-blown (overhead sistemistico enorme per 1 sviluppatore). Si valuta Akeneo solo se compaiono: workflow umano di data-entry, ereditarieta' attributi profonda, integrazione di N feed eterogenei contemporanei. > **Validato empiricamente in v2.3.3 (L2 done, 2026-05-06)**: vedi `arcocat/REPORT_L2.md`. Il PIM lite e' stato implementato come singleton FastMCP standalone in `arcocat/pim-lite/` con SQLite + JSON1 (non Postgres+JSONB: per Blum-only L2 SQLite basta, trigger esplicito di migrazione Postgres a 5k+ prodotti o p95 filtra() > 50ms). 363 prodotti popolati per categoria pilota `sistemi_box`, 26 test verdi inclusi 5 cross-validate (sezione successiva), latency sub-5ms. Su scala 100k prodotti SQLite stima ~37 MB, ancora gestibile. La scelta Postgres del modello teorico v2.3 resta corretta a regime (multi-brand, multi-categoria), ma non e' bloccante per il primo onboarding. #### Vincoli aspirazionali vs vincoli effettivi (lezione L2) Lo schema YAML del CategorySchema dichiara `obbligatorio: true` come **segnaletica architetturale**: indica all'esperto e al Supervisor quali attributi sono "asse principale" di filtro per quella categoria. La realta' empirica della popolazione, pero', e' spesso **segmentata per `tipo_componente`**: alcuni attributi "obbligatori" hanno senso solo per una sotto-popolazione. Esempio concreto da L2 (categoria `sistemi_box`, 363 prodotti): - 267 componenti **strutturali** (set_guide, spondina_lato, spondina_alta): hanno `NL_mm` popolato al 100%. - 96 **accessori** (frontali alluminio HG, viterie, motorizzazioni SERVO-DRIVE, traversi, kit ricambio): non hanno `NL_mm` per design del catalogo. Forzare regex su questi codici per estrarre un `NL_mm` produrrebbe **valori semanticamente errati** (interpretare la larghezza frontale 1500mm come `NL_mm=1500` e' un bug peggiore di `NL_mm=null`). **Decisione architetturale**: per il primo brand non formalizzare il vincolo condizionale (`obbligatorio_se_tipo_in: [...]`), tenere lo schema invariato come segnaletica e documentare la segmentazione **nei dati popolati e nei test** (es. `test_strutturali_hanno_NL_mm` come garanzia operativa). La formalizzazione contrattuale via attributo condizionale arriva a Step 6+ con 3°brand multi-categoria che mostri lo stesso pattern (per evitare astrazione prematura). Pattern Akeneo PIM canonico (attributi conditional per famiglia/categoria), che documentiamo come **punto aperto OP3 priorita' BASSA con trigger esplicito**: 3° brand con stesso pattern segmentato in 2+ categorie. #### Cross-validate Knowledge Tool brand vs PIM (pattern di validazione tra fonti) Pattern emerso empiricamente in L2: prima di chiudere un nodo della constellation (Knowledge Tool brand) o un singleton (PIM), validare che le **fonti diverse vedano la stessa verita' di catalogo** sui dati condivisi. Il PIM filtra strutturato e il Knowledge Tool brand validano narrativo, ma entrambi devono **convergere sui codici reali** del brand. Test concreto in L2 (`tests/test_cross_validate.py`): per 5 distinte canoniche generate dal Knowledge Tool brand (LEGRABOX 500/M, LEGRABOX 450/K, MERIVOBOX 500/M, TANDEMBOX_antaro 500/M, METABOX 450/M), per ogni codice con `status="in_db"` verificare che `pim.attributi_per_codice(codice)` ritorni `found=True`. Risultato: **42/42 codici trovati nel PIM, zero missing**. Conferma che le due fonti vedono lo stesso catalogo. Se uno o piu' codici fallissero, sarebbe segnale che `populate_pim` ha buchi nei pattern di estrazione e va indagato prima di procedere a Step 3 Supervisor. Generalizzabile: **prima di onboarding di un secondo brand**, scrivere un test cross-validate analogo con i 5-10 casi piu' rappresentativi del catalogo. E' una validazione architetturale "dei contratti tra fonti", non solo unit test isolati. Costa ~30 minuti di scrittura ed e' la garanzia di coerenza tra `filter` (PIM) e `validate` (Knowledge Tool) prima di metterli sotto il Supervisor. **Tool MCP esposto**: ```python @mcp.tool() def filtra_prodotti(categoria: str, attributi: dict, top_k: int = 50) -> list[dict]: """Filtraggio strutturato sui prodotti del catalogo cross-brand. Es: filtra_prodotti("lavastoviglie", {"larghezza_cm": 60, "classe_energetica": "A"}) Ritorna prodotti che soddisfano TUTTI i filtri (AND). """ schema = load_category_schema(categoria) where_clauses = [] for attr_name, value in attributi.items(): attr_def = schema.attribute(attr_name) if attr_def.tipo == "measurement": where_clauses.append(f"attributi->>'{attr_name}' = '{value}'") elif attr_def.tipo == "select": assert value in attr_def.valori, f"Valore {value} non ammesso" where_clauses.append(f"attributi->>'{attr_name}' = '{value}'") sql = f"SELECT * FROM prodotti WHERE categoria = ? AND {' AND '.join(where_clauses)} LIMIT {top_k}" return db.execute(sql, (categoria,)).fetchall() @mcp.tool() def attributi_per_codice(codice: str) -> dict: """Restituisce gli attributi PIM normalizzati di un codice.""" return db.execute("SELECT attributi FROM prodotti WHERE codice = ?", (codice,)).fetchone() ``` **Fonte dei dati**: tre opzioni realistiche, pesate per fattibilita': | Fonte | Pro | Contro | |---|---|---| | Estrazione da portali brand (API ufficiali) | Affidabile, aggiornata | Spesso richiede partnership commerciale, rate limit | | Estrazione da PDF brand (MinerU + curation) | Funziona ovunque | Costoso da estrarre attributi tipizzati | | Mappatura manuale (foglio Excel curato) | Sotto controllo totale | Sostenibile per centinaia di codici, non migliaia | In pratica: mix delle tre. Per i top 200 codici per categoria, mappatura manuale curata (e' la stragrande maggioranza del fatturato). Per la coda lunga, estrazione automatica con marker "non verificato" sui chunk dubbi. ### 3C. Rule Engine deterministico **Cosa e'**: un singleton che valuta vincoli tecnici espliciti (Constraint Satisfaction Problem) sui prodotti e sui contesti. Esempio: "se LF > 500 e portata > 40kg, usa 753.* non 750.*" (regola Blum). Riceve in input prodotto + contesto, ritorna `{ok, motivazione, alternative}`. **Perche' serve un componente dedicato**: i vincoli tecnici NON vivono bene in un LLM. Un LLM puo' interpretare male un range, dimenticare un edge case, applicare male una formula. Per i calcoli di portata di un cassetto B2B il margine di errore del LLM e' inaccettabile. Per i vincoli serve un evaluator deterministico Python con test unitari. **Pattern Karpathy esteso**: la fonte autoritativa delle regole resta `wiki_/regole/R*.md`, editabile da UI da un esperto di dominio (esattamente come oggi in BlumCat). Si **estende il frontmatter YAML** per portare la parte eseguibile (condizione, azione, test_cases) accanto alla parte narrativa (tabella decisione, note, vedi-anche). ```markdown --- id: R005 nome: "Cassetto LEGRABOX: set guida 750 vs 753 (high-load)" ambito: blum.legrabox versione: 1.2 fonte_pagine_fis: [248] # parte ESEGUIBILE dal Rule Engine condizione: all_of: - { fatto: famiglia, op: "=", valore: legrabox } - { fatto: portata_kg, op: ">", valore: 40 } - { fatto: NL_mm, op: ">", valore: 500 } azione: warn messaggio_template: "Per portata {{portata_kg}}kg e NL {{NL_mm}}mm usa 753.*, non 750.*" # test embedded (lint-friendly) test_cases: - { in: {famiglia: legrabox, portata_kg: 70, NL_mm: 600}, expect: warn } - { in: {famiglia: legrabox, portata_kg: 40, NL_mm: 400}, expect: pass } - { in: {famiglia: legrabox, portata_kg: 70, NL_mm: 500}, expect: pass } --- # R005 - Set guida LEGRABOX 753 (high-load) Quando il cassetto supera 40 kg di portata e ha NL > 500 mm, il set guide standard 750.* (40 kg) non e' adeguato. Va usato 753.* (70 kg). ## Tabella decisione | Portata | NL | Codice | |---------|-------------|--------| | 40 kg | qualsiasi | 750.* | | 70 kg | qualsiasi | 753.* | ## Note dell'esperto Il manuale fornitore p. 248 specifica le portate per famiglia di guida. Errore comune: usare 750.* per cassetti pesanti -> rottura cuscinetti nel medio termine. ## Vedi anche - [[D001 LEGRABOX]] (composizione canonica) - [[G001 cassetto cucina]] (decision tree) ``` **Doppia lettura della stessa fonte**: - L'esperto di dominio modifica body narrativo + tabella + (se necessario) le 4 righe di `condizione`. - Il Rule Engine al boot legge tutti gli R*.md, parsa il frontmatter, costruisce in memoria la lista di Rule eseguibili. Validator AI esistente (gia' in uso in BlumCat per pre-save) verifica anche che `condizione` sia ben formata e che i test_cases passino. **DSL della condizione**: linguaggio dichiarativo JSON minimale, no Drools o CLIPS. Operatori: `=`, `!=`, `>`, `<`, `>=`, `<=`, `in`, `not_in`, `contains`. Combinatori: `all_of`, `any_of`, `none_of`. I "fatti" sono campi del prodotto/contesto. **Engine evaluator** (~150 righe Python): ```python class RuleEngine: def __init__(self, rules_dir: str): self.rules = self._load_rules(rules_dir) def evaluate(self, brand: str, prodotto: dict, contesto: dict) -> EvaluationResult: applicable = [r for r in self.rules if r.ambito.startswith(f"{brand}.")] triggered = [] for rule in applicable: facts = {**prodotto, **contesto} if self._evaluate_condition(rule.condizione, facts): triggered.append({ "rule_id": rule.id, "azione": rule.azione, "messaggio": render_template(rule.messaggio_template, facts), "fonte_pagina": rule.fonte_pagine_fis }) return EvaluationResult( ok=not any(t["azione"] == "blocked" for t in triggered), triggered=triggered ) def _evaluate_condition(self, cond, facts): if "all_of" in cond: return all(self._evaluate_condition(c, facts) for c in cond["all_of"]) if "any_of" in cond: return any(self._evaluate_condition(c, facts) for c in cond["any_of"]) # foglia: { fatto, op, valore } return self._apply_op(cond["op"], facts.get(cond["fatto"]), cond["valore"]) ``` **Test unitari**: ogni R*.md contiene `test_cases` embedded. Pre-commit hook lancia tutti i test su tutte le regole. Se una modifica al frontmatter rompe un test, il commit fallisce. Editabilita' umana + sicurezza deterministica. ### 3D. Mexal MCP e Promo MCP **Mexal MCP** (gia' in uso in altri progetti, esteso in v2.0): singleton che espone Mexal/Passepartout (gestionale aziendale) come MCP. Tool principali: ```python @mcp.tool() def dettaglio_articolo(codice: str) -> dict @mcp.tool() def disponibilita(codice: str, magazzino: str = None) -> dict @mcp.tool() def prezzo_per_cliente(codice: str, cliente_id: str) -> dict @mcp.tool() def listino_per_classe(classe_sconto: str) -> dict @mcp.tool() def storico_acquisti(cliente_id: str, mesi: int = 24) -> dict @mcp.tool() def fatturato_corrente_anno(cliente_id: str) -> dict @mcp.tool() def verifica_credito(cliente_id: str) -> dict ``` **Promo MCP** (nuovo in v2.0): singleton dedicato alle promo Arco. Vive separato da Mexal perche' le promo sono spesso gestite con strumenti propri (Excel, PowerApps, DB custom) e non sono parte del gestionale ERP. Tool: ```python @mcp.tool() def promo_attive(filtri: dict, cliente_id: str = None) -> list[dict] @mcp.tool() def bundle_per_categoria(categorie: list[str]) -> list[dict] @mcp.tool() def scadenza_promo(codice: str) -> dict @mcp.tool() def progresso_target_sconto(cliente_id: str, codice: str) -> dict ``` L'ultimo tool e' un esempio concreto di valore consulenziale: "il cliente Rossi ha fatturato Bosch per 18.500 EUR sull'anno corrente, target prossimo scaglione sconto a 20.000 EUR. Aggiungere il modello SMV68N20EU per 1.800 EUR lo porterebbe oltre soglia, attivando lo sconto +3% retroattivo su tutto l'anno". Questo non e' "search", e' consulenza commerciale che richiede dati Mexal (fatturato attuale) + Promo (regole target) + Knowledge Tool brand (suggerire prodotto sensato per il caso). **Importante**: Mexal MCP e Promo MCP sono **visibili al Knowledge Tool**, non solo al Supervisor. Pattern emerso dalla review: se i Knowledge Tools brand non vedono Mexal, non possono fare suggerimenti commerciali ("aggiungi questo per arrivare al target sconto"). I "silos" tra livelli vanno evitati. ## Livello 4: Configuration Context (stato persistito tipizzato) Il quarto livello tiene **lo stato strutturato di una sessione utente**, distinto dalla history conversazione. La differenza e' radicale: - **History conversazione**: lista di messaggi user/assistant. Vive nel prompt (o estratta in un DB ma data al modello come testo). E' opaco: il modello "ricostruisce mentalmente" cosa ha proposto. - **Configuration Context**: oggetto JSON tipizzato. Vive in DB persistente. Tutti i livelli (Supervisor, PIM, Knowledge Tools, Rule Engine) lo leggono e aggiornano in modo strutturato. E' deterministico: il Rule Engine puo' valutarlo, il Supervisor puo' renderizzarlo. ### Schema base ```typescript type ConfigurationContext = { id: string // session_uuid o progetto_id tipo: "cucina" | "ufficio" | "negozio" | "altro" cliente_id?: string // se attivata fase commerciale moduli: Module[] vincoli_attivi: string[] // rule IDs gia' verificate (per audit) fonte_per_campo: Record created_at: ISO8601 updated_at: ISO8601 } type Module = { id: string tipo: "cassetto" | "elettrodomestico" | "anta" | "ripiano" | ... brand?: string // se determinato codice?: string // se selezionato attributi: Record // misure, classe, opzioni vincoli_emessi: Vincolo[] // dal Rule Engine } ``` ### Esempio di evoluzione di una sessione ```json // Turno 1: utente "voglio fare una cucina con cassetti LEGRABOX" { "id": "sess_abc123", "tipo": "cucina", "moduli": [ { "id": "m1", "tipo": "cassetto", "brand": "blum", "attributi": {"sistema": "LEGRABOX"} } ], "vincoli_attivi": [], "fonte_per_campo": { "moduli.m1.brand": "user_input" } } // Turno 2: utente "NL 500 mm, portata 40 kg" { "id": "sess_abc123", "tipo": "cucina", "moduli": [ { "id": "m1", "tipo": "cassetto", "brand": "blum", "attributi": {"sistema": "LEGRABOX", "NL_mm": 500, "portata_kg": 40, "guide_set": "750.5001S"} } ], "vincoli_attivi": ["R005:pass"], "fonte_per_campo": { "moduli.m1.NL_mm": "user_input", "moduli.m1.portata_kg": "user_input", "moduli.m1.guide_set": "BlumKnowledge.assemble_distinta" } } // Turno 3: utente "aggiungi una lavastoviglie 60 classe A" { "id": "sess_abc123", "tipo": "cucina", "moduli": [ { "id": "m1", ... }, { "id": "m2", "tipo": "elettrodomestico", "attributi": { "categoria": "lavastoviglie", "larghezza_cm": 60, "classe_energetica": "A" } } ], "vincoli_attivi": ["R005:pass", "X-COMPAT-001:warn"], "fonte_per_campo": { ..., "moduli.m2.categoria": "user_input", "moduli.m2.vincolo_X-COMPAT-001": "RuleEngine" } } ``` Il vincolo `X-COMPAT-001:warn` e' un esempio di vincolo cross-modulo emesso dal Rule Engine: "lavastoviglie da 60 in cucina con cassetti retrostanti ha rischio nicchia". Senza Configuration Context strutturato, questo vincolo non emerge: il LLM "perde memoria" tra turni. ### Persistenza - **Storage**: Postgres con colonna `state JSONB`, indici su `session_id` e `cliente_id`. - **TTL**: 30 giorni dopo l'ultima modifica (configurazioni abbandonate vengono ripulite). Estendibile su richiesta utente per progetti aperti. - **Snapshot per audit**: ad ogni transizione, append immutabile su `configuration_audit` (chi ha modificato cosa, da quale fonte, con quale rule_id triggered). ### Conseguenza architetturale Con Configuration Context come stato persistito: - Il **Rule Engine** puo' valutare vincoli cross-modulo (es. lavastoviglie + cassetti retrostanti). - Il **Supervisor** puo' renderizzare riepilogo strutturato in qualunque momento ("hai configurato: ..., vincoli aperti: ..."). - L'utente puo' tornare su una configurazione 5 giorni dopo, senza che il LLM debba "ricostruire" da una history conversazione lunga. - L'**offerta finale** generata da Mexal MCP attinge a un input strutturato, non a un testo che il modello ha tenuto a mente. - L'**audit** e' completo: ogni campo ha la sua fonte (chi/quando l'ha messo). Questo livello e' **assente** nel design v1.x. La sua aggiunta e' uno dei tre cambiamenti strutturali della v2.0 (gli altri due sono Typed Query Layer e PIM cross-brand). ## Livello 5: Infrastruttura Il quinto livello e' l'infrastruttura di esecuzione. E' il livello "boring" del sistema (deve solo funzionare bene), ma e' la fondazione su cui poggia tutto. Le scelte qui sono in continuita' col v1.x. | Componente | Strumento | Ruolo | |---|---|---| | Workflow durabili | Inngest | Orchestrator + event bus + queue | | Database | Postgres + pgvector | Tutto: PIM, Configuration Context, Inngest state, Langfuse data, audit log | | LLM | Anthropic API | Sonnet (Supervisor) + Haiku (slot filling, lookup) | | MCP runtime | FastMCP Python | Server tool per Knowledge / PIM / Mexal / Promo / Rule | | Observability | Langfuse + OpenTelemetry GenAI | Trace cross-livello | | Reverse proxy | Caddy | TLS + routing | | Edge | Cloudflare | DDoS + WAF | | Embedding (in uso BlumCat oggi) | sentence-transformers (mpnet 768d) | Embedding multilingua locale, no API esterna | | PDF parsing | MinerU + pdfplumber + PyMuPDF | Estrazione strutturata + raw text supplementare + pre-render JPG citazioni | | Application errors | Sentry | Errori applicativi backend | | Uptime monitor | UptimeRobot | Disponibilita' endpoint critici | Un singolo VPS Linux KVM 2-4 (2-4 vCPU, 8-16 GB RAM) basta per i mesi 1-12. Costo infrastruttura: ~10-15 EUR/mese + variabile API Anthropic (50-300 EUR/mese stimati). Il tooling dello stack v2.0 si distribuisce su **3 livelli temporali**, da non confondere: **A. In uso oggi nel sistema reale (BlumCat in produzione)**: SQLite + sentence-transformers locale (mpnet 768d) + FastAPI Python + Anthropic API + NSSM Windows (deploy). Pattern semplice ma funzionante. **B. Da introdurre nel migration path BlumCat -> arcocat v2.0** (10-13 settimane, vedi [Stack minimo per partire](#stack-minimo-per-partire)): **Postgres + pgvector** (sostituisce SQLite quando il volume cresce e serve PIM cross-brand) + **Inngest** (workflow durabili) + **Langfuse** (observability) + **FastMCP** (MCP server runtime) + **Caddy + Cloudflare** (network). Plus i 4 livelli architetturali nuovi (Typed Query Layer, Supervisor, PIM, Configuration Context). **C. Opzionali / Mese 4+ (solo se serve, dopo il primo brand onboardato)**: Voyage AI o Ollama (alternative embedding), Akeneo PIM full (solo se ereditarieta' attributi profonda), LiteLLM (gateway provider failover), Coolify (PaaS self-host), Outline (wiki interna), MkDocs (runbook). **Per Dotcom (concessionari Mexal) che vogliono partire con un cliente pilota**: livello A puo' bastare per i primi 2-3 mesi (refactoring del proprio core Mexal come MCP) + livello B graduale dal mese 4 quando emerge la necessita' di multi-brand strutturato. ## MCP server: cosa, quando, come MCP (Model Context Protocol, Anthropic, ora governato da Linux Foundation) e' il **collante** tra Supervisor e fonti. Un server MCP espone funzioni in modo standardizzato. Il client (Supervisor LLM o app diversa) si collega e *scopre da solo* quali funzioni ci sono, parametri, tipi di ritorno. Tre cose si possono esporre: - **Tool**: funzioni invocabili dall'agente, es. `cerca_cliente(query)` - **Resource**: dati read-only che l'agente puo' leggere, es. schema DB, listino - **Prompt**: template di prompt riutilizzabili ### Quando ha senso costruirlo | Scenario | MCP? | |---|---| | Esponi logica/sistemi proprietari a piu' tipi di agenti (Claude Code + Claude.ai + custom) | Si' | | Wrappi logica di business ad alto livello (3-4 chiamate gestionale in 1 tool) | Si' | | Standardizzi accesso a sistemi interni per il team | Si' | | Solo Claude Code per dev, gia' copri con una *skill* markdown | No, basta skill | | App Python deterministica: import diretto della libreria | No, no LLM nel mezzo | ### Skill Claude Code != MCP server Distinzione fondamentale, spesso confusa: | | Skill (es. `gestionale-webapi`) | MCP server | |---|---|---| | Cos'e' | File markdown con istruzioni | Processo con funzioni eseguibili | | Come Claude la usa | Legge il markdown, poi *esegue lui* via Bash/curl | Invoca direttamente i tool, riceve JSON | | Funziona con | Solo Claude Code (e client compatibili) | Qualsiasi client MCP | | Type safety | Nessuna | Schema tipizzato | | Effort | 1 file MD | Processo da scrivere e deployare | Tieni la skill di Claude Code per il tuo lavoro come sviluppatore. Costruisci l'MCP server solo il giorno in cui nasce il primo consumatore non-Claude-Code (Supervisor agentico, chatbot web, agente custom). ### Esempio minimo Python ```python from mcp.server.fastmcp import FastMCP import httpx, os mcp = FastMCP("gestionale-mcp") @mcp.tool() def cerca_cliente(ragione_sociale: str, limite: int = 10) -> list[dict]: """Cerca clienti per ragione sociale. Restituisce max `limite` risultati.""" r = httpx.get( f"{os.environ['ERP_BASE_URL']}/clienti", params={"q": ragione_sociale, "limit": limite}, auth=(os.environ['ERP_USER'], os.environ['ERP_PASSWORD']), ) return r.json() if __name__ == "__main__": mcp.run(transport="stdio") # locale; "streamable-http" per remoto ``` ### Pattern: tool ad alto livello Non esporre 1:1 le 50 endpoint del gestionale. Esponi 5-8 tool che orchestrano la logica: ``` cerca_cliente(query) consulta_disponibilita(codice) verifica_credito(cliente_id) # esposizione + storico pagamenti genera_offerta(cliente, articoli) # 3-10 chiamate gestionale sotto crea_ordine(offerta_id, conferma) storico_acquisti(cliente_id, mesi=12) ``` L'agente non deve sapere come funziona il gestionale, sa solo "voglio l'offerta per cliente X". E tu, dietro, sei libero di cambiare implementazione. ## Sicurezza, hard rules, approval Tre layer concentrici di controllo. Servono tutti e tre, non sono ridondanti. **Layer A — Orchestrator come supervisor strutturale.** Il workflow engine e' il primo controllore. Il Supervisor (Livello 2) e' il secondo: non improvvisa, esegue flussi tipizzati pre-definiti. **Layer B — Critic / Guardian agent.** Per i flussi critici, un secondo agente LLM specializzato valida l'output prima di proseguire. ```typescript const proposta = await step.run("genera", () => callAgent("comm-agent", { ... }) ); const validazione = await step.run("valida", () => callAgent("guardian-agent", { output: proposta, rules: ["non promettere sconti >20%", "tono coerente brand"] }) ); if (!validazione.ok) { // escalation umana o rigenerazione } ``` **Layer C — Hard rules nel codice.** Cose che nessun agente puo' aggirare, indipendentemente da quanto e' "intelligente": - **Budget cap**: ogni agente ha tetto giornaliero in euro. Si ferma a quota. **Plus: budget cap esplicito per session** (vedi 8° principio Cost variability). - **Action allowlist**: l'agente puo' leggere ma non modificare ordini sopra X EUR. - **Approval gate forzato**: azioni critiche (invio email cliente, modifica anagrafica, fatturazione) richiedono SEMPRE click umano. - **Rate limiting** sui tool MCP: massimo N chiamate al minuto per agente. - **PII redaction** prima del log su Langfuse. - **MCP ephemeral / lazy-connect (aggiunto in v2.2)**: i Knowledge Tool MCP NON devono essere connessi per intera sessione. Vanno **istanziati lazy on-demand e disposti post-uso**. Pattern emergente identificato in audit aprile 2026: tenere 12 MCP server live tutta la sessione quando ne usi 3 = 9 attack surface inutili + costo connessione pagato 24/7. A 30 brand questo diventa insostenibile. Hard rule: connect-on-call, dispose-on-return. Il **Rule Engine deterministico** (Livello 3C) si aggiunge a questi tre layer come quarto controllo per i vincoli tecnici di prodotto, non per regole di safety LLM. Un prompt si aggira con prompt injection. Una `if/else` Python no. Il guardian agent (Layer B) aiuta, ma il vero firewall e' il codice deterministico (Layer C). ## Observability: Langfuse + OpenTelemetry GenAI Cinque livelli da monitorare: | Livello | Cosa misuri | Strumento | |---|---|---| | Infrastructure | MCP server up? Endpoint risponde? | UptimeRobot + Sentry | | Trace agentico | Cosa ha fatto, perche', in che sequenza | Langfuse + OTel GenAI | | Cost | Euro per agente, per task, per cliente | Langfuse (nativo) | | Quality / Eval | Risposta accurata? Rispetta le regole? | Langfuse Datasets + Promptfoo | | Business outcome | Ha risolto il problema dell'utente? | Dashboard custom su Postgres Langfuse | OpenTelemetry GenAI fa da tessuto connettivo. Senza, hai osservabilita' a isole (un log qui, un grafico la'). Con, ogni richiesta utente attraversa lo stack lasciando una trace correlata unica: {` [Utente chiede in chat] | v [Typed Query Layer — span: parsing slot] | v [Supervisor — span: routing decision + filter-then-validate] | +-- [PIM span: filtra] | +-- [Knowledge Tool span: valida_compatibilita] | | | v | [Rule Engine span: evaluate] | +-- [Mexal MCP span: prezzo_per_cliente] | v [Configuration Context update — span] | v [Synth supervisor — span: risposta italiana] | v [Risposta a utente] `} Quando qualcosa va storto, in trenta secondi vedi se e' colpa del parser, del PIM, del Knowledge Tool, del Rule Engine, o di Mexal. Instrumentare e' una riga per tool: ```python from mcp.server.fastmcp import FastMCP from langfuse import observe mcp = FastMCP("blum-knowledge") @mcp.tool() @observe() # questa def cerca_knowledge(query: str, top_k: int = 5) -> list[dict]: """...""" return results ``` - **Settimana 1**: Langfuse self-hosted (Docker compose, un'ora). - **Mese 1-2**: solo le viste built-in di Langfuse. Vedi *cosa* misurare prima di costruire dashboard custom. - **Mese 3+**: dashboard custom solo per i KPI business che servono davvero. Costruire dashboard Grafana custom *prima* di avere sei settimane di dati reali. Spendi un mese a fare grafici che non guarderai. ## Auditabilita': l'UUID di correlazione Il "trucco" che cambia il gioco. Genera un UUID quando parte il workflow, propagato come metadato in *tutti* i livelli: ``` inngest_run_id = langfuse_trace_id = audit_id (campo custom su gestionale/CRM) = configuration_context.id (per stato di sessione) ``` Risultato: dato un record di business (offerta inviata, fattura, mail spedita, ordine modificato, configurazione salvata), in un click vai a vedere l'intera storia agentica che l'ha prodotto. Approvazioni umane incluse. Auditabilita' totale per ogni decisione, GDPR-ready (data subject request → trace immediata), "explainable AI" reale, debug 10x piu' veloce in produzione. Plus in v2.0: `Configuration Context` ha campo `fonte_per_campo` che traccia per ogni attributo della configurazione **chi l'ha messo** (utente, PIM, BrandKnowledge, Mexal, RuleEngine). Audit non solo del workflow, ma del **dato**. ## Memoria: principio dei 3 livelli Sezione consolidata in v1.1, mantenuta in v2.0. E' il principio architetturale che organizza dove vivono i dati nel sistema: **tre livelli, separati per natura e ciclo di vita**, in cui ogni dato ha un solo posto autoritativo. ### Livello 1, stato operativo (runtime, breve termine) **Cosa**: stato del workflow in corso, variabili di sessione (cliente corrente, ordine in elaborazione), contesto agente che cambia turno per turno, **Configuration Context** (Livello 4 della mappa). **Dove**: lo state durabile di Inngest + il Configuration Context Postgres JSONB. Per cache calde (es. token utente, dati di sessione TTL minuti) eventuale Redis aggiuntivo. **TTL**: ore o giorni per workflow run; 30 giorni default per Configuration Context. ### Livello 2, memoria di business (source of truth) **Cosa**: clienti, ordini, fatture, ticket, anagrafica articoli, pratiche aperte. Sono i dati veri dell'azienda. **Dove**: il **gestionale aziendale** (Mexal/Passepartout o equivalente), oppure il CRM/ERP. **Non** una tabella propria dell'agente. **TTL**: nessuno. Sono il dato vero, governato dai processi aziendali esistenti. L'agente legge e scrive sul gestionale tramite gli MCP server. **Non** duplica anagrafiche, ordini, codici articolo in tabelle proprie. Duplicazione = divergenza silenziosa garantita statisticamente. Il gestionale resta autoritativo, gli MCP server sono il layer di astrazione che lo rende consumabile dagli agenti. **Eccezione attentamente delimitata in v2.0**: il **PIM** (Livello 3B) NON duplica i dati del gestionale (anagrafica, prezzo, disponibilita': quelli restano in Mexal). Il PIM tiene **attributi tecnici cross-brand normalizzati** (larghezza_cm, classe_energetica) che il gestionale italiano tipicamente non strutturare. E' uno strato di arricchimento sopra Mexal, non un duplicato. Mexal resta source of truth per anagrafica, prezzo, disponibilita'. ### Livello 3, conoscenza (retrieval semantica + Rule Engine) **Cosa**: documenti, manuali tecnici, procedure, FAQ, email storiche, knowledge base. Materiale che l'agente "consulta" non "scrive". Plus regole tecniche eseguibili. **Dove**: SQLite per ogni Knowledge Tool brand (chunk + embedding 768d) + R*.md per regole eseguibili. Plus pgvector centrale solo se serve discovery cross-corpus (raro, vedi v2.0 sotto). **TTL**: aggiornamento periodico (annuale per manuali brand; opportunistico per regole). Vedi il flusso ETL notturno descritto in [Agentizzare un'azienda: timeline e flussi reali](/studio/flussi-agentici-pmi/) per il pattern operativo. Il livello "conoscenza" merita un trattamento a parte quando il source documentale e' un manuale tecnico voluminoso (centinaia di pagine, tabelle, formule, codici verificabili). Pattern completo (pipeline OCR doppia, wiki autoritativo a 4 categorie, single source of truth, cinque pattern di context engineering, eval set come gate): [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/). ### Vista d'insieme aggiornata v2.0 {` +----------------------------------------------------------+ | Livello 1 — Stato operativo (runtime, breve termine) | | Cosa: workflow state, variabili sessione, ConfigContext | | Dove: Inngest state + Configuration Context (Postgres) | | TTL: ore / giorni | +----------------------------------------------------------+ | Livello 2 — Memoria business (source of truth) | | Cosa: clienti, ordini, fatture, ticket, anagrafica | | Dove: gestionale aziendale (Mexal) o CRM | | TTL: nessuno (e' il dato vero) | | L'agente legge/scrive QUI tramite MCP, NON duplica | | Eccezione: PIM tiene attributi tecnici NORMALIZZATI | | (non duplica prezzo/disponibilita', li integra) | +----------------------------------------------------------+ | Livello 3 — Conoscenza + Regole eseguibili | | Cosa: wiki narrativi brand + R*.md regole tecniche | | Dove: SQLite per Knowledge Tool brand (chunks 768d) | | + R*.md frontmatter eseguibile per Rule Engine | | TTL: annuale (manuali) / opportunistico (regole) | +----------------------------------------------------------+ `} ## I 4 contratti tipizzati Sezione di riferimento per chi implementa o evolve l'architettura. Sono i quattro tipi di dato che attraversano i confini tra livelli. Definirli prima di scrivere codice e' la cosa piu' importante (consenso unanime sia da review architetturale comparativa interna che da valutazione esterna ChatGPT/Gemini). ### TypedQuery (output del Typed Query Layer) ```typescript type TypedQuery = { // intent della query, come classificato dal Layer 1 intent: "search" | "consult" | "configure" | "quote" | "compare" | "promo_check" | "lookup" // categoria prodotto (dal vocabolario PIM), se determinabile categoria?: string // filtri tipizzati, con valori validati contro lo schema PIM della categoria filtri?: Record // es. {larghezza_cm: 60, classe_energetica: "A"} // testo libero per la parte consulenziale, se presente testo_libero?: string // riferimento al Configuration Context attivo, se presente contesto_progetto_ref?: string // metadati del parsing meta: { confidence: number // 0..1 sull'intent classification parser_used: "regex" | "ner" | "llm" latency_ms: number } } type ScalarValue = string | number | boolean ``` E' codice puro. Lo definisce uno sviluppatore una volta, lo schema vive in TypeScript types o Pydantic models. Non e' editabile da umani non tecnici. ### CategorySchema (PIM lite, schema per categoria prodotto) E' **MD-Karpathy editabile da umani**. Vive in `wiki_arcocat/categorie/C*.md`. ```markdown --- id: C002 categoria: cerniera versione: 1.1 ereditato_da: null attributi: - { nome: brand, tipo: select, valori: [blum, hettich, salice], identifier_partial: true } - { nome: serie, tipo: text, identifier_partial: true } - { nome: angolo_apertura, tipo: select, valori: [95, 100, 110, 120, 155, 170], unita: "gradi" } - { nome: tipo_battuta, tipo: select, valori: [interna, esterna, sovrapposta] } - { nome: tipo_frontale, tipo: select, valori: [legno, vetro, alluminio, composito] } - { nome: spessore_frontale_mm, tipo: measurement, unita: mm } - { nome: peso_anta_kg_max, tipo: measurement, unita: kg } - { nome: codice, tipo: text, identifier: true } - { nome: tipo_tecnologia, tipo: select, valori: [standard, BLUMOTION, TIP-ON, SERVO-DRIVE] } campo_label: serie --- # C002 - Cerniere per anta Categoria di hardware per la rotazione delle ante di mobili. Multi-brand: Blum (sistema CLIP top), Hettich, Salice. ## Discriminanti chiave per la consulenza - **Angolo apertura**: 95 gradi (mobili a parete standard) vs 110 gradi (cucine moderne) vs 155-170 gradi (apertura totale per accesso ergonomico). - **Tipo battuta**: interna (frontale dentro al fianco) vs sovrapposta (frontale copre fianco). - **Tecnologia**: BLUMOTION = chiusura ammortizzata; TIP-ON = apertura senza maniglia. ## Regole correlate - [[R002 numero cerniere per anta]] - [[R007 cerniere per ante in vetro]] - [[R009 cerniere per ante pesanti]] ``` L'esperto Blum apre il file via UI, modifica `valori` di `angolo_apertura` per aggiungere un nuovo valore ammesso, salva. Il PIM al boot ricarica lo schema, le query con `angolo_apertura: 105` ora sono ammesse. ### Rule (formato Rule Engine, MD-Karpathy esteso con frontmatter eseguibile) E' **MD-Karpathy editabile da umani**, con frontmatter esteso per la parte eseguibile. Vive in `wiki_/regole/R*.md`. Esempio gia' mostrato per R005 LEGRABOX. Schema del frontmatter: ```yaml id: string # R005, R007, ecc. nome: string # nome leggibile ambito: string # "blum.legrabox", "bosch.lavastoviglie", "cross.compat" versione: string # semver (1.0, 1.1, 2.0) fonte_pagine_fis: number[] # pagine manuale per citazione # parte ESEGUIBILE condizione: ConditionExpression # DSL JSON (vedi sotto) azione: "allow" | "deny" | "warn" | "suggest" | "block" messaggio_template: string # template Jinja2 con {{ var }} # embedded test test_cases: - in: object expect: "pass" | "warn" | "block" ``` DSL della condizione: ```yaml # foglia { fatto: string, op: Op, valore: ScalarValue } # Op: "=" | "!=" | ">" | "<" | ">=" | "<=" | "in" | "not_in" | "contains" # combinatori { all_of: ConditionExpression[] } { any_of: ConditionExpression[] } { none_of: ConditionExpression[] } ``` Esempi piu' avanzati: ```yaml # Regola cross-modulo: lavastoviglie 60 con cassetti retrostanti id: X-COMPAT-001 ambito: cross.compat condizione: all_of: - { fatto: moduli.elettrodomestico.categoria, op: "=", valore: lavastoviglie } - { fatto: moduli.elettrodomestico.larghezza_cm, op: ">=", valore: 60 } - { fatto: moduli.cassetto_retro.presente, op: "=", valore: true } - { fatto: moduli.cassetto_retro.profondita_mm, op: "<", valore: 560 } azione: warn messaggio_template: "La lavastoviglie {{moduli.elettrodomestico.codice}} richiede nicchia 560mm. Hai cassetti retrostanti con profondita' {{moduli.cassetto_retro.profondita_mm}}mm: c'e' il rischio di non incastrarsi." test_cases: - in: moduli: elettrodomestico: { categoria: lavastoviglie, larghezza_cm: 60, codice: SMV68N20EU } cassetto_retro: { presente: true, profondita_mm: 540 } expect: warn ``` ### ConfigurationContext (stato di sessione persistito) E' codice puro. Schema gia' mostrato sopra. Persistito in Postgres `config_context` con colonna `state JSONB`, indici GIN su path comuni (`cliente_id`, `tipo`, `created_at`). ```typescript type ConfigurationContext = { id: string // = trace_id, audit_id tipo: "cucina" | "ufficio" | "negozio" | "altro" cliente_id?: string moduli: Module[] vincoli_attivi: VincoloRecord[] fonte_per_campo: Record created_at: ISO8601 updated_at: ISO8601 status: "draft" | "submitted" | "quoted" | "ordered" | "abandoned" } type Module = { id: string tipo: string // "cassetto" | "elettrodomestico" | ... brand?: string codice?: string attributi: Record vincoli_emessi: VincoloRecord[] } type VincoloRecord = { rule_id: string azione: "allow" | "deny" | "warn" | "suggest" | "block" messaggio: string fonte_pagina?: number triggered_at: ISO8601 } type FonteValue = "user_input" | "PIM" | "BrandKnowledge." | "Mexal." | "RuleEngine" ``` ### I 4 contratti come "compiler boundary" Questi quattro tipi sono il **confine tra livelli**. Una volta congelati (versione 1.0 di ciascuno), il sistema interno puo' evolvere mantenendoli compatibili. Si puo' cambiare LLM, vendor PIM, framework Supervisor, senza riscrivere tutto. Se invece si toccano (versione 2.0 di un contratto), tutti i consumatori vanno aggiornati, e va gestita la migrazione. Disciplina: **i 4 contratti vanno definiti PRIMA del codice**. E' la cosa piu' importante. ## Channel runtime: Hermes, OpenClaw Per i miei studi del 2026 ho considerato due agent runtime emergenti: Hermes Agent di Nous Research e OpenClaw. Sono agent runtime con bridge nativi verso 16+ piattaforme messaging (Telegram, WhatsApp, Discord, Slack, Signal, iMessage), memoria persistente, skills. Cosa NON sono: non sostituiscono Inngest, MCP, Langfuse. Si appoggiano agli MCP server. Non gestiscono workflow durabili business-critici. Quando entrerebbero in gioco: quando arriva la richiesta "voglio che il commerciale, il magazziniere e il cliente possano parlare con i miei agenti via Telegram/WhatsApp". Non prima. Cautele: | Pro | Contro | |---|---| | 16+ canali messaging gratis | Release ogni 7-10 giorni, instabilita' | | Memoria persistente built-in | Lock-in di framework giovane | | Setup veloce, prototipazione rapida | Track record di stabilita' ancora corto | | Compatibili con MCP server | Per workflow critici serve comunque Inngest sotto | Mese 1-3: non adottarli. Costruire fondamenta. Mese 4-6: rivalutare con dati Langfuse alla mano. ## I nove punti aperti (cosa non basta) La roadmap base copre il 70% di cio' che serve. Per un sistema veramente production-grade aziendale servono altri pezzi che vanno previsti, anche se non subito. ### 1. Identity, secret management, autenticazione | Cosa | Raccomandato | Quando | |---|---|---| | Secret vault | Infisical | Mese 2-3 | | Identity / SSO | Authelia | Mese 4-6 | | Auth tra agenti/MCP | API key + JWT firmati | Mese 2 | | Multi-tenant isolation | Postgres RLS nativo | Mese 4+ | ### 2. Data layer | Cosa | Raccomandato | Quando | |---|---|---| | Vector DB (RAG) per Knowledge Tools small (sotto 100k chunks) | SQLite + BLOB embedding 768d cosine in-process | Subito (in uso BlumCat) | | Vector DB (RAG) per scale maggiori (PIM, audit, multi-tenant) | pgvector su Postgres | Mese 2-3 (migration v2.0) | | Embedding model (in uso BlumCat oggi, locale, no API) | sentence-transformers mpnet 768d multilingua | Subito | | Embedding model (alternative quando OOM o serve qualita' superiore) | Voyage AI o locale via Ollama | Mese 4-6 (opzionale) | | Document storage (S3) | MinIO | Mese 2-3 | | PDF parsing (markdown strutturato + tabelle) | MinerU | Subito | | PDF parsing (raw text supplementare per formule grafiche) | pdfplumber | Subito (necessario, MinerU da solo perde formule) | | PDF rendering (pre-render JPG per chip citazione manuale inline) | PyMuPDF (`fitz`) | Subito | | FTS5 hybrid retrieval (BM25 nativo) | SQLite FTS5 nativo per Knowledge Tools | Subito | | Full PIM | Akeneo (solo se justified) | Mese 12+ | Un singolo Postgres con estensione pgvector copre: dati Langfuse, Inngest state, vector DB per RAG (se non basta SQLite), Configuration Context, audit log, PIM lite (con JSONB). Un servizio invece di cinque. SQLite locale per Knowledge Tool brand. MinIO solo per file binari. ### 3. Eval e testing avanzato | Cosa | Raccomandato | Quando | |---|---|---| | Eval framework | Promptfoo | Mese 3 | | LLM-as-judge | Langfuse evals nativo | Mese 3 | | RAG eval specifico | RAGAS (faithfulness, context precision) | Mese 3 | | Self-correcting RAG (CRAG) come gate post-Knowledge-Tool | Pattern Higress-RAG / arXiv:2602.23374. Adaptive routing + dual hybrid retrieval. Trigger: post-2°brand stabile, se eval mostra retrieval quality limitante in casi confidence-low | Post 2°brand (Mese 4-5 stimato). Aggiunto in v2.2. | | Red-teaming | PyRIT + Garak | Mese 4-5 | | Test Rule Engine | unittest + test_cases embedded in R*.md | Subito | ### 4. Compliance e governance (AI Act EU + GDPR) | Cosa | Raccomandato | Quando | |---|---|---| | PII detection / redaction | Microsoft Presidio | Mese 2 (subito se PII reali) | | Audit log immutabile | Postgres append-only + trigger | Mese 2-3 | | Data deletion (RTBF) | Soft-delete + crypto-shredding | Mese 3-4 | | Risk assessment AI Act | Documento markdown versionato (template `risk-assessment-AI-Act.md` con campi obbligatori: tipo applicazione, dati trattati, output, feedback umano, monitoraggio). **URGENTE**: enforcement Commissione EU dal 2 agosto 2026. | **Mese 2-3 (NON 4-6)**: aggiornato in v2.2 con urgenza esplicita. | | Data residency | VPS Italia/UE + Anthropic EU endpoint | Subito | ### 5. Cost optimization | Cosa | Raccomandato | Quando | |---|---|---| | Prompt caching | Anthropic native | Subito | | Model routing | Logica custom (Haiku per slot filling, Sonnet per Supervisor) | Mese 2-3 | | AI Gateway proxy | LiteLLM self-host | Mese 3-4 | | Batch API | Anthropic Batch (50% sconto) | Mese 3+ per task notturni | | Semantic cache | RedisVL su query ricorrenti | Mese 3-4 | | Local LLM fallback | Ollama + Llama/Qwen | Mese 6+ (richiede GPU) | ### 6. Disaster recovery e resilience | Cosa | Raccomandato | Quando | |---|---|---| | Backup database | WAL-G + cron | Subito | | Backup file storage | Restic o Borg | Mese 1-2 | | Circuit breaker | pybreaker | Mese 3 | | LLM provider failover | LiteLLM fallback rules | Mese 4-5 | | Graceful degradation | Pattern in codice | Mese 2-3 | ### 7. Network e infrastruttura | Cosa | Raccomandato | Quando | |---|---|---| | Reverse proxy + TLS | Caddy | Subito | | Container orchestration | Docker Compose | Subito | | PaaS self-host (alt) | Coolify | Mese 2-3 | | DDoS / WAF | Cloudflare free tier | Mese 2 | | SSH brute force | fail2ban | Subito | ### 8. Human interface | Cosa | Raccomandato | Quando | |---|---|---| | Admin dashboard / KPI | Metabase | Mese 3-4 | | Internal tool builder | Appsmith | Mese 4-6 | | Notifications multi-canale | Apprise | Mese 2 | | Feature flags / kill switch | Unleash | Mese 3-4 | | Editor wiki/regole UI | Custom Next.js + auth whitelist | Subito (per Knowledge Tools) | ### 9. Documentation operativa | Cosa | Raccomandato | Quando | |---|---|---| | Wiki / KB interna | Outline | Mese 2-3 | | ADR | Markdown in `/docs/adr/` versionato | Subito | | Runbook | Markdown + MkDocs Material | Mese 3-4 | | Wiki narrativi brand (Karpathy) | MD curato editabile via UI | Subito | | Schema PIM (CategorySchema) | MD curato in `wiki_arcocat/categorie/` | Subito | | Regole (R*.md) | MD curato in `wiki_/regole/` | Subito | Scrivere le ADR nel momento in cui prendi la decisione, non dopo. Una ADR di 200 parole scritta a freddo vale piu' di 2.000 parole "poi quando ho tempo" (mai). Template minimo: contesto, decisione, alternative considerate, conseguenze. ## Dimensionamento VPS Per uno stack agentico aziendale completo (Langfuse + Inngest + Postgres + MCP server + Caddy + Knowledge Tools + PIM + Configuration Context), un piano shared hosting non basta. Serve un VPS Linux con accesso root e Docker. | Piano | vCPU / RAM / SSD | Quando | Note | |---|---|---|---| | KVM 2 | 2 / 8 GB / 100 GB | MVP mese 1-3 | Sufficiente per Langfuse + Inngest + 2-3 MCP server + 1-2 Knowledge Tools | | KVM 4 | 4 / 16 GB / 200 GB | Mese 4-12 | Margine per crescita: knowledge tools multipli, PIM con dati reali, RAG | | KVM 8 | 8 / 32 GB / 400 GB | Mese 12+ se scali | Solo se hai molti knowledge tools concorrenti o RAG su grossi dataset | Se i tuoi clienti hanno dati sensibili (anagrafiche, ordini, dati commerciali), valutare un VPS in data center UE. Per l'API Anthropic, configurare l'endpoint EU. Tutto questo aiuta su GDPR e AI Act. ## Stack minimo per partire {` VPS Linux KVM 2 (~10 EUR/mese) +-- Docker Compose stack: +-- caddy reverse proxy + TLS +-- postgres un solo DB per tutto: | + pgvector - PIM lite (JSONB per categoria) | - Configuration Context | - Inngest state | - Langfuse data | - audit log immutabile +-- langfuse observability +-- inngest orchestrator + event bus + queue +-- typed-query-svc slot filling (regex + spaCy + Haiku fallback) +-- supervisor-svc Filter-then-Validate orchestrator +-- pim-mcp PIM lite MCP server +-- rule-engine-svc evaluator regole da R*.md +-- gestionale-mcp Mexal MCP server +-- promo-mcp Promo MCP server +-- blum-knowledge-mcp primo Knowledge Tool brand +-- (fail2ban sull'host) External SaaS (free tier): +-- Cloudflare DNS + WAF +-- UptimeRobot monitor critici +-- Sentry errori applicativi Anthropic API: pay-per-use (50-300 EUR/mese stimati) TOTALE: ~10-15 EUR infrastruttura + variabile API `} Tutto il resto della tabella sopra entra incrementalmente, in base a quale dolore emerge. Non installarli "preventivamente" e' la regola d'oro: aggiungi uno strumento *quando senti il dolore che risolve*, non prima. **Migration path da BlumCat (caso reale)**: BlumCat in produzione e' gia' un Knowledge Tool brand mascherato da chatbot. La transizione a v2.0 si fa in 5 step: 1. Estrarre la logica del classifier intent + tool call deterministici in un Knowledge Tool MCP autonomo (`blum-knowledge-mcp`). 2. Creare `pim-mcp` con primo schema categoria (es. cassetto, cerniera): popolare con i dati Blum gia' presenti. 3. Costruire Supervisor minimale (Python `if/elif` su intent) sopra Knowledge Tool + PIM. 4. Aggiungere Typed Query Layer davanti. 5. Estrarre Rule Engine standalone leggendo i R*.md di BlumCat (gia' Karpathy, basta estendere frontmatter). A quel punto, aggiungere un secondo brand (BoschKnowledge MCP) costa 1-2 settimane: clone del template Knowledge Tool, popolamento wiki narrativo, definizione regole specifiche brand. Il resto del sistema (Supervisor, PIM, Rule Engine, Typed Query) non si tocca. ## Pickup selettivi da letteratura industriale Sezione che traccia da dove vengono le scelte. Il design v2.0 e' triangolato su tre flussi indipendenti: **Articolo Yanli Liu "RAG, LLM Wiki, or GBrain?" (AI Advances Medium, 2026-04-25)**: - Adottato: lint workflow wiki (Karpathy pattern) come task DoIt mensile per orphan pages, stale claims, concept menzionati senza scheda. Skill come MD contract con frontmatter (estensione del pattern attuale BlumCat). - Scartato: Wiki Karpathy puro markdown+BM25 (SQLite+vector scala meglio per noi); cron skills GBrain con LLM-in-loop (DoIt fa lo stesso senza LLM-in-loop); Postgres+pgvector per casi che SQLite copre. **Articolo Pankaj "The Best RAG Architectures for AI Agents" (Medium, 2026-02-22)**: - Adottato: hybrid retrieval BM25+vector+RRF (k=60) come default per `cerca_knowledge` dei Knowledge Tools. RAGAS per eval RAG specifico (faithfulness, context precision). Semantic cache RedisVL per query ricorrenti. - Scartato: Weaviate (over-engineering per constellation di Knowledge Tools sotto 100k chunks; SQLite FTS5 + cosine + RRF Python copre); LightRAG (per discovery cross-corpus e' raro nel nostro caso); DSPy MIPROv2 (auto-prompt optimization confligge col principio "hard rules nel codice"; valutabile per ottimizzare narrative del system prompt, non per gate). **Anthropic "How we built our multi-agent research system" (engineering blog, 2025)**: - Adottato: orchestrator-worker pattern (lead agent + sub-agents) come fondazione del Supervisor v2.0. Pattern documentato +90.2% performance vs single-agent. - Strumento: LangGraph `langgraph-supervisor-py` come implementazione di riferimento, ma per la PMI Python deterministico minimale basta nella maggior parte dei casi. **Akeneo PIM (open source, docs)**: - Adottato: modello dati Family + Category + Attribute tipizzato + brand-as-attribute. Pattern di ereditarieta' attributi via `ereditato_da`. - Scartato: Akeneo full-blown (overhead sistemistico per 1 sviluppatore, valutabile solo se compaiono workflow data-entry strutturati e ereditarieta' profonda). Si replica il modello dati con Postgres JSONB. **Algolia federated search (4 tipi documentati)**: - Adottato: search-time merging con RRF per i casi cross-source dove serve aggregare (es. discovery cross-brand). - Concetto chiave assorbito: contenuti eterogenei (knowledge testuale, dati strutturati, business data) vanno in fonti separate ottimizzate per il loro tipo, NON in un indice unico. **Constructor.com (vendor B2B distributor pattern in produzione)**: - Validazione architetturale: PIM centralizzato + LLM Shopping Agent + Attribute Enrichment + Collections per bundle. Stesso pattern del v2.0, su scala enterprise. **Review esterna ChatGPT + Gemini (maggio 2026)**: - Convergenza forte (8 punti) su: asse mancante "Configuration Context", Filter-then-Validate, Typed Query Layer pre-LLM, Rule Engine deterministico, framing CPQ. Tutti integrati nel design v2.0. - Punto unico Gemini: "Knowledge Tools, NON agenti autonomi". Riformulato il modello constellation in questo senso. - Punto unico ChatGPT: distinzione tre stati Compatibile/Consigliato/Sconsigliato nella risposta. Adottato. ## Il meta-pattern: Claude-assisted ingest Sezione aggiunta in v2.3. E' il **meta-pattern** che rende fattibile l'intero design per un single-developer in PMI italiana. Senza esplicitarlo, l'effort necessario per costruire 5 livelli + 4 contratti + N Knowledge Tools sembra "troppo grosso per una PMI". Con questo pattern, diventa fattibile in 2-3 mesi part-time. ### Cosa e' (in pratica) Il developer (es. Andrea per BlumCat) lavora **in sessione iterativa con Claude Code** per costruire gli script Python che fanno il lavoro pesante: 1. **Definisci il problema**: "estrai dal PDF Bosch tutti i codici lavastoviglie con classe energetica" 2. **Claude scrive bozza script** (parser MinerU + regex + filtro) 3. **Tu testi**, scopri edge case, riporti a Claude 4. **Claude itera** (nuova bozza, nuovi test cases, miglior gestione edge case) 5. Dopo 3-5 iterazioni, lo script funziona Il risultato e' che **la struttura del sistema (PIM, wiki narrative, regole) e' per il 95% derivata dai dati stessi tramite questi script**, non scritta a mano da un esperto. L'esperto interno **non scrive YAML, non edita JSON**: corregge body markdown narrativo e aggiunge le **nozioni mentali** (regole non documentate che esistono solo nella sua testa). ### Perche' e' decisivo Per l'ecosistema PMI italiana, costruire pipeline data engineering custom per ogni cliente richiederebbe normalmente: - 1-2 data engineer dedicati per 3-6 mesi per il primo brand - 1 data engineer + 1 esperto dominio per 1-2 mesi per ogni brand successivo - Costi: 30-80k EUR per primo brand, 10-25k EUR per ogni successivo Con il pattern Claude-assisted ingest: - 1 developer Python competente + Claude in sessione iterativa per 2-3 settimane di ingest setup per il primo brand (gli script generati sono riusabili) - 1 developer + esperto dominio per 1 settimana di curation per ogni brand successivo (riuso script) - Costi: 8-15k EUR per primo brand setup, 2-4k EUR per ogni successivo Riduzione effort 5-10x rispetto al pattern tradizionale data engineering. **E' la ragione strutturale per cui questo design e' fattibile in PMI**. ### Cosa fa Python, cosa fa l'esperto, cosa fa il developer | Componente | Chi lo costruisce | Come | |---|---|---| | Parser MinerU/pdfplumber custom per il manuale brand | Developer + Claude (sessione iterativa) | 2-5 giorni di sessione Claude Code | | Estrattore tabelle codici e attributi tipizzati | Developer + Claude | 1-3 giorni | | Generatore frontmatter YAML per R*.md/D*.md/F*.md | Developer + Claude | 1-2 giorni | | Validator AI pre-save (Haiku ~$0.001 per save) | Developer + Claude | 1-2 giorni | | Body narrativo wiki (D*/G*/R*/F*) per top codici | **Esperto interno**, via UI editor wiki | 5-10 ore/settimana per 2-3 settimane | | Regole "non scritte" (nozioni mentali) | **Esperto interno**, via UI editor o sessione con dev | Stesso slot esperto sopra | | Gli altri 800 codici (longtail) | Python automatic con quality flag | Run notturno, esperto verifica solo flag rosso | Notare la divisione: **la complessita' tecnica e' nel codice (developer + Claude)**, **la conoscenza domain-specific e' nelle nozioni mentali (esperto)**. L'esperto NON deve diventare developer. Il developer NON deve diventare esperto del dominio. ### Conseguenza per onboarding multi-brand Quando arriva il 2°brand (es. Bosch dopo Blum), il developer NON ricostruisce i parser da zero. **Riadatta** quelli scritti per Blum: - Nuove regex per pattern codici Bosch (es. da `750.5001S` a `SMV68N20EU`) - Nuove categorie attributi (lavastoviglie ha attributi diversi da cassetto) - Stessa struttura di parsing, diverso input Questo riuso e' la ragione per cui le stime nel playbook scendono da "10-13 settimane primo brand" a "2-3 settimane secondo brand" a "5-7 giorni decimo brand". Non e' magia: e' il framework Claude-assisted gia' costruito che si propaga. Per Dotcom e simili: questo pattern e' il vostro **moltiplicatore**. Il primo cliente paga (parzialmente o pienamente) il setup framework con Claude. Tutti i clienti successivi riusano il framework, e voi vi posizionate come "team da 1-2 persone che competono con team da 10-20 di vendor enterprise". E' competitive advantage strutturale, non ottimismo. ## Pattern non adottati e perche' Sezione aggiunta in v2.2 (post audit 2026-05-05). Documenta esplicitamente i framework e i pattern che abbiamo **valutato e scartato**, con il razionale. Scopo: prevenire ri-discussioni circolari ai successivi audit ("non avevamo gia' deciso?"). | Framework / pattern | Valutato in | Decisione | Motivazione | |---|---|---|---| | **Microsoft Agent Framework 1.0 GA** (rilasciato 3 aprile 2026) | Audit 2026-05-05 | Scartato | Ottimo prodotto, ma ecosistema enterprise Microsoft / .NET, controcorrente del nostro principio "no enterprise vendor lock-in". Valore reale solo se Azure gia' nello stack (non e' il nostro caso). | | **DSPy 2.x** (160k download/mese, 16k stars) | Studio v1.x e riconfermato in audit 2026-05-05 | Scartato | Conflitto strutturale con principio 6 "Hard rules nel codice, non nei prompt": auto-tuning prompt produce output non auditabile, inaccettabile per CPQ B2B con offerte vere. Resta valutabile per ottimizzare narrative (esempi few-shot del system prompt), non per gate critici. | | **LangGraph come orchestrator principale** (LangGraph 1.0 GA 22 ottobre 2025, fonte primaria confermata) | Audit 2026-05-05 | Mantenuto solo come "minimale opzionale" | LangGraph 1.0 e' production-ready (Uber, LinkedIn, Klarna), ma full-blown e' over-engineered per single-brand. Il nostro Supervisor in Python deterministico (`if/elif` sui TypedQuery.intent) basta. LangGraph diventa interessante a N>5 worker concorrenti con gerarchie, scenario non immediato. | | **FastMCP 3.0** (rilasciato 19 gennaio 2026) | Audit 2026-05-05 | Rinviato | Architettura Components/Providers/Transforms e' elegante, hot reload utile, OpenTelemetry built-in tira via un layer custom. Migrazione 1.x → 3.0 NON banale (cambia il modello mentale). Trigger esplicito: rivalutare al 2°-3° brand attivo, dopo che il pattern Knowledge Tool MCP e' rodato. Costo: 1-2 settimane porting per Knowledge Tool. | | **Akeneo PIM Community Edition** | Studio v2.0 e riconfermato in audit 2026-05-05 | Scartato | Edition Community in regime di lenta manutenzione (ultimo significativo febbraio 2024, sospetto shift verso enterprise licensing). Conferma indiretta che PIM lite Postgres+JSONB e' strategicamente sicuro: non dipendiamo da progetto in deriva. | | **Anthropic Managed Agents** (citato in audit 2026-05-05 come "rilasciato 8 aprile 2026") | Audit 2026-05-05 | **NON CONFERMATO con fonte primaria** | Verifica diretta su anthropic.com/news non conferma esistenza prodotto. Probabile confabulazione di fonte terza nell'audit. Skippato fino a re-trigger con verifica primaria. Se in futuro Anthropic rilasciasse davvero un servizio simile: il calcolo costo/lock-in sara' (a) restiamo open-source self-hosted vs (b) deleghiamo orchestrazione a Anthropic. Per scala PMI ~50 conv/giorno, lock-in vendor sconsigliato a priori. | | **DOM-native browser agents** (alcuni paper HN aprile 2026 sostengono "agenti dovrebbero usare DOM nativo invece di API tool") | Audit 2026-05-05 | Scartato | Per CPQ B2B con sistemi gestionali stabili come Mexal, l'uso di API tool MCP e' piu' robusto del DOM scraping (DOM cambia, API stabili). Pattern non rilevante. | **Disclaimer onesto**: la lista NON e' definitiva. Ogni audit periodico (vedi `_private/PROMPT_AUDIT_PERIODICO.md`) puo' aggiungere righe nuove o promuovere "scartato" → "mantenuto" se il calcolo cambia. La cronologia delle decisioni vive in `_private/audit-log/`. ## Confidence statement Il design v2.0 e' il miglior approccio noto a maggio 2026 per il caso specifico (PMI italiana B2B distribuzione, 30+ brand, ~50 conv/g, dominio tecnico, codici verificabili, ERP italiana Mexal, integrazione consulenziale piu' che search). Triangolazione su quattro fonti indipendenti (CPQ classico, multi-agent paper Anthropic, PIM Akeneo, Filter-then-Validate documentato). Caveat onesti: 1. **Design non ancora implementato**: la verifica empirica si fara' con il primo onboarding di un secondo brand reale (es. BoschKnowledge oltre BlumKnowledge esistente). Promptfoo + RAGAS come gate. 2. **"Migliore" e' scope-specific**: per scale piu' grandi (enterprise multi-tenant, milioni di SKU, requisiti SLA), l'analisi costo-benefico cambia. 3. **SOTA evolve veloce**: agentic SOTA si aggiorna ogni settimana (LangGraph, MCP, nuovi pattern Anthropic). Regola: verificare framework SOTA prima di scrivere componenti custom. 4. **Eval baseline arcocat v2.3.6 ancorato quantitativamente** (validato in `arcocat/REPORT_EVAL_BASELINE.md`, v2.3.7): suite Promptfoo + RAGAS + arcocat custom metrics su 60 query MD-Karpathy / 11 Tier 2 ground truth. Stato baseline pre-R-rule: **grounding rate 30%** (% candidati con citazioni esplicite), **faithfulness 80%** (RAGAS, motivazioni grounded sui contexts), **context_precision 81%**, **status_accuracy Tier 2 91%** (10/11), **shortlist_recall 100%**, **3 sole R-rule citate** (R008/R009/R010 spondine LEGRABOX) su tutto il test set. Cost run completo $1.19 (sotto target $1.50), latency p95 7.7s, RAGAS success rate 100%. Lezione strategica primaria di v2.3.7: **eval baseline come ancoraggio quantitativo per gap noti**. Pre-eval i punti aperti (gap rule coverage, out-of-scope filter) erano qualitativi; post-eval sono misurati. Numeri come reference per ogni iterazione futura: re-run post-R-rule esperto Arco (atteso grounding >50%), post-2°brand (atteso shift distribution multi-brand), post-Step 5 Configuration Context. La transizione qualitativo→quantitativo e' principio metodologico, non tooling: rende citabili lo stato del sistema all'esperto di dominio quando decide priorita', e ai decisori che valutano l'investimento in iterazioni successive. Trigger di re-assessment formale del design: implementazione di Knowledge Tool secondo brand, primo refactor strutturale di un livello, nuovo SOTA pubblicato che cambia un'ipotesi cardine, incident produzione che evidenzi limite del design, **eval baseline drift superiore al 10% su una metrica chiave fra iterazioni adiacenti**. - L'agentizzazione di una PMI con cataloghi multi-brand e' un **CPQ con interfaccia conversazionale**, non un chatbot. Riconoscere il framing CPQ cambia tutto: si modella spazio configurazioni vincolate, non retrieval di prodotti. - Modello dati **bi-dimensionale**: knowledge consulenziale per brand (constellation di Knowledge Tools MCP, pattern Karpathy) + dato strutturato cross-brand (PIM lite singleton, Postgres JSONB con schema in MD-Karpathy). - **Cinque livelli**: Typed Query Layer (slot filling pre-LLM), Supervisor Filter-then-Validate, Fonti eterogenee (Knowledge Tools + PIM + Rule Engine + Mexal/Promo MCP), Configuration Context (stato persistito tipizzato), Infrastruttura (Inngest + Postgres + Anthropic). - Pattern centrale del Supervisor: **Filter-then-Validate**. PIM filtra (recall alto, precisione bassa), Knowledge Tool valida (precisione alta), Supervisor sintetizza con tre stati distinti (compatibile / consigliato / sconsigliato). In conflitto, vince il Knowledge Tool: il valore aggiunto consulenziale. - **Quattro contratti tipizzati** (TypedQuery, CategorySchema, Rule, ConfigurationContext) sono il confine tra livelli. Da definire PRIMA del codice. CategorySchema e Rule sono MD-Karpathy editabili; TypedQuery e ConfigurationContext sono codice puro. - **Karpathy come fondazione di rappresentazione del knowledge editabile** (wiki narrativi, regole, schemi categoria), NON come architettura completa. Sopra serve l'orchestrazione (Supervisor + Rule Engine + Configuration Context). Per applicazioni concrete dei flussi nel modello v2.0, vedi [Agentizzare un'azienda: timeline e flussi reali](/studio/flussi-agentici-pmi/) (v1.3 con il nuovo Flusso 5 cross-brand strutturato). Per il livello di knowledge editabile (pattern Karpathy applicato ai wiki narrativi brand), vedi [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/) (v1.1 con la nota sul ruolo nello stack v2.0). Per il playbook operativo passo-passo per onboardare un nuovo catalogo brand (PDF voluminoso) come Knowledge Tool dentro lo stack v2.1, vedi [Onboarding catalogo PDF: playbook 7 fasi](/studio/playbook-onboarding-catalogo-pdf/). Per una sintesi consulenziale a slide dei contenuti (35 slide, 30-40 min), pensata per software house e concessionari Mexal che vogliono posizionarsi sul livello agentico, vedi [Pensare in modo agentico](/presentazioni/agentizzare-azienda.html). Per una **mappa navigabile di tutta la ragnatela documentale** (4 studi + 2 presentazioni + glossario + cronologia + fonti), vedi [Mappa dello studio](/presentazioni/mappa-studio.html). Il livello runtime/enforcement, prima implicito nel Supervisor, si e' chiarito in **quattro componenti deterministici** che vivono accanto ai Knowledge Tools e diventano cittadini di primo piano dello stack. (1) **Domain Contracts**: 8 policy YAML come Single Source of Truth (valid_brands, brand_routing_patterns, sku_regex, native_columns, canonical_aliases, guardrail_rules, sku_invariants, kg_edge_types) caricate da loader Pydantic v2 con TTL 5s + mtime watch (~290 LOC). 5 engine migrati via try-import + fallback hardcoded: alias resolver, native columns getter, guardrail validator, SKU invariants (3 classi categoria ora alias di SKUInvariants base con ranges runtime), KG edge type inferrer. Cross-validator `check_contracts.py` (~280 LOC) come hard gate onboarding nuovo brand. (2) **Discovery Enforcement**: `_classify_filter_key(cat,key,val)` pre-dispatch validator (4 stati native / known_json / unknown_attribute / unknown_value) blocca filtri JSON su attributi non verificati con payload UNVERIFIED_ATTRIBUTE + sample valori disponibili; sessione mantiene `_SESSION_DISCOVERED[sid][cat]` primata via `list_distinct_values`/`list_attributes` (post-dispatch hook). Sui 27 golden: unknown_attribute 10%->0%, unknown_value 14%->7%, recovery automatico LLM via list_distinct_values nel 19% dei casi bloccati. (3) **Guardrail engine v1**: 4 regole YAML attive (GR001 cappa-piano cottura EN 60335 cross-brand, GR002 peso anta vs meccanismo AVENTOS, GR003 cerniere CLIP top FH x FG, GR004 dim vano incasso EU) con AST sandbox visitor manuale (no eval), slot extraction SKIP-on-missing, status PASS/WARN/BLOCK/SKIP, re-prompt LLM su BLOCK con max 1 retry. `check_safety` tool LLM-callable deprecato (coperto interamente dal validator runtime, tool count 16->15). (4) **Brand Routing classifier**: regex deterministic in `brand_router.py` (5 brand pattern, 10/10 smoke) + estensione `_build_tools(brand_filter)`, tool count crolla 16->7-9 per query brand-specifica, prompt char 18247->7916 (**-57%**), avg tokens_in per turn 17000->9214 (**-46%**), latency golden -13-18%. Tool `expand_brand_scope` come escape hatch in-session. Cinque nuove cose collegate ma ortogonali: (a) **Knowledge Graph emergente** (Fase 2) come tabelle SQLite additive in pim.db: wiki_entities + wiki_edges, 166 entita' e 557 edges su 5 brand indicizzati offline da indexer Python (~280 LOC), 3 tool LLM (find_related, find_citing, get_entity_details); (b) **Catalog RAG** (Fase 3) come secondo binario di retrieval accanto al wiki narrativo: PyMuPDF + BGE-M3 + sqlite-vec in DB separato `document_vectors.db`, 1973 entita' + 2295 chunks + 7550 mentions edges (lookup-driven post-rebuild) su 1980 PDF totali, 3 tool LLM (search_documents, get_document_section, find_documents_by_sku); (c) **Streaming + Prompt Caching via OpenRouter**: passa-through cache_control Anthropic transparentemente, TTFT 8s->1.5s percepito, cache HIT confermato 96% reduction sul prefisso system+tools 9582 token, costo prefisso ~$0.0096->~$0.0008 per call; (d) **Telemetry hardening**: log `model_observed` + `request_id` + `cache_creation/read_input_tokens` in `poc_traces/cost.jsonl` rivela behavioral drift OpenRouter (richiediamo `claude-haiku-4-5`, OR risponde `anthropic/claude-4.5-haiku-20251001`); (e) **Refresh orchestrator**: single entry-point `refresh_arcocat.py` (~290 LOC) lega 6 step (wiki indexer, document ingester opt-in, SKU audit, wiki vs pim drift L15, Fase 1 alignment L16, check_contracts, regression 27 golden), severity aggregata OK/WARN/FAIL exit 0/1/2, skill `arcocat-refresh` user-invocable. Lezione architetturale primaria integrata implicitamente nella sezione "Pattern definitivo" del livello Supervisor: **determinismo dove la verita' e' finita e oggettiva (fisica, normativa, schema), LLM dove l'ambiguita' e' valore aggiunto (intent, prosa, scoping)**. La classe safety hard, l'enforcement di schema PIM, l'alias resolution e l'invariant check sono stati spostati fuori dal prompt e dentro a contratti deterministici; il prompt si e' alleggerito di -57% senza perdita di capacita'. Studio teorico strutturalmente immutato sui 5 livelli, integrato sezione "Pattern non adottati e perche'" con riga: "tool LLM-callable per safety check (deprecato in v2.4: il validator runtime e' deterministico, citabile e testabile, l'LLM non aggiunge valore qui)". Modello bi-dimensionale (verticale brand + cross-cutting) si arricchisce di un terzo asse implicito: **runtime contratti** (Domain Contracts SSoT come backbone, Discovery + Guardrail + Routing come hook). Prossimo step strategico in discussione: entity-centric extraction offline come Fase 3.5 per alzare SKU recall (osservazione: il problema e' layout extraction, NON embedding), e onboarding 6o brand (Hafele/Hettich come stress test compatibilita' meccaniche, NON Smeg che e' elettrodomestici flat). Vedi `arcocat/HANDOFF_DOMAIN_CONTRACTS.md` (522 righe). Eval baseline arcocat v2.3.6 ancorata quantitativamente. Suite Promptfoo + RAGAS + arcocat custom metrics in `arcocat/eval/` (pacchetto standalone, NO modifica architettura nodi esistenti). 60 query MD-Karpathy in 6 bucket (10 baseline + 10 varianti + 12 prosa libera + 5 codici diretti + 13 borderline + 10 negative), edge case ratio 58%, 11 Tier 2 ground truth spalmati. Numeri baseline pre-R-rule: **grounding rate 30.08%** (% candidati con citazioni esplicite, vs L3 baseline 11% misurato su soli 5 query), **faithfulness RAGAS 80.4%**, **context_precision 81.1%**, **status_accuracy Tier 2 90.91%** (10/11, unico fail Q51 lavastoviglie out-of-scope come previsto), **shortlist_recall 100%**, distribuzione 36 consigliato + 66 compatibile + 21 sconsigliato (3 stati distinti), **3 sole R-rule citate** (R008/R009/R010 spondine LEGRABOX) su 60 query → conferma quantitativa empirica del gap rule coverage segnalato in REPORT_L3 sez 4.4 + REPORT_ITER2 sez 4.1. Cost run completo $1.19 (sotto target $1.50, ben dentro tetto $3.00). RAGAS success rate 100% (post-fix max_tokens=4096 vs 73% pilot CP3). Latency p50 3.4s / p95 7.7s. BlumCat in produzione invariato (uptime continuativo 119+ ore). Effort reale eval baseline: 5 checkpoint in ~3.5h calendar (vs piano 3-5 giorni master, factor 6-10x sotto-stima sui blocchi medi-grossi, consistente con pattern Claude end-to-end gia' osservato in L1+Iter1+L2+L3+Iter2+Step4). Lezione strategica primaria integrata nel master sez "Confidence statement" punto 4 (riformulato da "Eval empirico mancante" a "Eval baseline arcocat v2.3.6 ancorato quantitativamente"): **eval baseline come ancoraggio quantitativo per gap noti**. Pre-eval i punti aperti L3-OP3 (gap rule coverage Blum) e Eval-OP3 (out-of-scope filter) erano qualitativi; post-eval sono misurati e citabili dall'esperto di dominio. Trigger re-assessment esteso: drift >10% su metrica chiave fra iterazioni adiacenti diventa criterio formale. 2 lezioni operative aggiuntive in REPORT_EVAL_BASELINE: (a) cache Anthropic TTL 5min e' boundary realistico per batch eval (cost-per-query stimato post-cold-start sovrastima per batch lunghi dentro TTL); (b) bias Tier 2 va misurato non assunto (28% pilot vs 30% completo, +2pp trascurabile con spalmatura). Studio teorico v2.3 strutturalmente immutato. Prossimo step raccomandato: R-rule edit esperto Arco (parallelo, sblocca grounding 30% → >50% misurabile via re-run baseline) oppure Step 5 Configuration Context (ortogonale, sblocca multi-turno). Alternativa imminente: demo chatbot lato utente (pagina HTML + endpoint FastAPI sopra Supervisor MCP, ~2-3h pattern Claude). Vedi `arcocat/REPORT_EVAL_BASELINE.md`. Validazione empirica Step 4 TQL formale completata. Implementato Typed Query Layer in `arcocat/supervisor-mcp/` con scelta empirica Opzione C ibrido: regex raffinato + sinonimi tabulati in MD-Karpathy (`arcocat/wiki_arcocat/sinonimi/TQL_SINONIMI.md`), **LLM fallback NON implementato** (15/15 edge case L3-OP1 risolti senza). Pattern Karpathy esteso ai sinonimi: 8 enum tipo_componente (30+ varianti italiane) + 7 enum famiglia_box (13 varianti), editabili dall'esperto Arco senza release di codice Python. Plus fallback hard-coded subset L3 se MD assente. Suite test arcocat/supervisor-mcp passa da 20 (L3) a **56 verdi** (+36: 21 unit slot_filling + 14 edge case L3-OP1 mirati a separatori virgola, lowercase altezza, sinonimi nuovi, ordine variabile, conversione cm→mm). Cross-validate strict trasparenza Q1 regge 3/3 post-Step 4 (baseline Iter2 mantenuto). Grounding rate E2E sostanzialmente invariato (12% vs Iter2 11%), ma su 5 query EDGE nuove sale a 22% (segnale che TQL piu' completo porta candidati piu' rilevanti al KB). BlumCat in produzione invariato (uptime 120+ ore continuativo). Costo Step 4 cumulato: $0.109. Calendar-time 2.5h (vs piano 3-5h, factor 1.5x sotto-stima sui blocchi medi). Lezione architetturale primaria integrata nel master sez "Livello 1: Typed Query Layer", nuovo paragrafo "Validazione empirica Step 4": **il TQL non deve essere perfetto. Slot filling parziale + filter PIM ampio + Knowledge Tool brand intelligente = output consulenziale (pattern shortlist comparativa di default gia' validato in L3 Q5-bis). Investire ~5-10h per LLM puro ha ROI marginale; investire le stesse ore in R-rule formali nel KB brand (alza grounding 11% → >50%) ha ROI molto piu' alto**. Conferma metodologica della lezione iter2 "variance vs gap separabili": il valore consulenziale del CPQ nasce dal grounding regolatorio, non dalla precisione del parser di input. Plus implicazione operativa onboarding 2°brand documentata: aggiungere sinonimi al MD-Karpathy + CategorySchema PIM, niente codice Python. Studio teorico v2.3 strutturalmente immutato. Prossimo step raccomandato: aspettare R-rule edit esperto Arco (parallelo) e in parallelo scegliere fra Step 5 Configuration Context (integra naturalmente con TQL) o eval baseline Promptfoo+RAGAS (3-5 giorni, ROI alto post-Step 4 stabile per misurare oggettivamente progressi futuri). Vedi `arcocat/REPORT_STEP4_TQL.md`. Validazione empirica iterazione 2 minore BlumKnowledge completata. Modifica chirurgica `temperature=0` in `_call_haiku_judge` di `blum_knowledge_mcp/contract.py` (+1 riga). Test cross-validate Q1 stretto a strict equality `final_status` fine (3/3 run consecutivi pass, vs L3 bucket invariant + soft note). Suite test: blum-knowledge-mcp 25/25 verdi + supervisor-mcp 20/20 verdi, nessuna regressione. Distribuzione esiti rimisurata: 1 consigliato + 6 compatibile + 2 sconsigliato (vs L3 1+7+1, 753.5001S Q5b migrato `compatibile`→`sconsigliato` per ambiguity LLM su slot "portata"). Cache HIT 100% confermato post-fix (`temperature=0` ortogonale a `cache_control:ephemeral`). Costo iter2 cumulato: $0.104 (vs L3 $0.127, scope minore). Calendar-time 1.5h (vs piano 2-3h, factor 2x sotto-stima). Lezione metodologica primaria integrata nel master sez "Pattern definitivo `valida_compatibilita`": **variance vs gap coverage sono cause separabili, diagnosi tramite invarianza di (b) post-fix di (a)**. Iter2 ha verificato empiricamente: post-fix `temperature=0`, cross-validate diventa deterministico (variance risolta) ma grounding rate resta invariato a 11% (gap rule coverage Blum reale, non artefatto). L'invarianza del grounding rate post-fix e' la prova diagnostica che il gap NON era variance LLM. Conferma empirica della raccomandazione `temperature=0` (in v2.3.4 era teorica). Caveat empirico aggiunto al master: residuo variance ~5-10% su casi borderline (logit vicini), mitigabile a livello di regola formale nel KB anziche' sampling LLM piu' aggressivo. Plus bozza 5 R-rule (R015..R019) per esperto Arco in `arcocat/RULE_COVERAGE_ANALYSIS.md` (decisione fuori arcocat, parallela). BlumCat in produzione invariato (uptime continuativo 120+ ore). Studio teorico v2.3 strutturalmente immutato, integrate solo le 2 conferme empiriche nella sezione 3A. Prossimo step raccomandato: Step 4 TQL formale (indipendente da rule edit esperto, parallelo) o 2°brand (aspettare R-rule edit per non amplificare rumore variance + gap). Vedi `arcocat/REPORT_ITER2_BLUMKNOWLEDGE.md`. Validazione empirica L3 completata (Supervisor minimale Filter-then-Validate). Implementato `arcocat/supervisor-mcp/` come singleton FastMCP standalone: 1 tool MCP esposto (`risolvi_query`), slot filling deterministico (regex + sinonimi, no LLM pre-stage), orchestrator pipeline `pim.filtra + ThreadPoolExecutor parallel valida + synthesize 3 stati` (consigliato | compatibile | sconsigliato), BRAND_CLIENTS registry future-proof (oggi 1 brand: Blum). 20 test verdi (10 unit synthesizer pure + 5 E2E + 1 cross-validate trasparenza + 2 anti-pattern + 1 verify_upstream). Costo CP2 E2E $0.0324 sotto target $0.04, costo cumulato L3 $0.127 (di cui $0.063 diagnostic non-budget). BlumCat in produzione invariato (uptime continuativo 99+ ore durante L3). Effort reale: 1 sessione (~5h calendar), vs stima master 1 settimana = sotto-stima 5-7x consistente con L1+Iter1+L2. Distribuzione esiti: 1 consigliato + 7 compatibile + 1 sconsigliato (3 stati distinti rappresentati). 2 lezioni architetturali integrate nel master: (1) **Pattern Filter-then-Validate produce shortlist comparativa di default** (sezione "Livello 2: Supervisor pattern", nuovo paragrafo "Proprieta' emergente"): la composizione filter recall-alto + validate precision-alta produce naturalmente output consulenziale "ti propongo X, MA per il tuo caso Y e' meglio". Validato empiricamente in L3 Q5-bis (LEGRABOX 500 set guide portata 40 + peso anta 60kg → shortlist [750.5001S sconsigliato, 753.5001S compatibile]). Il pattern non richiede LLM judge sul synthesize ne' relax automatico nel filter. (2) **Non-determinismo LLM judge: trasparenza strutturale tiene, status fine no** (sezione "Pattern definitivo valida_compatibilita", nuova nota): con `temperature=1` default, variance fra chiamate consecutive su `consigliato↔compatibile`. Mitigation raccomandata: `temperature=0` in `valida_compatibilita`. Plus 4 punti aperti L3-OP1...4 documentati con trigger esplicito. Studio teorico v2.3 strutturalmente immutato, integrate solo le 2 lezioni nelle 2 sezioni canoniche. Prossimo step raccomandato: iterazione 2 minore BlumKnowledge (temperature=0 + analisi rule coverage Blum, effort 2-3h) prima di Step 4 TQL formale o 2°brand. Vedi `arcocat/REPORT_L3.md`. Validazione empirica L2 completata (PIM lite singleton + primo CategorySchema). Implementato `arcocat/pim-lite/` come singleton FastMCP standalone, brand-agnostico per design: SQLite + JSON1 (non Postgres+JSONB del modello teorico, sufficiente per prima categoria pilota), 3 tool MCP minimi (`filtra`, `attributi_per_codice`, `list_categorie`), schema MD-Karpathy in `wiki_arcocat/categorie/C001_sistemi_box.md` (frontmatter YAML eseguibile + body narrativo per esperto). 363 prodotti popolati read-only da `blumcat.db` (267 strutturali + 96 accessori), 26 test verdi, latency sub-5ms, BlumCat in produzione invariato (uptime continuativo 95+ ore durante L2). Effort reale: 1 sessione (~5h calendar), vs stima master 1-2 settimane = sotto-stima 8-15x con pattern Claude end-to-end (consistente con L1+Iter1). 2 lezioni architetturali integrate nella sezione 3B PIM lite: (1) **vincoli aspirazionali vs effettivi**: lo schema dichiara `obbligatorio: true` come segnaletica architettonica, la realta' empirica e' segmentata per `tipo_componente` (267 strutturali con NL_mm 100% + 96 accessori senza NL semantico). Decisione: tenere lo schema come segnaletica e documentare la segmentazione nei test (`test_strutturali_hanno_NL_mm` come garanzia operativa). Schema condizionale via `obbligatorio_se_tipo_in` rinviato a Step 6+ con 3°brand multi-categoria, per evitare astrazione prematura. (2) **Pattern cross-validate Knowledge Tool brand vs PIM**: per 5 distinte canoniche (LEGRABOX/MERIVOBOX/TANDEMBOX_antaro/METABOX) verificare che ogni codice `status="in_db"` sia presente nel PIM. Risultato L2: 42/42 codici trovati, zero missing. Generalizzabile come "test di coerenza tra fonti" prima di chiudere un nodo della constellation. Plus 3 punti aperti documentati con trigger esplicito: doppia famiglia per codice (OP1, MEDIO, 2°brand), performance scaling json_extract (OP2, MEDIO, 5k+ prodotti), schema condizionale (OP3, BASSO, 3°brand). Studio teorico v2.3 strutturalmente immutato, integrate solo le 2 lezioni nella sezione 3B. Prossimo step: L3 Supervisor minimale (Filter-then-Validate end-to-end). Vedi `arcocat/REPORT_L2.md`. Validazione empirica iterazione 1 (post-L1) completata. Pattern definitivo per `valida_compatibilita` integrato nella sezione "3A Knowledge Tools per brand": **rule completo cached + hint dinamico**. Anti-pattern empirico documentato: retrieval mirato chunk-level esclusivo come gating del rule set produce regression strutturale (cache miss + chunk-level rumoroso su rule set astratto), misurato in BlumCat con safety net 2/4 → 3/4, citazioni 1 → 0, costo +55%. Da NON ripetere quando si onboarda 2°brand. Il pattern definitivo (Iter1-B) valida invece miglioramento netto: safety net 2/4 → 1/4 dimezzato, citazioni totali 1 → 3 (+200%), cache HIT mantenuto 95.2%, costo invariato. Lezione strutturale: per Knowledge Tool brand con rule set < ~50 regole, "tutto cached + hint" e' il pattern ottimale; retrieval-only diventa necessita' solo > ~200 regole (caso edge). Per Blum (14 regole), Bosch atteso (~30-50), Whirlpool atteso (~20-40), il pattern resta valido. Studio teorico v2.3 strutturalmente immutato, integrato solo il pattern empirico nella sezione 3A. Validazione empirica L1 completata. Estratto da BlumCat in produzione il primo Knowledge Tool MCP standalone (`blum-knowledge-mcp`): 11 tool esposti (3 contratto comune + 8 dominio Blum), 20 test verdi, BlumCat in prod completamente invariato durante l'esperimento. Effort reale: 1 sessione Claude Code (~4 ore), vs stima "1 settimana" del master (sotto-stimato 5-7x grazie al pattern Claude end-to-end). 5 lezioni apprese documentate, due punti aperti identificati: (a) cosine plain non basta per retrieval type-mirato (le 14 regole sono soverchiate dai 2387 chunks mineru), (b) safety net "downgrade ok→warn se citazioni=[]" e' necessario, non cerotto. Prossimo step: iterazione 1 con hybrid retrieval (FTS5 + RRF k=60 + filtro fonte mirato) per ridurre safety net trigger da ~50% a ~20%, prima di procedere a L2 (PIM lite). Validazione mostra che il pattern centrale del v2.3 (Knowledge Tool MCP per brand con 3 tool minimi del contratto comune) e' implementabile in 4 ore reali per il primo brand, e che le 2 lezioni piu' importanti emergono solo nell'implementazione (non in audit teorici). Aggiornato `MAPPA_STUDIO.md` privato con cronologia migration. Studio teorico v2.3 strutturalmente immutato. Aggiunto **meta-pattern "Claude-assisted ingest"** come sezione dedicata prima dei pattern non adottati. E' il pattern di metodo che rende fattibile l'intero design v2.x per single-developer in PMI: lavoro iterativo developer + Claude Code in sessione per costruire script Python di parsing/extraction/curation, poi 95% della struttura del sistema (PIM, wiki narrative, regole) viene derivata dai dati stessi via questi script. L'esperto interno NON edita YAML/JSON: corregge body narrativo + aggiunge nozioni mentali. Riduzione effort 5-10x rispetto a pattern tradizionale data engineering (es. 8-15k EUR primo brand vs 30-80k EUR). Esplicitazione di un pattern che era implicito in BlumCat reale ma mai documentato. Trigger: discussione 2026-05-05 in cui e' emerso che i 2 rischi "PIM popolamento manuale" e "Karpathy YAML fragile" del v2.2 erano sopravvalutati perche' il pattern reale e' Claude-assisted, non manuale puro. Conseguenze: tempi onboarding rivisti nel playbook, presentazioni rinforzate sul perche' il modello e' fattibile per team piccoli. Bump post primo audit periodico (vedi `_private/audit-log/2026-05-05-audit-v1.md`). Cambiamenti: (1) **8° principio guida "Cost variability"** aggiunto (lezione GitHub Copilot agentic billing change aprile 2026: agentic workflows consumano 5-50x risorse vs chat, billing flat insostenibile, hard rule budget cap per session). (2) **Hard rule "MCP ephemeral / lazy-connect"** aggiunta nel Layer C sicurezza: Knowledge Tool MCP istanziati lazy on-demand e disposti post-uso, non persistenti per intera sessione (pattern emergente HN aprile 2026, a 30 brand insostenibile). (3) **CRAG (Corrective RAG)** promosso da "punto aperto generico" a "trigger esplicito post-2°brand" nei punti aperti Eval (Higress-RAG production-ready, arXiv:2602.23374). (4) **AI Act risk assessment** marcato URGENTE (Mese 2-3 invece di 4-6) per finestra enforcement Commissione EU 2 agosto 2026. (5) Nuova sezione **"Pattern non adottati e perche'"** con tabella esplicita: Microsoft Agent Framework (scartato per ecosistema MS), DSPy (scartato per conflitto hard rules), LangGraph full (mantenuto solo minimale opzionale), FastMCP 3.0 (rinviato a 2°-3° brand), Akeneo Community (scartato per slowdown manutenzione), Anthropic Managed Agents (NON CONFERMATO con fonte primaria, skippato fino a re-trigger). Scopo della sezione: prevenire ri-discussioni circolari ai prossimi audit. Confabulazioni dell'audit (Sonnet 5 "Fennec", Managed Agents) marcate apertamente come non verificate, bumpata regola "verifica fonte primaria" in PROMPT_AUDIT_PERIODICO.md v1.2. Allineamento di coerenza tooling. Audit interno ha identificato che il master v2.0 NON citava strumenti effettivamente in uso in BlumCat reale (sentence-transformers per embedding locale, pdfplumber per estrazione raw text supplementare, PyMuPDF per pre-render JPG citazioni inline). Aggiunti come strumenti P1/P2 espliciti nella tabella infrastruttura del Livello 5 e nella sezione "9 punti aperti / Data layer". Aggiunti anche Sentry e UptimeRobot esplicitamente (erano nascosti in "Observability" generica). Aggiunto Callout "Stato dei tooling: oggi vs futuro" che chiarisce i 3 livelli temporali del tooling: A = in uso oggi (BlumCat), B = da introdurre nel migration path v2.0, C = opzionale Mese 4+. Disambiguazione importante per chi legge: il design v2.0 e' una traiettoria di evoluzione da BlumCat reale, non una "lista della spesa" da comprare tutta insieme. Riscrittura strutturale (major). Il design v1.x modellava il problema su un solo asse (constellation per brand), pattern adeguato per il caso single-brand BlumCat ma incompleto per scenari cross-brand. La domanda "lavastoviglie 60 classe A" cross-brand ha fatto emergere lacune strutturali. Una review architetturale comparativa (PIM Akeneo, LangGraph Supervisor, Anthropic multi-agent paper, Algolia federated search, Constructor.com B2B distributor pattern, RRF, Reciprocal Rank Fusion) e una validazione esterna indipendente (ChatGPT + Gemini) hanno convergato sul **modello bi-dimensionale a 5 livelli**: Typed Query Layer (slot filling pre-LLM), Supervisor Filter-then-Validate, Fonti eterogenee (Knowledge Tools per brand + PIM lite + Rule Engine + Mexal/Promo MCP), Configuration Context (stato persistito tipizzato), Infrastruttura. Riformulato "constellation di brand agents" in **constellation di Knowledge Tools MCP** (NON agenti autonomi): il Supervisor centrale e' l'unico vero agente. Aggiunti **quattro contratti tipizzati** (TypedQuery, CategorySchema, Rule, ConfigurationContext) come confine tra livelli, da definire PRIMA del codice. Karpathy preservato come fondazione di rappresentazione del knowledge editabile (wiki narrativi, regole MD-Karpathy con frontmatter eseguibile, schemi categoria), NON come architettura completa. Framing **CPQ** introdotto esplicitamente: stiamo costruendo un Configure-Price-Quote con interfaccia conversazionale, pattern industriale documentato da 30 anni, non un chatbot. Hybrid retrieval (BM25 FTS5 + cosine + RRF k=60) come default per `cerca_knowledge` dei Knowledge Tools. Livello Configuration Context (stato persistito strutturato) come terzo cambiamento strutturale insieme a PIM e Typed Query. Aggiunta sezione "Pattern dati: constellation vs centralizzato" come correzione importante del default precedente. La v1.1/1.2 raccomandava implicitamente "Postgres+pgvector centrale" come pattern dati, che e' over-engineered per una PMI con costellazione di mini-agenti specializzati. **Default rivisto: constellation** (ogni mini-agente con suo SQLite + BLOB embedding stile BlumCat, sotto i 100k chunks per agente). Centralizzati solo Mexal via MCP, Langfuse, eventualmente Inngest. Aggiunta sotto-sezione "Federazione" che mostra come una "chat unificata che cerca su tutti i cataloghi" si risolve con Supervisor + workers federato, senza migrazione a centralizzato. Disciplina abilitante: ogni mini-agente espone cerca_knowledge(query) come tool MCP fin dal day 1. Aggiornamento poi superato dalla v2.0 (modello bi-dimensionale). Aggiunta sezione "Modello di esecuzione: tutto e' asincrono ed event-driven" come **blueprint vincolante** prima di costruire il primo agente reale. Fissa: principio "tutto async tranne HTTP utente", Inngest come substrato unico (event bus + queue + workflow engine), 4 trigger types ammessi (webhook / cron / chain / manual), retry policy esplicita per categoria errore, dead letter queue per fallimenti permanenti, `step.sleep` vs `step.waitForEvent` per long-running, concurrency e backpressure, l'unica eccezione sync legittima (HTTP utente in chat). Lista esplicita degli **anti-pattern da evitare** come guardrail permanente: niente polling, niente HTTP blocking, niente `time.sleep()`, niente mega-workflow monolitici, niente bypass DLQ. Aggiunta sezione "Memoria: principio dei 3 livelli" (stato operativo / business / conoscenza), con regola architetturale "l'agente non e' il database", schema di scoping (global/user/thread/pratica), pattern di write-back controllato. Cross-link al nuovo studio dedicato [Wiki narrativo AI-maintained](/studio/wiki-narrativo-ai-maintained/) per il livello 3 quando la fonte documentale e' un manuale tecnico voluminoso. Prima stesura. Mappa completa dello stack: 6 layer, 9 punti aperti, dimensionamento VPS, principi guida. Lo studio nasce da una serie di sessioni di progettazione interna su come introdurre agenti AI nei processi aziendali. --- ## Wiki narrativo AI-maintained: architettura di conoscenza compilata per cataloghi tecnici URL: https://andreapellizzari.it/studio/wiki-narrativo-ai-maintained/ Versione: 1.2 Aggiornato: 2026-05-14 Keywords: RAG, wiki narrativo, MinerU, context engineering, eval set, chatbot tecnico, PMI, Karpathy pattern, rule engine, frontmatter eseguibile > Pattern per costruire un chatbot tecnico interno su un manuale fornitore PDF voluminoso, senza allucinazioni sui codici articolo. Pipeline OCR doppia, wiki autoritativo a 4 categorie, single source of truth, cinque pattern di context engineering, eval set come gate. Nello stack v2.0 questo pattern descrive il livello *Knowledge editabile* (Knowledge Tools per brand + schema PIM + Rule Engine), una delle componenti del modello bi-dimensionale. Questo studio descrive il **livello Knowledge editabile** dello stack agentico v2.0 (vedi [Agentizzare una PMI: stack v2.0](/studio/stack-agentico-pmi/)): wiki narrativi per brand consumati dai Knowledge Tools MCP (constellation, Livello 3A), schema attributi per categoria del PIM lite (Livello 3B, formato `C*.md`), regole tecniche del Rule Engine (Livello 3C, formato `R*.md` con frontmatter eseguibile esteso). Il pattern Karpathy e' la **fondazione di rappresentazione del knowledge editabile da umani**, NON l'architettura completa: sopra serve l'orchestrazione (Supervisor Filter-then-Validate, Typed Query Layer, Configuration Context) descritta nello studio principale. Questo studio descrive un pattern progettuale che ho costruito per un chatbot tecnico interno aziendale, costruito sopra un manuale fornitore di circa 750 pagine. L'utenza e' interna e tecnica (commerciali, addetti al magazzino, tecnici di showroom): tollera un'interfaccia testuale ma non tollera codici articolo inventati o numeri sbagliati. Il dominio e' arredamento di precisione (componenti meccanici di precisione per cassetti e ante a ribalta), ma il pattern si applica a qualunque catalogo tecnico denso con codici verificabili. Lo chiamo **wiki narrativo AI-maintained**: una struttura di conoscenza compilata sopra agli estratti grezzi del manuale, autoritativa, editabile da un esperto interno, e progettata per essere consumata da un LLM senza allucinazioni. ## Quando applicare il pattern (e quando no) Si applica bene se: - Esiste un **manuale fornitore PDF voluminoso** (centinaia di pagine, tabelle, formule grafiche, schemi) che fa fede sul piano tecnico. - Il dominio ha **codici articolo verificabili** (nomenclatura strutturata: `ART-001`, `XYZ7M70E2`, `750.5001`) di cui e' possibile costruire un dataset enumerabile. - L'utenza e' interna e tecnica, non end-user generico. - Il volume di query e' basso/medio (decine al giorno, non migliaia al secondo): permette routing dinamico dei modelli e prompt caching senza ottimizzazioni infrastrutturali estreme. - Il manuale fornitore **cambia annualmente** (nuova edizione catalogo): serve un percorso di rigenerazione ripetibile, non un import una tantum. Non si applica bene se: - Il catalogo e' piccolo (sotto qualche centinaia di prodotti) o senza nomenclatura verificabile: in quel caso un semplice RAG su FAQ basta, e tutto l'apparato di gate / template / single source of truth e' over-engineering. - I prodotti sono descritti a parole, non a codici: senza un identificatore che il modello possa "verificare contro DB" non si neutralizza l'allucinazione. - L'utenza e' end-user generico (consumer): qui serve un'esperienza UX piu' ricca (immagini guidate, configuratore, e-commerce) e l'accuratezza tecnica passa in secondo piano. - Esiste gia' un'API strutturata del fornitore (BMEcat, schema standard): il punto di partenza e' l'API, non il PDF. ## 1. Pipeline OCR-ingest: dal PDF ai chunk searchable Trasformare un manuale tecnico in qualcosa che un LLM puo' consultare richiede **due estrattori complementari**, perche' nessun singolo strumento copre il 100%: - **MinerU**: ottimo su layout, tabelle, struttura. Esce un markdown leggibile + immagini segmentate. Perde pero' alcune **formule grafiche** rese come SVG/glifo (es. altezze quotate in disegni tecnici). - **pdfplumber**: estrae il **raw text** delle stesse pagine in modo brutale: tutte le formule grafiche tornano come testo flat. E' rumoroso, ma cattura cio' che MinerU perde. I due output vivono affiancati nella stessa cartella per famiglia, con due `fonte` distinti nel DB (`mineru` e `supplementare`). Il modello vede entrambi tramite `search_semantic`, e nei casi limite il chunk "supplementare" recupera cio' che il chunk "mineru" non aveva. {` PDF master (manuale fornitore) | +-- pdftoppm 200dpi --> pagine PNG (per vision in-session su pagine critiche) | +-- MinerU CLI -------> families//extracted.md (tabelle + struttura) | +-- pdfplumber -------> families//extracted_supplementary.md (raw text) | +-- pdftoppm 150dpi --> manuale_pages/pag-NNN.jpg (citazioni inline) extracted*.md | v chunker per fonte (script Python) | v DB (SQLite o Postgres) -- tabella chunks (testo + fonte + metadata) | v sentence-transformers (mpnet 768d) | v DB embeddings BLOB wiki_curato/D* G* R* F*.md (autoritativo, vedi sezione 2) | v reindex_wiki.py | v DB chunks (fonte: distinta/guide/regola/famiglia) DB --(cosine sim 768d)--> tool search_semantic esposto al LLM `} Artefatti intermedi che vale la pena conservare anche dopo build: - `pagine_breadcrumb.csv` (numero pagina, sezione manuale): serve per cross-ref e per popolare il frontmatter `pag_start`/`pag_end` delle schede wiki. - `albero_manuale.md`: tabella di contenuti ricostruita; utile per diff fra edizioni del catalogo. - `_status.json` per famiglia: marker che la pipeline ha completato senza errori (verde/rosso): serve per sapere cosa rilanciare in caso di interruzione. Un singolo OCR engine non basta. Tenere due estrattori in parallelo e accettare la duplicazione e' piu' affidabile che cercare lo strumento perfetto. ## 2. Wiki autoritativo a 4 categorie Le pagine MinerU e pdfplumber sono **read-only e immutabili** (rispecchiano il PDF). Sopra ad esse vive un **wiki MD curato, autoritativo, editabile via UI**, organizzato in 4 categorie: | Categoria | File | Cosa contiene | Quando crearne uno | |---|---|---|---| | **Distinte** (`D*.md`) | `wiki/distinte/D001_.md` | Composizione canonica per famiglia: lista componenti, pattern dei codici, varianti, optional, criteri di scelta | Una per famiglia "configurabile" (es. cassetto = guide + spondine + supporto + attacco) | | **Guide** (`G*.md`) | `wiki/guide/G002_.md` | Decision tree narrativo per scelte ("scegli X vs Y vs Z") | Una per asse decisionale ricorrente | | **Regole** (`R*.md`) | `wiki/regole/R002_.md` | Regola tecnica con frontmatter YAML (formula, range, fonti) e body MD discorsivo | Una per regola/formula trasversale (peso anta, numero cerniere, dimensionamento) | | **Famiglie** (`F*.md`) | `wiki/famiglie/F001_.md` | Scheda narrativa "cos'e' X, quando sceglierla, quando NON sceglierla, codici tipici" | Una per famiglia user-facing (anche solo "anagrafica narrativa") | A questo si affiancano gli **artefatti di indicizzazione** (pattern dei 3 layer di knowledge management AI-maintained): - `wiki/index.md`: catalogo navigabile rigenerato automaticamente. Il modello lo riceve **inline nel system prompt** come mappa di orientamento (cached). - `wiki/log.md`: append-only, audit trail di chi ha modificato cosa. L'editor wiki UI scrive qui automaticamente. ### Idempotenza ed editing Ogni MD e' autocontenuto, ha un frontmatter YAML stabile (`id`, `famiglia_id`, `pag_start`, `pag_end`) e un body markdown libero. Il salvataggio dall'editor UI crea un `.bak.` con retention 10. Modificare un valore in un MD si propaga al DB con un singolo `reindex_wiki.py --only ` (richiede ~2 min per categoria). Separa la fonte autoritativa (MD editabile) dagli estrattori grezzi (read-only). I primi sono il contratto col business: l'esperto interno corregge e firma. I secondi sono il "ground truth" tecnico da cui ripescare quando il wiki e' incompleto. ### Frontmatter eseguibile (estensione v2.0 per Rule Engine) Le `R*.md` (regole tecniche) possono essere lette in due modi dalla stessa fonte: come narrativa (esperto umano legge tabelle/note) e come **rule object eseguibile** (Rule Engine deterministico applica vincoli). L'estensione v2.0 introduce nel frontmatter una sezione `condizione + azione + test_cases` parsabile da un evaluator Python. ```markdown --- id: R005 nome: "Cassetto LEGRABOX: set guida 750 vs 753 (high-load)" ambito: blum.legrabox versione: 1.2 fonte_pagine_fis: [248] # parte ESEGUIBILE dal Rule Engine condizione: all_of: - { fatto: famiglia, op: "=", valore: legrabox } - { fatto: portata_kg, op: ">", valore: 40 } - { fatto: NL_mm, op: ">", valore: 500 } azione: warn messaggio_template: "Per portata {{portata_kg}}kg e NL {{NL_mm}}mm usa 753.*, non 750.*" # embedded test (lint-friendly, pre-commit hook) test_cases: - { in: {famiglia: legrabox, portata_kg: 70, NL_mm: 600}, expect: warn } - { in: {famiglia: legrabox, portata_kg: 40, NL_mm: 400}, expect: pass } --- # R005 - Set guida LEGRABOX 753 (high-load) Quando il cassetto supera 40 kg di portata e ha NL > 500 mm, il set guide standard 750.* (40 kg) non e' adeguato. Va usato 753.* (70 kg). ## Tabella decisione | Portata | NL | Codice | |---------|-------------|--------| | 40 kg | qualsiasi | 750.* | | 70 kg | qualsiasi | 753.* | ``` **Doppia lettura della stessa fonte**: - **L'esperto umano** modifica via UI body narrativo + tabella + (con minimo training) le 4 righe di `condizione`. - **Il Rule Engine** al boot legge tutti gli R*.md, parsa il frontmatter, costruisce in memoria la lista di Rule eseguibili. - **Il validator AI** (gia' in uso pre-save) verifica: syntax MD, codici inventati, sigle interne, **plus** `condizione` ben formata e `test_cases` che passano. DSL della condizione: combinatori `all_of`/`any_of`/`none_of`, foglie `{fatto, op, valore}`, operatori `=`/`!=`/`>`/`<`/`>=`/`<=`/`in`/`not_in`/`contains`. Engine evaluator ~150 righe Python, no Drools, no CLIPS. Stesso pattern (frontmatter eseguibile + body narrativo) si applica al **CategorySchema** del PIM (file `C*.md` in `wiki_/categorie/`): definisce attributi tipizzati per categoria prodotto + body narrativo che spiega il dominio. Knowledge come fonte editabile, struttura tipizzata come derivata. ## 3. Single source of truth: MD → Python → system prompt Il pattern piu' costoso e' la **divergenza silenziosa**: lo stesso parametro vive in 3 posti, viene aggiornato in 1, e nessuno se ne accorge per settimane. Caso reale del progetto: i parametri tecnici delle 4 famiglie principali (altezze, range numerici ammessi, portate, pattern del codice articolo, colori validi, attacchi) erano inizialmente duplicati in: 1. `wiki/distinte/D001-D004.md`: fonte autoritativa per l'editor wiki (umano). 2. `backend/system_prompt.py`: sezione "REFERENZE PARAMETRICHE", 3 tabelle hardcoded che il modello vedeva inline nel prompt cached. 3. `backend/tools.py`: dict Python `PATTERNS`, consumato dal tool composito `assemble_distinta`. Ogni cambio = **3 file da modificare**. In pratica: chi editava l'MD non toccava il Python, e viceversa. ### Soluzione Un solo modulo (`backend/distinta_parser.py`) parsa i `D*.md` e produce sia il dict Python (`load_patterns()`) sia il markdown della sezione del system prompt (`render_referenze_parametriche()`). Le altre due "copie" diventano **derivate**. ``` wiki/distinte/D*.md <- UNICA fonte editabile | v backend/distinta_parser.py <- parser MD permissivo | +--> load_patterns() -> consumato da tools.py +--> render_referenze_parametriche() -> sostituito in system_prompt.py al boot ``` ### Verifica di idempotenza Cambia un valore numerico in `D001`, riavvia. Sia la distinta generata dal tool sia il system prompt mostrano il nuovo valore. **Definizione di fatto**: 1 valore cambiato in 1 file, nessun altro file da toccare. Durante il refactor, la fonte MD per una famiglia dichiarava 4 colori validi (incluso uno marginale). Il vecchio dict Python ne aveva 3. Il refactor "ha vinto" e propagato il valore del MD: corretto se il MD e' giusto, **corretto anche se il MD e' sbagliato**. Senza un eval set indipendente, una refactor di questo tipo puo' propagare un errore latente del MD in tutti gli output del sistema. Vedi sezione 5. ## 4. Cinque pattern di context engineering Sono il cuore trasferibile del progetto. Cinque pattern che si sono dimostrati determinanti per **abbattere l'allucinazione e contenere il costo** in un dominio dove il modello "sa abbastanza" da inventare codici plausibili-ma-fasulli. ### 4.1 Catalogo inline nel system prompt cached, non tool da chiamare Un primo tentativo prevedeva un tool `read_wiki_index()` che il modello avrebbe dovuto chiamare per orientarsi. **Non funzionava**: i modelli piccoli (Haiku) non lo chiamavano, quelli grandi (Sonnet) lo chiamavano raramente, e in ogni caso aggiungeva un round di tool-use (latenza piu' token). Soluzione: il catalogo wiki (`index.md`, ~4500 token) e' **incluso direttamente nel system prompt**, dentro un blocco `cache_control: ephemeral`. Costo ammortizzato dopo il primo turno (sconto 90% sui token cached), zero round di tool-use, e il modello non puo' "dimenticarsi" di consultarlo. **Generalizzabile a**: tassonomie di prodotto, glossari, decision tree, qualunque "mappa di orientamento" sotto i 5-10k token che il modello deve avere SEMPRE sotto gli occhi. Sopra quella soglia, il prompt diventa difficile da debuggare e va ripensato. ### 4.2 Push automatico della distinta-template (non pull) Per le query di tipo "configurazione cassetto" il modello deve avere sotto gli occhi la composizione canonica della famiglia (5-6 gruppi di componenti, pattern dei codici, varianti). Un primo design lasciava al modello la chiamata `read_distinta(famiglia)`. **Non funzionava** affidabilmente: Haiku saltava il tool, partiva a memoria, sparava codici plausibili ma inventati. Soluzione: **classifier deterministico Python** (regex su keyword famiglia + keyword "distinta/configurazione") che riconosce l'intento e **inietta la distinta-template direttamente nel messaggio user**, prima di chiamare il modello. Marker `[ctx:Dxxx]` per idempotenza (non si inietta due volte nella stessa sessione). ```python # semplificato fam = detect_distinta_intent(user_msg) # regex intent classifier if fam: distinta_md = read_distinta(fam)["contenuto"] user_msg = f"[ctx:{d_id}]\n{distinta_md}\n\n[Richiesta utente]\n{user_msg}" ``` **Generalizzabile a**: ogni volta che esiste un "documento di riferimento corto" (sotto 5k token) la cui presenza e' **necessaria** per rispondere correttamente a una classe di query. Push deterministico e' piu' affidabile che sperare nel tool-use del modello. ### 4.3 Gate logic con marker (controllo del flusso server-side) Per casi in cui mancano **dati chiave** (es. "distinta cassetto" senza profondita' e senza portata) il modello tende ad **assumere default** e produrre comunque una risposta verosimile. Il problema: l'utente non si accorge che il valore e' inventato. Soluzione: lo stesso classifier rileva i dati mancanti (`_detect_NL_mm`, `_detect_portata` su tutto lo storico user, non solo l'ultimo turno) e **inietta una direttiva interna**: ``` [gate:portata] (direttiva interna - non mostrare all'utente) REGOLA RIGIDA: l'utente ha chiesto una distinta ma manca la portata. - NON chiamare assemble_distinta. - NON inventare default. - Chiedi SOLO la portata con [OPZIONI: 40 kg | 70 kg]. [Richiesta utente] distinta cassetto 500 ``` Quando invece i dati ci sono tutti, viene iniettata una direttiva opposta (`[gate:done]`): "produci subito la distinta, non chiedere accessori". **Generalizzabile a**: qualsiasi flusso conversazionale dove esistono "dati chiave senza i quali la risposta e' inventabile". La logica deterministica vive in Python (testabile, debuggabile), non nel prompt (opaco, non testabile, costoso). ### 4.4 `tool_choice` forzato API-side per task ad alto rischio Anche con tutta la prompt engineering del mondo, **istruire il modello "DEVI chiamare il tool X"** in linguaggio naturale e' inaffidabile, soprattutto su modelli piu' piccoli. Antipattern. Soluzione: usare il parametro nativo dell'API: ```python if rounds == 1 and force_tool_name: create_kwargs["tool_choice"] = {"type": "tool", "name": force_tool_name} ``` Quando il gate e' in stato `done` (tutti i dati chiave presenti), forziamo `assemble_distinta` al primo round. Il modello **deve** chiamarlo (lato API), poi nei round successivi `tool_choice` non e' piu' impostato e puo' rispondere liberamente. **Generalizzabile a**: ogni "punto di non ritorno" del flusso dove la chiamata di un tool specifico e' obbligatoria per mantenere la verifica dei dati. Non delegare al modello scelte che l'API permette di forzare. ### 4.5 Routing modello per rischio allucinazione, non per costo Il primo istinto e' "modello piccolo per tutto, modello grande solo se serve". Sbagliato: il rischio di allucinazione e' **inversamente proporzionale alla taglia del modello**. Su query complesse (decision tree multi-step, calcoli combinati, distinte) il modello piccolo non e' "lento", e' **piu' allucinatorio**. Routing implementato: - **Modello piccolo (Haiku)**: lookup secchi (codice, sigla, glossario), pattern regex `^cos['e']|cosa è|...` o presenza di codice articolo. Cache ben sfruttata. - **Modello grande (Sonnet)**: distinte, configurazioni, "scegli tra X e Y", calcoli, "differenza", "compatibile". Forzato anche dal gate `done` o dall'iniezione automatica di una distinta-template. - **Default**: modello piccolo (la maggioranza delle query e' lookup banale). L'eval set ha permesso di calibrare le regex: **4 bug reali** sono stati catturati passando da 5 a 15 casi di test (gate non riconosceva alcuni valori numerici per regex troppo stretta; parsing famiglia non catturava nomi senza suffisso). **Generalizzabile a**: ogni volta che si offrono modelli di diverse taglie sullo stesso flusso. Il driver del routing e' la natura della query, non solo il costo. E il routing va testato come si testa il codice. ## 5. Eval set come rete di sicurezza Il salto qualitativo tra "demo che funziona" e "sistema su cui basare un refactor" e' **avere un eval set deterministico**. {` Sviluppatore --> evals/run_evals.py | v loop per ogni caso (~30 casi) | +--> agent.chat() --> Anthropic API | | |<---------------------+ | v assertions: - must_call_tools - must_not_call_tools - must_inject_marker - response_must_contain (codici reali) - response_must_not_contain (codici allucinati) - response_must_not_contain_regex (pattern allucinazioni) - model_used | v report 30/30 PASS o N/30 FAIL con diff PRIMA del refactor: snapshot baseline DURANTE il refactor: ogni 15 min DOPO il refactor: gate per il deploy `} ### Tipologie di assertion che hanno valore reale | Tipo | Esempio | Cosa cattura | |---|---|---| | `must_call_tools` | `["assemble_distinta", "get_media"]` | Il modello segue il flusso atteso | | `must_not_call_tools` | `["assemble_distinta"]` quando manca un dato | Il gate sta bloccando | | `must_inject_marker` | `[gate:done]`, `[ctx:D008]` | I classifier deterministici Python funzionano | | `response_must_contain` | Codici articolo specifici | Il modello cita codici reali (verificati in DB) | | `response_must_not_contain` | Codici plausibili ma fasulli | Regression test per allucinazioni storiche | | `response_must_not_contain_regex` | Pattern di codici mai esistiti | Cattura allucinazioni "famiglia" | | `model_used` | `"claude-haiku-4-5-..."` | Il router sta scegliendo il modello giusto | ### Costo e cadenza realistica ~30 casi end-to-end completi (API reale, no mock) -> ~5 min wall-clock, ~$0.30. Si lancia prima di ogni refactor non-banale, prima di ogni deploy, e nuovi bug "in produzione" diventano nuovi casi (regression testing organico). Il costo e' trascurabile rispetto al costo di un'allucinazione in produzione su un dato tecnico. Ogni volta che si sistema un bug "vivo", aggiungerne il caso in `evals/cases.py` **prima** di considerare il fix completo. E' l'unica garanzia che la pezza non si stacchi al prossimo refactor. Il refactor "single source of truth" descritto in sezione 3 e' stato eseguito **dopo** aver verificato `28/30 PASS` come baseline. Senza questa baseline, sarebbe stato impossibile distinguere "rumore non deterministico del modello piccolo" (i 2 fail) da "regressione introdotta dal refactor". Con la baseline, il refactor e' stato accettato perche' i 28 PASS sono rimasti 28. ## 6. Anti-pattern incontrati e abbandonati Da onesti, **molte delle prime soluzioni non hanno funzionato**. Documentarle aiuta a non ripeterle. ### 6.1 Prompt testuale "DEVI chiamare il tool X" Tentativo: scrivere nel system prompt "se l'utente chiede una distinta, **DEVI** chiamare `assemble_distinta` prima di rispondere". Esito: il modello piccolo ignorava la direttiva nel ~30% dei casi e partiva a memoria. Il modello grande la rispettava di piu', ma comunque non al 100%. Sostituito da push deterministico server-side della distinta-template (sezione 4.2) e `tool_choice` API-side per forzare il tool al primo round (sezione 4.4). **Lezione**: il prompt e' un suggerimento, non un contratto. Per garanzie hard servono meccanismi hard (codice piu' parametri API). ### 6.2 Tool che il modello non chiamava mai Tentativo: esporre un tool `read_wiki_index()` per dare al modello una mappa del wiki da consultare a discrezione. Esito: il modello piccolo non lo chiamava praticamente mai, quello grande lo chiamava sporadicamente. In entrambi i casi un round di tool-use sprecato. Sostituito da catalogo wiki **inline nel system prompt cached** (sezione 4.1). Il modello non sceglie, ce l'ha gia' sotto gli occhi. **Lezione**: i tool che il modello "potrebbe" chiamare sono inutili. Tool = azioni che hanno effetti collaterali (DB, calcoli, lookup specifici). Per documenti di riferimento, push. ### 6.3 Hardcoding parametri in 3 posti Vedi sezione 3. Il pattern del MD-fonte + Python-derivato + prompt-derivato e' **trasferibile a tutto**: ogni volta che lo stesso valore appare in piu' file, e' solo questione di tempo prima che diverga in silenzio. ### 6.4 Iniettare la distinta-template SOLO sull'ultimo messaggio user Bug catturato dall'eval set: la query "distinta famiglia X" -> "40 kg" (secondo turno). Il classifier deterministico cercava la famiglia solo nel turno corrente ("40 kg" non contiene il nome famiglia) -> nessuna iniezione -> modello a memoria. Soluzione: il classifier ha **fallback all'intera history user concatenata**. Il chain "ricorda il contesto della distinta" nei turni successivi. E' esattamente il tipo di bug che senza eval-set passa inosservato. ## 7. Stack runtime tipico Non e' lo scopo di questo studio prescrivere lo stack: il pattern e' indipendente dall'infrastruttura. Per riferimento, lo stack su cui ho costruito il sistema: ``` Browser (LAN-only, in produzione interna) v FastAPI (uvicorn, NSSM service Windows) - /chat -> API LLM (rate limited) - /editor -> editor wiki UI (whitelist email) - /feedback -> wizard segnalazione + AI auto-fix - /admin/* -> backup, logs, restart, reindex Backend Python: - sentence-transformers paraphrase-multilingual-mpnet-base-v2 (768d, 1.1 GB cached) - Anthropic SDK con prompt caching ephemeral - SQLite (~20 MB: chunks + embedding BLOB + chat_session + feedback) - Rate limiter sliding window in-process (no Redis, no deps esterne) ``` ### Scelte di stack non ovvie - **SQLite + BLOB embedding 768d** anziche' vector DB dedicato: a 4-5k chunks la cosine similarity in-process con NumPy va piu' che bene (~50 ms su CPU, no setup). Sotto i 100k chunks e' la scelta piu' semplice e debugabile. Per scale piu' grandi: pgvector su Postgres, sempre preferito a soluzioni esterne. - **Routing modello regex-based**, NON tramite un classifier LLM (vedi sezione 4.5). - **Prompt caching ephemeral** sui blocchi `system` e `tools`: sconto ~90% sui token "fissi" (~7-8k/turno), ammortizza il costo di tenere catalogo, distinta-template e tabelle parametriche **inline nel prompt** anziche' come tool da chiamare. ## 8. Ciclo di vita annuale Un sistema di questo tipo regge nel tempo solo se la **rigenerazione del dataset** e' ripetibile. Quando il manuale fornitore cambia (annual update), serve un percorso documentato. {` [Nuovo PDF fornitore] | v [Branch git vYYYY-YYYY] | v [Backup pre-ingest: repo + DB] | v [Rigenera estratti: MinerU + pdfplumber] | v [Snapshot codici pre/post + diff] | v [Codici rimossi menzionati nel wiki?] --SI--> [Sostituire nei MD con nuovo equivalente] | | NO | |<---------------------------------------------+ v [Revisiona MD R*/D*/F*/G*] | v [Vision in-session per regole sensibili] | v [Rebuild DB + embedding] | v [Reindex wiki] | v [Eval set 30/30 PASS?] --NO--> [Triage diff: fix MD o codice] | | SI | |<-----------------------------+ v [Smoke test: 5 query benchmark] | v [Deploy + smoke prod] | v [Tag v.YYYY.0.0] `} I punti critici che fanno la differenza tra "ingestione che fila" e "ingestione che genera bug silenti": 1. **Snapshot codici PRIMA dello step OCR**. La rigenerazione `families/*/extracted.md` sovrascrive: perdere lo snapshot pre = niente diff possibile. 2. **Triage del diff codici**: i codici "rimossi" vanno cercati nei `D*.md` e nei `G*.md` e sostituiti, non lasciati orfani. Senza questo passaggio, il wiki cita codici fantasma fino al primo report utente. 3. **Vision in-session sulle regole sensibili**: se il body di una regola e' cambiato, e' piu' affidabile prendere uno screenshot ad-hoc della pagina nuova e farlo vedere al modello in conversazione, piuttosto che fidarsi del puro OCR per testi di policy. 4. **Eval set come gate di deploy**: 30/30 PASS prima del deploy. Un singolo fail su un test "core" (lookup, distinta, gate) e' motivo sufficiente per rinviare. 5. **Doc update post-ingest**: log nel wiki/log.md (audit trail), entry in handoff doc, eventuale aggiornamento conteggi nel doc di context. Non e' ceremonia: e' la mappa per il prossimo che ci mettera' mano. Stima realistica per un'edizione del catalogo intera: **1-2 giornate effettive** (3-4 h compute + 6-12 h focused-time umano). ## 9. Lessons learned Cinque punti che vale la pena interiorizzare prima di partire con un progetto analogo. ### 9.1 Pezze e organicita' Le pezze risolvono il problema **oggi**. L'organicita' lo previene **domani**. Servono entrambe, ma in tempi diversi: la pezza va in `_archive/scripts_one_shot/` con un commento esplicito, l'organicita' entra a pacchetti, e ha senso **solo se hai un eval set** che ti dice se il pacchetto ha rotto qualcosa. Senza eval set, l'organicita' e' un atto di fede. ### 9.2 Eval before refactor Il primo deliverable di valore non e' la feature: e' **l'infrastruttura di test**. Senza, ogni cambio e' una scommessa, e il debito di "non posso refactorare perche' non so cosa rompo" cresce esponenzialmente. Costruire 20-30 casi end-to-end deterministici e' un investimento di mezza giornata che si ripaga la prima volta che eviti una regressione in produzione. ### 9.3 Context engineering ≠ prompt engineering Il prompt e' la punta dell'iceberg. Sotto ci sono: cosa e' inline e cosa e' un tool, quale modello vede quale query, quale contesto viene iniettato in che momento, quali marker tracciano lo stato della conversazione, dove vivono le regole deterministiche (Python) e quelle discorsive (system prompt). **Context engineering** e' il design del sistema attorno al modello; il prompt ne e' solo un componente. Perdere settimane a tweakare le parole del system prompt quando il problema e' architetturale (es. tool sbagliato, modello sbagliato, dato non iniettato) e' il modo piu' rapido di non andare da nessuna parte. ### 9.4 Modello sbagliato e contesto sbagliato sono concomitanti Quando una risposta e' cattiva, raramente la causa e' univoca. Tipico stack-up: il **router** ha mandato la query al modello piccolo invece che a quello grande, **e** il classifier non ha iniettato la distinta-template, **e** il prompt non ha l'info parametrica giusta nelle referenze. Ognuno dei tre fix isolati avrebbe contribuito a peggiorare di poco; tutti e tre insieme spiegano il -90% di qualita'. Bisogna essere disposti a investigare a tutti e tre i livelli prima di concludere "il modello non capisce". ### 9.5 Single source of truth o si paga in debt Ogni valore che vive in 2+ posti diverge **prima o poi**. Non e' un'eventualita' remota: e' una garanzia statistica. La domanda non e' "se" ma "quando" e "quanto male me ne accorgo". L'investimento nel pattern MD-fonte + Python-derivato e' di 2-3 ore per le famiglie principali; il costo del debt non corretto e' infinito perche' si manifesta come piccoli errori sparsi che nessuno collega all'origine comune. Pagare l'organicita' una volta sola, all'inizio. ### 9.6 Knowledge Karpathy non basta da solo (lezione v2.0) Il pattern descritto in questo studio funziona benissimo per il caso che lo ha generato (chatbot tecnico interno single-brand su catalogo PDF voluminoso). Quando arrivano scenari multi-brand con **filtri strutturati cross-brand** (es. "lavastoviglie 60 classe A" su 3 brand di elettrodomestici), un wiki narrativo per brand non basta piu': - "60 cm" finisce in vector search e matcha "60 watt" su un altro prodotto. - "classe A" non e' normalizzato (l'utente puo' scrivere "A", "A+", "energy A"). - I vincoli cross-modulo (lavastoviglie 60 + cassetti retrostanti -> nicchia 560mm) non emergono perche' nessun knowledge brand "vede" la cucina intera. Il pattern Karpathy resta vincente per la **rappresentazione del knowledge editabile da umani** (wiki narrativi per brand, regole come `R*.md` con frontmatter eseguibile, schema attributi categoria come `C*.md`). Sopra serve l'orchestrazione architetturale: **Typed Query Layer** (slot filling pre-LLM), **Supervisor Filter-then-Validate** (PIM filtra cross-brand + Knowledge Tools brand validano), **Configuration Context** (stato di sessione persistito tipizzato), **Rule Engine deterministico** (che legge regole dai `R*.md` Karpathy ma le applica con evaluator Python). Lo studio principale [Agentizzare una PMI: stack, architettura, strumenti v2.0](/studio/stack-agentico-pmi/) descrive questa cornice. Il pattern wiki narrativo qui descritto e' la **fondazione di rappresentazione del knowledge**, non l'architettura completa di un sistema multi-brand consulenziale. Detto in altro modo: BlumCat in produzione e' un Knowledge Tool brand mascherato da chatbot. Funziona perche' il dominio e' single-brand. Il pattern v2.0 lo riformula come **uno dei N Knowledge Tools** dello stack, con ruoli e contratti chiariti. ## Checklist di partenza Se stai per partire con un chatbot tecnico interno su catalogo PDF denso (altro fornitore, altro settore), questo e' il minimo praticabile per non rifare gli stessi errori. - Identifica i **codici verificabili** del dominio (devono esistere, devono essere enumerabili in DB, devono essere il "gold" su cui assertare). - **Doppio OCR** dal giorno 1 (tabelle + raw text). Non scegliere un singolo strumento. - **Wiki MD autoritativo** sopra agli estratti grezzi, con frontmatter YAML strutturato. Categorie minime: composizione (D), decisioni (G), regole (R), narrativa (F). - **Eval set deterministico** entro la prima settimana, prima di qualunque feature non-banale. Almeno 10-15 casi: 1 lookup, 1 configurazione completa, 1 gate bloccante, 1 regola con calcolo, 1 edge case di allucinazione storica. - **Routing modello regex-based**, non LLM-based. Calibrato dall'eval set. - **Push del contesto e' meglio del pull dal modello** per documenti di riferimento corti (sotto 5k token). - **Gate logic in Python**, non nel prompt. Marker testuali come stato leggibile. - **`tool_choice` API-side** per task ad alto rischio, non promemoria nel prompt. - **Single source of truth** per ogni parametro che rischia di duplicarsi: MD editabile -> parser -> Python derivato + prompt derivato. - **Runbook annual update** scritto la prima volta che riesci a rigenerare il dataset, non la seconda. La memoria del processo evapora velocemente. - Un chatbot tecnico interno su manuale PDF voluminoso si costruisce su tre piani: **estrattori grezzi** (MinerU + pdfplumber), **wiki autoritativo** (4 categorie editabili: Distinte, Guide, Regole, Famiglie), **layer di compilazione** (parser MD -> Python e system prompt derivati). - Cinque pattern di **context engineering** sono determinanti: catalogo inline cached, push automatico documenti corti, gate logic server-side con marker, `tool_choice` forzato API-side, routing modello per rischio (non per costo). - L'**eval set** e' l'infrastruttura piu' importante prima di qualunque feature: senza, ogni refactor e' una scommessa. 30 casi end-to-end + assertion specifiche (`must_call_tools`, `response_must_contain`, regression su allucinazioni storiche). - La **single source of truth** non e' un nice-to-have: e' una garanzia statistica contro la divergenza silenziosa che, in un dominio con codici verificabili, si manifesta come errori sparsi che non collegherai mai all'origine comune. - Il **frontmatter eseguibile** (estensione v2.0) permette di leggere la stessa fonte in due modi: narrativa per l'esperto umano + rule object per Rule Engine deterministico. Doppia lettura, singola fonte. - Il pattern Karpathy e' **fondazione di rappresentazione**, non architettura completa. Per scenari multi-brand cross-categoria (CPQ con consulenza tecnica) sopra serve l'orchestrazione: Typed Query Layer, Supervisor Filter-then-Validate, Configuration Context, Rule Engine. Per la cornice architetturale completa dentro cui questo pattern vive (stack v2.0 a 5 livelli con modello bi-dimensionale knowledge verticale + cross-cutting orizzontale), vedi [Agentizzare una PMI: stack, architettura, strumenti v2.0](/studio/stack-agentico-pmi/). Per applicazioni concrete dei flussi, vedi [Agentizzare un'azienda: timeline e flussi reali v1.2](/studio/flussi-agentici-pmi/). Il pattern wiki narrativo, originariamente "data not code per esperto umano", si e' esteso in tre direzioni in due settimane di esercizio cross-brand. (1) **Wiki -> Knowledge Graph emergente** (Fase 2): indexer offline Python (~280 LOC) walk-a `wiki_arcocat//{regole,famiglie}/*.md` + cross-brand `categorie/distinte`, estrae identifier via `^([RFDC])(\d{1,4})_` e riusa il pattern `_extract_wiki_refs` (3 regex: prose "vedi R001", paren "(R001)", MD link "[F005](../...)") per scoprire edge fra entita'. Risultato su 5 brand: 166 wiki_entities + 557 wiki_edges in pim.db come tabelle additive (wiki_entities con id `:`, brand, type, title, file_path, content_hash SHA-256, ts_indexed; wiki_edges con from, to, edge_type, anchor_text). Edge_type inferito: regola->famiglia=applicable_to, famiglia->regola=see_also, distinta->*=depends_on, scheda_categoria->*=specifies. Tre tool LLM-callable: `find_related(entity_id, depth=1)` forward, `find_citing(entity_id)` reverse, `get_entity_details(entity_id, include_content=True)`. Il wiki Karpathy era un grafo *implicito* nei link in prosa; ora e' indicizzato e percorribile dall'agente in un singolo tool call. (2) **Canonical alias registry come SSoT runtime**: file YAML `canonical_aliases.yaml` con 15+ concept (pirolitico, no frost, Serie 8, acciaio inox con brand_overrides, induzione, abbattibile, HK top, vapore AMBIGUOUS, ecc.), tool `get_synonyms(text, brand)` con TTL 5s e match case-insensitive. Risolve la classe di bug "termine commerciale Bosch (Autopulizia pirolitica) vs query utente (pirolisi)" senza toccare codice Python: l'esperto edita il file, l'agente impara. (3) **Domain Contracts come gate**: 8 policy YAML in `domain_contracts/` (valid_brands, brand_routing_patterns, sku_regex, native_columns, canonical_aliases, guardrail_rules, sku_invariants, kg_edge_types) caricate da loader Pydantic v2 e cross-validate da `check_contracts.py` (~280 LOC) prima di ogni refresh. Onboarding 6o brand passa per un hard gate `check_contracts` exit 0 invece che per dieci diff manuali in altrettanti file Python. Anti-pattern abbandonati che hanno chiarito quando il pattern NON e' sufficiente: (a) SKU recall basso = layout extraction problem, NON embedding, da risolvere con entity-centric extraction offline (regex SKU + table + page anchor + adjacency + inverted index SKU-first), non con embedding piu' grossi; (b) cataloghi PDF voluminosi non vivono dentro al wiki narrativo, vivono in un secondo binario indicizzato a parte (vedi Catalog RAG nello stack v2.4); (c) le R-rule fisiche/normative sono migrate in `_guardrails/*.yaml` con AST sandbox manuale (no eval), il wiki narrativo conserva la prosa esplicativa cross-link-ata. Lezione integrata implicitamente nella sezione 9 "Lessons learned" come 9.7: **il wiki narrativo Karpathy regge come fondazione di rappresentazione, ma cresce per sedimentazione di tre layer separati: il body narrativo per l'umano, l'index strutturale per la macchina (KG offline), i contratti dichiarativi per il runtime (Domain Contracts SSoT)**. Studio strutturalmente immutato sui 4 pattern di base; le sezioni nuove (KG emergente, alias registry, Domain Contracts) sono materiale per v2.0 quando un 6o brand stress-testera' l'ontologia su compatibilita' meccaniche (ferramenta, NON elettrodomestici flat). Vedi `arcocat/HANDOFF_DOMAIN_CONTRACTS.md`. Allineamento allo stack v2.0. Aggiunto Callout in apertura che chiarisce il **ruolo nello stack v2.0**: questo studio descrive il livello *Knowledge editabile* (Knowledge Tools per brand + schema PIM in `C*.md` + Rule Engine che legge da `R*.md`), una delle componenti del modello bi-dimensionale, NON l'architettura completa. Aggiunta sotto-sezione "Frontmatter eseguibile (estensione v2.0)" alla sezione 2 (Wiki autoritativo): il frontmatter delle `R*.md` puo' essere esteso con `condizione` + `azione` + `test_cases` per essere letto come rule object da un Rule Engine deterministico Python (~150 righe). Doppia lettura della stessa fonte: narrativa per l'esperto umano + struttura tipizzata per la macchina. Stesso pattern applicato al `CategorySchema` del PIM lite (file `C*.md`). Aggiunta lessons learned 9.6 "Knowledge Karpathy non basta da solo": chiarisce dove il pattern smette di essere sufficiente (scenari multi-brand cross-categoria) e quale orchestrazione serve sopra (vedi studio principale v2.0). Prima stesura. Pattern completo del wiki narrativo AI-maintained: pipeline OCR dual-extractor, wiki autoritativo a 4 categorie (Distinte/Guide/Regole/Famiglie), single source of truth MD -> Python -> system prompt, cinque pattern di context engineering, eval set come gate di refactor, anti-pattern abbandonati, ciclo di vita annuale, lessons learned. Lo studio nasce dall'esperienza diretta di costruzione di un chatbot tecnico interno su un manuale fornitore di circa 750 pagine. --- ## Studio: cosa e' e come funziona URL: https://andreapellizzari.it/studio/manifesto/ Versione: 1.0 Creato: 2026-05-04 Keywords: metodo, knowledge management, documentazione viva > Perche' esiste questa sezione, come si differenzia dal diario, come leggere lo stato 'in evoluzione' di un documento. Dichiarazione di metodo della sezione. ## Perche' una sezione "Studio" Il diario raccoglie appunti datati: voci brevi, ognuna chiusa il giorno in cui la scrivo. Le case study raccontano lavori finiti. Tra queste due forme manca uno spazio: i **ragionamenti che evolvono**, le architetture che chiariscono nel tempo, le mappe di un dominio che cresce con l'esperienza. Lo Studio e' quello spazio. ## Cosa entra qui Documenti tecnici di taglia media (1500-5000 parole) su argomenti specifici: stack agentici, processi aziendali, integrazioni gestionali, design system. Non recensioni, non tutorial, non opinioni veloci. **Mappe di un terreno che ho percorso piu' volte.** Ogni studio appartiene a un *topic principale* (architetture AI, processi, integrazioni, design, performance, retrospettive). I topic sono pochi e crescono lentamente: un nuovo topic nasce quando ho almeno due studi che lo richiedono, mai per uno solo. ## Come si legge "in evoluzione" Ogni studio ha quattro stati possibili: - **Draft**: bozza non ancora pubblicata. Non compare in elenco. - **Evolving**: pubblicato ma in attivo aggiornamento. Aspettati cambiamenti significativi nei prossimi mesi. - **Stable**: la struttura e' assestata. Possibili ritocchi, nessuna riorganizzazione. - **Archived**: superato o non piu' rappresentativo. Mantenuto per riferimento storico, escluso dall'elenco principale. Il *versioning* di ogni studio segue un semver semplificato: 1.0 e' la prima stesura, 1.1 e' un'integrazione di sezione, 2.0 e' un cambio di filosofia. ## Cosa NON entra qui Dettagli operativi di clienti specifici. Codici articolo, URL interni, credenziali, processi proprietari riconoscibili. Tutto cio' che potrebbe identificare una controparte aziendale resta in versione completa fuori dal sito. Quello che leggi qui e' la **versione astratta**, applicabile a contesti simili al mio (PMI manifatturiere italiane, integrazioni gestionali, sviluppo in-house). ## Sinergia con i Concetti I [Concetti](/concetti/) sono voci di vocabolario: definizioni brevi, glossario tecnico. Gli Studi sono saggi: argomentazioni lunghe. I primi sono porte d'ingresso, i secondi sono case in cui camminare. In fondo a ogni studio trovi i concetti correlati; in fondo a ogni concetto trovi gli studi che lo approfondiscono. E' questa la geometria che provo a costruire: un quaderno aperto dove le idee si parlano tra loro, e dove l'aggiornamento non cancella la storia ma la mostra. ---