diff --git a/migrations/002_full_schema.sql b/migrations/002_full_schema.sql new file mode 100644 index 0000000..da6eeb2 --- /dev/null +++ b/migrations/002_full_schema.sql @@ -0,0 +1,408 @@ +-- ========================================== +-- Chat Switchboard - PostgreSQL Schema +-- ========================================== + +-- Enable extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; +CREATE EXTENSION IF NOT EXISTS "vector"; -- For embeddings (pgvector) + +-- ========================================== +-- Core Tables +-- ========================================== + +-- Users +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + display_name VARCHAR(100), + avatar_url TEXT, + role VARCHAR(20) DEFAULT 'user', -- user, admin, moderator + is_active BOOLEAN DEFAULT true, + settings JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_login_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_users_email ON users(email); + +-- API Configurations (user's API keys) +CREATE TABLE api_configs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, -- "OpenAI", "Claude", "Local Ollama" + provider VARCHAR(50) NOT NULL, -- openai, anthropic, ollama, openrouter + endpoint TEXT NOT NULL, + api_key_encrypted TEXT, -- Encrypted at rest + model_default VARCHAR(100), + config JSONB DEFAULT '{}'::jsonb, -- Custom settings per provider + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_api_configs_user ON api_configs(user_id); + +-- ========================================== +-- Feature 1: CHATS (User to LLM) +-- ========================================== + +CREATE TABLE chats ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL, + model VARCHAR(100), -- Current model for this chat + api_config_id UUID REFERENCES api_configs(id) ON DELETE SET NULL, + system_prompt TEXT, + settings JSONB DEFAULT '{}'::jsonb, -- temperature, max_tokens, etc + is_archived BOOLEAN DEFAULT false, + is_pinned BOOLEAN DEFAULT false, + folder VARCHAR(100), -- For organization + tags TEXT[], -- For filtering + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_chats_user ON chats(user_id); +CREATE INDEX idx_chats_updated ON chats(updated_at DESC); +CREATE INDEX idx_chats_tags ON chats USING GIN(tags); + +CREATE TABLE chat_messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + chat_id UUID REFERENCES chats(id) ON DELETE CASCADE, + role VARCHAR(20) NOT NULL, -- user, assistant, system, tool + content TEXT NOT NULL, + model VARCHAR(100), -- Which model generated this (for assistant) + tokens_used INTEGER, + tool_calls JSONB, -- Function calls made + metadata JSONB DEFAULT '{}'::jsonb, -- thinking_blocks, attachments, etc + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_chat_messages_chat ON chat_messages(chat_id, created_at); + +-- Model routing history (for analytics/debugging) +CREATE TABLE model_routing_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + chat_id UUID REFERENCES chats(id) ON DELETE CASCADE, + message_id UUID REFERENCES chat_messages(id) ON DELETE CASCADE, + requested_model VARCHAR(100), + routed_model VARCHAR(100), + reason TEXT, -- "cost_optimization", "context_length", "manual", "fallback" + latency_ms INTEGER, + cost_usd NUMERIC(10, 6), + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_routing_log_chat ON model_routing_log(chat_id); + +-- ========================================== +-- Feature 2: CHANNELS (User to User + AI) +-- ========================================== + +CREATE TABLE channels ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + type VARCHAR(20) DEFAULT 'public', -- public, private, dm + owner_id UUID REFERENCES users(id) ON DELETE SET NULL, + settings JSONB DEFAULT '{}'::jsonb, -- ai_participants, webhooks, etc + is_archived BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_channels_name ON channels(name); +CREATE INDEX idx_channels_type ON channels(type); + +-- Channel memberships +CREATE TABLE channel_members ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + role VARCHAR(20) DEFAULT 'member', -- owner, admin, member + joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(channel_id, user_id) +); + +CREATE INDEX idx_channel_members_channel ON channel_members(channel_id); +CREATE INDEX idx_channel_members_user ON channel_members(user_id); + +-- Channel messages +CREATE TABLE channel_messages ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + channel_id UUID REFERENCES channels(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- NULL for AI messages + content TEXT NOT NULL, + mentions JSONB, -- {users: [uuid], models: [name]} + thread_id UUID REFERENCES channel_messages(id) ON DELETE CASCADE, -- For threading + attachments JSONB, -- Files, images, etc + reactions JSONB DEFAULT '{}'::jsonb, -- {emoji: [user_ids]} + is_ai_message BOOLEAN DEFAULT false, + ai_model VARCHAR(100), -- If AI generated + edited_at TIMESTAMP WITH TIME ZONE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_channel_messages_channel ON channel_messages(channel_id, created_at DESC); +CREATE INDEX idx_channel_messages_thread ON channel_messages(thread_id); +CREATE INDEX idx_channel_messages_mentions ON channel_messages USING GIN(mentions); + +-- ========================================== +-- Feature 3: NOTES & KNOWLEDGE BASES +-- ========================================== + +CREATE TABLE notes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL, + content TEXT NOT NULL, + content_type VARCHAR(20) DEFAULT 'markdown', -- markdown, html, plain + folder VARCHAR(100), + tags TEXT[], + is_pinned BOOLEAN DEFAULT false, + is_shared BOOLEAN DEFAULT false, + share_token UUID UNIQUE DEFAULT uuid_generate_v4(), + parent_note_id UUID REFERENCES notes(id) ON DELETE SET NULL, -- For hierarchies + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_notes_user ON notes(user_id); +CREATE INDEX idx_notes_updated ON notes(updated_at DESC); +CREATE INDEX idx_notes_tags ON notes USING GIN(tags); +CREATE INDEX idx_notes_share_token ON notes(share_token) WHERE is_shared = true; + +-- Knowledge Bases (Collections of documents) +CREATE TABLE knowledge_bases ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(200) NOT NULL, + description TEXT, + embedding_model VARCHAR(100) DEFAULT 'text-embedding-ada-002', + settings JSONB DEFAULT '{}'::jsonb, -- chunk_size, overlap, etc + is_public BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_knowledge_bases_user ON knowledge_bases(user_id); + +-- Documents in knowledge bases +CREATE TABLE kb_documents ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE, + filename VARCHAR(500) NOT NULL, + content TEXT NOT NULL, + content_type VARCHAR(50), -- application/pdf, text/markdown, etc + file_size INTEGER, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_kb_documents_kb ON kb_documents(kb_id); + +-- Document chunks (for RAG) +CREATE TABLE kb_chunks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + document_id UUID REFERENCES kb_documents(id) ON DELETE CASCADE, + kb_id UUID REFERENCES knowledge_bases(id) ON DELETE CASCADE, + content TEXT NOT NULL, + embedding vector(1536), -- For pgvector similarity search + chunk_index INTEGER, + metadata JSONB DEFAULT '{}'::jsonb, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_kb_chunks_document ON kb_chunks(document_id); +CREATE INDEX idx_kb_chunks_embedding ON kb_chunks USING ivfflat (embedding vector_cosine_ops); + +-- ========================================== +-- Feature 4: PLUGIN ORCHESTRATION +-- ========================================== + +-- Installed extensions/plugins +CREATE TABLE extensions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name VARCHAR(100) UNIQUE NOT NULL, + version VARCHAR(20) NOT NULL, + author VARCHAR(100), + description TEXT, + runtime VARCHAR(20), -- python, go, node, rust + entry_point TEXT NOT NULL, + port INTEGER, + manifest JSONB NOT NULL, -- Full extension.json + is_enabled BOOLEAN DEFAULT true, + is_system BOOLEAN DEFAULT false, -- Core extensions + install_source VARCHAR(200), -- URL or marketplace ID + installed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_extensions_enabled ON extensions(is_enabled); + +-- Extension tools/functions +CREATE TABLE extension_tools ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + extension_id UUID REFERENCES extensions(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + description TEXT, + parameters_schema JSONB NOT NULL, -- JSON Schema + response_schema JSONB, + is_enabled BOOLEAN DEFAULT true, + UNIQUE(extension_id, name) +); + +CREATE INDEX idx_extension_tools_extension ON extension_tools(extension_id); + +-- Tool usage log (for analytics) +CREATE TABLE tool_usage_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tool_id UUID REFERENCES extension_tools(id) ON DELETE SET NULL, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + chat_id UUID REFERENCES chats(id) ON DELETE SET NULL, + channel_id UUID REFERENCES channels(id) ON DELETE SET NULL, + input_params JSONB, + output_result JSONB, + execution_time_ms INTEGER, + success BOOLEAN, + error_message TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_tool_usage_tool ON tool_usage_log(tool_id); +CREATE INDEX idx_tool_usage_user ON tool_usage_log(user_id); + +-- ========================================== +-- Feature 5: WORKFLOWS (Unique Feature!) +-- ========================================== + +-- Workflow definitions (DAG of AI operations) +CREATE TABLE workflows ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(200) NOT NULL, + description TEXT, + graph JSONB NOT NULL, -- Node-edge DAG structure + input_schema JSONB, -- Expected inputs + output_schema JSONB, -- Expected outputs + is_public BOOLEAN DEFAULT false, + is_template BOOLEAN DEFAULT false, + tags TEXT[], + usage_count INTEGER DEFAULT 0, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_workflows_user ON workflows(user_id); +CREATE INDEX idx_workflows_public ON workflows(is_public) WHERE is_public = true; +CREATE INDEX idx_workflows_tags ON workflows USING GIN(tags); + +-- Workflow executions +CREATE TABLE workflow_executions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + workflow_id UUID REFERENCES workflows(id) ON DELETE CASCADE, + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + input_data JSONB, + output_data JSONB, + status VARCHAR(20), -- running, completed, failed + steps JSONB, -- Execution trace + total_cost_usd NUMERIC(10, 6), + total_time_ms INTEGER, + error_message TEXT, + started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + completed_at TIMESTAMP WITH TIME ZONE +); + +CREATE INDEX idx_workflow_executions_workflow ON workflow_executions(workflow_id); +CREATE INDEX idx_workflow_executions_user ON workflow_executions(user_id); + +-- ========================================== +-- Shared/Utility Tables +-- ========================================== + +-- File uploads +CREATE TABLE files ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + filename VARCHAR(500) NOT NULL, + content_type VARCHAR(100), + file_size INTEGER, + storage_path TEXT NOT NULL, -- S3/local path + is_public BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_files_user ON files(user_id); + +-- Webhooks +CREATE TABLE webhooks ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(100), + url TEXT NOT NULL, + events TEXT[] NOT NULL, -- chat.message, channel.message, etc + secret VARCHAR(100), + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_webhooks_user ON webhooks(user_id); + +-- Audit log +CREATE TABLE audit_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + action VARCHAR(100) NOT NULL, + resource_type VARCHAR(50), + resource_id UUID, + metadata JSONB, + ip_address INET, + user_agent TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX idx_audit_log_user ON audit_log(user_id); +CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC); + +-- ========================================== +-- Functions & Triggers +-- ========================================== + +-- Auto-update updated_at timestamps +CREATE OR REPLACE FUNCTION update_updated_at() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER users_updated_at BEFORE UPDATE ON users + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER chats_updated_at BEFORE UPDATE ON chats + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER channels_updated_at BEFORE UPDATE ON channels + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER notes_updated_at BEFORE UPDATE ON notes + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER knowledge_bases_updated_at BEFORE UPDATE ON knowledge_bases + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); +CREATE TRIGGER workflows_updated_at BEFORE UPDATE ON workflows + FOR EACH ROW EXECUTE FUNCTION update_updated_at(); + +-- Increment workflow usage count +CREATE OR REPLACE FUNCTION increment_workflow_usage() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE workflows SET usage_count = usage_count + 1 WHERE id = NEW.workflow_id; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER workflow_executions_insert AFTER INSERT ON workflow_executions + FOR EACH ROW EXECUTE FUNCTION increment_workflow_usage();