andreapellizzari.it
Indice studio
Architetture AIv2.3.715892 parole · 40 min

Agentizzare una PMI: stack, architettura, strumenti

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.

Creato: Aggiornato: In evoluzione

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.

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 prodottoCPQ
Scope tipicoFind productConfigure + Price + Quote
Stato di sessioneStoria conversazioneConfigurazione strutturata
VincoliDocumentati a paroleCostraint Satisfaction Problem
OutputRisposta testualeConfigurazione valida + offerta
ValidazioneLLM-basedRule engine deterministico
Failure modeAllucina dato plausibileRefuso 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.

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).

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 queryEsempioDimensione 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) e' il routing intelligence che decide quali fonti chiamare per ogni query, in che ordine, con che pattern.

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:

RuoloCosa fa
Event busPub/sub di eventi strutturati. Pubblichi con inngest.send({ name: "x", data: {...} }), le funzioni in ascolto reagiscono.
Queue durabileI lavori sono persistiti in Postgres. Sopravvivono ai restart, ai crash, ai deploy. Niente perdita di stato.
Workflow engineOrchestra 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.

// 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 errorePolicyTentativi
Errore transitorio (rete, timeout, 5xx)Exponential backoff Inngest default4 (default)
Chiamata gestionale (Mexal): timeout / busyRetry rapido + circuit breaker pybreaker davanti3 retry, poi breaker apre per 1 min
Errore client (4xx, validazione fallita)NESSUN retry: errore deterministico, ritentare e' inutile0
Fallimento permanente dopo retryVa in dead letter queue (tabella failed_workflows Postgres)0
LLM provider down (Anthropic 5xx)Fallback su provider secondario via LiteLLM1 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:

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.

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:

// 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
}
// 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
}
// Input
"aggiungi al progetto cucina la lavastoviglie SMV68N20EU"

// Output
{
  intent: "configure",
  categoria: "lavastoviglie",
  filtri: { codice: "SMV68N20EU" },
  testo_libero: null,
  contesto_progetto_ref: "cucina_<session_id>"
}

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.

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:

MetricaL3 (regex L3-OP1 fragile)Step 4 (regex raffinato + MD)Note
Edge case L3-OP1 risolti0/1015/15separatori virgola, lowercase altezza, sinonimi nuovi
Test supervisor-mcp20/20 verdi56/56 verdi+36 nuovi (21 unit slot_filling + 15 edge case)
Cross-validate strict trasparenza Q1strict 3/3 (Iter2)strict 3/3baseline mantenuto, nessuna regressione
Grounding rate 5 query L3 baseline11%12%invariato (atteso, TQL non aggiunge regole)
Grounding rate 5 query EDGE nuoven/a22%TQL piu' completo → candidati piu' rilevanti al LLM judge
Cost per query (cache HIT)$0.0035$0.0035invariato (LLM judge unico stage costoso)
Latency aggiuntiva TQL~1ms regex~1ms regex + cached MD loadinvariata 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.

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:

StatoSignificatoEsempio
CompatibileFiltro PIM ok, ma Knowledge non ha valutato"Modello X soddisfa larghezza 60 e classe A"
ConsigliatoFiltro PIM ok + Knowledge approva attivamente"Modello X soddisfa filtri e si adatta al tuo progetto cucina"
SconsigliatoFiltro 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.

Pattern documentato da Anthropic

Il pattern Supervisor con worker e' documentato esplicitamente nel paper How we built our 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). 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:

@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):

@mcp.tool()
def get_regola(rid: str) -> dict:
    """Ritorna la regola R<rid> (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):

MetricaL1 (rule completo, no hint)Iter1 (rule completo + hint)
Safety net trigger2/4 (50%)1/4 (25%)
Citazioni totali (4 casi)13
Casi grounded12
Cache HIT100%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.

---
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:

@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':

FonteProContro
Estrazione da portali brand (API ufficiali)Affidabile, aggiornataSpesso richiede partnership commerciale, rate limit
Estrazione da PDF brand (MinerU + curation)Funziona ovunqueCostoso da estrarre attributi tipizzati
Mappatura manuale (foglio Excel curato)Sotto controllo totaleSostenibile 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_<brand>/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).

---
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):

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:

@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:

@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

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<string, "PIM" | "BrandKnowledge" | "Mexal" | "user_input">
  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<string, ScalarValue>  // misure, classe, opzioni
  vincoli_emessi: Vincolo[]               // dal Rule Engine
}

Esempio di evoluzione di una sessione

// 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.

ComponenteStrumentoRuolo
Workflow durabiliInngestOrchestrator + event bus + queue
DatabasePostgres + pgvectorTutto: PIM, Configuration Context, Inngest state, Langfuse data, audit log
LLMAnthropic APISonnet (Supervisor) + Haiku (slot filling, lookup)
MCP runtimeFastMCP PythonServer tool per Knowledge / PIM / Mexal / Promo / Rule
ObservabilityLangfuse + OpenTelemetry GenAITrace cross-livello
Reverse proxyCaddyTLS + routing
EdgeCloudflareDDoS + WAF
Embedding (in uso BlumCat oggi)sentence-transformers (mpnet 768d)Embedding multilingua locale, no API esterna
PDF parsingMinerU + pdfplumber + PyMuPDFEstrazione strutturata + raw text supplementare + pre-render JPG citazioni
Application errorsSentryErrori applicativi backend
Uptime monitorUptimeRobotDisponibilita' 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).

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

ScenarioMCP?
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 teamSi'
Solo Claude Code per dev, gia' copri con una skill markdownNo, basta skill
App Python deterministica: import diretto della libreriaNo, no LLM nel mezzo

Skill Claude Code != MCP server

Distinzione fondamentale, spesso confusa:

Skill (es. gestionale-webapi)MCP server
Cos'e'File markdown con istruzioniProcesso con funzioni eseguibili
Come Claude la usaLegge il markdown, poi esegue lui via Bash/curlInvoca direttamente i tool, riceve JSON
Funziona conSolo Claude Code (e client compatibili)Qualsiasi client MCP
Type safetyNessunaSchema tipizzato
Effort1 file MDProcesso da scrivere e deployare

Esempio minimo 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.

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.

Observability: Langfuse + OpenTelemetry GenAI

Cinque livelli da monitorare:

LivelloCosa misuriStrumento
InfrastructureMCP server up? Endpoint risponde?UptimeRobot + Sentry
Trace agenticoCosa ha fatto, perche', in che sequenzaLangfuse + OTel GenAI
CostEuro per agente, per task, per clienteLangfuse (nativo)
Quality / EvalRisposta accurata? Rispetta le regole?Langfuse Datasets + Promptfoo
Business outcomeHa 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:

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

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.

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 per il pattern operativo.

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)

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<string, ScalarValue>  // 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.

---
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_<brand>/regole/R*.md. Esempio gia' mostrato per R005 LEGRABOX.

Schema del frontmatter:

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:

# 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:

# 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).

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<string, FonteValue>
  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<string, ScalarValue>
  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.<tool>" | "Mexal.<tool>" | "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:

ProContro
16+ canali messaging gratisRelease ogni 7-10 giorni, instabilita'
Memoria persistente built-inLock-in di framework giovane
Setup veloce, prototipazione rapidaTrack record di stabilita' ancora corto
Compatibili con MCP serverPer workflow critici serve comunque Inngest sotto

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

CosaRaccomandatoQuando
Secret vaultInfisicalMese 2-3
Identity / SSOAutheliaMese 4-6
Auth tra agenti/MCPAPI key + JWT firmatiMese 2
Multi-tenant isolationPostgres RLS nativoMese 4+

2. Data layer

CosaRaccomandatoQuando
Vector DB (RAG) per Knowledge Tools small (sotto 100k chunks)SQLite + BLOB embedding 768d cosine in-processSubito (in uso BlumCat)
Vector DB (RAG) per scale maggiori (PIM, audit, multi-tenant)pgvector su PostgresMese 2-3 (migration v2.0)
Embedding model (in uso BlumCat oggi, locale, no API)sentence-transformers mpnet 768d multilinguaSubito
Embedding model (alternative quando OOM o serve qualita' superiore)Voyage AI o locale via OllamaMese 4-6 (opzionale)
Document storage (S3)MinIOMese 2-3
PDF parsing (markdown strutturato + tabelle)MinerUSubito
PDF parsing (raw text supplementare per formule grafiche)pdfplumberSubito (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 ToolsSubito
Full PIMAkeneo (solo se justified)Mese 12+

3. Eval e testing avanzato

CosaRaccomandatoQuando
Eval frameworkPromptfooMese 3
LLM-as-judgeLangfuse evals nativoMese 3
RAG eval specificoRAGAS (faithfulness, context precision)Mese 3
Self-correcting RAG (CRAG) come gate post-Knowledge-ToolPattern Higress-RAG / arXiv:2602.23374. Adaptive routing + dual hybrid retrieval. Trigger: post-2°brand stabile, se eval mostra retrieval quality limitante in casi confidence-lowPost 2°brand (Mese 4-5 stimato). Aggiunto in v2.2.
Red-teamingPyRIT + GarakMese 4-5
Test Rule Engineunittest + test_cases embedded in R*.mdSubito

4. Compliance e governance (AI Act EU + GDPR)

CosaRaccomandatoQuando
PII detection / redactionMicrosoft PresidioMese 2 (subito se PII reali)
Audit log immutabilePostgres append-only + triggerMese 2-3
Data deletion (RTBF)Soft-delete + crypto-shreddingMese 3-4
Risk assessment AI ActDocumento 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 residencyVPS Italia/UE + Anthropic EU endpointSubito

5. Cost optimization

CosaRaccomandatoQuando
Prompt cachingAnthropic nativeSubito
Model routingLogica custom (Haiku per slot filling, Sonnet per Supervisor)Mese 2-3
AI Gateway proxyLiteLLM self-hostMese 3-4
Batch APIAnthropic Batch (50% sconto)Mese 3+ per task notturni
Semantic cacheRedisVL su query ricorrentiMese 3-4
Local LLM fallbackOllama + Llama/QwenMese 6+ (richiede GPU)

6. Disaster recovery e resilience

CosaRaccomandatoQuando
Backup databaseWAL-G + cronSubito
Backup file storageRestic o BorgMese 1-2
Circuit breakerpybreakerMese 3
LLM provider failoverLiteLLM fallback rulesMese 4-5
Graceful degradationPattern in codiceMese 2-3

7. Network e infrastruttura

CosaRaccomandatoQuando
Reverse proxy + TLSCaddySubito
Container orchestrationDocker ComposeSubito
PaaS self-host (alt)CoolifyMese 2-3
DDoS / WAFCloudflare free tierMese 2
SSH brute forcefail2banSubito

8. Human interface

CosaRaccomandatoQuando
Admin dashboard / KPIMetabaseMese 3-4
Internal tool builderAppsmithMese 4-6
Notifications multi-canaleAppriseMese 2
Feature flags / kill switchUnleashMese 3-4
Editor wiki/regole UICustom Next.js + auth whitelistSubito (per Knowledge Tools)

9. Documentation operativa

CosaRaccomandatoQuando
Wiki / KB internaOutlineMese 2-3
ADRMarkdown in /docs/adr/ versionatoSubito
RunbookMarkdown + MkDocs MaterialMese 3-4
Wiki narrativi brand (Karpathy)MD curato editabile via UISubito
Schema PIM (CategorySchema)MD curato in wiki_arcocat/categorie/Subito
Regole (R*.md)MD curato in wiki_<brand>/regole/Subito

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.

PianovCPU / RAM / SSDQuandoNote
KVM 22 / 8 GB / 100 GBMVP mese 1-3Sufficiente per Langfuse + Inngest + 2-3 MCP server + 1-2 Knowledge Tools
KVM 44 / 16 GB / 200 GBMese 4-12Margine per crescita: knowledge tools multipli, PIM con dati reali, RAG
KVM 88 / 32 GB / 400 GBMese 12+ se scaliSolo se hai molti knowledge tools concorrenti o RAG su grossi dataset

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

ComponenteChi lo costruisceCome
Parser MinerU/pdfplumber custom per il manuale brandDeveloper + Claude (sessione iterativa)2-5 giorni di sessione Claude Code
Estrattore tabelle codici e attributi tipizzatiDeveloper + Claude1-3 giorni
Generatore frontmatter YAML per R*.md/D*.md/F*.mdDeveloper + Claude1-2 giorni
Validator AI pre-save (Haiku ~$0.001 per save)Developer + Claude1-2 giorni
Body narrativo wiki (D*/G*/R*/F*) per top codiciEsperto interno, via UI editor wiki5-10 ore/settimana per 2-3 settimane
Regole "non scritte" (nozioni mentali)Esperto interno, via UI editor o sessione con devStesso slot esperto sopra
Gli altri 800 codici (longtail)Python automatic con quality flagRun 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.

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 / patternValutato inDecisioneMotivazione
Microsoft Agent Framework 1.0 GA (rilasciato 3 aprile 2026)Audit 2026-05-05ScartatoOttimo 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-05ScartatoConflitto 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-05Mantenuto 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-05RinviatoArchitettura 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 EditionStudio v2.0 e riconfermato in audit 2026-05-05ScartatoEdition 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-05NON CONFERMATO con fonte primariaVerifica 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-05ScartatoPer 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.

Per applicazioni concrete dei flussi nel modello v2.0, vedi Agentizzare un'azienda: timeline e flussi reali (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 (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. 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. Per una mappa navigabile di tutta la ragnatela documentale (4 studi + 2 presentazioni + glossario + cronologia + fonti), vedi Mappa dello studio.

Registro aggiornamenti

  1. v2.3.7

    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.

  2. v2.3.6

    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.

  3. v2.3.5

    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 compatibilesconsigliato 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.

  4. v2.3.4

    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.

  5. v2.3.3

    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.

  6. v2.3.2

    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.

  7. v2.3.1

    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.

  8. v2.3

    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.

  9. v2.2

    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.

  10. v2.1

    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.

  11. v2.0

    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.

  12. v1.3

    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).

  13. v1.2

    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.

  14. v1.1

    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 per il livello 3 quando la fonte documentale e' un manuale tecnico voluminoso.

  15. v1.0

    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.