-- ============================================================================= -- ALPHA_PROJECT — Database "pompeo" — Schema v2 -- ============================================================================= -- Applicare su: postgresql://martin@postgres.persistence.svc.cluster.local:5432/pompeo -- -- Esecuzione dal cluster: -- sudo microk8s kubectl run psql-pompeo --rm -it \ -- --image=postgres:17-alpine --namespace=persistence \ -- --env="PGPASSWORD=" --restart=Never \ -- -- psql "postgresql://martin@postgres:5432/pompeo" -f /dev/stdin < postgres.sql -- -- Esecuzione via port-forward: -- sudo microk8s kubectl port-forward svc/postgres -n persistence 5432:5432 -- psql "postgresql://martin@localhost:5432/pompeo" -f postgres.sql -- ============================================================================= \c pompeo -- --------------------------------------------------------------------------- -- Estensioni -- --------------------------------------------------------------------------- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- full-text similarity search su subject/detail -- ============================================================================= -- 1. USER_PROFILE -- Preferenze statiche per utente. Aggiornata manualmente o via agent action. -- user_id 'shared' = preferenze della casa (visibili a tutti). -- ============================================================================= CREATE TABLE IF NOT EXISTS user_profile ( user_id TEXT PRIMARY KEY, display_name TEXT, language TEXT NOT NULL DEFAULT 'it', timezone TEXT NOT NULL DEFAULT 'Europe/Rome', notification_style TEXT NOT NULL DEFAULT 'concise', -- 'concise' | 'verbose' quiet_start TIME NOT NULL DEFAULT '23:00', quiet_end TIME NOT NULL DEFAULT '07:00', preferences JSONB, -- freeform: soglie, preferenze extra per agente updated_at TIMESTAMP NOT NULL DEFAULT now() ); -- Utenti iniziali INSERT INTO user_profile (user_id, display_name) VALUES ('martin', 'Martin'), ('shared', 'Shared') ON CONFLICT (user_id) DO NOTHING; -- ============================================================================= -- 2. MEMORY_FACTS -- Fatti episodici prodotti da tutti gli agenti. TTL tramite expires_at. -- qdrant_id: riferimento al punto vettoriale corrispondente nella collection "episodes". -- ============================================================================= CREATE TABLE IF NOT EXISTS memory_facts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL DEFAULT 'martin', source TEXT NOT NULL, -- 'email' | 'calendar' | 'iot' | 'paperless' | 'n8n' | ... source_ref TEXT, -- ID esterno per dedup (es. UID evento Google, thread_id email) category TEXT, -- 'finance' | 'personal' | 'work' | 'health' | ... subject TEXT, detail JSONB, -- payload flessibile per-source action_required BOOLEAN NOT NULL DEFAULT false, action_text TEXT, created_at TIMESTAMP NOT NULL DEFAULT now(), expires_at TIMESTAMP, -- NULL = permanente qdrant_id UUID, -- FK logico → collection "episodes" pompeo_note TEXT, -- inner monologue dell'LLM al momento dell'insert entity_refs JSONB -- entità estratte: {people, places, products, amounts} ); CREATE INDEX IF NOT EXISTS idx_memory_facts_user_source_cat ON memory_facts(user_id, source, category); CREATE INDEX IF NOT EXISTS idx_memory_facts_expires ON memory_facts(expires_at) WHERE expires_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_memory_facts_action ON memory_facts(user_id, action_required) WHERE action_required = true; CREATE INDEX IF NOT EXISTS idx_memory_facts_source_ref ON memory_facts(source_ref) WHERE source_ref IS NOT NULL; -- Dedup: prevents duplicate inserts for same event (used by Calendar Agent and others) CREATE UNIQUE INDEX IF NOT EXISTS memory_facts_dedup_idx ON memory_facts(user_id, source, source_ref) WHERE source_ref IS NOT NULL; -- ============================================================================= -- 3. FINANCE_DOCUMENTS -- Documenti finanziari strutturati (bollette, fatture, cedolini). -- paperless_doc_id: riferimento al documento in Paperless-ngx. -- ============================================================================= CREATE TABLE IF NOT EXISTS finance_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL DEFAULT 'martin', paperless_doc_id INT, -- ID documento in Paperless-ngx correspondent TEXT, amount NUMERIC(10,2), currency TEXT NOT NULL DEFAULT 'EUR', doc_date DATE, doc_type TEXT, -- 'bolletta' | 'fattura' | 'cedolino' | ... tags TEXT[], raw_text TEXT, -- testo OCR grezzo (per embedding) created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_finance_docs_user_date ON finance_documents(user_id, doc_date DESC); CREATE INDEX IF NOT EXISTS idx_finance_docs_correspondent ON finance_documents(user_id, correspondent); -- ============================================================================= -- 4. BEHAVIORAL_CONTEXT -- Contesto comportamentale prodotto dall'IoT Agent e dal Calendar Agent. -- Usato dal Proactive Arbiter per rispettare DND e stimare presence. -- ============================================================================= CREATE TABLE IF NOT EXISTS behavioral_context ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL DEFAULT 'martin', event_type TEXT, -- 'sport_event' | 'dog_walk' | 'work_session' | 'commute' | ... start_at TIMESTAMP, end_at TIMESTAMP, do_not_disturb BOOLEAN NOT NULL DEFAULT false, home_presence_expected BOOLEAN, notes TEXT, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_behavioral_ctx_user_time ON behavioral_context(user_id, start_at, end_at); CREATE INDEX IF NOT EXISTS idx_behavioral_ctx_dnd ON behavioral_context(user_id, do_not_disturb) WHERE do_not_disturb = true; -- ============================================================================= -- 5. AGENT_MESSAGES -- Blackboard: ogni agente pubblica qui le proprie osservazioni. -- Il Proactive Arbiter legge, decide (notify/defer/discard) e aggiorna. -- Corrisponde al message schema definito in alpha/README.md. -- ============================================================================= CREATE TABLE IF NOT EXISTS agent_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agent TEXT NOT NULL, -- 'mail' | 'calendar' | 'iot' | 'finance' | 'infra' | ... priority TEXT NOT NULL, -- 'low' | 'high' event_type TEXT NOT NULL, -- 'new_fact' | 'reminder' | 'alert' | 'behavioral_observation' user_id TEXT NOT NULL DEFAULT 'martin', subject TEXT, detail JSONB, source_ref TEXT, -- ID record Postgres o ref esterna expires_at TIMESTAMP, arbiter_decision TEXT, -- NULL (pending) | 'notify' | 'defer' | 'discard' arbiter_reason TEXT, created_at TIMESTAMP NOT NULL DEFAULT now(), processed_at TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_agent_msgs_pending ON agent_messages(user_id, priority, created_at) WHERE arbiter_decision IS NULL; CREATE INDEX IF NOT EXISTS idx_agent_msgs_agent_type ON agent_messages(agent, event_type, created_at); CREATE INDEX IF NOT EXISTS idx_agent_msgs_expires ON agent_messages(expires_at) WHERE expires_at IS NOT NULL AND arbiter_decision IS NULL; -- ============================================================================= -- 6. HA_SENSOR_CONFIG -- Allowlist dinamica dei sensori Home Assistant monitorati dall'IoT Agent. -- Pattern = regex, matchato contro gli entity_id di Home Assistant. -- Evita regole hardcoded nel workflow — aggiungere sensori = INSERT. -- ============================================================================= CREATE TABLE IF NOT EXISTS ha_sensor_config ( id SERIAL PRIMARY KEY, pattern TEXT NOT NULL, -- regex pattern (es. 'sensor\.pixel_10_.*') user_id TEXT NOT NULL DEFAULT 'martin', group_name TEXT NOT NULL, -- 'mobile_device' | 'work_presence' | 'entertainment' | ... description TEXT, active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_ha_sensor_config_user ON ha_sensor_config(user_id, active); -- Seed: sensori significativi per Martin INSERT INTO ha_sensor_config (pattern, user_id, group_name, description) VALUES ('sensor\.pixel_10_.*', 'martin', 'mobile_device', 'Tutti i sensori Pixel 10'), ('binary_sensor\.pixel_10_.*', 'martin', 'mobile_device', 'Sensori binari Pixel 10'), ('device_tracker\.ey_hp', 'martin', 'work_presence', 'Laptop EY (router tracker)'), ('media_player\.spotify_martin', 'martin', 'entertainment', 'Spotify Martin'), ('person\.martin_tahiraj', 'martin', 'presence', 'Zona GPS Martin'), ('sensor\.pixel_watch_.*', 'martin', 'wearable', 'Pixel Watch 4 (futuro)'), ('sensor\.pixel_10_heart_rate', 'martin', 'health', 'Frequenza cardiaca'), ('sensor\.pixel_10_daily_steps', 'martin', 'health', 'Passi giornalieri'), ('sensor\.pixel_10_sleep_duration', 'martin', 'health', 'Durata sonno'), ('sensor\.pixel_10_next_alarm', 'martin', 'routine', 'Prossima sveglia') ON CONFLICT DO NOTHING; -- ============================================================================= -- 7. CONTACTS -- Grafo di persone multi-tenant. Ogni riga = una relazione (user_id → subject). -- Traversabile dall'LLM: user_id=martin → cugino → euris, poi user_id=euris → padre → mujsi -- → l'LLM inferisce che Mujsi è lo zio di Martin. -- Il campo 'details' è narrativa libera ottimizzata per consumo LLM. -- ============================================================================= CREATE TABLE IF NOT EXISTS contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id TEXT NOT NULL, -- chi "possiede" questa relazione subject TEXT NOT NULL, -- nome normalizzato del contatto relation TEXT NOT NULL, -- 'cugino' | 'zio' | 'madre' | 'moglie' | 'amico' | ... city TEXT, country TEXT, profession TEXT, aliases TEXT[], -- soprannomi/varianti nome ['Eri', 'Mucho'] born_year INT, details TEXT, -- narrativa libera per l'LLM (storia, carattere, note) metadata JSONB, -- extra strutturato: {email, phone, children, spouse, ...} created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (user_id, subject) ); CREATE INDEX IF NOT EXISTS idx_contacts_user ON contacts(user_id); CREATE INDEX IF NOT EXISTS idx_contacts_subject_trgm ON contacts USING gin(subject gin_trgm_ops); -- similarity search su nome CREATE INDEX IF NOT EXISTS idx_contacts_aliases ON contacts USING gin(aliases); -- ricerca per alias/soprannome -- ============================================================================= -- 8. MEMORY_FACTS_ARCHIVE -- Fatti scaduti spostati qui dal cleanup settimanale. -- Struttura identica a memory_facts + campi archivio. -- Un punto Qdrant "episodio settimanale" riassume il batch archiviato. -- ============================================================================= CREATE TABLE IF NOT EXISTS memory_facts_archive ( id UUID NOT NULL, user_id TEXT NOT NULL DEFAULT 'martin', source TEXT NOT NULL, source_ref TEXT, category TEXT, subject TEXT, detail JSONB, action_required BOOLEAN NOT NULL DEFAULT false, action_text TEXT, pompeo_note TEXT, entity_refs JSONB, qdrant_id UUID, created_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ, archived_at TIMESTAMPTZ NOT NULL DEFAULT now(), archive_reason TEXT NOT NULL DEFAULT 'expired' -- 'expired' | 'superseded' | 'merged' ); CREATE INDEX IF NOT EXISTS idx_mf_archive_user_date ON memory_facts_archive(user_id, archived_at DESC); CREATE INDEX IF NOT EXISTS idx_mf_archive_source ON memory_facts_archive(user_id, source, category); -- ============================================================================= -- Fine script -- ============================================================================= \echo '✅ Schema pompeo applicato correttamente.' \echo ' Tabelle: user_profile, memory_facts, memory_facts_archive, contacts, finance_documents, behavioral_context, agent_messages, ha_sensor_config'