Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,405 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 563ae5ac1d0d
|
||||
Revises:
|
||||
Create Date: 2026-07-18 14:41:47.620604
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import pgvector.sqlalchemy
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "563ae5ac1d0d"
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"departments",
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name"),
|
||||
)
|
||||
op.create_table(
|
||||
"jobs",
|
||||
sa.Column("type", sa.String(length=100), nullable=False),
|
||||
sa.Column("payload", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"pending",
|
||||
"running",
|
||||
"done",
|
||||
"failed",
|
||||
name="jobstatus",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"run_after",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("last_error", sa.Text(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_jobs_status_run_after", "jobs", ["status", "run_after"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"templates",
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column("version", sa.String(length=20), nullable=False),
|
||||
sa.Column("config", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column("is_builtin", sa.Boolean(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("email", sa.String(length=320), nullable=False),
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column(
|
||||
"role",
|
||||
sa.Enum("member", "admin", name="userrole", native_enum=False, length=32),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=False),
|
||||
sa.Column("department_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["department_id"], ["departments.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||
op.create_table(
|
||||
"auth_sessions",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_auth_sessions_user_id"), "auth_sessions", ["user_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"documents",
|
||||
sa.Column("title", sa.String(length=500), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"draft",
|
||||
"published",
|
||||
"archived",
|
||||
name="documentstatus",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"visibility",
|
||||
sa.Enum(
|
||||
"public",
|
||||
"department",
|
||||
"restricted",
|
||||
name="documentvisibility",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("content_md", sa.Text(), nullable=False),
|
||||
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column("author_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("department_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["author_id"], ["users.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["department_id"], ["departments.id"], ondelete="SET NULL"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"chunks",
|
||||
sa.Column("document_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("chunk_index", sa.Integer(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"embedding", pgvector.sqlalchemy.vector.VECTOR(dim=1024), nullable=False
|
||||
),
|
||||
sa.Column(
|
||||
"tsv",
|
||||
postgresql.TSVECTOR(),
|
||||
sa.Computed(
|
||||
"to_tsvector('german'::regconfig, "
|
||||
"content || ' ' || coalesce(meta->>'heading_path', ''))",
|
||||
persisted=True,
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("meta", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("document_id", "chunk_index"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_chunks_document_id"), "chunks", ["document_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
"ix_chunks_embedding_hnsw",
|
||||
"chunks",
|
||||
["embedding"],
|
||||
unique=False,
|
||||
postgresql_using="hnsw",
|
||||
postgresql_ops={"embedding": "vector_cosine_ops"},
|
||||
)
|
||||
op.create_index(
|
||||
"ix_chunks_tsv", "chunks", ["tsv"], unique=False, postgresql_using="gin"
|
||||
)
|
||||
op.create_table(
|
||||
"conversations",
|
||||
sa.Column(
|
||||
"mode",
|
||||
sa.Enum(
|
||||
"query",
|
||||
"insight",
|
||||
name="conversationmode",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"active",
|
||||
"completed",
|
||||
"abandoned",
|
||||
name="conversationstatus",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_conversations_user_id"), "conversations", ["user_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"doc_permissions",
|
||||
sa.Column("document_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("department_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"level",
|
||||
sa.Enum("read", name="permissionlevel", native_enum=False, length=32),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["department_id"], ["departments.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["document_id"], ["documents.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("document_id", "department_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"messages",
|
||||
sa.Column("conversation_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"role",
|
||||
sa.Enum(
|
||||
"user",
|
||||
"assistant",
|
||||
"system",
|
||||
name="messagerole",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["conversation_id"], ["conversations.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_messages_conversation_id"),
|
||||
"messages",
|
||||
["conversation_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_messages_conversation_id"), table_name="messages")
|
||||
op.drop_table("messages")
|
||||
op.drop_table("doc_permissions")
|
||||
op.drop_index(op.f("ix_conversations_user_id"), table_name="conversations")
|
||||
op.drop_table("conversations")
|
||||
op.drop_index("ix_chunks_tsv", table_name="chunks", postgresql_using="gin")
|
||||
op.drop_index(
|
||||
"ix_chunks_embedding_hnsw",
|
||||
table_name="chunks",
|
||||
postgresql_using="hnsw",
|
||||
postgresql_ops={"embedding": "vector_cosine_ops"},
|
||||
)
|
||||
op.drop_index(op.f("ix_chunks_document_id"), table_name="chunks")
|
||||
op.drop_table("chunks")
|
||||
op.drop_table("documents")
|
||||
op.drop_index(op.f("ix_auth_sessions_user_id"), table_name="auth_sessions")
|
||||
op.drop_table("auth_sessions")
|
||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||
op.drop_table("users")
|
||||
op.drop_table("templates")
|
||||
op.drop_index("ix_jobs_status_run_after", table_name="jobs")
|
||||
op.drop_table("jobs")
|
||||
op.drop_table("departments")
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user