Files
Alpha/CHANGELOG.md
Martin afc182292a feat: Media Agent Blocco A+B + Calendar Agent fix
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>
2026-03-21 18:51:11 +00:00

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