Archimede
Assistente WhatsApp per un'associazione di ripetizioni
Bot WhatsApp per Eureka, associazione di promozione sociale che organizza ripetizioni. Riconosce dal numero il ruolo di chi scrive — studente, tutor, famiglia, segreteria — e risponde con Gemini attingendo a una knowledge base curata dalla segreteria, oppure gira la conversazione a un operatore come ticket. Una console web permette alla segreteria di leggere le chat, assegnare i ruoli e aggiornare ciò che il bot sa. Due servizi indipendenti (FastAPI e Next.js) su un PostgreSQL condiviso, esposti solo tramite Cloudflare Tunnel.
- Anno
- 2026 — oggi
- Ruolo
- Sviluppo full-stack
- Stato
- In produzione
- Committente
- Eureka
Panoramica
Un contatto scrive su WhatsApp; il sistema riconosce dal numero se è uno studente iscritto, un tutor, una famiglia o la segreteria, e di conseguenza risponde in autonomia con un modello linguistico oppure gira la conversazione a un operatore. La segreteria lavora da una console web dedicata: legge le conversazioni, assegna i ruoli, blocca i molesti e aggiorna la knowledge base che il bot usa per rispondere. Gran parte dell'utenza è minorenne, quindi privacy, retention e controllo degli accessi sono requisiti di progetto, non rifiniture.
Problema
Una piccola organizzazione riceve su WhatsApp molte domande ripetitive — orari, come iscriversi, come prenotare una lezione — mescolate a richieste che hanno bisogno di una persona. Serviva:
- filtrare in automatico le domande a risposta nota, senza che il bot "prometta" cose sbagliate;
- non perdere mai un messaggio che richiede un umano, con una coda di ticket per la segreteria;
- dare risposte diverse a ruoli diversi: uno studente e un amministratore non vedono le stesse informazioni;
- permettere alla segreteria di correggere ciò che il bot sa senza toccare il codice;
- restare gestibile da poche persone, con costi d'infrastruttura minimi.
Architettura
- Gateway (Python / FastAPI): riceve i webhook firmati di Meta, deduplica, scarica gli allegati, applica rate limit, riconosce il ruolo del mittente e sceglie tra risposta automatica (Gemini 2.5 Flash con function calling) e handoff alla segreteria.
- Console (Next.js / TypeScript / Auth.js / Drizzle): pannello della segreteria. Login con magic link via email, sessioni JWT, ruolo riletto dal DB a ogni azione, CSP con nonce per richiesta.
- I due servizi non si parlano mai direttamente: condividono un PostgreSQL con due database, ognuno scrive solo il proprio e legge quello dell'altro. Il vincolo "un writer per tabella" è applicativo.
- Ingresso pubblico unico via Cloudflare Tunnel (TLS lato Cloudflare), con lo stack Docker dell'infrastruttura separato da quello delle app: un rebuild non riavvia il database.
WhatsApp Cloud API
│ webhook (firma HMAC)
▼
Cloudflare Tunnel ─► nginx ─► Gateway (FastAPI) ──► Gemini 2.5 Flash
│
PostgreSQL 16 ◄──► Console (Next.js) ── segreteria
Implementazione
La pipeline a decisioni ordinate è la logica più delicata del sistema ed è congelata dai test: firma + dedup → dati della console disponibili? → rate limit → download media → mittente bloccato? → intercetti tutor → orario di segreteria → risposta con Gemini. Il webhook risponde 200 subito e l'elaborazione prosegue in background.
I contenuti del bot vivono in due cicli distinti: i prompt (personalità e regole) versionati nel repo; la knowledge (i fatti sull'associazione) su Postgres, modificabile dalla console, segmentata per ruolo dalla cartella del documento. I .md nel repo sono solo il seme iniziale.
I task periodici — pulizia della retention, escalation dei ticket, sync del libro soci — girano senza scheduler esterno, fuori dall'event loop, e /health va in 503 se restano indietro.
Scelta di progetto
Senza i dati dei ruoli dalla console il bot tace, invece di trattare tutti come sconosciuti. Meglio non rispondere che rispondere con informazioni incomplete.
Difficoltà
- L'ordine delle decisioni non è ovvio: spostare un controllo nella pipeline cambia il comportamento in modi sottili; l'unica difesa è la batteria di test che lo fissa.
- "Sembra funzionante mentre non lo è": un audit pre-produzione ha portato a scelte controintuitive, come peggiorare la disponibilità apparente pur di non dare risposte sbagliate.
- Due sorgenti di verità per i contenuti: prima la knowledge era su file montati in sola lettura e le modifiche della console non arrivavano mai al bot; risolto spostandola su Postgres.
- Utenza minorenne: retention a 30 giorni applicata dal codice, con cancellazione a cascata anche sull'indice di ricerca full-text.
Risultati
- In produzione, con un ambiente di test separato su un secondo clone della VPS e uno script di deploy che rifiuta branch disallineati o working tree sporco.
- Base di codice contenuta e testata: pipeline, ruoli, orari di segreteria e isolamento della knowledge hanno test dedicati; CI su GitHub Actions per type check, lint, build e test.
- Infrastruttura a basso costo: una sola VPS, PostgreSQL condiviso, nessun servizio esposto direttamente, ingresso via Cloudflare Tunnel.
Cosa ho imparato
- Su un canale dove il bot "promette" orari e iscrizioni, le risposte fisse vanno centralizzate e tenute fuori dal modello: sono l'unico punto di verità verso l'utente.
- La resilienza vera è decidere cosa fare quando una dipendenza è giù: tacere in modo sicuro è meglio che rispondere con dati incompleti.
- Separare due servizi tramite database, con un writer per tabella, tiene i deploy indipendenti senza introdurre una coda di messaggi.
- Scrivere gli ADR ripaga: le scelte controintuitive restano comprensibili a mesi di distanza.