- New table: contacts (multi-tenant person graph with aliases, details, GIN indexes) - New table: memory_facts_archive (expired facts cleanup destination) - memory_facts: added pompeo_note TEXT and entity_refs JSONB columns - Applied live to Patroni primary (postgres-1, namespace persistence, DB pompeo) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
283 lines
13 KiB
SQL
283 lines
13 KiB
SQL
-- =============================================================================
|
|
-- 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=<password>" --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'
|