Files
Alpha/CHANGELOG.md
Martin 5cf8b71915 docs(changelog): add B1/B2 debug fixes + Copilot token refresh
- B1 Media Library Sync: 7 iterazioni di fix (exec 345→360)
  Token Copilot: genericCredential→predefined + POST→GET
  GPT-4.1: method+headers+body mancanti
  Apostrofi SQL: Code node Prepara Detail
  ON CONFLICT partial index syntax (WHERE source_ref IS NOT NULL)
  SplitInBatches v3: output[1] per loop body
  Qdrant payload: Code node Prepara Qdrant
- B2 Watch History Sync: fix preventivi da pattern B1, exec 362 
- GitHub Copilot device flow token refresh (user_code 559F-1D14)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-21 20:33:33 +00:00

558 lines
29 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] Pompeo — Alexa Voice Interface + fix Upsert Memoria
### 🎙️ Pompeo — Alexa Voice Interface [Webhook] (`JJ6B3w8i1bL7Q0rr`) ✅ NEW
Webhook n8n che funge da bridge tra la skill Alexa "Pompeo" e GPT-4.1 con contesto memoria.
**Architettura:**
```
AWS Lambda (pompeo-alexa-bridge)
└→ POST /webhook/pompeo-alexa (secret: X-N8N-Webhook-Secret)
└→ Leggi Memoria (Postgres: ultimi 15 fatti memory_facts)
└→ Ottieni Token Copilot
└→ Pompeo Core (Code)
├→ LaunchRequest → TTS di benvenuto (no GPT)
├→ StopIntent / CancelIntent → TTS commiato (no GPT)
├→ HelpIntent → TTS aiuto (no GPT)
└→ IntentRequest → GPT-4.1 con contesto memoria → tts_response
```
**Nodo centrale `Pompeo Core`:**
- Valida il secret `cXvt0LZK9koIqgzcJZgV2qb4ymlEwpa7`
- Routing intelligente: intents semplici (Launch/Stop/Help) risposta istantanea, evitando GPT
- Per `IntentRequest`: costruisce prompt con memoria recente + oggi, chiama GPT-4.1
- Ritorna `{ tts_response: "..." }` — testo letto ad alta voce da Echo
**Lambda AWS:**
- Nome: `pompeo-alexa-bridge` | Runtime: Python 3.11
- `N8N_WEBHOOK_URL`: `https://orchestrator.mt-home.uk/webhook/pompeo-alexa`
- `N8N_SECRET_TOKEN`: `cXvt0LZK9koIqgzcJZgV2qb4ymlEwpa7`
- Trigger: Alexa Skills Kit (Skill ID da collegare nella Alexa Developer Console)
**Test verificati:**
```
LaunchRequest → {"tts_response":"Ciao Martin! Sono Pompeo, il tuo assistente. Dimmi pure!"}
IntentRequest → GPT risponde con contesto memoria ✅
```
**Note tecniche (bug n8n):**
- `responseMode: lastNode` su workflow con nodi emoji nel nome → connessioni corrotte (surrogate pairs)
- Fix: nomi nodi ASCII, `json.dump(..., ensure_ascii=False)` per evitare corruzione chiavi
- `alwaysOutputData: true` sul Postgres node per gestire il caso 0 righe memoria
### 🐛 Fix: `ON CONFLICT ON CONSTRAINT` → `ON CONFLICT (cols) WHERE ...`
- **Problema**: il constraint `memory_facts_dedup_idx` esiste come UNIQUE INDEX parziale, non come named CONSTRAINT → `ON CONFLICT ON CONSTRAINT` fallisce
- **Fix**: cambiato in `ON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL DO UPDATE SET ...`
- **Fix applicato a**: Gmail Digest (`1lIKvVJQIcva30YM`) + Paperless Upload (`GBPFFq8rmbdFrNn9`)
- **Fix applicato a**: rimossa la cast `::jsonb` sul campo `detail` (colonna TEXT)
---
## [2026-03-21] Media Agent completo (Blocco A + B1 + B2) + Calendar Agent fix
### 🎬 Blocco A — Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) ✅
Webhook in real-time su PlaybackStart / PlaybackStop da Jellyfin. Workflow attivo e verificato end-to-end con Ghost in the Shell.
**Flusso:**
```
Jellyfin Plugin → POST /webhook/jellyfin-playback
└→ 🔀 Normalizza Evento (Code — JSON.parse body + field mapping)
└→ 🔀 Start o Stop?
├→ [start] 💾 PG - Apri Sessione + 💬 PG - Msg Start
└→ [stop] 💾 PG - Chiudi Sessione + 💬 PG - Msg Stop
```
**Postgres:**
- `behavioral_context`: INSERT on start (`do_not_disturb=true`, notes: `{item, device, item_type}`), UPDATE on stop (`end_at=now()`, `do_not_disturb=false`)
- `agent_messages`: soggetto `▶️ Ghost in the Shell (Chrome - PC)` / `⏹️ Ghost in the Shell (Chrome - PC)`
**Comportamento verificato (test reale):**
- PlaybackStart → INSERT corretto, `do_not_disturb=true`
- PlaybackStop → fired solo alla **chiusura del player** (non alla pausa) — comportamento nativo Jellyfin
- Filtro utente: solo `UserId = 42369255a7c64917a28fc26d4c7f8265` (Martin)
**Bug fix applicati durante il debugging:**
| # | Problema | Causa | Fix |
|---|---|---|---|
| 1 | Webhook 404 in queue mode | `webhookId` non era top-level nel nodo JSON → n8n genera path dinamico `{workflowId}/{nodeName}/{path}` che il webhook pod non carica | `webhookId` impostato come campo top-level del nodo |
| 2 | SSL Patroni — self-signed cert reject | n8n Postgres node usa `rejectUnauthorized=true` di default; Patroni forza `hostssl` | `NODE_TLS_REJECT_UNAUTHORIZED=0` su `n8n-app` e `n8n-app-worker` deployments |
| 3 | `Variable $1 out of range` Postgres | `additionalFields.queryParams` + `$json.*` non funziona in Postgres node v2 di n8n 2.5.2 | Migrato a SQL inline con espressioni `{{ $json.field }}` |
| 4 | Code node filtra tutto — `[]` | Jellyfin invia body come **stringa JSON** (`$json.body = '{"ServerId":...}'`), non oggetto | Aggiunto `JSON.parse($json.body)` nel Code node |
| 5 | Campi undefined | Nomi campo reali Jellyfin: `Name`, `DeviceName`/`Client`, `UserId` (senza dashes) | Aggiornati i riferimenti nel Code node |
| 6 | PlaybackStop non arrivava | Il plugin Jellyfin triggerizza Stop solo alla chiusura del player, non alla pausa | Documentato come comportamento atteso |
**Recupero n8n API token:** il token era corrotto nel contesto LLM (summarizzazione). Recuperato direttamente dal DB n8n: `SELECT "apiKey", label FROM user_api_keys;` sul database `n8n`.
---
### 🎬 Blocco B1 — Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) ✅
Weekly cron (domenica 03:00). Costruisce la **memoria delle preferenze cinematografiche** di Martin.
**Flusso:**
```
⏰ Cron domenica 03:00
└→ 🎬 HTTP Radarr (/radarr/api/v3/movie) ──┐
└→ 📺 HTTP Sonarr (/sonarr/api/v3/series) ──┤
🔀 Merge Libreria (Code)
└→ 🔑 Token Copilot
└→ 🤖 GPT-4.1 Analisi
└→ 💾 PG Upsert Preferenze
└→ 🔁 Loop Items
└→ 🔢 Ollama Embed
└→ 🗄️ Qdrant Upsert
```
**Dati estratti dal GPT:** `top_genres`, `preferred_types`, `library_stats`, `taste_summary`, `notable_patterns`
**Postgres:** `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) — upsert ON CONFLICT
**Qdrant `media_preferences`** (collection creata, 768-dim Cosine):
- Embedding: Ollama `nomic-embed-text` su `"{title} {year} {genres} {type}"`
- Payload: `{title, year, type, genres, status, source, source_id, expires_at (+6 mesi)}`
- Utilità per Pompeo: query semantica tipo *"film sci-fi che piacciono a Martin"*
**Endpoint interni:**
- Radarr: `http://radarr.media.svc.cluster.local:7878/radarr/api/v3/movie?apikey=922d1405ab1147019d98a2997d941765` (23 film)
- Sonarr: `http://sonarr.media.svc.cluster.local:8989/sonarr/api/v3/series?apikey=22140655993a4ff6bf12314813ec6982`
- Ollama: `http://ollama.ai.svc.cluster.local:11434/api/embeddings` — model `nomic-embed-text` (768-dim, multilingual) ✅ operativo
- Qdrant: `http://qdrant.persistence.svc.cluster.local:6333` — api-key: sealed secret `qdrant-api-secret` (`__Montecarlo00!`)
> **Nota infrastruttura**: Radarr e Sonarr girano entrambi nel pod `mediastack` (namespace `media`), ma espongono servizi `ClusterIP` separati. Dall'esterno del cluster le NodePort (30878, 30989) erano irraggiungibili; dall'interno funzionano correttamente. Radarr risponde su `/radarr/` come base path (redirect 307 senza base path).
---
### 🎞️ Blocco B2 — Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) ✅
Daily cron (04:00). Costruisce la **memoria della cronologia di visione** di Martin.
**Flusso:**
```
⏰ Cron ogni giorno 04:00
└→ 🎞️ HTTP Jellyfin (/Users/{id}/Items?Recursive=true&SortBy=DatePlayed&Limit=100)
└→ 🔀 Filtra Visti (PlayCount>0, last 90 days)
└→ ❓ Ha Visti? (IF node)
├→ [no] ⛔ Stop
└→ [sì] 🔑 Token Copilot → 🤖 GPT-4.1 → 🔍 Parse → 💾 PG Upsert
```
**Dati estratti dal GPT:** `recent_favorites`, `preferred_genres`, `watch_patterns`, `completion_rate`, `notes`
**Postgres:** `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d)
**Jellyfin API token Pompeo:**
- Creato via `POST /Auth/Keys?app=Pompeo` autenticandosi come `admin` (password `__Montecarlo00!`, auth locale — separata dall'Authentik SSO)
- Token: `d153606c1ca54574a20d2b40fcf1b02e`
- Martin UserId: `42369255a7c64917a28fc26d4c7f8265` (da DB SQLite Jellyfin + confermato dai payload webhook)
> **Nota**: Jellyfin usa Authentik SSO (OIDC) per il login via browser, ma `admin` ha ancora l'auth provider locale attivo. Il token API è separato dall'autenticazione SSO e non scade.
---
### Fix workflow esistenti
#### 📅 Calendar Agent (`4ZIEGck9n4l5qaDt`) — 2 bug fix
Il workflow falliva ogni 30 minuti con `column "undefined" does not exist`.
**Bug 1 — `🗑️ Cleanup Cancellati`**: quando HA non ha eventi nel range (risposta vuota), Parse GPT restituisce `[{json:{skip:true}}]`. L'espressione nel Cleanup:
```js
.all().map(i => "'" + i.json.uid.replace(/'/g,"''") + "'").join(',')
```
chiamava `.replace()` su `undefined` (uid non esiste sull'item skip) → l'intera espressione `{{ }}` valutava a `undefined` JavaScript → n8n lo inseriva **senza virgolette** nella SQL → PostgreSQL interpretava `undefined` come nome di colonna.
Fix: aggiunto `.filter(i => i.json.uid)` prima del `.map()` + `String()` wrapper.
**Bug 2 — `💾 Postgres - Salva Evento`**: ON CONFLICT UPDATE includeva `updated_at = NOW()` ma la colonna `updated_at` non esiste in `memory_facts`. Rimosso dalla clausola DO UPDATE.
---
### 🔑 GitHub Copilot OAuth Token — Refresh via Device Flow
Il token `ghu_...` usato da n8n per autenticarsi verso l'API Copilot era scaduto.
**Procedura device flow (da `infra/cluster/automation/n8n/README.md`):**
1. `POST https://github.com/login/device/code` → user_code `559F-1D14`
2. Autorizzazione su `https://github.com/login/device` (mobile GitHub)
3. Poll `https://github.com/login/oauth/access_token` → nuovo token `ghu_OWzvq1caigaABGrT...`
**Fix applicato:** credenziale n8n `vBwUxlzKrX3oDHyN` aggiornata con il nuovo token.
---
### 🐛 B1 — Media Library Sync: debug fixes (exec 345→360)
Il workflow B1, dopo la creazione del sub-agent, ha richiesto 7 iterazioni di fix prima di girare a regime.
| # | Exec | Nodo | Problema | Fix |
|---|---|---|---|---|
| 1 | 345 | 🔑 Token Copilot | `genericCredentialType` senza credential → 401 | → `predefinedCredentialType` + credential `vBwUxlzKrX3oDHyN` |
| 2 | 350 | 🔑 Token Copilot | Method `POST` invece di `GET` → 404 | → `GET https://api.github.com/copilot_internal/v2/token` |
| 3 | 351 | 🤖 GPT-4.1 | Nodo creato dal sub-agent senza `method`, `headers`, `body` | → `POST` + `Copilot-Integration-Id` + `Editor-Version` headers + body JSON con prompt |
| 4 | 352 | 💾 PG Upsert | Apostrofo nel testo GPT italiano (`d'animazione`) → syntax error SQL | → aggiunto nodo `🔧 Prepara Detail` con `.split("'").join("''")`|
| 5 | 353 | 💾 PG Upsert | `ON CONFLICT (cols)` fallisce su index parziale senza WHERE | → `ON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL` |
| 6 | 358 | 🔁 SplitInBatches | typeVersion 3: output[0]=done (0 items), output[1]=loop body | → ricollegato a output[1] |
| 7 | 359 | 🗄️ Qdrant Upsert | Body expression `{{ JSON.stringify({...}) }}` con multipli `$('Node')` → parse error | → aggiunto nodo `🔧 Prepara Qdrant` Code che costruisce il payload, HTTP manda `{{ $json.body }}` |
**Exec 360: ✅ 24 item elaborati, 24 punti Qdrant, 1 row `memory_facts` (source=`media_library`)**
---
### 🐛 B2 — Jellyfin Watch History Sync: fix applicati preventivamente
Grazie ai pattern imparati da B1, i fix su B2 sono stati applicati **prima** dell'esecuzione reale:
| Nodo | Fix applicato |
|---|---|
| 🎞️ HTTP Jellyfin | Header `Authorization: MediaBrowser Token="d153606c1ca54574a20d2b40fcf1b02e"` |
| 🔑 Token Copilot | `GET` + `predefinedCredentialType` + credential `vBwUxlzKrX3oDHyN` |
| 🤖 GPT-4.1 | `POST` + headers Copilot + body JSON con prompt |
| 💾 PG Upsert | `ON CONFLICT (user_id, source, source_ref) WHERE source_ref IS NOT NULL` |
| 🔧 Prepara Detail B2 | Code node per escape apostrofi SQL |
**Exec 361: ❌** (esecuzione precedente ai fix) → **Exec 362: ✅ success**`memory_facts` row `source=jellyfin` inserita.
> **Nota**: Jellyfin ha risposto con storia di visione vuota (Ghost in the Shell guardato al 6%, `Played=false`, `PlayCount=0`) → il branch "Ha Visti?" → Stop è il comportamento corretto. Il workflow si popola quando Martin completerà una visione.
---
## [2026-03-21] Paperless Upload — integrazione memoria Postgres + Qdrant
### Modifiche al workflow `📄 Paperless — Upload Documento [Multi]` (`GBPFFq8rmbdFrNn9`)
Aggiunto branch parallelo di salvataggio in memoria dopo `Paperless - Patch Metadati`:
```
Paperless - Patch Metadati ──┬──> Telegram - Conferma Upload (invariato)
└──> 🧠 Salva in Memoria ──> 💾 Upsert Memoria
```
**`🧠 Salva in Memoria` (Code):**
- Genera embedding del testo (`{title}\n\n{OCR excerpt}`) via Ollama `nomic-embed-text` (768 dim)
- Upsert in Qdrant collection `knowledge` con payload: `user_id`, `source`, `doc_id`, `title`, `category`, `doc_type`, `correspondent`, `created_date`, `tags`
- Prepara record per Postgres con `source_ref=paperless-{doc_id}`, TTL variabile per tipo doc (90gg ricevute, 180gg bollette, 365gg default, 730gg cedolini)
**`💾 Upsert Memoria` (Postgres → `mRqzxhSboGscolqI`):**
- `INSERT INTO memory_facts` con `source='paperless'`, dedup `ON CONFLICT memory_facts_dedup_idx DO UPDATE`
- Salva anche `qdrant_id` (UUID del punto Qdrant) per cross-reference futuro
### Qdrant collections riconfigurate
Ricreate `knowledge` e `episodes` con `size=768` (nomic-embed-text) — erano a 1536 (OpenAI legacy, 0 points).
---
## [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