mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-13 14:15:52 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
928 lines
33 KiB
Python
928 lines
33 KiB
Python
"""0001 initial schema — consolidated baseline for user-data tables.
|
|
|
|
Revision ID: 0001_initial
|
|
Revises:
|
|
Create Date: 2026-04-13
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
|
|
|
|
revision: str = "0001_initial"
|
|
down_revision: Union[str, None] = None
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ------------------------------------------------------------------
|
|
# Extensions
|
|
# ------------------------------------------------------------------
|
|
op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto";')
|
|
op.execute('CREATE EXTENSION IF NOT EXISTS "citext";')
|
|
|
|
# ------------------------------------------------------------------
|
|
# Trigger functions
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION set_updated_at() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION ensure_user_exists() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.user_id IS NOT NULL THEN
|
|
INSERT INTO users (user_id) VALUES (NEW.user_id)
|
|
ON CONFLICT (user_id) DO NOTHING;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_message_attachment_refs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
UPDATE conversation_messages
|
|
SET attachments = array_remove(attachments, OLD.id)
|
|
WHERE OLD.id = ANY(attachments);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_agent_extra_source_refs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
UPDATE agents
|
|
SET extra_source_ids = array_remove(extra_source_ids, OLD.id)
|
|
WHERE OLD.id = ANY(extra_source_ids);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION cleanup_user_agent_prefs() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
DECLARE
|
|
agent_id_text text := OLD.id::text;
|
|
BEGIN
|
|
UPDATE users
|
|
SET agent_preferences = jsonb_set(
|
|
jsonb_set(
|
|
agent_preferences,
|
|
'{pinned}',
|
|
COALESCE((
|
|
SELECT jsonb_agg(e)
|
|
FROM jsonb_array_elements(
|
|
COALESCE(agent_preferences->'pinned', '[]'::jsonb)
|
|
) e
|
|
WHERE (e #>> '{}') <> agent_id_text
|
|
), '[]'::jsonb)
|
|
),
|
|
'{shared_with_me}',
|
|
COALESCE((
|
|
SELECT jsonb_agg(e)
|
|
FROM jsonb_array_elements(
|
|
COALESCE(agent_preferences->'shared_with_me', '[]'::jsonb)
|
|
) e
|
|
WHERE (e #>> '{}') <> agent_id_text
|
|
), '[]'::jsonb)
|
|
)
|
|
WHERE agent_preferences->'pinned' @> to_jsonb(agent_id_text)
|
|
OR agent_preferences->'shared_with_me' @> to_jsonb(agent_id_text);
|
|
RETURN OLD;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE FUNCTION conversation_messages_fill_user_id() RETURNS trigger
|
|
LANGUAGE plpgsql AS $$
|
|
BEGIN
|
|
IF NEW.user_id IS NULL THEN
|
|
SELECT user_id INTO NEW.user_id
|
|
FROM conversations
|
|
WHERE id = NEW.conversation_id;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
"""
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Tables
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL UNIQUE,
|
|
agent_preferences JSONB NOT NULL
|
|
DEFAULT '{"pinned": [], "shared_with_me": []}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE prompts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE user_tools (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
custom_name TEXT,
|
|
display_name TEXT,
|
|
description TEXT,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
config_requirements JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
actions JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
status BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE token_usage (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT,
|
|
api_key TEXT,
|
|
agent_id UUID,
|
|
prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
generated_tokens INTEGER NOT NULL DEFAULT 0,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE token_usage ADD CONSTRAINT token_usage_attribution_chk "
|
|
"CHECK (user_id IS NOT NULL OR api_key IS NOT NULL) NOT VALID;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE user_logs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
user_id TEXT,
|
|
endpoint TEXT,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
data JSONB,
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE stack_logs (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
activity_id TEXT NOT NULL,
|
|
endpoint TEXT,
|
|
level TEXT,
|
|
user_id TEXT,
|
|
api_key TEXT,
|
|
query TEXT,
|
|
stacks JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE agent_folders (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
parent_id UUID REFERENCES agent_folders(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE sources (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
language TEXT,
|
|
date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
model TEXT,
|
|
type TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
retriever TEXT,
|
|
sync_frequency TEXT,
|
|
tokens TEXT,
|
|
file_path TEXT,
|
|
remote_data JSONB,
|
|
directory_structure JSONB,
|
|
file_name_map JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE agents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
agent_type TEXT,
|
|
status TEXT NOT NULL,
|
|
key CITEXT UNIQUE,
|
|
image TEXT,
|
|
source_id UUID REFERENCES sources(id) ON DELETE SET NULL,
|
|
extra_source_ids UUID[] NOT NULL DEFAULT '{}',
|
|
chunks INTEGER,
|
|
retriever TEXT,
|
|
prompt_id UUID REFERENCES prompts(id) ON DELETE SET NULL,
|
|
tools JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
json_schema JSONB,
|
|
models JSONB,
|
|
default_model_id TEXT,
|
|
folder_id UUID REFERENCES agent_folders(id) ON DELETE SET NULL,
|
|
workflow_id UUID,
|
|
limited_token_mode BOOLEAN NOT NULL DEFAULT false,
|
|
token_limit INTEGER,
|
|
limited_request_mode BOOLEAN NOT NULL DEFAULT false,
|
|
request_limit INTEGER,
|
|
allow_system_prompt_override BOOLEAN NOT NULL DEFAULT false,
|
|
shared BOOLEAN NOT NULL DEFAULT false,
|
|
shared_token CITEXT UNIQUE,
|
|
shared_metadata JSONB,
|
|
incoming_webhook_token CITEXT UNIQUE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_used_at TIMESTAMPTZ,
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
op.execute(
|
|
"ALTER TABLE token_usage ADD CONSTRAINT token_usage_agent_fk "
|
|
"FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE SET NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE attachments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
upload_path TEXT NOT NULL,
|
|
mime_type TEXT,
|
|
size BIGINT,
|
|
content TEXT,
|
|
token_count INTEGER,
|
|
openai_file_id TEXT,
|
|
google_file_uri TEXT,
|
|
metadata JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE memories (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
path TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE todos (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
todo_id INTEGER,
|
|
title TEXT NOT NULL,
|
|
completed BOOLEAN NOT NULL DEFAULT false,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE notes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
tool_id UUID REFERENCES user_tools(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE connector_sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
provider TEXT NOT NULL,
|
|
server_url TEXT,
|
|
session_token TEXT UNIQUE,
|
|
user_email TEXT,
|
|
status TEXT,
|
|
token_info JSONB,
|
|
session_data JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
expires_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE conversations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL,
|
|
name TEXT,
|
|
api_key TEXT,
|
|
is_shared_usage BOOLEAN NOT NULL DEFAULT false,
|
|
shared_token TEXT,
|
|
date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
shared_with TEXT[] NOT NULL DEFAULT '{}'::text[],
|
|
compression_metadata JSONB,
|
|
legacy_mongo_id TEXT,
|
|
CONSTRAINT conversations_api_key_nonempty_chk
|
|
CHECK (api_key IS NULL OR api_key <> '')
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE conversation_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
position INTEGER NOT NULL,
|
|
prompt TEXT,
|
|
response TEXT,
|
|
thought TEXT,
|
|
sources JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
tool_calls JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
attachments UUID[] NOT NULL DEFAULT '{}'::uuid[],
|
|
model_id TEXT,
|
|
message_metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
feedback JSONB,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
user_id TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE shared_conversations (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
is_promptable BOOLEAN NOT NULL DEFAULT false,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
uuid UUID NOT NULL,
|
|
first_n_queries INTEGER NOT NULL DEFAULT 0,
|
|
api_key TEXT,
|
|
prompt_id UUID REFERENCES prompts(id) ON DELETE SET NULL,
|
|
chunks INTEGER,
|
|
CONSTRAINT shared_conversations_api_key_nonempty_chk
|
|
CHECK (api_key IS NULL OR api_key <> '')
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE pending_tool_state (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
messages JSONB NOT NULL,
|
|
pending_tool_calls JSONB NOT NULL,
|
|
tools_dict JSONB NOT NULL,
|
|
tool_schemas JSONB NOT NULL,
|
|
agent_config JSONB NOT NULL,
|
|
client_tools JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflows (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
current_graph_version INTEGER NOT NULL DEFAULT 1,
|
|
legacy_mongo_id TEXT
|
|
);
|
|
"""
|
|
)
|
|
# Backfill the agents.workflow_id FK now that workflows exists.
|
|
# The column was created without a FK (forward reference to a table
|
|
# that hadn't been declared yet); add the constraint here so workflow
|
|
# deletion still cascades through to agent unset.
|
|
op.execute(
|
|
"ALTER TABLE agents ADD CONSTRAINT agents_workflow_fk "
|
|
"FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE SET NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_nodes (
|
|
id UUID DEFAULT gen_random_uuid() NOT NULL,
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
graph_version INTEGER NOT NULL,
|
|
node_type TEXT NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
node_id TEXT NOT NULL,
|
|
title TEXT,
|
|
description TEXT,
|
|
position JSONB NOT NULL DEFAULT '{"x": 0, "y": 0}'::jsonb,
|
|
legacy_mongo_id TEXT,
|
|
PRIMARY KEY (id),
|
|
CONSTRAINT workflow_nodes_id_wf_ver_key
|
|
UNIQUE (id, workflow_id, graph_version)
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_edges (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
graph_version INTEGER NOT NULL,
|
|
from_node_id UUID NOT NULL,
|
|
to_node_id UUID NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
edge_id TEXT NOT NULL,
|
|
source_handle TEXT,
|
|
target_handle TEXT,
|
|
CONSTRAINT workflow_edges_from_node_fk
|
|
FOREIGN KEY (from_node_id, workflow_id, graph_version)
|
|
REFERENCES workflow_nodes(id, workflow_id, graph_version) ON DELETE CASCADE,
|
|
CONSTRAINT workflow_edges_to_node_fk
|
|
FOREIGN KEY (to_node_id, workflow_id, graph_version)
|
|
REFERENCES workflow_nodes(id, workflow_id, graph_version) ON DELETE CASCADE
|
|
);
|
|
"""
|
|
)
|
|
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE workflow_runs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
workflow_id UUID NOT NULL REFERENCES workflows(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL,
|
|
status TEXT NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ended_at TIMESTAMPTZ,
|
|
result JSONB,
|
|
inputs JSONB,
|
|
steps JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
legacy_mongo_id TEXT,
|
|
CONSTRAINT workflow_runs_status_chk
|
|
CHECK (status IN ('pending', 'running', 'completed', 'failed'))
|
|
);
|
|
"""
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Indexes
|
|
# ------------------------------------------------------------------
|
|
op.execute("CREATE INDEX agent_folders_user_idx ON agent_folders (user_id);")
|
|
|
|
op.execute("CREATE INDEX agents_user_idx ON agents (user_id);")
|
|
op.execute("CREATE INDEX agents_shared_idx ON agents (shared) WHERE shared = true;")
|
|
op.execute("CREATE INDEX agents_status_idx ON agents (status);")
|
|
op.execute("CREATE INDEX agents_source_id_idx ON agents (source_id);")
|
|
op.execute("CREATE INDEX agents_prompt_id_idx ON agents (prompt_id);")
|
|
op.execute("CREATE INDEX agents_folder_id_idx ON agents (folder_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX agents_legacy_mongo_id_uidx "
|
|
"ON agents (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX attachments_user_idx ON attachments (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX attachments_legacy_mongo_id_uidx "
|
|
"ON attachments (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
# MCP and OAuth connectors share the ``provider`` slot, so the
|
|
# dedup key is ``(user_id, server_url, provider)``: MCP rows
|
|
# differentiate by server_url (one per MCP server), OAuth rows
|
|
# have server_url = NULL and differentiate by provider alone.
|
|
# COALESCE lets NULL server_url participate in the constraint.
|
|
"CREATE UNIQUE INDEX connector_sessions_user_endpoint_uidx "
|
|
"ON connector_sessions (user_id, COALESCE(server_url, ''), provider);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX connector_sessions_expiry_idx "
|
|
"ON connector_sessions (expires_at) WHERE expires_at IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX connector_sessions_server_url_idx "
|
|
"ON connector_sessions (server_url) WHERE server_url IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX connector_sessions_legacy_mongo_id_uidx "
|
|
"ON connector_sessions (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversation_messages_conv_pos_uidx "
|
|
"ON conversation_messages (conversation_id, position);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX conversation_messages_user_ts_idx "
|
|
"ON conversation_messages (user_id, timestamp DESC);"
|
|
)
|
|
|
|
op.execute("CREATE INDEX conversations_user_date_idx ON conversations (user_id, date DESC);")
|
|
op.execute("CREATE INDEX conversations_agent_idx ON conversations (agent_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversations_shared_token_uidx "
|
|
"ON conversations (shared_token) WHERE shared_token IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX conversations_api_key_date_idx "
|
|
"ON conversations (api_key, date DESC) WHERE api_key IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX conversations_legacy_mongo_id_uidx "
|
|
"ON conversations (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX memories_user_tool_path_uidx "
|
|
"ON memories (user_id, tool_id, path);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX memories_user_path_null_tool_uidx "
|
|
"ON memories (user_id, path) WHERE tool_id IS NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX memories_path_prefix_idx "
|
|
"ON memories (user_id, tool_id, path text_pattern_ops);"
|
|
)
|
|
op.execute("CREATE INDEX memories_tool_id_idx ON memories (tool_id);")
|
|
|
|
op.execute("CREATE UNIQUE INDEX notes_user_tool_uidx ON notes (user_id, tool_id);")
|
|
op.execute("CREATE INDEX notes_tool_id_idx ON notes (tool_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX notes_legacy_mongo_id_uidx "
|
|
"ON notes (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX pending_tool_state_conv_user_uidx "
|
|
"ON pending_tool_state (conversation_id, user_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE INDEX pending_tool_state_expires_idx ON pending_tool_state (expires_at);"
|
|
)
|
|
|
|
op.execute("CREATE INDEX prompts_user_id_idx ON prompts (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX prompts_legacy_mongo_id_uidx "
|
|
"ON prompts (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX shared_conversations_user_idx ON shared_conversations (user_id);")
|
|
op.execute("CREATE INDEX shared_conversations_conv_idx ON shared_conversations (conversation_id);")
|
|
op.execute(
|
|
"CREATE INDEX shared_conversations_prompt_id_idx ON shared_conversations (prompt_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX shared_conversations_uuid_uidx ON shared_conversations (uuid);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX shared_conversations_dedup_uidx "
|
|
"ON shared_conversations (conversation_id, user_id, is_promptable, first_n_queries, COALESCE(api_key, ''));"
|
|
)
|
|
|
|
op.execute("CREATE INDEX sources_user_idx ON sources (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX sources_legacy_mongo_id_uidx "
|
|
"ON sources (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX user_tools_legacy_mongo_id_uidx "
|
|
"ON user_tools (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX agent_folders_legacy_mongo_id_uidx "
|
|
"ON agent_folders (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute("CREATE INDEX agent_folders_parent_idx ON agent_folders (parent_id);")
|
|
op.execute("CREATE INDEX agents_workflow_idx ON agents (workflow_id);")
|
|
|
|
op.execute('CREATE INDEX stack_logs_timestamp_idx ON stack_logs ("timestamp" DESC);')
|
|
op.execute('CREATE INDEX stack_logs_user_ts_idx ON stack_logs (user_id, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX stack_logs_level_ts_idx ON stack_logs (level, "timestamp" DESC);')
|
|
op.execute("CREATE INDEX stack_logs_activity_idx ON stack_logs (activity_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX stack_logs_mongo_id_uidx "
|
|
"ON stack_logs (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX todos_user_tool_idx ON todos (user_id, tool_id);")
|
|
op.execute("CREATE INDEX todos_tool_id_idx ON todos (tool_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX todos_legacy_mongo_id_uidx "
|
|
"ON todos (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX todos_tool_todo_id_uidx "
|
|
"ON todos (tool_id, todo_id) WHERE todo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute('CREATE INDEX token_usage_user_ts_idx ON token_usage (user_id, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX token_usage_key_ts_idx ON token_usage (api_key, "timestamp" DESC);')
|
|
op.execute('CREATE INDEX token_usage_agent_ts_idx ON token_usage (agent_id, "timestamp" DESC);')
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX token_usage_mongo_id_uidx "
|
|
"ON token_usage (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute('CREATE INDEX user_logs_user_ts_idx ON user_logs (user_id, "timestamp" DESC);')
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX user_logs_mongo_id_uidx "
|
|
"ON user_logs (mongo_id) WHERE mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX user_tools_user_id_idx ON user_tools (user_id);")
|
|
|
|
op.execute("CREATE INDEX workflow_edges_from_node_idx ON workflow_edges (from_node_id);")
|
|
op.execute("CREATE INDEX workflow_edges_to_node_idx ON workflow_edges (to_node_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_edges_wf_ver_eid_uidx "
|
|
"ON workflow_edges (workflow_id, graph_version, edge_id);"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_nodes_wf_ver_nid_uidx "
|
|
"ON workflow_nodes (workflow_id, graph_version, node_id);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_nodes_legacy_mongo_id_uidx "
|
|
"ON workflow_nodes (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX workflow_runs_workflow_idx ON workflow_runs (workflow_id);")
|
|
op.execute("CREATE INDEX workflow_runs_user_idx ON workflow_runs (user_id);")
|
|
op.execute(
|
|
"CREATE INDEX workflow_runs_status_started_idx "
|
|
"ON workflow_runs (status, started_at DESC);"
|
|
)
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflow_runs_legacy_mongo_id_uidx "
|
|
"ON workflow_runs (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
op.execute("CREATE INDEX workflows_user_idx ON workflows (user_id);")
|
|
op.execute(
|
|
"CREATE UNIQUE INDEX workflows_legacy_mongo_id_uidx "
|
|
"ON workflows (legacy_mongo_id) WHERE legacy_mongo_id IS NOT NULL;"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# user_id foreign keys (deferrable so backfills can stage rows)
|
|
# ------------------------------------------------------------------
|
|
user_fk_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"attachments",
|
|
"connector_sessions",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"pending_tool_state",
|
|
"prompts",
|
|
"shared_conversations",
|
|
"sources",
|
|
"stack_logs",
|
|
"todos",
|
|
"token_usage",
|
|
"user_logs",
|
|
"user_tools",
|
|
"workflow_runs",
|
|
"workflows",
|
|
)
|
|
for table in user_fk_tables:
|
|
op.execute(
|
|
f"ALTER TABLE {table} "
|
|
f"ADD CONSTRAINT {table}_user_id_fk "
|
|
f"FOREIGN KEY (user_id) REFERENCES users(user_id) "
|
|
f"ON DELETE RESTRICT DEFERRABLE INITIALLY IMMEDIATE;"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Triggers
|
|
# ------------------------------------------------------------------
|
|
updated_at_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"prompts",
|
|
"sources",
|
|
"todos",
|
|
"user_tools",
|
|
"users",
|
|
"workflows",
|
|
)
|
|
for table in updated_at_tables:
|
|
op.execute(
|
|
f"CREATE TRIGGER {table}_set_updated_at "
|
|
f"BEFORE UPDATE ON {table} "
|
|
f"FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) "
|
|
f"EXECUTE FUNCTION set_updated_at();"
|
|
)
|
|
|
|
ensure_user_tables = (
|
|
"agent_folders",
|
|
"agents",
|
|
"attachments",
|
|
"connector_sessions",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"memories",
|
|
"notes",
|
|
"pending_tool_state",
|
|
"prompts",
|
|
"shared_conversations",
|
|
"sources",
|
|
"stack_logs",
|
|
"todos",
|
|
"token_usage",
|
|
"user_logs",
|
|
"user_tools",
|
|
"workflow_runs",
|
|
"workflows",
|
|
)
|
|
for table in ensure_user_tables:
|
|
op.execute(
|
|
f"CREATE TRIGGER {table}_ensure_user "
|
|
f"BEFORE INSERT OR UPDATE OF user_id ON {table} "
|
|
f"FOR EACH ROW EXECUTE FUNCTION ensure_user_exists();"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE TRIGGER conversation_messages_fill_user "
|
|
"BEFORE INSERT ON conversation_messages "
|
|
"FOR EACH ROW EXECUTE FUNCTION conversation_messages_fill_user_id();"
|
|
)
|
|
|
|
op.execute(
|
|
"CREATE TRIGGER attachments_cleanup_message_refs "
|
|
"AFTER DELETE ON attachments "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_message_attachment_refs();"
|
|
)
|
|
op.execute(
|
|
"CREATE TRIGGER agents_cleanup_user_prefs "
|
|
"AFTER DELETE ON agents "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_user_agent_prefs();"
|
|
)
|
|
op.execute(
|
|
"CREATE TRIGGER sources_cleanup_agent_extra_refs "
|
|
"AFTER DELETE ON sources "
|
|
"FOR EACH ROW EXECUTE FUNCTION cleanup_agent_extra_source_refs();"
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Seed sentinel __system__ user (system/template sources attribute here)
|
|
# ------------------------------------------------------------------
|
|
op.execute(
|
|
"INSERT INTO users (user_id) VALUES ('__system__') "
|
|
"ON CONFLICT (user_id) DO NOTHING;"
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Nuclear downgrade: drop everything this migration created. The
|
|
# ordering drops FK-bearing children before parents; CASCADE would
|
|
# also work but explicit ordering is easier to reason about in code
|
|
# review.
|
|
tables_in_drop_order = (
|
|
"workflow_edges",
|
|
"workflow_runs",
|
|
"workflow_nodes",
|
|
"workflows",
|
|
"pending_tool_state",
|
|
"shared_conversations",
|
|
"conversation_messages",
|
|
"conversations",
|
|
"connector_sessions",
|
|
"notes",
|
|
"todos",
|
|
"memories",
|
|
"attachments",
|
|
"agents",
|
|
"sources",
|
|
"agent_folders",
|
|
"stack_logs",
|
|
"user_logs",
|
|
"token_usage",
|
|
"user_tools",
|
|
"prompts",
|
|
"users",
|
|
)
|
|
for table in tables_in_drop_order:
|
|
op.execute(f"DROP TABLE IF EXISTS {table} CASCADE;")
|
|
|
|
for fn in (
|
|
"conversation_messages_fill_user_id",
|
|
"cleanup_user_agent_prefs",
|
|
"cleanup_agent_extra_source_refs",
|
|
"cleanup_message_attachment_refs",
|
|
"ensure_user_exists",
|
|
"set_updated_at",
|
|
):
|
|
op.execute(f"DROP FUNCTION IF EXISTS {fn}();")
|