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
PythonFastAPIGeminiPostgreSQLTypeScriptNext.jsDrizzle ORMAuth.jsDockerCloudflare TunnelWhatsApp Cloud API

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.