Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ This setup involves the setting of both the frontend and backend components. We

#### Database Schema

The database automatically creates the following tables:
- `conversations` - Stores conversation metadata (id, user_id, title, timestamps)
The database uses [Alembic](https://alembic.sqlalchemy.org/) for schema migrations. On startup, the backend automatically applies any pending migrations. The following tables are managed:
- `conversations` - Stores conversation metadata (uuid, title, timestamps)
- `messages` - Stores individual messages within conversations

**Database Migration Commands** (from `backend/`):
```bash
make db-migrate # Apply pending migrations
make db-revision msg="description" # Generate a new migration from model changes
make db-downgrade # Roll back one migration
make db-history # Show migration history
make db-current # Show current revision
```

#### Setting Up PostgreSQL Database Variables

The backend uses PostgreSQL for conversation persistence. Configure these database variables in your `.env` file:
Expand Down
20 changes: 20 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ mcp:
.PHONY: chat
chat:
@uv run python -m chatbot

.PHONY: db-migrate
db-migrate:
@uv run alembic upgrade head

.PHONY: db-revision
db-revision:
@uv run alembic revision --autogenerate -m "$(msg)"

.PHONY: db-downgrade
db-downgrade:
@uv run alembic downgrade -1

.PHONY: db-history
db-history:
@uv run alembic history

.PHONY: db-current
db-current:
@uv run alembic current
43 changes: 43 additions & 0 deletions backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[alembic]
script_location = src/database/alembic
prepend_sys_path = .
version_path_separator = os

# sqlalchemy.url is set programmatically in env.py
sqlalchemy.url =

[post_write_hooks]

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"alembic>=1.13.0",
"asyncpg>=0.30.0",
"cryptography>=46.0.5",
"faiss-cpu==1.12.0",
Expand Down
52 changes: 52 additions & 0 deletions backend/src/database/alembic/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from logging.config import fileConfig

from sqlalchemy import engine_from_config, pool

from alembic import context

from src.database.config import get_database_url
from src.database.models import Base

config = context.config

if config.config_file_name is not None:
fileConfig(config.config_file_name, disable_existing_loggers=False)

config.set_main_option("sqlalchemy.url", get_database_url())

target_metadata = Base.metadata


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions backend/src/database/alembic/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
56 changes: 56 additions & 0 deletions backend/src/database/alembic/versions/0001_initial_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Initial schema

Revision ID: 0001
Revises:
Create Date: 2026-03-01

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


revision: str = "0001"
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:
op.create_table(
"conversations",
sa.Column("uuid", sa.Uuid(), primary_key=True),
sa.Column("title", sa.String(500), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
)

op.create_table(
"messages",
sa.Column("uuid", sa.Uuid(), primary_key=True),
sa.Column(
"conversation_uuid",
sa.Uuid(),
sa.ForeignKey("conversations.uuid", ondelete="CASCADE"),
nullable=False,
),
sa.Column("role", sa.String(50), nullable=False),
sa.Column("content", sa.Text(), nullable=False),
sa.Column("context_sources", sa.JSON(), nullable=True),
sa.Column("tools", sa.JSON(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
)

op.create_index(
"ix_messages_conversation_uuid",
"messages",
["conversation_uuid"],
)


def downgrade() -> None:
op.drop_index("ix_messages_conversation_uuid", table_name="messages")
op.drop_table("messages")
op.drop_table("conversations")
42 changes: 34 additions & 8 deletions backend/src/database/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import os
import logging
from pathlib import Path
from typing import Generator, Optional
from sqlalchemy import create_engine, inspect, text, Engine
from sqlalchemy.orm import sessionmaker, Session
Expand Down Expand Up @@ -40,6 +41,32 @@ def is_database_available() -> bool:
return False


def run_migrations() -> None:
"""Run Alembic migrations to bring the database schema up to date.

Auto-detects pre-Alembic databases (app tables exist but no alembic_version
table) and stamps them with the current head revision so that future
migrations apply cleanly.
"""
from alembic.config import Config
from alembic import command

alembic_ini = Path(__file__).resolve().parents[2] / "alembic.ini"
alembic_cfg = Config(str(alembic_ini))

# Stamp pre-Alembic databases so migrations don't try to recreate tables
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
has_app_tables = "conversations" in existing_tables
has_alembic = "alembic_version" in existing_tables

if has_app_tables and not has_alembic:
logger.info("Pre-Alembic database detected — stamping with current head.")
command.stamp(alembic_cfg, "head")
else:
command.upgrade(alembic_cfg, "head")


def init_database() -> bool:
global engine, SessionLocal, _db_initialized

Expand All @@ -64,15 +91,14 @@ def init_database() -> bool:
logger.warning("Database is not available. Will retry on next access.")
return False

inspector = inspect(engine)
existing_tables = inspector.get_table_names()

if not existing_tables or "conversations" not in existing_tables:
logger.info("Initializing database tables...")
try:
run_migrations()
except Exception:
logger.warning(
"Alembic migration failed; falling back to create_all for table creation.",
exc_info=True,
)
Base.metadata.create_all(bind=engine)
logger.info("Database tables created successfully")
else:
logger.debug("Database tables already exist")

_db_initialized = True
return True
Expand Down
Loading