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.
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 prodotto | CPQ | |
|---|---|---|
| Scope tipico | Find product | Configure + Price + Quote |
| Stato di sessione | Storia conversazione | Configurazione strutturata |
| Vincoli | Documentati a parole | Costraint Satisfaction Problem |
| Output | Risposta testuale | Configurazione valida + offerta |
| Validazione | LLM-based | Rule engine deterministico |
| Failure mode | Allucina dato plausibile | Refuso esplicito ("non e' compatibile, perche'") |
Riconoscere il framing CPQ ha tre conseguenze immediate sul design:
- Non si modella il problema come "retrieval di prodotti". Si modella come "navigazione vincolata in uno spazio di configurazioni valide".
- 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).
- 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 query | Esempio | Dimensione attivata |
|---|---|---|
| Consulenza narrativa | "come scelgo cerniera per anta da 18kg?" | Knowledge tool brand (Blum) |
| Filtro strutturato single-brand | "cerniera Blum 110° spessore 19mm" | Knowledge tool brand + suoi attributi |
| Filtro strutturato cross-brand | "lavastoviglie 60 classe A" | PIM (singleton) |
| Lookup esatto | "cosa e' 750.5001S?" | Knowledge tool brand |
| Disponibilita' / prezzo | "ho la SMV68N20EU? prezzo per cliente Rossi?" | Mexal MCP |
| Promo | "promo attive su lavastoviglie?" | Promo MCP |
| Bundle cross-categoria | "bundle Blum + lavastoviglie?" | Promo MCP + Mexal |
| Composizione complessa | "offerta cucina 60 cliente Rossi" | Tutti, con stato in Configuration Context |
| Comparativo | "Bosch SMV68 vs Whirlpool W7" | PIM + 2 knowledge tools brand + Mexal |
Il Supervisor (vedi Livello 2) 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:
| Ruolo | Cosa fa |
|---|---|
| Event bus | Pub/sub di eventi strutturati. Pubblichi con inngest.send({ name: "x", data: {...} }), le funzioni in ascolto reagiscono. |
| Queue durabile | I lavori sono persistiti in Postgres. Sopravvivono ai restart, ai crash, ai deploy. Niente perdita di stato. |
| Workflow engine | Orchestra step in sequenza con step.run, step.sleep, step.waitForEvent. Retry automatici. |
Il framing unitario evita di pensare a tre componenti separati: e' un solo layer, una sola dashboard, un solo database, una sola API mentale.
I quattro tipi di trigger
Tutto cio' che accade nel sistema parte da uno di questi quattro trigger. Sono i soli ammessi: ogni nuovo workflow deve appartenere a una di queste categorie.
// 1. WEBHOOK in ingresso da sistemi esterni (gestionale, fornitori, ecc.)
inngest.createFunction(
{ id: "onboarding-cliente" },
{ event: "erp/cliente.creato" }, // listener su evento
async ({ event, step }) => { ... }
);
inngest.send({ name: "erp/cliente.creato", data: {...} });
// 2. CRON schedulato (lavori periodici)
inngest.createFunction(
{ id: "etl-catalogo-notturno" },
{ cron: "0 3 * * *" }, // ogni notte alle 03:00
async ({ step }) => { ... }
);
// 3. CHAIN interna (workflow che spawna un altro workflow)
await step.sendEvent("notifica-completamento", {
name: "comm/onboarding.completato",
data: { cliente_id }
});
// 4. MANUAL dashboard (replay, debug, recovery)
// Lanciato manualmente dalla dashboard Inngest, utile per:
// - replay di un workflow fallito
// - test di un nuovo agente in staging
// - recovery dopo un disastro
Criteri di scelta:
- Webhook: quando un sistema esterno deve notificarci che qualcosa e' successo. Sempre preferibile a polling.
- Cron: lavori legittimamente periodici (ETL notturni, report giornalieri, pulizie). Mai per "controllare se ci sono novita'": quello e' un anti-pattern (vedi sotto).
- Chain: per disaccoppiare flussi che logicamente appartengono a momenti diversi. Es. "onboarding completato" emette un evento che fa partire "invio benvenuto", invece di tutto in un unico mega-workflow.
- Manual: solo per intervento umano consapevole, mai come sostituto di trigger automatici.
Retry policy esplicita
Inngest fa retry automatici di default. Le scelte specifiche sono queste, e si applicano a tutti i workflow:
| Categoria errore | Policy | Tentativi |
|---|---|---|
| Errore transitorio (rete, timeout, 5xx) | Exponential backoff Inngest default | 4 (default) |
| Chiamata gestionale (Mexal): timeout / busy | Retry rapido + circuit breaker pybreaker davanti | 3 retry, poi breaker apre per 1 min |
| Errore client (4xx, validazione fallita) | NESSUN retry: errore deterministico, ritentare e' inutile | 0 |
| Fallimento permanente dopo retry | Va in dead letter queue (tabella failed_workflows Postgres) | 0 |
| LLM provider down (Anthropic 5xx) | Fallback su provider secondario via LiteLLM | 1 fallback |
Per gli errori in DLQ: alert immediato via Apprise al canale operativo, replay manuale dalla dashboard quando l'incidente e' risolto. Mai ignorare la DLQ: e' il segnale che qualcosa di non transitorio e' rotto.
Long-running: step.sleep vs step.waitForEvent
Due strumenti diversi per "aspettare", da non confondere:
step.sleep("3 days"): attesa di tempo passivo. Il workflow riprende dopo 3 giorni, indipendentemente da cosa succeda. Costo: zero (Inngest non tiene il processo vivo, lo riprende quando serve).step.waitForEvent("approval", { timeout: "24h", match: "data.cliente_id" }): attesa di un evento specifico (con timeout). Il workflow riprende quando l'evento arriva, oppure va in timeout. Costo: zero finche' non si sblocca.
Durata massima: in pratica illimitata (giorni, settimane). Costo durante l'attesa: zero. Nessun processo vivo, nessuna risorsa allocata. E' la magia del durable execution.
Backpressure e concurrency
Quando un picco di eventi arriva (es. import massivo di 500 clienti, errore di un sistema esterno che genera 1000 webhook duplicati), serve un limite per non saturare downstream o l'API LLM:
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:
- 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.
- NER (Named Entity Recognition) leggero con spaCy italiano per estrazione di entita' generiche (numeri con unita', nomi di marchi, codici articolo). Funziona offline, ~50ms.
- 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:
| Metrica | L3 (regex L3-OP1 fragile) | Step 4 (regex raffinato + MD) | Note |
|---|---|---|---|
| Edge case L3-OP1 risolti | 0/10 | 15/15 | separatori virgola, lowercase altezza, sinonimi nuovi |
| Test supervisor-mcp | 20/20 verdi | 56/56 verdi | +36 nuovi (21 unit slot_filling + 15 edge case) |
| Cross-validate strict trasparenza Q1 | strict 3/3 (Iter2) | strict 3/3 | baseline mantenuto, nessuna regressione |
| Grounding rate 5 query L3 baseline | 11% | 12% | invariato (atteso, TQL non aggiunge regole) |
| Grounding rate 5 query EDGE nuove | n/a | 22% | TQL piu' completo → candidati piu' rilevanti al LLM judge |
| Cost per query (cache HIT) | $0.0035 | $0.0035 | invariato (LLM judge unico stage costoso) |
| Latency aggiuntiva TQL | ~1ms regex | ~1ms regex + cached MD load | invariata operativamente |
Implicazione operativa per onboarding 2°brand: il TQL e' ora robusto su Blum. Per Bosch/Hettich/altro, l'onboarding consiste nell'aggiungere sinonimi al MD-Karpathy (es. tipo_componente Hettich avra' "guide push-to-open", "Quadro", "Easy-Move", varianti tedesche/inglesi) e schema CategorySchema PIM (es. C002_lavastoviglie con attributi larghezza_cm, classe_energetica). Niente codice Python da toccare per il TQL.
Livello 2: Supervisor pattern - Filter-then-Validate
Il secondo livello e' il router intelligente che riceve TypedQuery, decide quali fonti del Livello 3 chiamare, in che ordine, e sintetizza la risposta. Non e' un agente "intelligente" nel senso tradizionale (non improvvisa): e' un orchestratore deterministico che esegue un flusso prevedibile.
Il pattern: Filter-then-Validate
L'ordine di chiamata delle fonti non e' parallelo. E' sequenziale e gerarchico:
- Filter (PIM, recall alto, precisione bassa): per query con filtri strutturati, il PIM produce candidate set deterministico. "Lavastoviglie 60 classe A" -> 12 codici candidati.
- 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.
- 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:
| Stato | Significato | Esempio |
|---|---|---|
| Compatibile | Filtro PIM ok, ma Knowledge non ha valutato | "Modello X soddisfa larghezza 60 e classe A" |
| Consigliato | Filtro PIM ok + Knowledge approva attivamente | "Modello X soddisfa filtri e si adatta al tuo progetto cucina" |
| Sconsigliato | Filtro PIM ok ma Knowledge segnala problema | "Modello X soddisfa filtri MA richiede nicchia 560mm, nel tuo progetto solo 540mm disponibili" |
In caso di sconsigliato, vince il Knowledge Tool. Mostrare comunque il prodotto come "compatibile per filtro" senza menzionare la criticita' rinuncia esattamente al differenziatore consulenziale che e' il valore aggiunto del sistema.
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.5001Scompatibile(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:
- Cache miss strutturale: le regole selezionate variano per call →
cache_control:ephemeralnon aggancia. - Chunk-level retrieval su rule set astratto e' rumoroso: query lunghe con token specifici producono regole off-topic, nascondendo regole pertinenti.
- Top-k=3-5 esclude regole pertinenti per query arricchite di token specifici.
Conclusione: retrieval mirato chunk-level e' utile come HINT (priorita' nel user_block), NON come gating esclusivo del rule set.
Quando passare a retrieval-only: solo se rule set > ~200 regole (~200k token, fuori dalla finestra cache pratica). Per Blum (14 regole), Bosch atteso (~30-50), Whirlpool atteso (~20-40), il pattern "tutto cached + hint" resta ottimale. Per dominii regolatori densi (farmaceutico, finanziario) dove le regole possono superare i 200, retrieval-only o tiering diventa necessita' tecnica, ma e' caso edge.
Validazione numerica iterazione 1 BlumCat (2026-05-05):
| Metrica | L1 (rule completo, no hint) | Iter1 (rule completo + hint) |
|---|---|---|
| Safety net trigger | 2/4 (50%) | 1/4 (25%) |
| Citazioni totali (4 casi) | 1 | 3 |
| Casi grounded | 1 | 2 |
| Cache HIT | 100% | 95.2% |
| Costo per call | $0.003 | $0.0028 |
Pattern Iter1 quindi: stesse caratteristiche L1 (cache HIT, costo) + miglioramento netto su grounding (citazioni 3x, safety net dimezzato). Da adottare come default in tutti i Knowledge Tool brand v2.x.
Nota in v2.3.4 sul non-determinismo del judge (validato in arcocat/REPORT_L3.md sez 4.2 + confermato in arcocat/REPORT_ITER2_BLUMKNOWLEDGE.md v2.3.5): con temperature default Anthropic = 1, il LLM judge produce variance fra chiamate consecutive sullo stesso prompt. In L3 cross-validate Q1 misurato: stesso codice, stessa pipeline, esiti consigliato vs compatibile su due run a 5s di distanza. La trasparenza strutturale del Supervisor tiene (codici + bucket usable/blocked sono invariant deterministici), il status fine no. Mitigation raccomandata: passare temperature=0 a valida_compatibilita (modifica chirurgica di 1 riga, validata empiricamente in iter2 minore BlumKnowledge: cross-validate strict equality regge 3/3 run consecutivi). Il giudizio tecnico diventa riproducibile, l'output testuale resta naturale (system prompt + few-shot guidano lo stile, non la creativita' su temperature). Compatibile con cache_control:ephemeral (cache HIT 100% post-fix confermato). Tradeoff netto a favore di temperature=0 per use case validation.
Caveat empirico (iter2): temperature=0 riduce variance al ~95-100% sui casi non-borderline e al ~80-90% sui casi borderline (logit vicini fra opzioni alternative). Sui casi borderline il residuo si manifesta come oscillazione compatibile↔sconsigliato, mitigabile a livello di regola formale nel KB (preferibile a top_k=1 o judge piu' grande). Pattern: la stabilita' definitiva nasce da grounding regolatorio piu' forte, non da sampling LLM piu' aggressivo.
Lezione metodologica "variance vs gap coverage" (lezione primaria iter2): per Knowledge Tool brand con LLM judge, due sintomi sono diagnosticamente confusi finche' non si separa temperature. Sintomi: (a) variance status fine fra chiamate consecutive sullo stesso prompt, (b) grounding rate basso (poche citazioni esplicite). Diagnosi corretta: (a) e' rumore di sampling LLM, (b) e' assenza di regole formali nel KB. Verifica empirica iter2 BlumKnowledge: post-fix temperature=0, cross-validate diventa deterministico (a risolto) ma grounding rate resta invariato a 11% (b inalterato). L'invarianza di (b) post-fix di (a) e' essa stessa la prova diagnostica che (b) e' gap reale, non artefatto di sampling. Implicazione operativa: prima fix variance, poi misura gap; senza fix variance la misura del gap e' rumorosa. Generalizzabile a ogni nuovo brand del constellation.
3B. PIM lite per attributi cross-brand
Cosa e': il Product Information Management lite. Un singleton che tiene gli attributi tipizzati per categoria prodotto, brand-as-attribute, schema definito in MD-Karpathy. Permette query strutturate cross-brand del tipo "lavastoviglie 60 classe A".
Modello dati ispirato Akeneo:
- Categoria (es.
lavastoviglie,cerniera,cassetto): gerarchia merceologica. - Schema attributi per categoria (es. lavastoviglie ha sempre
larghezza_cm,classe_energetica,capacita_coperti,tipo_incasso,brand,modello). - Attribute tipizzato:
measurement(con unita'),select(con valori ammessi),boolean,text. - Brand e' un attributo del prodotto, non un'entita' separata.
- Channel / locale come "viste" diverse (b2b, b2c, ecommerce, print): se servono.
Schema in MD-Karpathy: invece di definire lo schema in Python o JSON Schema rigido, lo schema vive in wiki_arcocat/categorie/C*.md con frontmatter YAML eseguibile + body markdown narrativo. Stesso pattern del wiki narrativo dei Knowledge Tools. Editabile da UI da un esperto di dominio senza toccare codice.
---
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 inarcocat/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 pilotasistemi_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_mmpopolato al 100%. - 96 accessori (frontali alluminio HG, viterie, motorizzazioni SERVO-DRIVE, traversi, kit ricambio): non hanno
NL_mmper design del catalogo. Forzare regex su questi codici per estrarre unNL_mmprodurrebbe valori semanticamente errati (interpretare la larghezza frontale 1500mm comeNL_mm=1500e' un bug peggiore diNL_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':
| Fonte | Pro | Contro |
|---|---|---|
| Estrazione da portali brand (API ufficiali) | Affidabile, aggiornata | Spesso richiede partnership commerciale, rate limit |
| Estrazione da PDF brand (MinerU + curation) | Funziona ovunque | Costoso da estrarre attributi tipizzati |
| Mappatura manuale (foglio Excel curato) | Sotto controllo totale | Sostenibile per centinaia di codici, non migliaia |
In pratica: mix delle tre. Per i top 200 codici per categoria, mappatura manuale curata (e' la stragrande maggioranza del fatturato). Per la coda lunga, estrazione automatica con marker "non verificato" sui chunk dubbi.
3C. Rule Engine deterministico
Cosa e': un singleton che valuta vincoli tecnici espliciti (Constraint Satisfaction Problem) sui prodotti e sui contesti. Esempio: "se LF > 500 e portata > 40kg, usa 753.* non 750.*" (regola Blum). Riceve in input prodotto + contesto, ritorna {ok, motivazione, alternative}.
Perche' serve un componente dedicato: i vincoli tecnici NON vivono bene in un LLM. Un LLM puo' interpretare male un range, dimenticare un edge case, applicare male una formula. Per i calcoli di portata di un cassetto B2B il margine di errore del LLM e' inaccettabile. Per i vincoli serve un evaluator deterministico Python con test unitari.
Pattern Karpathy esteso: la fonte autoritativa delle regole resta wiki_<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
condizionesia 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 susession_idecliente_id. - TTL: 30 giorni dopo l'ultima modifica (configurazioni abbandonate vengono ripulite). Estendibile su richiesta utente per progetti aperti.
- Snapshot per audit: ad ogni transizione, append immutabile su
configuration_audit(chi ha modificato cosa, da quale fonte, con quale rule_id triggered).
Conseguenza architetturale
Con Configuration Context come stato persistito:
- Il Rule Engine puo' valutare vincoli cross-modulo (es. lavastoviglie + cassetti retrostanti).
- Il Supervisor puo' renderizzare riepilogo strutturato in qualunque momento ("hai configurato: ..., vincoli aperti: ...").
- L'utente puo' tornare su una configurazione 5 giorni dopo, senza che il LLM debba "ricostruire" da una history conversazione lunga.
- L'offerta finale generata da Mexal MCP attinge a un input strutturato, non a un testo che il modello ha tenuto a mente.
- L'audit e' completo: ogni campo ha la sua fonte (chi/quando l'ha messo).
Questo livello e' assente nel design v1.x. La sua aggiunta e' uno dei tre cambiamenti strutturali della v2.0 (gli altri due sono Typed Query Layer e PIM cross-brand).
Livello 5: Infrastruttura
Il quinto livello e' l'infrastruttura di esecuzione. E' il livello "boring" del sistema (deve solo funzionare bene), ma e' la fondazione su cui poggia tutto. Le scelte qui sono in continuita' col v1.x.
| Componente | Strumento | Ruolo |
|---|---|---|
| Workflow durabili | Inngest | Orchestrator + event bus + queue |
| Database | Postgres + pgvector | Tutto: PIM, Configuration Context, Inngest state, Langfuse data, audit log |
| LLM | Anthropic API | Sonnet (Supervisor) + Haiku (slot filling, lookup) |
| MCP runtime | FastMCP Python | Server tool per Knowledge / PIM / Mexal / Promo / Rule |
| Observability | Langfuse + OpenTelemetry GenAI | Trace cross-livello |
| Reverse proxy | Caddy | TLS + routing |
| Edge | Cloudflare | DDoS + WAF |
| Embedding (in uso BlumCat oggi) | sentence-transformers (mpnet 768d) | Embedding multilingua locale, no API esterna |
| PDF parsing | MinerU + pdfplumber + PyMuPDF | Estrazione strutturata + raw text supplementare + pre-render JPG citazioni |
| Application errors | Sentry | Errori applicativi backend |
| Uptime monitor | UptimeRobot | Disponibilita' endpoint critici |
Un singolo VPS Linux KVM 2-4 (2-4 vCPU, 8-16 GB RAM) basta per i mesi 1-12. Costo infrastruttura: ~10-15 EUR/mese + variabile API Anthropic (50-300 EUR/mese stimati).
MCP server: cosa, quando, come
MCP (Model Context Protocol, Anthropic, ora governato da Linux Foundation) e' il collante tra Supervisor e fonti. Un server MCP espone funzioni in modo standardizzato. Il client (Supervisor LLM o app diversa) si collega e scopre da solo quali funzioni ci sono, parametri, tipi di ritorno.
Tre cose si possono esporre:
- Tool: funzioni invocabili dall'agente, es.
cerca_cliente(query) - Resource: dati read-only che l'agente puo' leggere, es. schema DB, listino
- Prompt: template di prompt riutilizzabili
Quando ha senso costruirlo
| Scenario | MCP? |
|---|---|
| Esponi logica/sistemi proprietari a piu' tipi di agenti (Claude Code + Claude.ai + custom) | Si' |
| Wrappi logica di business ad alto livello (3-4 chiamate gestionale in 1 tool) | Si' |
| Standardizzi accesso a sistemi interni per il team | Si' |
| Solo Claude Code per dev, gia' copri con una skill markdown | No, basta skill |
| App Python deterministica: import diretto della libreria | No, no LLM nel mezzo |
Skill Claude Code != MCP server
Distinzione fondamentale, spesso confusa:
Skill (es. gestionale-webapi) | MCP server | |
|---|---|---|
| Cos'e' | File markdown con istruzioni | Processo con funzioni eseguibili |
| Come Claude la usa | Legge il markdown, poi esegue lui via Bash/curl | Invoca direttamente i tool, riceve JSON |
| Funziona con | Solo Claude Code (e client compatibili) | Qualsiasi client MCP |
| Type safety | Nessuna | Schema tipizzato |
| Effort | 1 file MD | Processo da scrivere e deployare |
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:
| Livello | Cosa misuri | Strumento |
|---|---|---|
| Infrastructure | MCP server up? Endpoint risponde? | UptimeRobot + Sentry |
| Trace agentico | Cosa ha fatto, perche', in che sequenza | Langfuse + OTel GenAI |
| Cost | Euro per agente, per task, per cliente | Langfuse (nativo) |
| Quality / Eval | Risposta accurata? Rispetta le regole? | Langfuse Datasets + Promptfoo |
| Business outcome | Ha risolto il problema dell'utente? | Dashboard custom su Postgres Langfuse |
OpenTelemetry GenAI fa da tessuto connettivo. Senza, hai osservabilita' a isole (un log qui, un grafico la'). Con, ogni richiesta utente attraversa lo stack lasciando una trace correlata unica:
[Utente chiede in chat]
|
v
[Typed Query Layer — span: parsing slot]
|
v
[Supervisor — span: routing decision + filter-then-validate]
|
+-- [PIM span: filtra]
|
+-- [Knowledge Tool span: valida_compatibilita]
| |
| v
| [Rule Engine span: evaluate]
|
+-- [Mexal MCP span: prezzo_per_cliente]
|
v
[Configuration Context update — span]
|
v
[Synth supervisor — span: risposta italiana]
|
v
[Risposta a utente]
Quando qualcosa va storto, in trenta secondi vedi se e' colpa del parser, del PIM, del Knowledge Tool, del Rule Engine, o di Mexal.
Instrumentare e' una riga per tool:
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:
| Pro | Contro |
|---|---|
| 16+ canali messaging gratis | Release ogni 7-10 giorni, instabilita' |
| Memoria persistente built-in | Lock-in di framework giovane |
| Setup veloce, prototipazione rapida | Track record di stabilita' ancora corto |
| Compatibili con MCP server | Per workflow critici serve comunque Inngest sotto |
I nove punti aperti (cosa non basta)
La roadmap base copre il 70% di cio' che serve. Per un sistema veramente production-grade aziendale servono altri pezzi che vanno previsti, anche se non subito.
1. Identity, secret management, autenticazione
| Cosa | Raccomandato | Quando |
|---|---|---|
| Secret vault | Infisical | Mese 2-3 |
| Identity / SSO | Authelia | Mese 4-6 |
| Auth tra agenti/MCP | API key + JWT firmati | Mese 2 |
| Multi-tenant isolation | Postgres RLS nativo | Mese 4+ |
2. Data layer
| Cosa | Raccomandato | Quando |
|---|---|---|
| Vector DB (RAG) per Knowledge Tools small (sotto 100k chunks) | SQLite + BLOB embedding 768d cosine in-process | Subito (in uso BlumCat) |
| Vector DB (RAG) per scale maggiori (PIM, audit, multi-tenant) | pgvector su Postgres | Mese 2-3 (migration v2.0) |
| Embedding model (in uso BlumCat oggi, locale, no API) | sentence-transformers mpnet 768d multilingua | Subito |
| Embedding model (alternative quando OOM o serve qualita' superiore) | Voyage AI o locale via Ollama | Mese 4-6 (opzionale) |
| Document storage (S3) | MinIO | Mese 2-3 |
| PDF parsing (markdown strutturato + tabelle) | MinerU | Subito |
| PDF parsing (raw text supplementare per formule grafiche) | pdfplumber | Subito (necessario, MinerU da solo perde formule) |
| PDF rendering (pre-render JPG per chip citazione manuale inline) | PyMuPDF (fitz) | Subito |
| FTS5 hybrid retrieval (BM25 nativo) | SQLite FTS5 nativo per Knowledge Tools | Subito |
| Full PIM | Akeneo (solo se justified) | Mese 12+ |
3. Eval e testing avanzato
| Cosa | Raccomandato | Quando |
|---|---|---|
| Eval framework | Promptfoo | Mese 3 |
| LLM-as-judge | Langfuse evals nativo | Mese 3 |
| RAG eval specifico | RAGAS (faithfulness, context precision) | Mese 3 |
| Self-correcting RAG (CRAG) come gate post-Knowledge-Tool | Pattern Higress-RAG / arXiv:2602.23374. Adaptive routing + dual hybrid retrieval. Trigger: post-2°brand stabile, se eval mostra retrieval quality limitante in casi confidence-low | Post 2°brand (Mese 4-5 stimato). Aggiunto in v2.2. |
| Red-teaming | PyRIT + Garak | Mese 4-5 |
| Test Rule Engine | unittest + test_cases embedded in R*.md | Subito |
4. Compliance e governance (AI Act EU + GDPR)
| Cosa | Raccomandato | Quando |
|---|---|---|
| PII detection / redaction | Microsoft Presidio | Mese 2 (subito se PII reali) |
| Audit log immutabile | Postgres append-only + trigger | Mese 2-3 |
| Data deletion (RTBF) | Soft-delete + crypto-shredding | Mese 3-4 |
| Risk assessment AI Act | Documento markdown versionato (template risk-assessment-AI-Act.md con campi obbligatori: tipo applicazione, dati trattati, output, feedback umano, monitoraggio). URGENTE: enforcement Commissione EU dal 2 agosto 2026. | Mese 2-3 (NON 4-6): aggiornato in v2.2 con urgenza esplicita. |
| Data residency | VPS Italia/UE + Anthropic EU endpoint | Subito |
5. Cost optimization
| Cosa | Raccomandato | Quando |
|---|---|---|
| Prompt caching | Anthropic native | Subito |
| Model routing | Logica custom (Haiku per slot filling, Sonnet per Supervisor) | Mese 2-3 |
| AI Gateway proxy | LiteLLM self-host | Mese 3-4 |
| Batch API | Anthropic Batch (50% sconto) | Mese 3+ per task notturni |
| Semantic cache | RedisVL su query ricorrenti | Mese 3-4 |
| Local LLM fallback | Ollama + Llama/Qwen | Mese 6+ (richiede GPU) |
6. Disaster recovery e resilience
| Cosa | Raccomandato | Quando |
|---|---|---|
| Backup database | WAL-G + cron | Subito |
| Backup file storage | Restic o Borg | Mese 1-2 |
| Circuit breaker | pybreaker | Mese 3 |
| LLM provider failover | LiteLLM fallback rules | Mese 4-5 |
| Graceful degradation | Pattern in codice | Mese 2-3 |
7. Network e infrastruttura
| Cosa | Raccomandato | Quando |
|---|---|---|
| Reverse proxy + TLS | Caddy | Subito |
| Container orchestration | Docker Compose | Subito |
| PaaS self-host (alt) | Coolify | Mese 2-3 |
| DDoS / WAF | Cloudflare free tier | Mese 2 |
| SSH brute force | fail2ban | Subito |
8. Human interface
| Cosa | Raccomandato | Quando |
|---|---|---|
| Admin dashboard / KPI | Metabase | Mese 3-4 |
| Internal tool builder | Appsmith | Mese 4-6 |
| Notifications multi-canale | Apprise | Mese 2 |
| Feature flags / kill switch | Unleash | Mese 3-4 |
| Editor wiki/regole UI | Custom Next.js + auth whitelist | Subito (per Knowledge Tools) |
9. Documentation operativa
| Cosa | Raccomandato | Quando |
|---|---|---|
| Wiki / KB interna | Outline | Mese 2-3 |
| ADR | Markdown in /docs/adr/ versionato | Subito |
| Runbook | Markdown + MkDocs Material | Mese 3-4 |
| Wiki narrativi brand (Karpathy) | MD curato editabile via UI | Subito |
| Schema PIM (CategorySchema) | MD curato in wiki_arcocat/categorie/ | Subito |
| Regole (R*.md) | MD curato in wiki_<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.
| Piano | vCPU / RAM / SSD | Quando | Note |
|---|---|---|---|
| KVM 2 | 2 / 8 GB / 100 GB | MVP mese 1-3 | Sufficiente per Langfuse + Inngest + 2-3 MCP server + 1-2 Knowledge Tools |
| KVM 4 | 4 / 16 GB / 200 GB | Mese 4-12 | Margine per crescita: knowledge tools multipli, PIM con dati reali, RAG |
| KVM 8 | 8 / 32 GB / 400 GB | Mese 12+ se scali | Solo se hai molti knowledge tools concorrenti o RAG su grossi dataset |
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:
- Estrarre la logica del classifier intent + tool call deterministici in un Knowledge Tool MCP autonomo (
blum-knowledge-mcp). - Creare
pim-mcpcon primo schema categoria (es. cassetto, cerniera): popolare con i dati Blum gia' presenti. - Costruire Supervisor minimale (Python
if/elifsu intent) sopra Knowledge Tool + PIM. - Aggiungere Typed Query Layer davanti.
- 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_knowledgedei 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-pycome 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:
- Definisci il problema: "estrai dal PDF Bosch tutti i codici lavastoviglie con classe energetica"
- Claude scrive bozza script (parser MinerU + regex + filtro)
- Tu testi, scopri edge case, riporti a Claude
- Claude itera (nuova bozza, nuovi test cases, miglior gestione edge case)
- Dopo 3-5 iterazioni, lo script funziona
Il risultato e' che la struttura del sistema (PIM, wiki narrative, regole) e' per il 95% derivata dai dati stessi tramite questi script, non scritta a mano da un esperto. L'esperto interno non scrive YAML, non edita JSON: corregge body markdown narrativo e aggiunge le nozioni mentali (regole non documentate che esistono solo nella sua testa).
Perche' e' decisivo
Per l'ecosistema PMI italiana, costruire pipeline data engineering custom per ogni cliente richiederebbe normalmente:
- 1-2 data engineer dedicati per 3-6 mesi per il primo brand
- 1 data engineer + 1 esperto dominio per 1-2 mesi per ogni brand successivo
- Costi: 30-80k EUR per primo brand, 10-25k EUR per ogni successivo
Con il pattern Claude-assisted ingest:
- 1 developer Python competente + Claude in sessione iterativa per 2-3 settimane di ingest setup per il primo brand (gli script generati sono riusabili)
- 1 developer + esperto dominio per 1 settimana di curation per ogni brand successivo (riuso script)
- Costi: 8-15k EUR per primo brand setup, 2-4k EUR per ogni successivo
Riduzione effort 5-10x rispetto al pattern tradizionale data engineering. E' la ragione strutturale per cui questo design e' fattibile in PMI.
Cosa fa Python, cosa fa l'esperto, cosa fa il developer
| Componente | Chi lo costruisce | Come |
|---|---|---|
| Parser MinerU/pdfplumber custom per il manuale brand | Developer + Claude (sessione iterativa) | 2-5 giorni di sessione Claude Code |
| Estrattore tabelle codici e attributi tipizzati | Developer + Claude | 1-3 giorni |
| Generatore frontmatter YAML per R*.md/D*.md/F*.md | Developer + Claude | 1-2 giorni |
| Validator AI pre-save (Haiku ~$0.001 per save) | Developer + Claude | 1-2 giorni |
| Body narrativo wiki (D*/G*/R*/F*) per top codici | Esperto interno, via UI editor wiki | 5-10 ore/settimana per 2-3 settimane |
| Regole "non scritte" (nozioni mentali) | Esperto interno, via UI editor o sessione con dev | Stesso slot esperto sopra |
| Gli altri 800 codici (longtail) | Python automatic con quality flag | Run notturno, esperto verifica solo flag rosso |
Notare la divisione: la complessita' tecnica e' nel codice (developer + Claude), la conoscenza domain-specific e' nelle nozioni mentali (esperto). L'esperto NON deve diventare developer. Il developer NON deve diventare esperto del dominio.
Conseguenza per onboarding multi-brand
Quando arriva il 2°brand (es. Bosch dopo Blum), il developer NON ricostruisce i parser da zero. Riadatta quelli scritti per Blum:
- Nuove regex per pattern codici Bosch (es. da
750.5001SaSMV68N20EU) - 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 / pattern | Valutato in | Decisione | Motivazione |
|---|---|---|---|
| Microsoft Agent Framework 1.0 GA (rilasciato 3 aprile 2026) | Audit 2026-05-05 | Scartato | Ottimo prodotto, ma ecosistema enterprise Microsoft / .NET, controcorrente del nostro principio "no enterprise vendor lock-in". Valore reale solo se Azure gia' nello stack (non e' il nostro caso). |
| DSPy 2.x (160k download/mese, 16k stars) | Studio v1.x e riconfermato in audit 2026-05-05 | Scartato | Conflitto strutturale con principio 6 "Hard rules nel codice, non nei prompt": auto-tuning prompt produce output non auditabile, inaccettabile per CPQ B2B con offerte vere. Resta valutabile per ottimizzare narrative (esempi few-shot del system prompt), non per gate critici. |
| LangGraph come orchestrator principale (LangGraph 1.0 GA 22 ottobre 2025, fonte primaria confermata) | Audit 2026-05-05 | Mantenuto solo come "minimale opzionale" | LangGraph 1.0 e' production-ready (Uber, LinkedIn, Klarna), ma full-blown e' over-engineered per single-brand. Il nostro Supervisor in Python deterministico (if/elif sui TypedQuery.intent) basta. LangGraph diventa interessante a N>5 worker concorrenti con gerarchie, scenario non immediato. |
| FastMCP 3.0 (rilasciato 19 gennaio 2026) | Audit 2026-05-05 | Rinviato | Architettura Components/Providers/Transforms e' elegante, hot reload utile, OpenTelemetry built-in tira via un layer custom. Migrazione 1.x → 3.0 NON banale (cambia il modello mentale). Trigger esplicito: rivalutare al 2°-3° brand attivo, dopo che il pattern Knowledge Tool MCP e' rodato. Costo: 1-2 settimane porting per Knowledge Tool. |
| Akeneo PIM Community Edition | Studio v2.0 e riconfermato in audit 2026-05-05 | Scartato | Edition Community in regime di lenta manutenzione (ultimo significativo febbraio 2024, sospetto shift verso enterprise licensing). Conferma indiretta che PIM lite Postgres+JSONB e' strategicamente sicuro: non dipendiamo da progetto in deriva. |
| Anthropic Managed Agents (citato in audit 2026-05-05 come "rilasciato 8 aprile 2026") | Audit 2026-05-05 | NON CONFERMATO con fonte primaria | Verifica diretta su anthropic.com/news non conferma esistenza prodotto. Probabile confabulazione di fonte terza nell'audit. Skippato fino a re-trigger con verifica primaria. Se in futuro Anthropic rilasciasse davvero un servizio simile: il calcolo costo/lock-in sara' (a) restiamo open-source self-hosted vs (b) deleghiamo orchestrazione a Anthropic. Per scala PMI ~50 conv/giorno, lock-in vendor sconsigliato a priori. |
| DOM-native browser agents (alcuni paper HN aprile 2026 sostengono "agenti dovrebbero usare DOM nativo invece di API tool") | Audit 2026-05-05 | Scartato | Per CPQ B2B con sistemi gestionali stabili come Mexal, l'uso di API tool MCP e' piu' robusto del DOM scraping (DOM cambia, API stabili). Pattern non rilevante. |
Disclaimer onesto: la lista NON e' definitiva. Ogni audit periodico (vedi _private/PROMPT_AUDIT_PERIODICO.md) puo' aggiungere righe nuove o promuovere "scartato" → "mantenuto" se il calcolo cambia. La cronologia delle decisioni vive in _private/audit-log/.
Confidence statement
Il design v2.0 e' il miglior approccio noto a maggio 2026 per il caso specifico (PMI italiana B2B distribuzione, 30+ brand, ~50 conv/g, dominio tecnico, codici verificabili, ERP italiana Mexal, integrazione consulenziale piu' che search). Triangolazione su quattro fonti indipendenti (CPQ classico, multi-agent paper Anthropic, PIM Akeneo, Filter-then-Validate documentato). Caveat onesti:
- 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.
- "Migliore" e' scope-specific: per scale piu' grandi (enterprise multi-tenant, milioni di SKU, requisiti SLA), l'analisi costo-benefico cambia.
- SOTA evolve veloce: agentic SOTA si aggiorna ogni settimana (LangGraph, MCP, nuovi pattern Anthropic). Regola: verificare framework SOTA prima di scrivere componenti custom.
- 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
- 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). Vediarcocat/REPORT_EVAL_BASELINE.md. - 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). Vediarcocat/REPORT_STEP4_TQL.md. - v2.3.5
Validazione empirica iterazione 2 minore BlumKnowledge completata. Modifica chirurgica
temperature=0in_call_haiku_judgediblum_knowledge_mcp/contract.py(+1 riga). Test cross-validate Q1 stretto a strict equalityfinal_statusfine (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 migratocompatibile→sconsigliatoper ambiguity LLM su slot "portata"). Cache HIT 100% confermato post-fix (temperature=0ortogonale acache_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 definitivovalida_compatibilita": variance vs gap coverage sono cause separabili, diagnosi tramite invarianza di (b) post-fix di (a). Iter2 ha verificato empiricamente: post-fixtemperature=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 raccomandazionetemperature=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 inarcocat/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). Vediarcocat/REPORT_ITER2_BLUMKNOWLEDGE.md. - 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 pipelinepim.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): contemperature=1default, variance fra chiamate consecutive suconsigliato↔compatibile. Mitigation raccomandata:temperature=0invalida_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. Vediarcocat/REPORT_L3.md. - 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 inwiki_arcocat/categorie/C001_sistemi_box.md(frontmatter YAML eseguibile + body narrativo per esperto). 363 prodotti popolati read-only dablumcat.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 dichiaraobbligatorio: truecome segnaletica architettonica, la realta' empirica e' segmentata pertipo_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_mmcome garanzia operativa). Schema condizionale viaobbligatorio_se_tipo_inrinviato 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 codicestatus="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). Vediarcocat/REPORT_L2.md. - v2.3.2
Validazione empirica iterazione 1 (post-L1) completata. Pattern definitivo per
valida_compatibilitaintegrato 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. - 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). AggiornatoMAPPA_STUDIO.mdprivato con cronologia migration. Studio teorico v2.3 strutturalmente immutato. - 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.
- 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. - 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.
- 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_knowledgedei Knowledge Tools. Livello Configuration Context (stato persistito strutturato) come terzo cambiamento strutturale insieme a PIM e Typed Query. - 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). - 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.sleepvsstep.waitForEventper 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, nientetime.sleep(), niente mega-workflow monolitici, niente bypass DLQ. - 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.
- 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.