-- 008_knowledge_bases.sql -- Knowledge Bases: KB metadata, documents, chunks, channel links. -- Requires: pgvector extension already installed (via db-bootstrap.sh). -- ── Knowledge Bases ────────────────────────── CREATE TABLE IF NOT EXISTS knowledge_bases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'global', -- global, team, personal owner_id UUID REFERENCES users(id) ON DELETE CASCADE, team_id UUID REFERENCES teams(id) ON DELETE CASCADE, -- Snapshot of embedding config at creation time. -- Changing the model requires a full re-embed (rebuild endpoint). -- { "provider_config_id": "...", "model_id": "...", "dimensions": 1536 } embedding_config JSONB NOT NULL DEFAULT '{}', -- Denormalized stats, updated by ingest pipeline. document_count INT NOT NULL DEFAULT 0, chunk_count INT NOT NULL DEFAULT 0, total_bytes BIGINT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', -- active, processing, error created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT kb_scope_check CHECK ( (scope = 'global' AND owner_id IS NULL) OR (scope = 'team' AND team_id IS NOT NULL) OR (scope = 'personal' AND owner_id IS NOT NULL) ) ); CREATE INDEX IF NOT EXISTS idx_kb_scope ON knowledge_bases(scope); CREATE INDEX IF NOT EXISTS idx_kb_owner ON knowledge_bases(owner_id) WHERE owner_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_kb_team ON knowledge_bases(team_id) WHERE team_id IS NOT NULL; -- ── KB Documents ───────────────────────────── -- One row per uploaded file. Blobs live in ObjectStore under: -- kb/{kb_id}/{doc_id}_{filename} CREATE TABLE IF NOT EXISTS kb_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, filename TEXT NOT NULL, content_type TEXT NOT NULL, size_bytes BIGINT NOT NULL, storage_key TEXT NOT NULL, extracted_text TEXT, -- full text for re-chunking chunk_count INT NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'pending', -- pending, chunking, embedding, ready, error error TEXT, uploaded_by UUID NOT NULL REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_kbdoc_kb ON kb_documents(kb_id); -- ── KB Chunks ──────────────────────────────── -- Chunked text + embedding vector for similarity search. -- vector(3072) accommodates all current embedding models: -- OpenAI ada-002 / text-embedding-3-small = 1536 -- OpenAI text-embedding-3-large = 3072 -- Open-source models = typically 768-1024 -- Smaller vectors are zero-padded at insert time. CREATE TABLE IF NOT EXISTS kb_chunks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, document_id UUID NOT NULL REFERENCES kb_documents(id) ON DELETE CASCADE, chunk_index INT NOT NULL, content TEXT NOT NULL, token_count INT NOT NULL DEFAULT 0, embedding VECTOR(3072), metadata JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_kbchunk_kb ON kb_chunks(kb_id); CREATE INDEX IF NOT EXISTS idx_kbchunk_doc ON kb_chunks(document_id); -- NOTE: IVFFlat index for similarity search is created after first data -- load (needs rows to train). The ingest handler creates it: -- CREATE INDEX idx_kbchunk_embedding ON kb_chunks -- USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); -- ── Channel-KB Links ───────────────────────── -- Which KBs are active for a given channel. CREATE TABLE IF NOT EXISTS channel_knowledge_bases ( channel_id UUID NOT NULL REFERENCES channels(id) ON DELETE CASCADE, kb_id UUID NOT NULL REFERENCES knowledge_bases(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT true, added_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (channel_id, kb_id) );