La mia chat AI locale con Python e Gemma — Parte 13: promemoria persistenti

  • Home
  • AI & ChatBot
  • La mia chat AI locale con Python e Gemma — Parte 13: promemoria persistenti
Image

La mia chat AI locale con Python e Gemma — Parte 13: promemoria persistenti

Quando abbiamo iniziato a lavorare su un assistente desktop locale con notifiche di Windows, ci siamo posti una domanda che suona semplice ma scava profondo: può un modello di linguaggio non solo generare testo, ma creare dati strutturati che il codice poi esegue? La risposta è sì, ma il percorso da “ricordami fra due minuti di chiamare il dentista” a un promemoria salvato su disco rivela come la vera intelligenza non sia tutta nel modello, a volte è nel vincolo che gli metti intorno.

L’idea: un agente che comanda

Fino a quel momento, tutto il flusso era unidirezionale: utente → modello → testo → mostra a schermo. Ma che succede se inverti il flusso? Utente → modello → comando strutturato → il computer lo esegue. Nasce l’agente “Promemoria”: quando lo scegli, non stai chiedendo al modello di raccontarti una storia, gli stai chiedendo di produrre un JSON che il server interpreterà e salverà su disco. È la differenza tra “dimmi una barzelletta” e “dimmi i dati di un cliente nel formato che riesco a importare”.

Il problema? Un modello lasciato libero genera testo fluente. Racchiudilo in un vincolo di sintassi e inizia a fare strani errori. Ecco dove entra la grammatica.

La grammatica GBNF

GBNF sta per Generative Backus-Naur Form, suona accademico, ma è semplicemente una lista di regole su quale testo è “legale” e quale no. È come le regole della sintassi, ma scritte in un modo che un motore può capire durante la generazione, non dopo.

Immagina di dire a un LLM “dimmi sì o no”, probabilmente genererà “Sì” e basta. Ma che succede se aggiunge un preambolo inutile come “Beh, considerando tutto…” e poi “no”? Hai fallito perché il modello sa come scrivere italiano fluente, non come stare dentro vincoli stretti.

Con la GBNF, il motore llama.cpp fa una cosa diversa, a ogni singolo token che il modello sta per generare, calcola in anticipo quali token manterrebbero l’output ancora valido, e azzera la probabilità di tutti gli altri. Il modello non può scegliere una graffa fuori posto, letteralmente non gli viene data la possibilità. La nostra grammatica per un promemoria era così:

Plaintext
root   ::= "{" titolo data_o_intervallo "}"
titolo ::= "\"titolo\"" ":" "\"" [caratteri] "\""
data_o_intervallo ::= assoluta | relativa
assoluta ::= "\"data\"" ":" "\"AAAA-MM-GG\"" "," "\"ora\"" ":" "\"HH:MM\""
relativa ::= "\"fra\"" ":" [numero] "," "\"unita\"" ":" "\"minuti\"|\"ore\"|\"giorni\"|\"settimane\""

Tradotto in italiano: il modello può scrivere un oggetto JSON con un titolo, e poi può scegliere tra due forme, una con data/ora precisa, una con “fra N minuti”. Non può generare una graffa in più, non può inventare un campo descrizione, non può mettersi a scrivere un preambolo. La grammatica lo vincola, token per token, a stare dentro queste righe.

Il vincolo token per token

Qui sta la magia e il motivo per cui la grammatica GBNF è diversa da un semplice controllo fatto dopo.

Immagina due scenari:

Scenario A (controllo DOPO): Il modello genera {"titolo": "Dentista", "data": "2026-02-31", "ora": "10:00"}. Il JSON è sintatticamente perfetto. Arriva a Python, json.loads() riesce, ma poi datetime.strptime() dice “il 31 febbraio non esiste”. Il promemoria viene rifiutato. L’utente vede un errore. Brutto.

Scenario B (vincolo DURANTE, con la grammatica): Il modello sta per generare “02-31” e non ha il permesso di farlo? No, il motore ha già calcolato che i soli “giorni” legali per febbraio sono 01-28 (o 29 in bisestili). Il modello sceglie 28 perché è l’unica opzione che gli rimane con probabilità non nulla.

Con la grammatica, il modello non può sbagliare sulla forma. Sulla semantica, invece il significato rimane possibile: può scegliere il 31 febbraio se glielo permetti, e lì scatta il secondo strato di difesa: Python.

Ma c’è un terzo strato e è il più interessante.

Non chiedergli l’aritmetica…

Alle 23:17, l’utente dice “ricordami fra 2 minuti”. Con la forma assoluta della grammatica, il modello doveva calcolare: “adesso sono le 23:17, fra 2 minuti sono le 23:19”. Suona semplice.

Ha fatto "ora": "00:19" — minuti giusti, ora sbagliata, data non avanzata.

Il modello ha sbagliato l’aritmetica dell’orologio. Non è un errore di forma (la grammatica l’avrebbe impedito). È un errore di senso e ha capito che servivano 19 minuti, ma non ha fatto il riporto di mezzanotte.

Qui arriva l’insegnamento vero: non puoi chiedere al modello di fare matematica nel contesto di generazione di testo strutturato. Non è quello che sa fare bene.

Soluzione? Una seconda forma della grammatica, la forma relativa:

Plaintext
relativa ::= "\"fra\"" ":" [numero] "," "\"unita\"" ":" "\"minuti\"|\"ore\"|\"giorni\"|\"settimane\""

Ora il modello non calcola niente. Legge "fra 2 minuti" e scrive:
{"titolo": "Accendere il condizionatore", "fra": 2, "unita": "minuti"}

Copia il numero, sceglie una parola. Le moltiplicazioni (2 minuti × 1 = 2 minuti, 3 ore × 60 = 180 minuti) e il riporto di mezzanotte le fa Python, che non sbaglia mai:
quando = (datetime.now() + timedelta(minutes=fra * UNITA_IN_MINUTI[unita])).replace(second=0, microsecond=0)

Il principio: il modello serve dove c’è ambiguità linguistica da sciogliere (“domani” → quale giorno?). Dove il calcolo è puro, il codice è più sicuro.

Il parsing di Python

Ora il modello ha finito di generare. Lo stream si chiude. Arriva un JSON:

Plaintext
{"titolo": "Dentista", "fra": 2, "unita": "minuti"}

Qui Python entra in azione. Non è un parser generico, è specifico per il significato che vogliamo dare a quel JSON.

  1. json.loads(testo) — parsa il JSON grezzo. Grazie alla grammatica, questo quasi sempre riesce.
  2. valida_fra(titolo, fra, unita) — questo è il secondo strato di controllo, quello che la grammatica non può coprire:
    • Il titolo è una stringa non vuota?
    • fra è un numero positivo? (Non può essere -3, non può essere True che Python lo interpreta come 1)
    • unita è una delle quattro scelte permesse?
    • L’intervallo non è già passato?
    • L’intervallo non è smisuratamente lontano (> 5 anni)?
  3. Creazione e salvataggio — se passa, il record viene scritto su data/promemoria.json:
Plaintext
{
  "id": "20260810-231945",
  "titolo": "Dentista",
  "quando": "2026-08-10T23:19",
  "stato": "attesa",
  "evento_id": ""
}

L’id lo assegna il server (non il modello — è il principio che ogni chiave viene dal server o da una fonte affidabile). Lo stato parte da “attesa”. L’evento_id è vuoto perché è stato scritto a mano (se venisse da Google Calendar avrebbe l’id dell’evento).

Il thread daemon che completa il quadro

Intanto, in background, un thread scritto in Python gira ogni 20 secondi:

Plaintext
def controlla(adesso):
    for gruppo in raggruppa_per_istante(scaduti(elenca(), adesso)):
        titolo, testo = testo_notifica(gruppo, adesso)
        notifica(titolo, testo)  # Windows Shell Notification
        for voce in gruppo:
            segna_mostrato(voce["id"], adesso)

Niente di magico, legge il file, confronta i timestamp, manda una notifica di Windows (via ctypes, senza dipendenze nuove). Se due promemoria scadono nello stesso minuto, una notifica sola li elenca entrambi. Se il file è corrotto, il thread sopravvive e riprova al giro dopo. Se il processo è spento, i promemoria “arretrati” compaiono come tali al riavvio.

Questo breve viaggio dovrebbe farci riflettere sul fatto che non si tratta di stabilire se il modello sappia risolvere tutto oppure non sappia nulla, ma di affidargli i compiti per cui è realmente efficace e costruire intorno a esso i vincoli necessari. La conclusione apre poi uno sguardo sul futuro, suggerendo che questa architettura con vincoli durante la generazione e validazione a posteriori, diventerà probabilmente sempre più comune.

La vera intelligenza non è solo nel modello. È anche nel vincolo.

Se questo articolo ti è piaciuto, restiamo in contatto su linkedin a: https://www.linkedin.com/in/andreatonin/

Releated Posts

Private Vision: i LoRA e la matematica della personalizzazione — Parte 4

La cartella loras/ di Private Vision era piena di roba fatta da altri. Funzionava, per carità: Private Vision…

DiByAndrea Tonin Set 9, 2026

Dalla Skill all’Agente AI: automatizzare le note spese con ChatGPT e Google Workspace

Prima abbiamo distinto Progetti, GPT e Skill. Un Progetto è uno spazio di lavoro, un GPT è un…

DiByAndrea Tonin Ago 23, 2026

Anatomia di una Skill di ChatGPT: quando il prompt diventa una procedura

Negli ultimi mesi abbiamo imparato a scrivere prompt sempre più strutturati. Non più il classico “fammi un testo…

DiByAndrea Tonin Ago 23, 2026

Progetti, GPT e Skill: la differenza spiegata bene

Negli ultimi anni ChatGPT è passato dall’essere “una chat intelligente” a diventare un piccolo ecosistema di strumenti: Progetti,…

DiByAndrea Tonin Ago 23, 2026