# 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" | knowledge ← user_id: "martin" | "shared" | preferences ← user_id: "martin" | "shared" | ``` 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