Files
Alpha/CHANGELOG.md

684 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ALPHA_PROJECT — Changelog
Tutte le modifiche significative al progetto ALPHA_PROJECT sono documentate qui.
---
## [2026-03-21] AWS Lambda Bridge for Alexa Skill "Pompeo"
Completata la pianificazione e l'implementazione della funzione AWS Lambda che funge da ponte tra la skill Alexa "Pompeo" e il backend n8n. Questo conclude la parte di sviluppo locale della "Phase 4 — Voice Interface (Pompeo)".
### Pianificazione e Progettazione
- **Creato `aws-lambda/README.md`**: Un piano di deploy dettagliato che include:
- Analisi dei requisiti funzionali e non funzionali (costo zero, distribuzione privata).
- Progettazione dell'architettura (Lambda come "thin bridge").
- Specifiche per il ruolo IAM (`AWSLambdaBasicExecutionRole`).
- Design della logica della funzione in Python.
- Una guida passo-passo per la creazione delle risorse su AWS (IAM, Lambda) e sulla Alexa Developer Console.
- **Ricerca e Scelte Tecniche**:
- Confermato che il piano gratuito di AWS Lambda è sufficiente per un costo nullo.
- Stabilito che la distribuzione privata si ottiene mantenendo la skill in stato "In Sviluppo", senza pubblicarla.
- Definito l'uso di un "Invocation Name" di due parole (es. "maggiordomo Pompeo") per rispettare le policy di Alexa.
### Implementazione
- **Struttura del Progetto**: Creata la sottocartella `aws-lambda/src` per il codice sorgente.
- **Codice Sorgente**: Implementata la funzione `lambda_handler` in `aws-lambda/src/index.py`, che inoltra le richieste Alexa al webhook n8n in modo sicuro.
- **Dipendenze**: Definite le dipendenze (`requests`) in `aws-lambda/src/requirements.txt`.
- **Pacchetto di Deploy**: Creato l'ambiente virtuale Python e installate le dipendenze direttamente nella cartella `src` per preparare il pacchetto di deploy.
- **Artefatto Finale**: Generato il file `aws-lambda/pompeo-alexa-bridge.zip`, pronto per essere caricato sulla console AWS Lambda.
---
## [2026-03-21] Jellyfin Playback Agent — Blocco A completato
### Nuovo workflow n8n
- **`🎬 Pompeo — Jellyfin Playback [Webhook]`** (`AyrKWvboPldzZPsM`): riceve webhook da Jellyfin (PlaybackStart / PlaybackStop), filtra per utente `martin` (userId whitelist), e scrive su Postgres:
- **PlaybackStart** → INSERT in `behavioral_context` (`event_type=watching_media`, `do_not_disturb=true`, notes con item/device/session_id) + INSERT in `agent_messages` (soggetto `▶️ <titolo> (<device>)`)
- **PlaybackStop** → UPDATE su riga aperta più recente (`end_at=now()`, `do_not_disturb=false`) + INSERT in `agent_messages` (soggetto `⏹️ ...`)
### Bug risolti (infrastruttura n8n)
- **Webhook path n8n v2**: per registrare un webhook con path statico via API, il campo `webhookId` va impostato come attributo top-level del nodo (non dentro `parameters`). Senza di esso n8n genera il path dinamico `{workflowId}/{nodeName}/{path}` che il webhook pod non carica correttamente in queue mode.
- **SSL Postgres / Patroni**: le credential Postgres create via API usavano SSL con `rejectUnauthorized=true` di default, incompatibile con il certificato self-signed di Patroni. Fix: aggiunto `NODE_TLS_REJECT_UNAUTHORIZED=0` ai deployment `n8n-app` e `n8n-app-worker`.
- **queryParams Postgres node**: `additionalFields.queryParams` con espressioni `$json.*` non funziona correttamente in n8n v2.5.2. Fix: valori inline nella SQL via espressioni n8n `{{ $json.field }}`.
### Configurazione Jellyfin
- Webhook plugin Jellyfin configurato su `http://n8n-app-webhook.automation.svc.cluster.local/webhook/jellyfin-playback` (POST, eventi: PlaybackStart + PlaybackStop)
---
## [2026-03-21] Daily Digest — integrazione memoria Postgres
### Modifiche al workflow `📬 Gmail — Daily Digest [Schedule]` (`1lIKvVJQIcva30YM`)
Aggiunto branch parallelo di salvataggio fatti in memoria dopo la classificazione GPT:
```
Parse risposta GPT-4.1 ──┬──> Telegram - Invia Report (invariato)
├──> Dividi Email (invariato)
└──> 🧠 Estrai Fatti ──> 🔀 Ha Fatti? ──> 💾 Upsert Memoria
```
**`🧠 Estrai Fatti` (Code):**
- Filtra le email con `action != 'trash'` e summary non vuoto
- Chiama GPT-4.1 in batch per estrarre per ogni email: `fact`, `category`, `ttl_days`, `pompeo_note`, `entity_refs`
- Calcola `expires_at` da `ttl_days` (14gg prenotazioni, 45gg bollette, 90gg lavoro/condominio, 30gg default)
- Restituisce un item per ogni fatto da persistere
**`💾 Upsert Memoria` (Postgres node → `mRqzxhSboGscolqI`):**
- `INSERT INTO memory_facts` con `source='email'`, `source_ref=threadId`
- `ON CONFLICT ON CONSTRAINT memory_facts_dedup_idx DO UPDATE` → aggiorna se lo stesso thread viene riprocessato
- Campi salvati: `category`, `subject`, `detail` (JSONB), `action_required`, `action_text`, `pompeo_note`, `entity_refs`, `expires_at`
### Fix contestuale
- Aggiunto `newer_than:1d` alla query Gmail su entrambi i nodi fetch — evitava di rifetchare email vecchie di mesi non marcate `Processed`
---
## [2026-03-21] Schema DB v2 — contacts, memory_facts_archive, entity_refs
### Nuove tabelle
- **`contacts`**: grafo di persone multi-tenant. Ogni riga modella una relazione `user_id → subject` con `relation`, `city`, `country`, `profession`, `aliases[]`, `born_year`, `details` (narrativa libera per LLM) e `metadata` JSONB. Traversabile ricorsivamente per inferire relazioni di secondo grado (es. Martin → zio Mujsi → figlio Euris → cugino di primo grado da parte di madre). Indici GIN su `subject` (trigram) e `aliases` per similarity search.
- **`memory_facts_archive`**: destinazione del cleanup settimanale dei fatti scaduti. Struttura identica a `memory_facts` + `archived_at` + `archive_reason` (`expired` | `superseded` | `merged`). I fatti archiviati vengono poi condensati in un episodio Qdrant settimanale.
### Colonne aggiunte a `memory_facts`
- **`pompeo_note TEXT`**: inner monologue dell'LLM al momento dell'insert — il "perché" del fatto (già in uso nel Calendar Agent, ora standardizzato su tutti i source).
- **`entity_refs JSONB`**: entità estratte dal fatto strutturato — `{people: [], places: [], products: [], amounts: []}`. Permette query SQL su persone/luoghi senza full-text scan (es. `entity_refs->'people' ? 'euris vruzhaj'`).
### Applicato a
- `alpha/db/postgres.sql` aggiornato (schema v2)
- Live su Patroni primary (`postgres-1`, namespace `persistence`, DB `pompeo`)
---
## [2026-03-21] Actual Budget — Import Estratto Conto via Telegram
### Nuovi workflow
- **`💰 Actual — Import Estratto Conto [Telegram]`** (`qtvB3r0cgejyCxUp`): importa l'estratto conto Banca Sella (CSV) in Actual Budget tramite Telegram.
- Trigger: documento Telegram con caption `Estratto conto`
- Parse CSV Banca Sella (separatore `;`, date `gg/mm/aaaa`, importi con `.` decimale)
- Skip automatico di `SALDO FINALE` e `SALDO INIZIALE`
- Classificazione GPT-4.1 in batch da 30 transazioni: assegna payee e categoria, crea automaticamente i mancanti su Actual
- Import via `/transactions/import` con dedup nativo tramite `imported_id` (pattern `banca-sella-{Id}` o hash fallback)
- Report Telegram con nuove transazioni importate, già presenti e totale CSV
- **`⏰ Actual — Reminder Estratto Conto [Schedule]`** (`w0oJ1i6sESvaB5W1`): reminder giornaliero (09:00) su Telegram se il task Google "Actual - Estratto conto" nella lista "Finanze" è scaduto.
### Note tecniche
- Binary data letta con `getBinaryDataBuffer()` (compatibile con filesystem binary mode di n8n)
- Loop GPT gestito con iterazione interna nel Code node (no `splitInBatches` — instabile con input multipli)
- Payee/categorie mancanti creati al volo e riutilizzati nei batch successivi della stessa run
- Dedup Actual: `added` = nuove, `updated` = già presenti
---
## [2026-03-21] Calendar Agent — fix sincronizzazione e schedule
### Problemi risolti
- **`ON CONFLICT DO NOTHING``DO UPDATE`**: gli eventi modificati (orario, titolo) venivano ignorati. Ora vengono aggiornati in Postgres.
- **Cleanup eventi cancellati**: aggiunto step `🗑️ Cleanup Cancellati` che esegue `DELETE FROM memory_facts WHERE source_ref NOT IN (UID attuali da HA)` per la finestra 7 giorni. Se Martin cancella un meeting, sparisce da Postgres al prossimo run.
- **Schedule `*/30 * * * *`**: da cron 06:30 giornaliero a ogni 30 minuti — il calendario Postgres è sempre allineato alla source of truth (HA/Google Calendar).
### Flusso aggiornato
```
... → 📋 Parse GPT → 🗑️ Cleanup Cancellati → 🔀 Riemetti → 💾 Upsert → 📦 → 📱
```
---
## [2026-03-20] Calendar Agent — primo workflow Pompeo in produzione
### Cosa è stato fatto
Primo agente Pompeo deployato e attivo su n8n: `📅 Pompeo — Calendar Agent [Schedule]` (ID `4ZIEGck9n4l5qaDt`).
### Design
- **Sorgente dati**: Home Assistant REST API usata come proxy Google Calendar — evita OAuth Google diretto in n8n e funziona per tutti i 25 calendari registrati in HA.
- **Calendari tracciati** (12): Lavoro, Famiglia, Spazzatura, Pulizie, Formula 1, WEC, Inter, Compleanni, Varie, Festività Italia, Films (Radarr), Serie TV (Sonarr).
- **LLM enrichment**: GPT-4.1 (via Copilot) classifica ogni evento: category, action_required, do_not_disturb, priority, behavioral_context, pompeo_note.
- **Dedup**: `memory_facts.source_ref` = HA event UID; `ON CONFLICT DO NOTHING` su indice unico parziale.
- **Telegram briefing**: ogni mattina alle 06:30, riepilogo eventi prossimi 7 giorni raggruppati per calendario.
### Migrazioni DB applicate
- `ALTER TABLE memory_facts ADD COLUMN source_ref TEXT` — colonna per ID esterno di dedup
- `CREATE UNIQUE INDEX memory_facts_dedup_idx ON memory_facts (user_id, source, source_ref) WHERE source_ref IS NOT NULL`
- `CREATE INDEX idx_memory_facts_source_ref ON memory_facts (source_ref) WHERE source_ref IS NOT NULL`
### Credential n8n create
| ID | Nome | Tipo |
|---|---|---|
| `u0JCseXGnDG5hS9F` | Home Assistant API | HTTP Header Auth |
| `mRqzxhSboGscolqI` | Pompeo — PostgreSQL | Postgres (pompeo/martin) |
### Flusso workflow
```
⏰ Schedule (06:30) → 📅 Range → 🔑 Token Copilot
→ 📋 Calendari (12 items) → 📡 HA Fetch (×12) → 🏷️ Estrai + Tag
→ 📝 Prompt (dedup) → 🤖 GPT-4.1 → 📋 Parse
→ 💾 Postgres Upsert (memory_facts) → 📦 Aggrega → 📱 Telegram
```
---
## [2026-03-21] ADR — Message Broker: nessun broker dedicato
### Decisione
**Non verrà deployato un message broker dedicato** (né NATS JetStream né Redis Streams). Il blackboard pattern viene implementato interamente su PostgreSQL + webhook n8n.
### Ragionamento
Al momento della progettazione iniziale, il broker era necessario per disaccoppiare gli agenti dall'Arbiter. Con l'introduzione della tabella `agent_messages` nel database `pompeo`, questo obiettivo è già raggiunto:
```
Agente n8n → INSERT agent_messages (arbiter_decision = NULL)
Arbiter → SELECT WHERE arbiter_decision IS NULL (polling a cron)
→ UPDATE arbiter_decision = 'notify' | 'defer' | 'discard'
```
Il flusso high-priority (bypass immediato dell'Arbiter) viene gestito con una chiamata diretta al **webhook n8n dell'Arbiter** da parte dell'agente — zero infrastruttura aggiuntiva.
### Alternative valutate
| Opzione | Esito | Motivazione |
|---|---|---|
| `agent_messages` su PostgreSQL | ✅ **Adottata** | Già deployata, persistente, queryabile, audit log gratuito |
| Redis Streams | ⏸ Rimandato | Già in cluster, valutabile se volume cresce |
| NATS JetStream | ❌ Scartato | Nuovo componente da operare, overkill per il volume attuale (pochi msg/ora) e per il caso d'uso single-household |
### Impatto su README.md
La sezione "Message Broker (Blackboard Pattern)" rimane valida concettualmente. Il campo `agent` e il message schema definiti nel README vengono rispettati nella tabella `agent_messages` — cambia solo il mezzo di trasporto (Postgres invece di NATS/Redis).
---
## [2026-03-21] PostgreSQL — Database "pompeo" e schema ALPHA_PROJECT
### Overview
Creato il database `pompeo` sul cluster Patroni (namespace `persistence`) e applicato lo schema iniziale per la memoria strutturata di Pompeo. Seconda milestone della Phase 0 — Infrastructure Bootstrap.
---
### Modifica manifest Patroni
Aggiunto `pompeo: martin` nella sezione `databases` di `infra/cluster/persistence/patroni/postgres.yaml`. Il database è stato creato automaticamente dallo Zalando Operator senza downtime sugli altri database.
Script DDL idempotente disponibile in: `alpha/db/postgres.sql`
---
### Design decision — Multi-tenancy anche in PostgreSQL
Coerentemente con la scelta adottata per Qdrant, tutte le tabelle includono il campo `user_id TEXT NOT NULL DEFAULT 'martin'`. I valori `'martin'` e `'shared'` sono seedati in `user_profile` come utenti iniziali del sistema.
Aggiungere un nuovo utente in futuro non richiede modifiche allo schema — è sufficiente inserire una riga in `user_profile` e usare il nuovo `user_id` negli INSERT.
---
### Design decision — agent_messages come blackboard persistente
La tabella `agent_messages` implementa il **blackboard pattern** del message broker: ogni agente n8n inserisce le proprie osservazioni con `arbiter_decision = NULL` (pending). Il Proactive Arbiter legge i messaggi in coda, decide (`notify` / `defer` / `discard`) e aggiorna `arbiter_decision`, `arbiter_reason` e `processed_at`.
Rispetto a usare solo NATS/Redis come broker, questo approccio garantisce un **audit log permanente** di tutte le osservazioni e decisioni, interrogabile via SQL per debug, tuning e analisi storiche.
---
### Schema creato
**5 tabelle** nel database `pompeo`:
| Tabella | Ruolo |
|---|---|
| `user_profile` | Preferenze statiche per utente (lingua, timezone, stile notifiche, quiet hours). Seed: `martin`, `shared` |
| `memory_facts` | Fatti episodici prodotti da tutti gli agenti, con TTL (`expires_at`) e riferimento al punto Qdrant (`qdrant_id`) |
| `finance_documents` | Documenti finanziari strutturati: bollette, fatture, cedolini. Include `raw_text` per embedding |
| `behavioral_context` | Contesto IoT/comportamentale per l'Arbiter: DND, home presence, tipo evento |
| `agent_messages` | Blackboard del message broker — osservazioni agenti + decisioni Arbiter |
**15 index** totali:
| Index | Tabella | Tipo |
|---|---|---|
| `idx_memory_facts_user_source_cat` | `memory_facts` | `(user_id, source, category)` |
| `idx_memory_facts_expires` | `memory_facts` | `(expires_at)` WHERE NOT NULL |
| `idx_memory_facts_action` | `memory_facts` | `(user_id, action_required)` WHERE true |
| `idx_finance_docs_user_date` | `finance_documents` | `(user_id, doc_date DESC)` |
| `idx_finance_docs_correspondent` | `finance_documents` | `(user_id, correspondent)` |
| `idx_behavioral_ctx_user_time` | `behavioral_context` | `(user_id, start_at, end_at)` |
| `idx_behavioral_ctx_dnd` | `behavioral_context` | `(user_id, do_not_disturb)` WHERE true |
| `idx_agent_msgs_pending` | `agent_messages` | `(user_id, priority, created_at)` WHERE pending |
| `idx_agent_msgs_agent_type` | `agent_messages` | `(agent, event_type, created_at)` |
| `idx_agent_msgs_expires` | `agent_messages` | `(expires_at)` WHERE pending AND NOT NULL |
---
### Phase 0 — Stato aggiornato
- [x] ~~Deploy **Qdrant** sul cluster~~ ✅ 2026-03-21
- [x] ~~Collections Qdrant con multi-tenancy `user_id`~~ ✅ 2026-03-21
- [x] ~~Payload indexes Qdrant~~ ✅ 2026-03-21
- [x] ~~Database `pompeo` + schema PostgreSQL~~ ✅ 2026-03-21
- [ ] Verify embedding endpoint via Copilot (`text-embedding-3-small`)
- [ ] Migrazione a Ollama `nomic-embed-text` (quando LLM server è online)
---
## [2026-03-21] Qdrant — Deploy e setup collections (Phase 0)
### Overview
Completato il deploy di **Qdrant v1.17.0** sul cluster Kubernetes (namespace `persistence`) e la creazione delle collections per la memoria semantica di Pompeo. Questa è la prima milestone della Phase 0 — Infrastructure Bootstrap.
---
### Deploy infrastruttura
Qdrant deployato via Helm chart ufficiale (`qdrant/qdrant`) nel namespace `persistence`, coerente con il pattern infrastrutturale esistente (Longhorn storage, Sealed Secrets, ServiceMonitor Prometheus).
**Risorse create:**
| Risorsa | Dettaglio |
|---|---|
| StatefulSet `qdrant` | 1/1 pod Running, image `qdrant/qdrant:v1.17.0` |
| PVC `qdrant-storage-qdrant-0` | 20Gi Longhorn RWO |
| Service `qdrant` | ClusterIP — porte 6333 (REST), 6334 (gRPC), 6335 (p2p) |
| SealedSecret `qdrant-api-secret` | API key cifrata, namespace `persistence` |
| ServiceMonitor `qdrant` | Prometheus scraping su `:6333/metrics`, label `release: monitoring` |
**Endpoint interno:** `qdrant.persistence.svc.cluster.local:6333`
Manifest in: `infra/cluster/persistence/qdrant/`
---
### Design decision — Multi-tenancy collections (Opzione B)
**Problema affrontato**: nominare le collections `martin_episodes`, `martin_knowledge`, `martin_preferences` avrebbe vincolato Pompeo ad essere esclusivamente un assistente personale singolo, rendendo impossibile — senza migration — estendere il sistema ad altri membri della famiglia in futuro.
**Scelta adottata**: architettura multi-tenant con 3 collection condivise e isolamento via campo `user_id` nel payload di ogni punto vettoriale.
```
episodes ← user_id: "martin" | "shared" | <futuri utenti>
knowledge ← user_id: "martin" | "shared" | <futuri utenti>
preferences ← user_id: "martin" | "shared" | <futuri utenti>
```
Il valore `"shared"` è riservato a dati della casa/famiglia visibili a tutti gli utenti (es. calendario condiviso, documenti di casa, finanze comuni). Le query n8n usano un filtro `should: [user_id=martin, user_id=shared]` per recuperare sia il contesto personale che quello condiviso.
**Vantaggi**: aggiungere un nuovo utente domani non richiede alcuna modifica infrastrutturale — solo includere il nuovo `user_id` negli upsert e nelle query.
---
### Collections create
Tutte e 3 le collections sono operative (status `green`):
| Collection | Contenuto |
|---|---|
| `episodes` | Fatti episodici con timestamp (email, IoT, calendario, conversazioni) |
| `knowledge` | Documenti, note Outline, newsletter, knowledge base |
| `preferences` | Preferenze, abitudini e pattern comportamentali per utente |
**Payload schema comune** (5 index su ogni collection):
| Campo | Tipo | Scopo |
|---|---|---|
| `user_id` | keyword | Filtro multi-tenant (`"martin"`, `"shared"`) |
| `source` | keyword | Origine del dato (`"email"`, `"calendar"`, `"iot"`, `"paperless"`, …) |
| `category` | keyword | Dominio semantico (`"finance"`, `"work"`, `"personal"`, …) |
| `date` | datetime | Timestamp del fatto — filtrabile per range |
| `action_required` | bool | Flag per il Proactive Arbiter |
**Dimensione vettori**: 1536 (compatibile con `text-embedding-3-small` via GitHub Copilot — bootstrap phase). Da rivedere alla migrazione verso `nomic-embed-text` su Ollama.
---
### Phase 0 — Stato al momento del deploy Qdrant
- [x] ~~Deploy **Qdrant** sul cluster~~
- [x] ~~Creazione collections con multi-tenancy `user_id`~~
- [x] ~~Payload indexes: `user_id`, `source`, `category`, `date`, `action_required`~~
- [x] ~~Run **PostgreSQL migrations** su Patroni~~ ✅ completato nella sessione stessa
## [2026-03-21] Jellyfin Playback Agent — Blocco A completato
### Nuovo workflow n8n
- **`🎬 Pompeo — Jellyfin Playback [Webhook]`** (`AyrKWvboPldzZPsM`): riceve webhook da Jellyfin (PlaybackStart / PlaybackStop), filtra per utente `martin` (userId whitelist), e scrive su Postgres:
- **PlaybackStart** → INSERT in `behavioral_context` (`event_type=watching_media`, `do_not_disturb=true`, notes con item/device/session_id) + INSERT in `agent_messages` (soggetto `▶️ <titolo> (<device>)`)
- **PlaybackStop** → UPDATE su riga aperta più recente (`end_at=now()`, `do_not_disturb=false`) + INSERT in `agent_messages` (soggetto `⏹️ ...`)
### Bug risolti (infrastruttura n8n)
- **Webhook path n8n v2**: per registrare un webhook con path statico via API, il campo `webhookId` va impostato come attributo top-level del nodo (non dentro `parameters`). Senza di esso n8n genera il path dinamico `{workflowId}/{nodeName}/{path}` che il webhook pod non carica correttamente in queue mode.
- **SSL Postgres / Patroni**: le credential Postgres create via API usavano SSL con `rejectUnauthorized=true` di default, incompatibile con il certificato self-signed di Patroni. Fix: aggiunto `NODE_TLS_REJECT_UNAUTHORIZED=0` ai deployment `n8n-app` e `n8n-app-worker`.
- **queryParams Postgres node**: `additionalFields.queryParams` con espressioni `$json.*` non funziona correttamente in n8n v2.5.2. Fix: valori inline nella SQL via espressioni n8n `{{ $json.field }}`.
### Configurazione Jellyfin
- Webhook plugin Jellyfin configurato su `http://n8n-app-webhook.automation.svc.cluster.local/webhook/jellyfin-playback` (POST, eventi: PlaybackStart + PlaybackStop)
---
## [2026-03-21] Daily Digest — integrazione memoria Postgres
### Modifiche al workflow `📬 Gmail — Daily Digest [Schedule]` (`1lIKvVJQIcva30YM`)
Aggiunto branch parallelo di salvataggio fatti in memoria dopo la classificazione GPT:
```
Parse risposta GPT-4.1 ──┬──> Telegram - Invia Report (invariato)
├──> Dividi Email (invariato)
└──> 🧠 Estrai Fatti ──> 🔀 Ha Fatti? ──> 💾 Upsert Memoria
```
**`🧠 Estrai Fatti` (Code):**
- Filtra le email con `action != 'trash'` e summary non vuoto
- Chiama GPT-4.1 in batch per estrarre per ogni email: `fact`, `category`, `ttl_days`, `pompeo_note`, `entity_refs`
- Calcola `expires_at` da `ttl_days` (14gg prenotazioni, 45gg bollette, 90gg lavoro/condominio, 30gg default)
- Restituisce un item per ogni fatto da persistere
**`💾 Upsert Memoria` (Postgres node → `mRqzxhSboGscolqI`):**
- `INSERT INTO memory_facts` con `source='email'`, `source_ref=threadId`
- `ON CONFLICT ON CONSTRAINT memory_facts_dedup_idx DO UPDATE` → aggiorna se lo stesso thread viene riprocessato
- Campi salvati: `category`, `subject`, `detail` (JSONB), `action_required`, `action_text`, `pompeo_note`, `entity_refs`, `expires_at`
### Fix contestuale
- Aggiunto `newer_than:1d` alla query Gmail su entrambi i nodi fetch — evitava di rifetchare email vecchie di mesi non marcate `Processed`
---
## [2026-03-21] Schema DB v2 — contacts, memory_facts_archive, entity_refs
### Nuove tabelle
- **`contacts`**: grafo di persone multi-tenant. Ogni riga modella una relazione `user_id → subject` con `relation`, `city`, `country`, `profession`, `aliases[]`, `born_year`, `details` (narrativa libera per LLM) e `metadata` JSONB. Traversabile ricorsivamente per inferire relazioni di secondo grado (es. Martin → zio Mujsi → figlio Euris → cugino di primo grado da parte di madre). Indici GIN su `subject` (trigram) e `aliases` per similarity search.
- **`memory_facts_archive`**: destinazione del cleanup settimanale dei fatti scaduti. Struttura identica a `memory_facts` + `archived_at` + `archive_reason` (`expired` | `superseded` | `merged`). I fatti archiviati vengono poi condensati in un episodio Qdrant settimanale.
### Colonne aggiunte a `memory_facts`
- **`pompeo_note TEXT`**: inner monologue dell'LLM al momento dell'insert — il "perché" del fatto (già in uso nel Calendar Agent, ora standardizzato su tutti i source).
- **`entity_refs JSONB`**: entità estratte dal fatto strutturato — `{people: [], places: [], products: [], amounts: []}`. Permette query SQL su persone/luoghi senza full-text scan (es. `entity_refs->'people' ? 'euris vruzhaj'`).
### Applicato a
- `alpha/db/postgres.sql` aggiornato (schema v2)
- Live su Patroni primary (`postgres-1`, namespace `persistence`, DB `pompeo`)
---
## [2026-03-21] Actual Budget — Import Estratto Conto via Telegram
### Nuovi workflow
- **`💰 Actual — Import Estratto Conto [Telegram]`** (`qtvB3r0cgejyCxUp`): importa l'estratto conto Banca Sella (CSV) in Actual Budget tramite Telegram.
- Trigger: documento Telegram con caption `Estratto conto`
- Parse CSV Banca Sella (separatore `;`, date `gg/mm/aaaa`, importi con `.` decimale)
- Skip automatico di `SALDO FINALE` e `SALDO INIZIALE`
- Classificazione GPT-4.1 in batch da 30 transazioni: assegna payee e categoria, crea automaticamente i mancanti su Actual
- Import via `/transactions/import` con dedup nativo tramite `imported_id` (pattern `banca-sella-{Id}` o hash fallback)
- Report Telegram con nuove transazioni importate, già presenti e totale CSV
- **`⏰ Actual — Reminder Estratto Conto [Schedule]`** (`w0oJ1i6sESvaB5W1`): reminder giornaliero (09:00) su Telegram se il task Google "Actual - Estratto conto" nella lista "Finanze" è scaduto.
### Note tecniche
- Binary data letta con `getBinaryDataBuffer()` (compatibile con filesystem binary mode di n8n)
- Loop GPT gestito con iterazione interna nel Code node (no `splitInBatches` — instabile con input multipli)
- Payee/categorie mancanti creati al volo e riutilizzati nei batch successivi della stessa run
- Dedup Actual: `added` = nuove, `updated` = già presenti
---
## [2026-03-21] Calendar Agent — fix sincronizzazione e schedule
### Problemi risolti
- **`ON CONFLICT DO NOTHING``DO UPDATE`**: gli eventi modificati (orario, titolo) venivano ignorati. Ora vengono aggiornati in Postgres.
- **Cleanup eventi cancellati**: aggiunto step `🗑️ Cleanup Cancellati` che esegue `DELETE FROM memory_facts WHERE source_ref NOT IN (UID attuali da HA)` per la finestra 7 giorni. Se Martin cancella un meeting, sparisce da Postgres al prossimo run.
- **Schedule `*/30 * * * *`**: da cron 06:30 giornaliero a ogni 30 minuti — il calendario Postgres è sempre allineato alla source of truth (HA/Google Calendar).
### Flusso aggiornato
```
... → 📋 Parse GPT → 🗑️ Cleanup Cancellati → 🔀 Riemetti → 💾 Upsert → 📦 → 📱
```
---
## [2026-03-20] Calendar Agent — primo workflow Pompeo in produzione
### Cosa è stato fatto
Primo agente Pompeo deployato e attivo su n8n: `📅 Pompeo — Calendar Agent [Schedule]` (ID `4ZIEGck9n4l5qaDt`).
### Design
- **Sorgente dati**: Home Assistant REST API usata come proxy Google Calendar — evita OAuth Google diretto in n8n e funziona per tutti i 25 calendari registrati in HA.
- **Calendari tracciati** (12): Lavoro, Famiglia, Spazzatura, Pulizie, Formula 1, WEC, Inter, Compleanni, Varie, Festività Italia, Films (Radarr), Serie TV (Sonarr).
- **LLM enrichment**: GPT-4.1 (via Copilot) classifica ogni evento: category, action_required, do_not_disturb, priority, behavioral_context, pompeo_note.
- **Dedup**: `memory_facts.source_ref` = HA event UID; `ON CONFLICT DO NOTHING` su indice unico parziale.
- **Telegram briefing**: ogni mattina alle 06:30, riepilogo eventi prossimi 7 giorni raggruppati per calendario.
### Migrazioni DB applicate
- `ALTER TABLE memory_facts ADD COLUMN source_ref TEXT` — colonna per ID esterno di dedup
- `CREATE UNIQUE INDEX memory_facts_dedup_idx ON memory_facts (user_id, source, source_ref) WHERE source_ref IS NOT NULL`
- `CREATE INDEX idx_memory_facts_source_ref ON memory_facts (source_ref) WHERE source_ref IS NOT NULL`
### Credential n8n create
| ID | Nome | Tipo |
|---|---|---|
| `u0JCseXGnDG5hS9F` | Home Assistant API | HTTP Header Auth |
| `mRqzxhSboGscolqI` | Pompeo — PostgreSQL | Postgres (pompeo/martin) |
### Flusso workflow
```
⏰ Schedule (06:30) → 📅 Range → 🔑 Token Copilot
→ 📋 Calendari (12 items) → 📡 HA Fetch (×12) → 🏷️ Estrai + Tag
→ 📝 Prompt (dedup) → 🤖 GPT-4.1 → 📋 Parse
→ 💾 Postgres Upsert (memory_facts) → 📦 Aggrega → 📱 Telegram
```
---
## [2026-03-21] ADR — Message Broker: nessun broker dedicato
### Decisione
**Non verrà deployato un message broker dedicato** (né NATS JetStream né Redis Streams). Il blackboard pattern viene implementato interamente su PostgreSQL + webhook n8n.
### Ragionamento
Al momento della progettazione iniziale, il broker era necessario per disaccoppiare gli agenti dall'Arbiter. Con l'introduzione della tabella `agent_messages` nel database `pompeo`, questo obiettivo è già raggiunto:
```
Agente n8n → INSERT agent_messages (arbiter_decision = NULL)
Arbiter → SELECT WHERE arbiter_decision IS NULL (polling a cron)
→ UPDATE arbiter_decision = 'notify' | 'defer' | 'discard'
```
Il flusso high-priority (bypass immediato dell'Arbiter) viene gestito con una chiamata diretta al **webhook n8n dell'Arbiter** da parte dell'agente — zero infrastruttura aggiuntiva.
### Alternative valutate
| Opzione | Esito | Motivazione |
|---|---|---|
| `agent_messages` su PostgreSQL | ✅ **Adottata** | Già deployata, persistente, queryabile, audit log gratuito |
| Redis Streams | ⏸ Rimandato | Già in cluster, valutabile se volume cresce |
| NATS JetStream | ❌ Scartato | Nuovo componente da operare, overkill per il volume attuale (pochi msg/ora) e per il caso d'uso single-household |
### Impatto su README.md
La sezione "Message Broker (Blackboard Pattern)" rimane valida concettualmente. Il campo `agent` e il message schema definiti nel README vengono rispettati nella tabella `agent_messages` — cambia solo il mezzo di trasporto (Postgres invece di NATS/Redis).
---
## [2026-03-21] PostgreSQL — Database "pompeo" e schema ALPHA_PROJECT
### Overview
Creato il database `pompeo` sul cluster Patroni (namespace `persistence`) e applicato lo schema iniziale per la memoria strutturata di Pompeo. Seconda milestone della Phase 0 — Infrastructure Bootstrap.
---
### Modifica manifest Patroni
Aggiunto `pompeo: martin` nella sezione `databases` di `infra/cluster/persistence/patroni/postgres.yaml`. Il database è stato creato automaticamente dallo Zalando Operator senza downtime sugli altri database.
Script DDL idempotente disponibile in: `alpha/db/postgres.sql`
---
### Design decision — Multi-tenancy anche in PostgreSQL
Coerentemente con la scelta adottata per Qdrant, tutte le tabelle includono il campo `user_id TEXT NOT NULL DEFAULT 'martin'`. I valori `'martin'` e `'shared'` sono seedati in `user_profile` come utenti iniziali del sistema.
Aggiungere un nuovo utente in futuro non richiede modifiche allo schema — è sufficiente inserire una riga in `user_profile` e usare il nuovo `user_id` negli INSERT.
---
### Design decision — agent_messages come blackboard persistente
La tabella `agent_messages` implementa il **blackboard pattern** del message broker: ogni agente n8n inserisce le proprie osservazioni con `arbiter_decision = NULL` (pending). Il Proactive Arbiter legge i messaggi in coda, decide (`notify` / `defer` / `discard`) e aggiorna `arbiter_decision`, `arbiter_reason` e `processed_at`.
Rispetto a usare solo NATS/Redis come broker, questo approccio garantisce un **audit log permanente** di tutte le osservazioni e decisioni, interrogabile via SQL per debug, tuning e analisi storiche.
---
### Schema creato
**5 tabelle** nel database `pompeo`:
| Tabella | Ruolo |
|---|---|
| `user_profile` | Preferenze statiche per utente (lingua, timezone, stile notifiche, quiet hours). Seed: `martin`, `shared` |
| `memory_facts` | Fatti episodici prodotti da tutti gli agenti, con TTL (`expires_at`) e riferimento al punto Qdrant (`qdrant_id`) |
| `finance_documents` | Documenti finanziari strutturati: bollette, fatture, cedolini. Include `raw_text` per embedding |
| `behavioral_context` | Contesto IoT/comportamentale per l'Arbiter: DND, home presence, tipo evento |
| `agent_messages` | Blackboard del message broker — osservazioni agenti + decisioni Arbiter |
**15 index** totali:
| Index | Tabella | Tipo |
|---|---|---|
| `idx_memory_facts_user_source_cat` | `memory_facts` | `(user_id, source, category)` |
| `idx_memory_facts_expires` | `memory_facts` | `(expires_at)` WHERE NOT NULL |
| `idx_memory_facts_action` | `memory_facts` | `(user_id, action_required)` WHERE true |
| `idx_finance_docs_user_date` | `finance_documents` | `(user_id, doc_date DESC)` |
| `idx_finance_docs_correspondent` | `finance_documents` | `(user_id, correspondent)` |
| `idx_behavioral_ctx_user_time` | `behavioral_context` | `(user_id, start_at, end_at)` |
| `idx_behavioral_ctx_dnd` | `behavioral_context` | `(user_id, do_not_disturb)` WHERE true |
| `idx_agent_msgs_pending` | `agent_messages` | `(user_id, priority, created_at)` WHERE pending |
| `idx_agent_msgs_agent_type` | `agent_messages` | `(agent, event_type, created_at)` |
| `idx_agent_msgs_expires` | `agent_messages` | `(expires_at)` WHERE pending AND NOT NULL |
---
### Phase 0 — Stato aggiornato
- [x] ~~Deploy **Qdrant** sul cluster~~ ✅ 2026-03-21
- [x] ~~Collections Qdrant con multi-tenancy `user_id`~~ ✅ 2026-03-21
- [x] ~~Payload indexes Qdrant~~ ✅ 2026-03-21
- [x] ~~Database `pompeo` + schema PostgreSQL~~ ✅ 2026-03-21
- [ ] Verify embedding endpoint via Copilot (`text-embedding-3-small`)
- [ ] Migrazione a Ollama `nomic-embed-text` (quando LLM server è online)
---
## [2026-03-21] Qdrant — Deploy e setup collections (Phase 0)
### Overview
Completato il deploy di **Qdrant v1.17.0** sul cluster Kubernetes (namespace `persistence`) e la creazione delle collections per la memoria semantica di Pompeo. Questa è la prima milestone della Phase 0 — Infrastructure Bootstrap.
---
### Deploy infrastruttura
Qdrant deployato via Helm chart ufficiale (`qdrant/qdrant`) nel namespace `persistence`, coerente con il pattern infrastrutturale esistente (Longhorn storage, Sealed Secrets, ServiceMonitor Prometheus).
**Risorse create:**
| Risorsa | Dettaglio |
|---|---|
| StatefulSet `qdrant` | 1/1 pod Running, image `qdrant/qdrant:v1.17.0` |
| PVC `qdrant-storage-qdrant-0` | 20Gi Longhorn RWO |
| Service `qdrant` | ClusterIP — porte 6333 (REST), 6334 (gRPC), 6335 (p2p) |
| SealedSecret `qdrant-api-secret` | API key cifrata, namespace `persistence` |
| ServiceMonitor `qdrant` | Prometheus scraping su `:6333/metrics`, label `release: monitoring` |
**Endpoint interno:** `qdrant.persistence.svc.cluster.local:6333`
Manifest in: `infra/cluster/persistence/qdrant/`
---
### Design decision — Multi-tenancy collections (Opzione B)
**Problema affrontato**: nominare le collections `martin_episodes`, `martin_knowledge`, `martin_preferences` avrebbe vincolato Pompeo ad essere esclusivamente un assistente personale singolo, rendendo impossibile — senza migration — estendere il sistema ad altri membri della famiglia in futuro.
**Scelta adottata**: architettura multi-tenant con 3 collection condivise e isolamento via campo `user_id` nel payload di ogni punto vettoriale.
```
episodes ← user_id: "martin" | "shared" | <futuri utenti>
knowledge ← user_id: "martin" | "shared" | <futuri utenti>
preferences ← user_id: "martin" | "shared" | <futuri utenti>
```
Il valore `"shared"` è riservato a dati della casa/famiglia visibili a tutti gli utenti (es. calendario condiviso, documenti di casa, finanze comuni). Le query n8n usano un filtro `should: [user_id=martin, user_id=shared]` per recuperare sia il contesto personale che quello condiviso.
**Vantaggi**: aggiungere un nuovo utente domani non richiede alcuna modifica infrastrutturale — solo includere il nuovo `user_id` negli upsert e nelle query.
---
### Collections create
Tutte e 3 le collections sono operative (status `green`):
| Collection | Contenuto |
|---|---|
| `episodes` | Fatti episodici con timestamp (email, IoT, calendario, conversazioni) |
| `knowledge` | Documenti, note Outline, newsletter, knowledge base |
| `preferences` | Preferenze, abitudini e pattern comportamentali per utente |
**Payload schema comune** (5 index su ogni collection):
| Campo | Tipo | Scopo |
|---|---|---|
| `user_id` | keyword | Filtro multi-tenant (`"martin"`, `"shared"`) |
| `source` | keyword | Origine del dato (`"email"`, `"calendar"`, `"iot"`, `"paperless"`, …) |
| `category` | keyword | Dominio semantico (`"finance"`, `"work"`, `"personal"`, …) |
| `date` | datetime | Timestamp del fatto — filtrabile per range |
| `action_required` | bool | Flag per il Proactive Arbiter |
**Dimensione vettori**: 1536 (compatibile con `text-embedding-3-small` via GitHub Copilot — bootstrap phase). Da rivedere alla migrazione verso `nomic-embed-text` su Ollama.
---
### Phase 0 — Stato al momento del deploy Qdrant
- [x] ~~Deploy **Qdrant** sul cluster~~
- [x] ~~Creazione collections con multi-tenancy `user_id`~~
- [x] ~~Payload indexes: `user_id`, `source`, `category`, `date`, `action_required`~~
- [x] ~~Run **PostgreSQL migrations** su Patroni~~ ✅ completato nella sessione stessa