From a145c0126c79bf2e3a0de8acd20d2ac43b071cd9 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 21 Mar 2026 18:37:52 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(paperless):=20add=20memory=20persisten?= =?UTF-8?q?ce=20=E2=80=94=20Ollama=20embed=20+=20Qdrant=20+=20Postgres?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parallel branch from Paperless Patch Metadati - nomic-embed-text (768 dim) via Ollama for OCR text embedding - Qdrant knowledge collection upsert with full doc metadata payload - memory_facts upsert with source=paperless, dedup by paperless-{doc_id} - Qdrant collections recreated at 768 dim (were 1536 legacy, 0 points) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5be7416..5eaf8a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ Tutte le modifiche significative al progetto ALPHA_PROJECT sono documentate qui. --- +## [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] Jellyfin Playback Agent β€” Blocco A completato ### Nuovo workflow n8n From afc182292a208b15ed3cc1cc523df9ceee3e6a1a Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 21 Mar 2026 18:51:11 +0000 Subject: [PATCH 2/3] feat: Media Agent Blocco A+B + Calendar Agent fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blocco A β€” Jellyfin Playback Agent (AyrKWvboPldzZPsM): - Webhook PlaybackStart/Stop β†’ behavioral_context + agent_messages - Fix: JSON.parse Jellyfin body, SSL Patroni, Postgres queryParams inline Blocco B1 β€” Media Library Sync (o3uM1xDLTAKw4D6E): - Weekly cron: Radarr+Sonarr β†’ GPT-4.1 β†’ memory_facts + Qdrant - Qdrant collection media_preferences creata (768-dim nomic-embed-text) Blocco B2 β€” Jellyfin Watch History Sync (K07e4PPANXDkmQsr): - Daily cron: Jellyfin history (90d) β†’ GPT-4.1 β†’ memory_facts - Jellyfin API token Pompeo creato via admin auth Calendar Agent fix (4ZIEGck9n4l5qaDt): - Cleanup: filter(i=>i.json.uid) per evitare undefined non quotato in SQL - Salva Evento: rimosso updated_at=NOW() (colonna non esistente) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 58 +++++++++++++++++++++++++++++++++++++ README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eaf8a9..c5f9dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,64 @@ Tutte le modifiche significative al progetto ALPHA_PROJECT sono documentate qui. --- +## [2026-03-21] Media Agent β€” Blocco A+B completo + Calendar Agent fix + +### Nuovi workflow n8n + +#### 🎬 Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) β€” Blocco A βœ… + +Webhook in real-time che intercetta PlaybackStart / PlaybackStop da Jellyfin. + +**Bug fix applicati (debugging session):** +1. **Webhook path** β€” `webhookId` deve essere top-level nel nodo JSON (non in `parameters`), altrimenti n8n v2.5.2 in queue mode registra path come `{workflowId}/{nodeName}/{path}` non raggiungibile +2. **SSL Patroni** β€” `NODE_TLS_REJECT_UNAUTHORIZED=0` aggiunto a `n8n-app` e `n8n-app-worker` deployments (Patroni forza `hostssl`) +3. **Postgres queryParams** β€” Postgres node v2 non valuta correttamente `$json.*` in `queryParams` β†’ migrato a SQL inline con `{{ $json.field }}` +4. **Jellyfin body format** β€” il plugin Webhook di Jellyfin invia il body come stringa JSON embedded (`$json.body = '{"ServerId":...}'`) β†’ aggiunto `JSON.parse()` nel Code node +5. **Jellyfin field names** β€” campi reali: `Name` (non `ItemName`), `DeviceName`/`Client`, `UserId` (senza trattini) + +**Comportamento verificato:** +- PlaybackStart β†’ INSERT `behavioral_context` (`do_not_disturb=true`) +- PlaybackStop (chiusura player) β†’ UPDATE `end_at`, `do_not_disturb=false` +- `agent_messages` β†’ `▢️ Ghost in the Shell (Chrome - PC)` / `⏹️ …` +- La pausa NON triggerizza PlaybackStop (solo la chiusura del player lo fa) + +--- + +#### 🎬 Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) β€” Blocco B1 βœ… + +Weekly cron (domenica 03:00). Radarr + Sonarr β†’ GPT-4.1 β†’ Postgres + Qdrant. + +- Fetch Radarr `/radarr/api/v3/movie` + Sonarr `/sonarr/api/v3/series` +- Merge e normalizzazione: `{type, title, year, genres, status, source, source_id}` +- GPT-4.1 (GitHub Copilot): analisi taste β†’ `{top_genres, preferred_types, library_stats, taste_summary, notable_patterns}` +- Postgres upsert: `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) +- Loop per ogni titolo: Ollama `nomic-embed-text` (768-dim) β†’ Qdrant `media_preferences` (Cosine) + +**Qdrant collection `media_preferences` creata** (PUT `/collections/media_preferences`, vettori 768-dim Cosine). + +--- + +#### 🎞️ Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) β€” Blocco B2 βœ… + +Daily cron (04:00). Jellyfin history β†’ GPT-4.1 β†’ Postgres. + +- Fetch `/Users/{martin_id}/Items` ultimi 100 played, filtro PlayCount > 0 e last 90 days +- GPT-4.1: `{recent_favorites, preferred_genres, watch_patterns, completion_rate, notes}` +- Postgres upsert: `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d) + +**Jellyfin API token** (`d153606c1ca54574a20d2b40fcf1b02e`) creato via `POST /Auth/Keys?app=Pompeo` con sessione admin (`admin` / `__Montecarlo00!`). + +--- + +### Fix workflow esistenti + +#### πŸ“… Calendar Agent (`4ZIEGck9n4l5qaDt`) β€” 2 bug fix + +1. **`πŸ—‘οΈ Cleanup Cancellati` β€” `column "undefined" does not exist`**: l'espressione `.map(i => "'" + i.json.uid.replace(...)+"'")` chiamava `.replace()` su `undefined` quando Parse GPT restituisce `{skip:true}` β†’ l'intera espressione `{{ }}` valutava a `undefined` (non quotato) β†’ PostgreSQL interpretava `undefined` come identificatore. Fix: aggiunto `.filter(i => i.json.uid)` prima del `.map()` + `String()` wrapper. +2. **`πŸ’Ύ Postgres - Salva Evento` β€” `updated_at`**: ON CONFLICT UPDATE includeva `updated_at = NOW()` ma la colonna non esiste in `memory_facts`. Rimosso. + +--- + ## [2026-03-21] Paperless Upload β€” integrazione memoria Postgres + Qdrant ### Modifiche al workflow `πŸ“„ Paperless β€” Upload Documento [Multi]` (`GBPFFq8rmbdFrNn9`) diff --git a/README.md b/README.md index d9c9146..5e56c00 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,62 @@ Imports bank CSV statements (Banca Sella format) into Actual Budget via Telegram **Common pattern across Paperless + Actual workflows**: GitHub Copilot token is obtained fresh at each run (`GET https://api.github.com/copilot_internal/v2/token`), then used for `POST https://api.githubcopilot.com/chat/completions` with model `gpt-4.1`. +### 🎬 Pompeo β€” Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) βœ… Active + +Webhook-based β€” triggered in real time by the Jellyfin Webhook plugin on every `PlaybackStart` / `PlaybackStop` event. + +- Webhook path: `jellyfin-playback` (via `https://orchestrator.mt-home.uk/webhook/jellyfin-playback`) +- Normalizes the Jellyfin payload (body is a JSON-encoded string β†’ `JSON.parse` required) +- On **PlaybackStart**: INSERT into `behavioral_context` (`event_type=watching_media`, `do_not_disturb=true`, metadata in `notes` JSONB: `item`, `device`, `item_type`) +- On **PlaybackStop**: UPDATE `behavioral_context` β†’ set `end_at=now()`, `do_not_disturb=false` +- Both events write to `agent_messages` (subject: `▢️ {title} ({device})` or `⏹️ …`) +- **User filter**: only processes events for `UserId = 42369255a7c64917a28fc26d4c7f8265` (Martin) +- **Jellyfin stop behavior**: `PlaybackStop` fires only on player close, NOT on pause + +Known quirks fixed: +- Jellyfin webhook plugin sends body as `$json.body` (JSON string) β€” must `JSON.parse()` before reading fields +- Real Jellyfin field names: `Name` (not `ItemName`), `DeviceName` / `Client`, `UserId` (no dashes) + +--- + +### 🎬 Pompeo β€” Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) βœ… Active + +Weekly cron β€” every **Sunday at 03:00**. + +- Fetches all movies from **Radarr** (`/radarr/api/v3/movie`) and all series from **Sonarr** (`/sonarr/api/v3/series`) +- Merges into unified list: `{type, title, year, genres, status: available|missing|monitored|unmonitored, source, source_id}` +- **GPT-4.1 analysis**: extracts `top_genres`, `preferred_types`, `library_stats`, `taste_summary`, `notable_patterns` +- **Postgres upsert**: `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) +- **Per-item loop**: for each movie/series β†’ Ollama `nomic-embed-text` (768-dim) β†’ Qdrant upsert into `media_preferences` collection + - Qdrant payload: `{title, year, type, genres, status, source, source_id, expires_at (+6 months)}` + +Internal endpoints used: +- `http://radarr.media.svc.cluster.local:7878/radarr/api/v3/movie?apikey=922d1405ab1147019d98a2997d941765` +- `http://sonarr.media.svc.cluster.local:8989/sonarr/api/v3/series?apikey=22140655993a4ff6bf12314813ec6982` +- `http://ollama.ai.svc.cluster.local:11434/api/embeddings` (model: `nomic-embed-text`) +- `http://qdrant.persistence.svc.cluster.local:6333` (api-key: sealed secret `qdrant-api-secret`) + +--- + +### 🎞️ Pompeo β€” Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) βœ… Active + +Daily cron β€” every day at **04:00**. + +- Fetches last 100 played/partially-played items for Martin from Jellyfin API + - Endpoint: `/Users/42369255a7c64917a28fc26d4c7f8265/Items?Recursive=true&IncludeItemTypes=Movie,Episode&SortBy=DatePlayed&SortOrder=Descending&Limit=100` + - Auth: `Authorization: MediaBrowser Token="d153606c1ca54574a20d2b40fcf1b02e"` (Pompeo API key) +- Filters items with `PlayCount > 0` and `LastPlayedDate` within 90 days +- **GPT-4.1 analysis**: extracts `recent_favorites`, `preferred_genres`, `watch_patterns`, `completion_rate`, `notes` +- **Postgres upsert**: `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d) +- Skips silently if no played items found (IF node guards the LLM call) + +Jellyfin credentials: +- API Token (app: Pompeo): `d153606c1ca54574a20d2b40fcf1b02e` β€” created via `POST /Auth/Keys?app=Pompeo` with admin session +- Martin UserId: `42369255a7c64917a28fc26d4c7f8265` (from Jellyfin SQLite DB / webhook payload) +- Admin user `admin` with password `__Montecarlo00!` (local auth, SSO via Authentik is separate) + +--- + ### πŸ“… Pompeo β€” Calendar Agent [Schedule] (`4ZIEGck9n4l5qaDt`) βœ… Active Runs every morning at 06:30 (and on-demand via manual trigger). @@ -407,6 +463,15 @@ Calendars: Lavoro, Famiglia, Spazzatura, Pulizie, Formula 1, WEC, Inter, Complea | `ZIVFNgI3esCKuYXc` | Google Calendar account | Google Calendar OAuth2 (also used for Tasks API) | | `u0JCseXGnDG5hS9F` | Home Assistant API | HTTP Header Auth (long-lived HA token) | | `mRqzxhSboGscolqI` | Pompeo β€” PostgreSQL | Postgres (database: `pompeo`, user: `martin`) | +| `u0JCseXGnDG5hS9F` | Home Assistant API | HTTP Header Auth (long-lived HA token) | + +### Qdrant Collections + +| Collection | Dimensions | Distance | Model | Used by | +|---|---|---|---|---| +| `media_preferences` | 768 | Cosine | `nomic-embed-text` (Ollama) | Media Library Sync (B1) | + +Qdrant API key: sealed secret `qdrant-api-secret` in namespace `persistence` β†’ `__Montecarlo00!` --- @@ -437,8 +502,8 @@ Calendars: Lavoro, Famiglia, Spazzatura, Pulizie, Formula 1, WEC, Inter, Complea - Tabelle: `user_profile`, `memory_facts` (+ `source_ref` + dedup index), `finance_documents`, `behavioral_context`, `agent_messages` - Multi-tenancy: campo `user_id` su tutte le tabelle, seed `martin` + `shared` - Script DDL: `alpha/db/postgres.sql` -- [ ] Verify embedding endpoint via Copilot (`text-embedding-3-small`) as bootstrap fallback -- [ ] Plan migration to local Ollama embedding model once LLM server is online +- [x] Verify embedding endpoint via Copilot (`text-embedding-3-small`) as bootstrap fallback +- [x] ~~Plan migration to local Ollama embedding model once LLM server is online~~ βœ… Active β€” `nomic-embed-text` via `http://ollama.ai.svc.cluster.local:11434` (768-dim, multilingual) - [ ] Create `ha_sensor_config` table in Postgres and seed initial sensor patterns --- @@ -465,6 +530,17 @@ Calendars: Lavoro, Famiglia, Spazzatura, Pulizie, Formula 1, WEC, Inter, Complea - Telegram daily briefing at 06:30 - **Phase 2**: add weekly Qdrant embedding for semantic retrieval +- [x] ~~**Jellyfin Playback Agent**~~ βœ… 2026-03-21 β€” `AyrKWvboPldzZPsM` + - Webhook: PlaybackStart β†’ `behavioral_context` INSERT (`do_not_disturb=true`), PlaybackStop β†’ UPDATE (`end_at`, `do_not_disturb=false`) + - `agent_messages` populated with `▢️`/`⏹️` + title + device + - User filter: Martin only (UserId `42369255…`) + +- [x] ~~**Media Library Sync (B1)**~~ βœ… 2026-03-21 β€” `o3uM1xDLTAKw4D6E` + - Weekly (Sunday 03:00): Radarr + Sonarr β†’ GPT-4.1 taste analysis β†’ `memory_facts` + Qdrant `media_preferences` (768-dim, nomic-embed-text) + +- [x] ~~**Jellyfin Watch History Sync (B2)**~~ βœ… 2026-03-21 β€” `K07e4PPANXDkmQsr` + - Daily (04:00): Jellyfin play history (90d window) β†’ GPT-4.1 pattern analysis β†’ `memory_facts` + - [ ] **Finance Agent** (extend beyond Paperless) - Read Actual Budget export or API - Persist transactions, monthly summaries to `finance_documents` From c5a8bec2c250e37cc333755f9a7deaec258087e9 Mon Sep 17 00:00:00 2001 From: Martin Date: Sat, 21 Mar 2026 19:03:56 +0000 Subject: [PATCH 3/3] docs: CHANGELOG refactor - Media Agent entry espansa e deduplicata - Entry Blocco A+B riscritta con flusso dettagliato, tabella bug fix, note infrastruttura Radarr/Sonarr/Jellyfin/Ollama - Rimossa sezione duplicata 'Jellyfin Playback Agent Blocco A' (assorbita nell'entry principale) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 145 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 94 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5f9dcd..71e8515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,52 +4,106 @@ Tutte le modifiche significative al progetto ALPHA_PROJECT sono documentate qui. --- -## [2026-03-21] Media Agent β€” Blocco A+B completo + Calendar Agent fix +## [2026-03-21] Media Agent completo (Blocco A + B1 + B2) + Calendar Agent fix -### Nuovi workflow n8n +### 🎬 Blocco A β€” Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) βœ… -#### 🎬 Jellyfin Playback Agent [Webhook] (`AyrKWvboPldzZPsM`) β€” Blocco A βœ… +Webhook in real-time su PlaybackStart / PlaybackStop da Jellyfin. Workflow attivo e verificato end-to-end con Ghost in the Shell. -Webhook in real-time che intercetta PlaybackStart / PlaybackStop da Jellyfin. +**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 +``` -**Bug fix applicati (debugging session):** -1. **Webhook path** β€” `webhookId` deve essere top-level nel nodo JSON (non in `parameters`), altrimenti n8n v2.5.2 in queue mode registra path come `{workflowId}/{nodeName}/{path}` non raggiungibile -2. **SSL Patroni** β€” `NODE_TLS_REJECT_UNAUTHORIZED=0` aggiunto a `n8n-app` e `n8n-app-worker` deployments (Patroni forza `hostssl`) -3. **Postgres queryParams** β€” Postgres node v2 non valuta correttamente `$json.*` in `queryParams` β†’ migrato a SQL inline con `{{ $json.field }}` -4. **Jellyfin body format** β€” il plugin Webhook di Jellyfin invia il body come stringa JSON embedded (`$json.body = '{"ServerId":...}'`) β†’ aggiunto `JSON.parse()` nel Code node -5. **Jellyfin field names** β€” campi reali: `Name` (non `ItemName`), `DeviceName`/`Client`, `UserId` (senza trattini) +**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:** -- PlaybackStart β†’ INSERT `behavioral_context` (`do_not_disturb=true`) -- PlaybackStop (chiusura player) β†’ UPDATE `end_at`, `do_not_disturb=false` -- `agent_messages` β†’ `▢️ Ghost in the Shell (Chrome - PC)` / `⏹️ …` -- La pausa NON triggerizza PlaybackStop (solo la chiusura del player lo fa) +**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`. --- -#### 🎬 Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) β€” Blocco B1 βœ… +### 🎬 Blocco B1 β€” Media Library Sync [Schedule] (`o3uM1xDLTAKw4D6E`) βœ… -Weekly cron (domenica 03:00). Radarr + Sonarr β†’ GPT-4.1 β†’ Postgres + Qdrant. +Weekly cron (domenica 03:00). Costruisce la **memoria delle preferenze cinematografiche** di Martin. -- Fetch Radarr `/radarr/api/v3/movie` + Sonarr `/sonarr/api/v3/series` -- Merge e normalizzazione: `{type, title, year, genres, status, source, source_id}` -- GPT-4.1 (GitHub Copilot): analisi taste β†’ `{top_genres, preferred_types, library_stats, taste_summary, notable_patterns}` -- Postgres upsert: `memory_facts` (source=`media_library`, source_ref=`media_preferences_summary`, expires +7d) -- Loop per ogni titolo: Ollama `nomic-embed-text` (768-dim) β†’ Qdrant `media_preferences` (Cosine) +**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 +``` -**Qdrant collection `media_preferences` creata** (PUT `/collections/media_preferences`, vettori 768-dim Cosine). +**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). --- -#### 🎞️ Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) β€” Blocco B2 βœ… +### 🎞️ Blocco B2 β€” Jellyfin Watch History Sync [Schedule] (`K07e4PPANXDkmQsr`) βœ… -Daily cron (04:00). Jellyfin history β†’ GPT-4.1 β†’ Postgres. +Daily cron (04:00). Costruisce la **memoria della cronologia di visione** di Martin. -- Fetch `/Users/{martin_id}/Items` ultimi 100 played, filtro PlayCount > 0 e last 90 days -- GPT-4.1: `{recent_favorites, preferred_genres, watch_patterns, completion_rate, notes}` -- Postgres upsert: `memory_facts` (source=`jellyfin`, source_ref=`watch_history_summary`, expires +30d) +**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 +``` -**Jellyfin API token** (`d153606c1ca54574a20d2b40fcf1b02e`) creato via `POST /Auth/Keys?app=Pompeo` con sessione admin (`admin` / `__Montecarlo00!`). +**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. --- @@ -57,8 +111,17 @@ Daily cron (04:00). Jellyfin history β†’ GPT-4.1 β†’ Postgres. #### πŸ“… Calendar Agent (`4ZIEGck9n4l5qaDt`) β€” 2 bug fix -1. **`πŸ—‘οΈ Cleanup Cancellati` β€” `column "undefined" does not exist`**: l'espressione `.map(i => "'" + i.json.uid.replace(...)+"'")` chiamava `.replace()` su `undefined` quando Parse GPT restituisce `{skip:true}` β†’ l'intera espressione `{{ }}` valutava a `undefined` (non quotato) β†’ PostgreSQL interpretava `undefined` come identificatore. Fix: aggiunto `.filter(i => i.json.uid)` prima del `.map()` + `String()` wrapper. -2. **`πŸ’Ύ Postgres - Salva Evento` β€” `updated_at`**: ON CONFLICT UPDATE includeva `updated_at = NOW()` ma la colonna non esiste in `memory_facts`. Rimosso. +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. --- @@ -88,26 +151,6 @@ Ricreate `knowledge` e `episodes` con `size=768` (nomic-embed-text) β€” erano a --- -## [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 `▢️ ()`) - - **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`)