Spaces:
Sleeping
Sleeping
| -- ============================================================ | |
| -- Meridian β Supabase Database Schema (unified, idempotent) | |
| -- Run this entire file in: Supabase Dashboard β SQL Editor β New Query | |
| -- | |
| -- Safe to re-run: every CREATE uses IF NOT EXISTS, every ALTER uses | |
| -- IF NOT EXISTS, the function is DROPped before CREATE so return-type | |
| -- changes don't error. | |
| -- ============================================================ | |
| -- ββ Enable pgvector ββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE EXTENSION IF NOT EXISTS vector; | |
| -- ββ Profiles ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS public.profiles ( | |
| id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, | |
| display_name TEXT, | |
| age INT CHECK (age BETWEEN 13 AND 120), | |
| preferred_modality TEXT DEFAULT 'cbt' | |
| CHECK (preferred_modality IN ('cbt','act','dbt','mindfulness','sfbt')), | |
| consent_given BOOLEAN NOT NULL DEFAULT false, | |
| consent_at TIMESTAMPTZ, | |
| created_at TIMESTAMPTZ DEFAULT now(), | |
| updated_at TIMESTAMPTZ DEFAULT now() | |
| ); | |
| -- ββ Sessions ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Replaces the in-memory session_store dict in main.py. | |
| -- prob_history is Phase 10's per-turn SVM depression probability | |
| -- trajectory β see backend/agents/diagnosis.py::_decide(). | |
| CREATE TABLE IF NOT EXISTS public.sessions ( | |
| id TEXT PRIMARY KEY, | |
| user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, | |
| mode TEXT NOT NULL CHECK (mode IN ('diagnosis','therapy')), | |
| modality TEXT CHECK (modality IN ('cbt','act','dbt','mindfulness','sfbt')), | |
| embedding_buffer JSONB NOT NULL DEFAULT '[]', | |
| char_buffer_total INT NOT NULL DEFAULT 0, | |
| prob_history JSONB NOT NULL DEFAULT '[]', | |
| diagnosis_ready BOOLEAN NOT NULL DEFAULT false, | |
| crisis_detected BOOLEAN NOT NULL DEFAULT false, | |
| risk_scores JSONB NOT NULL DEFAULT '{}', | |
| created_at TIMESTAMPTZ DEFAULT now(), | |
| updated_at TIMESTAMPTZ DEFAULT now() | |
| ); | |
| -- Phase 10 backfill β ensures existing DBs without prob_history | |
| -- get the column added. No-op on fresh DBs (column already exists | |
| -- from the CREATE TABLE above). | |
| ALTER TABLE public.sessions | |
| ADD COLUMN IF NOT EXISTS prob_history JSONB NOT NULL DEFAULT '[]'; | |
| CREATE INDEX IF NOT EXISTS sessions_user_id_idx ON public.sessions(user_id); | |
| -- ββ Messages ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS public.messages ( | |
| id BIGSERIAL PRIMARY KEY, | |
| session_id TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE, | |
| role TEXT NOT NULL CHECK (role IN ('user','assistant')), | |
| content TEXT NOT NULL, | |
| created_at TIMESTAMPTZ DEFAULT now() | |
| ); | |
| CREATE INDEX IF NOT EXISTS messages_session_id_idx ON public.messages(session_id); | |
| -- ββ Audit Log βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CREATE TABLE IF NOT EXISTS public.audit_log ( | |
| id BIGSERIAL PRIMARY KEY, | |
| user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, | |
| session_id TEXT, | |
| event_type TEXT NOT NULL, | |
| -- event_type values: | |
| -- 'consent_accepted' β user accepted ToS on onboarding page | |
| -- 'crisis_detected' β crisis node fired during a chat turn | |
| -- 'diagnosis_result' β MentalRoBERTa SVM classification completed | |
| -- 'guardian_block' β guardian agent blocked a request | |
| -- 'rate_limit_hit' β user exceeded 20 req/min | |
| -- 'phq9_assessment' β completed 9-question PHQ-9 cycle | |
| metadata JSONB NOT NULL DEFAULT '{}', | |
| created_at TIMESTAMPTZ DEFAULT now() | |
| ); | |
| CREATE INDEX IF NOT EXISTS audit_log_user_id_idx ON public.audit_log(user_id); | |
| CREATE INDEX IF NOT EXISTS audit_log_event_type_idx ON public.audit_log(event_type); | |
| CREATE INDEX IF NOT EXISTS audit_log_created_at_idx ON public.audit_log(created_at DESC); | |
| -- ββ Memory Embeddings (pgvector β replaces in-memory ChromaDB) ββββ | |
| -- Uses sentence-transformers/all-MiniLM-L6-v2 β 384 dimensions. | |
| CREATE TABLE IF NOT EXISTS public.memory_embeddings ( | |
| id BIGSERIAL PRIMARY KEY, | |
| session_id TEXT REFERENCES public.sessions(id) ON DELETE CASCADE, | |
| user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, | |
| content TEXT NOT NULL, | |
| embedding vector(384), | |
| created_at TIMESTAMPTZ DEFAULT now() | |
| ); | |
| -- IVFFlat index for approximate nearest-neighbour search. | |
| -- Run AFTER inserting at least 100 rows for best performance. | |
| CREATE INDEX IF NOT EXISTS memory_embeddings_vector_idx | |
| ON public.memory_embeddings | |
| USING ivfflat (embedding vector_cosine_ops) | |
| WITH (lists = 50); | |
| -- ββ PHQ-9 Trajectory ββββββββββββββββββββββββββββββββββββββββββββββ | |
| -- phq9_responses β every individual answered question | |
| -- phq9_assessments β sealed 9-question totals + severity (chart source) | |
| CREATE TABLE IF NOT EXISTS public.phq9_responses ( | |
| id BIGSERIAL PRIMARY KEY, | |
| user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, | |
| session_id TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE, | |
| question_index INT NOT NULL CHECK (question_index BETWEEN 0 AND 8), | |
| score INT NOT NULL CHECK (score BETWEEN 0 AND 3), | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| CREATE INDEX IF NOT EXISTS phq9_responses_session_idx | |
| ON public.phq9_responses(session_id); | |
| CREATE INDEX IF NOT EXISTS phq9_responses_user_idx | |
| ON public.phq9_responses(user_id, created_at DESC); | |
| CREATE TABLE IF NOT EXISTS public.phq9_assessments ( | |
| id BIGSERIAL PRIMARY KEY, | |
| user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, | |
| session_id TEXT NOT NULL REFERENCES public.sessions(id) ON DELETE CASCADE, | |
| total_score INT NOT NULL CHECK (total_score BETWEEN 0 AND 27), | |
| severity TEXT NOT NULL CHECK (severity IN | |
| ('minimal','mild','moderate','moderately severe','severe')), | |
| -- 9 scores in question-index order [s0, s1, ..., s8] | |
| responses JSONB NOT NULL DEFAULT '[]', | |
| created_at TIMESTAMPTZ NOT NULL DEFAULT now() | |
| ); | |
| CREATE INDEX IF NOT EXISTS phq9_assessments_user_idx | |
| ON public.phq9_assessments(user_id, created_at DESC); | |
| -- ββ Row Level Security ββββββββββββββββββββββββββββββββββββββββββββ | |
| -- Users can ONLY read/write their own rows. Enforced at DB level. | |
| ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.sessions ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.messages ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.memory_embeddings ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.phq9_responses ENABLE ROW LEVEL SECURITY; | |
| ALTER TABLE public.phq9_assessments ENABLE ROW LEVEL SECURITY; | |
| -- audit_log is write-only for users, readable only via service key (server-side) | |
| ALTER TABLE public.audit_log ENABLE ROW LEVEL SECURITY; | |
| -- Profiles: own row only | |
| DROP POLICY IF EXISTS "profiles: own row" ON public.profiles; | |
| CREATE POLICY "profiles: own row" ON public.profiles | |
| FOR ALL USING (auth.uid() = id); | |
| -- Sessions: own rows only | |
| DROP POLICY IF EXISTS "sessions: own rows" ON public.sessions; | |
| CREATE POLICY "sessions: own rows" ON public.sessions | |
| FOR ALL USING (auth.uid() = user_id); | |
| -- Messages: own sessions' messages only | |
| DROP POLICY IF EXISTS "messages: own sessions" ON public.messages; | |
| CREATE POLICY "messages: own sessions" ON public.messages | |
| FOR ALL USING ( | |
| session_id IN ( | |
| SELECT id FROM public.sessions WHERE user_id = auth.uid() | |
| ) | |
| ); | |
| -- Memory: own rows only | |
| DROP POLICY IF EXISTS "memory: own rows" ON public.memory_embeddings; | |
| CREATE POLICY "memory: own rows" ON public.memory_embeddings | |
| FOR ALL USING (auth.uid() = user_id); | |
| -- PHQ-9: own rows only | |
| DROP POLICY IF EXISTS "phq9_responses: own rows" ON public.phq9_responses; | |
| CREATE POLICY "phq9_responses: own rows" ON public.phq9_responses | |
| FOR ALL USING (auth.uid() = user_id); | |
| DROP POLICY IF EXISTS "phq9_assessments: own rows" ON public.phq9_assessments; | |
| CREATE POLICY "phq9_assessments: own rows" ON public.phq9_assessments | |
| FOR ALL USING (auth.uid() = user_id); | |
| -- Audit log: users cannot read; service key (backend) can write via service role | |
| DROP POLICY IF EXISTS "audit_log: service write only" ON public.audit_log; | |
| CREATE POLICY "audit_log: service write only" ON public.audit_log | |
| FOR INSERT WITH CHECK (true); -- backend uses service key which bypasses RLS | |
| -- ββ Auto-update updated_at ββββββββββββββββββββββββββββββββββββββββ | |
| CREATE OR REPLACE FUNCTION public.set_updated_at() | |
| RETURNS TRIGGER LANGUAGE plpgsql AS $$ | |
| BEGIN | |
| NEW.updated_at = now(); | |
| RETURN NEW; | |
| END; | |
| $$; | |
| DROP TRIGGER IF EXISTS set_profiles_updated_at ON public.profiles; | |
| CREATE TRIGGER set_profiles_updated_at | |
| BEFORE UPDATE ON public.profiles | |
| FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); | |
| DROP TRIGGER IF EXISTS set_sessions_updated_at ON public.sessions; | |
| CREATE TRIGGER set_sessions_updated_at | |
| BEFORE UPDATE ON public.sessions | |
| FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); | |
| -- ============================================================ | |
| -- Memory retrieval RPC | |
| -- Called from backend/agents/memory.py::retrieve_similar. | |
| -- Returns the top-N most semantically similar past exchanges | |
| -- for a given user, across ALL of their sessions. | |
| -- | |
| -- IMPORTANT: we DROP first because Postgres won't let | |
| -- CREATE OR REPLACE change a function's return type. If you've | |
| -- ever run a version of this with a different return signature | |
| -- (e.g. session_id uuid instead of text), the CREATE OR REPLACE | |
| -- alone will fail with error 42P13. The explicit DROP makes | |
| -- the rerun safe regardless of what's already in the DB. | |
| -- ============================================================ | |
| DROP FUNCTION IF EXISTS public.match_memory_embeddings(vector, uuid, int); | |
| CREATE OR REPLACE FUNCTION public.match_memory_embeddings( | |
| query_embedding vector(384), | |
| target_user_id uuid, | |
| match_count int DEFAULT 2 | |
| ) | |
| RETURNS TABLE ( | |
| id bigint, | |
| session_id text, | |
| content text, | |
| similarity float | |
| ) | |
| LANGUAGE plpgsql | |
| STABLE | |
| AS $$ | |
| BEGIN | |
| RETURN QUERY | |
| SELECT | |
| m.id, | |
| m.session_id, | |
| m.content, | |
| -- pgvector's <=> is cosine distance (0 = identical, 2 = opposite). | |
| -- We return similarity = 1 - distance so callers can sort/threshold | |
| -- on the more intuitive "higher is better" scale if they want. | |
| 1 - (m.embedding <=> query_embedding) AS similarity | |
| FROM public.memory_embeddings m | |
| WHERE m.user_id = target_user_id | |
| AND m.embedding IS NOT NULL | |
| ORDER BY m.embedding <=> query_embedding | |
| LIMIT match_count; | |
| END; | |
| $$; | |
| GRANT EXECUTE ON FUNCTION public.match_memory_embeddings(vector, uuid, int) | |
| TO authenticated, service_role; | |
| -- ββ Useful admin queries (run manually in SQL editor) βββββββββββββ | |
| -- Crisis events last 7 days: | |
| -- SELECT u.email, a.session_id, a.metadata, a.created_at | |
| -- FROM public.audit_log a JOIN auth.users u ON a.user_id = u.id | |
| -- WHERE a.event_type = 'crisis_detected' | |
| -- AND a.created_at > now() - interval '7 days' | |
| -- ORDER BY a.created_at DESC; | |
| -- Daily active users (last 30 days): | |
| -- SELECT date_trunc('day', created_at) AS day, COUNT(DISTINCT user_id) | |
| -- FROM public.sessions GROUP BY 1 ORDER BY 1 DESC LIMIT 30; | |
| -- PHQ-9 severity distribution last 30 days: | |
| -- SELECT severity, COUNT(*) | |
| -- FROM public.phq9_assessments | |
| -- WHERE created_at > now() - interval '30 days' | |
| -- GROUP BY 1 ORDER BY 1; | |
| -- Per-user PHQ-9 trajectory (deltas): | |
| -- SELECT user_id, created_at, total_score, | |
| -- total_score - LAG(total_score) OVER ( | |
| -- PARTITION BY user_id ORDER BY created_at | |
| -- ) AS delta | |
| -- FROM public.phq9_assessments | |
| -- ORDER BY user_id, created_at; | |
| -- Adaptive diagnosis β average turns to fire (Phase 10): | |
| -- SELECT AVG(jsonb_array_length(prob_history)) AS avg_turns_to_fire | |
| -- FROM public.sessions | |
| -- WHERE mode = 'diagnosis' AND diagnosis_ready = true; |