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

29 KiB
Raw Blame History

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 CONSTRAINTON 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:

.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: successmemory_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 NOTHINGDO 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

  • Deploy Qdrant sul cluster 2026-03-21
  • Collections Qdrant con multi-tenancy user_id 2026-03-21
  • Payload indexes Qdrant 2026-03-21
  • 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

  • Deploy Qdrant sul cluster
  • Creazione collections con multi-tenancy user_id
  • Payload indexes: user_id, source, category, date, action_required
  • Run PostgreSQL migrations su Patroni completato nella sessione stessa