Spaces:
Running
Running
fix: Fixing requirements.txt inconsistencies
Browse files- Dockerfile +1 -0
- alembic.ini +149 -0
- alembic/README +1 -0
- alembic/env.py +130 -0
- alembic/script.py.mako +33 -0
- alembic/versions/046aabbd3fee_removing_refresh_tokens_table.py +47 -0
- alembic/versions/0f6538b84133_initial_supabase_schema.py +346 -0
- main.py +27 -4
- rag/rag.py +4 -0
- requirements.txt +12 -2
Dockerfile
CHANGED
|
@@ -6,6 +6,7 @@ WORKDIR /app
|
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
ffmpeg \
|
| 8 |
libsndfile1 \
|
|
|
|
| 9 |
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
|
| 11 |
COPY requirements.txt .
|
|
|
|
| 6 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 7 |
ffmpeg \
|
| 8 |
libsndfile1 \
|
| 9 |
+
git \
|
| 10 |
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
|
| 12 |
COPY requirements.txt .
|
alembic.ini
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A generic, single database configuration.
|
| 2 |
+
|
| 3 |
+
[alembic]
|
| 4 |
+
# path to migration scripts.
|
| 5 |
+
# this is typically a path given in POSIX (e.g. forward slashes)
|
| 6 |
+
# format, relative to the token %(here)s which refers to the location of this
|
| 7 |
+
# ini file
|
| 8 |
+
script_location = %(here)s/alembic
|
| 9 |
+
|
| 10 |
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
| 11 |
+
# Uncomment the line below if you want the files to be prepended with date and time
|
| 12 |
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
| 13 |
+
# for all available tokens
|
| 14 |
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
| 15 |
+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
| 16 |
+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
| 17 |
+
|
| 18 |
+
# sys.path path, will be prepended to sys.path if present.
|
| 19 |
+
# defaults to the current working directory. for multiple paths, the path separator
|
| 20 |
+
# is defined by "path_separator" below.
|
| 21 |
+
prepend_sys_path = .
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# timezone to use when rendering the date within the migration file
|
| 25 |
+
# as well as the filename.
|
| 26 |
+
# If specified, requires the tzdata library which can be installed by adding
|
| 27 |
+
# `alembic[tz]` to the pip requirements.
|
| 28 |
+
# string value is passed to ZoneInfo()
|
| 29 |
+
# leave blank for localtime
|
| 30 |
+
# timezone =
|
| 31 |
+
|
| 32 |
+
# max length of characters to apply to the "slug" field
|
| 33 |
+
# truncate_slug_length = 40
|
| 34 |
+
|
| 35 |
+
# set to 'true' to run the environment during
|
| 36 |
+
# the 'revision' command, regardless of autogenerate
|
| 37 |
+
# revision_environment = false
|
| 38 |
+
|
| 39 |
+
# set to 'true' to allow .pyc and .pyo files without
|
| 40 |
+
# a source .py file to be detected as revisions in the
|
| 41 |
+
# versions/ directory
|
| 42 |
+
# sourceless = false
|
| 43 |
+
|
| 44 |
+
# version location specification; This defaults
|
| 45 |
+
# to <script_location>/versions. When using multiple version
|
| 46 |
+
# directories, initial revisions must be specified with --version-path.
|
| 47 |
+
# The path separator used here should be the separator specified by "path_separator"
|
| 48 |
+
# below.
|
| 49 |
+
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
| 50 |
+
|
| 51 |
+
# path_separator; This indicates what character is used to split lists of file
|
| 52 |
+
# paths, including version_locations and prepend_sys_path within configparser
|
| 53 |
+
# files such as alembic.ini.
|
| 54 |
+
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
| 55 |
+
# to provide os-dependent path splitting.
|
| 56 |
+
#
|
| 57 |
+
# Note that in order to support legacy alembic.ini files, this default does NOT
|
| 58 |
+
# take place if path_separator is not present in alembic.ini. If this
|
| 59 |
+
# option is omitted entirely, fallback logic is as follows:
|
| 60 |
+
#
|
| 61 |
+
# 1. Parsing of the version_locations option falls back to using the legacy
|
| 62 |
+
# "version_path_separator" key, which if absent then falls back to the legacy
|
| 63 |
+
# behavior of splitting on spaces and/or commas.
|
| 64 |
+
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
| 65 |
+
# behavior of splitting on spaces, commas, or colons.
|
| 66 |
+
#
|
| 67 |
+
# Valid values for path_separator are:
|
| 68 |
+
#
|
| 69 |
+
# path_separator = :
|
| 70 |
+
# path_separator = ;
|
| 71 |
+
# path_separator = space
|
| 72 |
+
# path_separator = newline
|
| 73 |
+
#
|
| 74 |
+
# Use os.pathsep. Default configuration used for new projects.
|
| 75 |
+
path_separator = os
|
| 76 |
+
|
| 77 |
+
# set to 'true' to search source files recursively
|
| 78 |
+
# in each "version_locations" directory
|
| 79 |
+
# new in Alembic version 1.10
|
| 80 |
+
# recursive_version_locations = false
|
| 81 |
+
|
| 82 |
+
# the output encoding used when revision files
|
| 83 |
+
# are written from script.py.mako
|
| 84 |
+
# output_encoding = utf-8
|
| 85 |
+
|
| 86 |
+
# database URL. This is consumed by the user-maintained env.py script only.
|
| 87 |
+
# other means of configuring database URLs may be customized within the env.py
|
| 88 |
+
# file.
|
| 89 |
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
[post_write_hooks]
|
| 93 |
+
# post_write_hooks defines scripts or Python functions that are run
|
| 94 |
+
# on newly generated revision scripts. See the documentation for further
|
| 95 |
+
# detail and examples
|
| 96 |
+
|
| 97 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 98 |
+
# hooks = black
|
| 99 |
+
# black.type = console_scripts
|
| 100 |
+
# black.entrypoint = black
|
| 101 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 102 |
+
|
| 103 |
+
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
| 104 |
+
# hooks = ruff
|
| 105 |
+
# ruff.type = module
|
| 106 |
+
# ruff.module = ruff
|
| 107 |
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 108 |
+
|
| 109 |
+
# Alternatively, use the exec runner to execute a binary found on your PATH
|
| 110 |
+
# hooks = ruff
|
| 111 |
+
# ruff.type = exec
|
| 112 |
+
# ruff.executable = ruff
|
| 113 |
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 114 |
+
|
| 115 |
+
# Logging configuration. This is also consumed by the user-maintained
|
| 116 |
+
# env.py script only.
|
| 117 |
+
[loggers]
|
| 118 |
+
keys = root,sqlalchemy,alembic
|
| 119 |
+
|
| 120 |
+
[handlers]
|
| 121 |
+
keys = console
|
| 122 |
+
|
| 123 |
+
[formatters]
|
| 124 |
+
keys = generic
|
| 125 |
+
|
| 126 |
+
[logger_root]
|
| 127 |
+
level = WARNING
|
| 128 |
+
handlers = console
|
| 129 |
+
qualname =
|
| 130 |
+
|
| 131 |
+
[logger_sqlalchemy]
|
| 132 |
+
level = WARNING
|
| 133 |
+
handlers =
|
| 134 |
+
qualname = sqlalchemy.engine
|
| 135 |
+
|
| 136 |
+
[logger_alembic]
|
| 137 |
+
level = INFO
|
| 138 |
+
handlers =
|
| 139 |
+
qualname = alembic
|
| 140 |
+
|
| 141 |
+
[handler_console]
|
| 142 |
+
class = StreamHandler
|
| 143 |
+
args = (sys.stderr,)
|
| 144 |
+
level = NOTSET
|
| 145 |
+
formatter = generic
|
| 146 |
+
|
| 147 |
+
[formatter_generic]
|
| 148 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 149 |
+
datefmt = %H:%M:%S
|
alembic/README
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
Generic single-database configuration.
|
alembic/env.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import sys
|
| 4 |
+
import urllib.parse
|
| 5 |
+
from logging.config import fileConfig
|
| 6 |
+
|
| 7 |
+
from sqlalchemy import engine_from_config, pool
|
| 8 |
+
from alembic import context
|
| 9 |
+
|
| 10 |
+
# Ensure both src and project root / app root are on sys.path
|
| 11 |
+
_here = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
_parent = os.path.abspath(os.path.join(_here, ".."))
|
| 13 |
+
_grandparent = os.path.abspath(os.path.join(_parent, ".."))
|
| 14 |
+
for _p in (_grandparent, _parent):
|
| 15 |
+
if _p and _p not in sys.path:
|
| 16 |
+
sys.path.insert(0, _p)
|
| 17 |
+
|
| 18 |
+
from src.config import settings
|
| 19 |
+
from src.db_models import Base
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# this is the Alembic Config object, which provides
|
| 23 |
+
# access to the values within the .ini file in use.
|
| 24 |
+
config = context.config
|
| 25 |
+
|
| 26 |
+
# Interpret the config file for Python logging.
|
| 27 |
+
if config.config_file_name is not None:
|
| 28 |
+
fileConfig(config.config_file_name)
|
| 29 |
+
|
| 30 |
+
# Set target metadata for autogenerate support
|
| 31 |
+
target_metadata = Base.metadata
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def sanitize_db_url(url: str) -> str:
|
| 35 |
+
"""Sanitize database URL by stripping pgbouncer flags and URL-encoding password special chars."""
|
| 36 |
+
if not url or "://" not in url:
|
| 37 |
+
return url
|
| 38 |
+
if "pgbouncer=true" in url:
|
| 39 |
+
url = url.replace("?pgbouncer=true&", "?").replace("&pgbouncer=true", "").replace("?pgbouncer=true", "")
|
| 40 |
+
|
| 41 |
+
# URL-encode un-encoded special characters in password
|
| 42 |
+
pattern = r"^(?P<scheme>[^:]+://)(?P<user>[^:]+):(?P<password>.+)@(?P<host>[^@:]+:\d+/.+)$"
|
| 43 |
+
m = re.match(pattern, url)
|
| 44 |
+
if m:
|
| 45 |
+
scheme = m.group("scheme")
|
| 46 |
+
user = m.group("user")
|
| 47 |
+
password = m.group("password")
|
| 48 |
+
host = m.group("host")
|
| 49 |
+
if "%" not in password:
|
| 50 |
+
password = urllib.parse.quote(password, safe="")
|
| 51 |
+
return f"{scheme}{user}:{password}@{host}"
|
| 52 |
+
return url
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# Dynamically inject DATABASE_URL from config.env / settings
|
| 56 |
+
db_url = sanitize_db_url(settings.database_url or os.getenv("DATABASE_URL", ""))
|
| 57 |
+
|
| 58 |
+
if db_url:
|
| 59 |
+
# Escape % as %% for configparser interpolation
|
| 60 |
+
config.set_main_option("sqlalchemy.url", db_url.replace("%", "%%"))
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def run_migrations_offline() -> None:
|
| 64 |
+
"""Run migrations in 'offline' mode."""
|
| 65 |
+
context.configure(
|
| 66 |
+
url=db_url,
|
| 67 |
+
target_metadata=target_metadata,
|
| 68 |
+
literal_binds=True,
|
| 69 |
+
dialect_opts={"paramstyle": "named"},
|
| 70 |
+
compare_type=my_compare_type,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
with context.begin_transaction():
|
| 74 |
+
context.run_migrations()
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def my_compare_type(context, inspected_column, metadata_column, inspected_type, metadata_type):
|
| 78 |
+
# Ignore equivalent string/text type differences between Supabase TEXT and SQLAlchemy String
|
| 79 |
+
ins_type = type(inspected_type).__name__.upper()
|
| 80 |
+
meta_type = type(metadata_type).__name__.upper()
|
| 81 |
+
if ins_type in ("TEXT", "VARCHAR", "STRING") and meta_type in ("TEXT", "VARCHAR", "STRING"):
|
| 82 |
+
return False
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def include_object(object, name, type_, reflected, compare_to):
|
| 87 |
+
"""
|
| 88 |
+
Only report table-level additions and removals.
|
| 89 |
+
Ignore column, index, FK, and constraint differences on existing tables
|
| 90 |
+
to avoid noise from schema drift between Supabase native schema and ORM models.
|
| 91 |
+
"""
|
| 92 |
+
# Always include tables (so new/removed tables are detected)
|
| 93 |
+
if type_ == "table":
|
| 94 |
+
return True
|
| 95 |
+
# Skip everything else on tables that already exist in the DB
|
| 96 |
+
return False
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def run_migrations_online() -> None:
|
| 100 |
+
"""Run migrations in 'online' mode."""
|
| 101 |
+
if not db_url:
|
| 102 |
+
return
|
| 103 |
+
|
| 104 |
+
configuration = config.get_section(config.config_ini_section, {})
|
| 105 |
+
if db_url:
|
| 106 |
+
configuration["sqlalchemy.url"] = db_url
|
| 107 |
+
|
| 108 |
+
connectable = engine_from_config(
|
| 109 |
+
configuration,
|
| 110 |
+
prefix="sqlalchemy.",
|
| 111 |
+
poolclass=pool.NullPool,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
with connectable.connect() as connection:
|
| 115 |
+
context.configure(
|
| 116 |
+
connection=connection,
|
| 117 |
+
target_metadata=target_metadata,
|
| 118 |
+
compare_type=False,
|
| 119 |
+
compare_nullable=False,
|
| 120 |
+
include_object=include_object,
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
with context.begin_transaction():
|
| 124 |
+
context.run_migrations()
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
if context.is_offline_mode():
|
| 128 |
+
run_migrations_offline()
|
| 129 |
+
else:
|
| 130 |
+
run_migrations_online()
|
alembic/script.py.mako
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""${message}
|
| 2 |
+
|
| 3 |
+
Revision ID: ${up_revision}
|
| 4 |
+
Revises: ${down_revision | comma,n}
|
| 5 |
+
Create Date: ${create_date}
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
try:
|
| 13 |
+
import pgvector
|
| 14 |
+
except ImportError:
|
| 15 |
+
pass
|
| 16 |
+
${imports if imports else ""}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# revision identifiers, used by Alembic.
|
| 20 |
+
revision: str = ${repr(up_revision)}
|
| 21 |
+
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
| 22 |
+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
| 23 |
+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def upgrade() -> None:
|
| 27 |
+
"""Upgrade schema."""
|
| 28 |
+
${upgrades if upgrades else "pass"}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def downgrade() -> None:
|
| 32 |
+
"""Downgrade schema."""
|
| 33 |
+
${downgrades if downgrades else "pass"}
|
alembic/versions/046aabbd3fee_removing_refresh_tokens_table.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Removing Refresh Tokens Table
|
| 2 |
+
|
| 3 |
+
Revision ID: 046aabbd3fee
|
| 4 |
+
Revises: 0f6538b84133
|
| 5 |
+
Create Date: 2026-08-15 06:27:29.825530
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
try:
|
| 13 |
+
import pgvector
|
| 14 |
+
except ImportError:
|
| 15 |
+
pass
|
| 16 |
+
from sqlalchemy.dialects import postgresql
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# revision identifiers, used by Alembic.
|
| 20 |
+
revision: str = '046aabbd3fee'
|
| 21 |
+
down_revision: Union[str, Sequence[str], None] = '0f6538b84133'
|
| 22 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 23 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def upgrade() -> None:
|
| 27 |
+
"""Upgrade schema."""
|
| 28 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 29 |
+
op.drop_table('refresh_tokens')
|
| 30 |
+
# ### end Alembic commands ###
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def downgrade() -> None:
|
| 34 |
+
"""Downgrade schema."""
|
| 35 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 36 |
+
op.create_table('refresh_tokens',
|
| 37 |
+
sa.Column('id', sa.UUID(), server_default=sa.text('gen_random_uuid()'), autoincrement=False, nullable=False),
|
| 38 |
+
sa.Column('user_id', sa.UUID(), autoincrement=False, nullable=False),
|
| 39 |
+
sa.Column('token_hash', sa.TEXT(), autoincrement=False, nullable=False),
|
| 40 |
+
sa.Column('expires_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
|
| 41 |
+
sa.Column('revoked', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False),
|
| 42 |
+
sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False),
|
| 43 |
+
sa.ForeignKeyConstraint(['user_id'], ['profiles.id'], name=op.f('refresh_tokens_user_id_fkey'), ondelete='CASCADE'),
|
| 44 |
+
sa.PrimaryKeyConstraint('id', name=op.f('refresh_tokens_pkey')),
|
| 45 |
+
sa.UniqueConstraint('token_hash', name=op.f('refresh_tokens_token_hash_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
|
| 46 |
+
)
|
| 47 |
+
# ### end Alembic commands ###
|
alembic/versions/0f6538b84133_initial_supabase_schema.py
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Initial Supabase schema
|
| 2 |
+
|
| 3 |
+
Revision ID: 0f6538b84133
|
| 4 |
+
Revises:
|
| 5 |
+
Create Date: 2026-08-09 22:55:09.716607
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
from sqlalchemy.dialects import postgresql
|
| 13 |
+
import pgvector
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# revision identifiers, used by Alembic.
|
| 17 |
+
revision: str = '0f6538b84133'
|
| 18 |
+
down_revision: Union[str, Sequence[str], None] = None
|
| 19 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 20 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def upgrade() -> None:
|
| 24 |
+
"""Upgrade schema."""
|
| 25 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 26 |
+
op.alter_column('chat_messages', 'role',
|
| 27 |
+
existing_type=sa.TEXT(),
|
| 28 |
+
type_=sa.String(),
|
| 29 |
+
nullable=True)
|
| 30 |
+
op.alter_column('chat_messages', 'content',
|
| 31 |
+
existing_type=sa.TEXT(),
|
| 32 |
+
nullable=True)
|
| 33 |
+
op.alter_column('chat_messages', 'retrieved_chunk_ids',
|
| 34 |
+
existing_type=postgresql.ARRAY(sa.UUID()),
|
| 35 |
+
type_=postgresql.JSONB(astext_type=sa.Text()),
|
| 36 |
+
existing_nullable=True)
|
| 37 |
+
op.drop_index(op.f('idx_chat_messages_session_id'), table_name='chat_messages')
|
| 38 |
+
op.alter_column('chat_sessions', 'material_id',
|
| 39 |
+
existing_type=sa.UUID(),
|
| 40 |
+
nullable=False)
|
| 41 |
+
op.alter_column('chat_sessions', 'title',
|
| 42 |
+
existing_type=sa.TEXT(),
|
| 43 |
+
type_=sa.String(),
|
| 44 |
+
existing_nullable=True)
|
| 45 |
+
op.drop_index(op.f('idx_chat_sessions_material_id'), table_name='chat_sessions')
|
| 46 |
+
op.drop_index(op.f('idx_chat_sessions_user_id'), table_name='chat_sessions')
|
| 47 |
+
op.drop_constraint(op.f('chat_sessions_material_id_fkey'), 'chat_sessions', type_='foreignkey')
|
| 48 |
+
op.create_foreign_key(None, 'chat_sessions', 'materials', ['material_id'], ['id'], ondelete='CASCADE')
|
| 49 |
+
op.drop_index(op.f('idx_material_chunks_material_id'), table_name='material_chunks')
|
| 50 |
+
op.drop_constraint(op.f('material_chunks_material_id_chunk_index_key'), 'material_chunks', type_='unique')
|
| 51 |
+
op.alter_column('material_embeddings', 'embedding',
|
| 52 |
+
existing_type=pgvector.sqlalchemy.vector.VECTOR(dim=384),
|
| 53 |
+
nullable=True)
|
| 54 |
+
op.drop_index(op.f('idx_embeddings_chunk_id'), table_name='material_embeddings')
|
| 55 |
+
op.drop_index(op.f('idx_embeddings_material_id'), table_name='material_embeddings')
|
| 56 |
+
op.drop_index(op.f('idx_embeddings_vector'), table_name='material_embeddings', postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 57 |
+
op.alter_column('materials', 'source_type',
|
| 58 |
+
existing_type=sa.TEXT(),
|
| 59 |
+
type_=sa.String(),
|
| 60 |
+
existing_nullable=False)
|
| 61 |
+
op.alter_column('materials', 'title',
|
| 62 |
+
existing_type=sa.TEXT(),
|
| 63 |
+
type_=sa.String(),
|
| 64 |
+
existing_nullable=False)
|
| 65 |
+
op.alter_column('materials', 'file_path',
|
| 66 |
+
existing_type=sa.TEXT(),
|
| 67 |
+
type_=sa.String(),
|
| 68 |
+
existing_nullable=True)
|
| 69 |
+
op.alter_column('materials', 'url',
|
| 70 |
+
existing_type=sa.TEXT(),
|
| 71 |
+
type_=sa.String(),
|
| 72 |
+
existing_nullable=True)
|
| 73 |
+
op.alter_column('materials', 'status',
|
| 74 |
+
existing_type=sa.TEXT(),
|
| 75 |
+
type_=sa.String(),
|
| 76 |
+
existing_nullable=False,
|
| 77 |
+
existing_server_default=sa.text("'pending'::text"))
|
| 78 |
+
op.alter_column('materials', 'error_message',
|
| 79 |
+
existing_type=sa.TEXT(),
|
| 80 |
+
type_=sa.String(),
|
| 81 |
+
existing_nullable=True)
|
| 82 |
+
op.alter_column('materials', 'vector_store_path',
|
| 83 |
+
existing_type=sa.TEXT(),
|
| 84 |
+
type_=sa.String(),
|
| 85 |
+
existing_nullable=True)
|
| 86 |
+
op.alter_column('materials', 'title_normalized',
|
| 87 |
+
existing_type=sa.TEXT(),
|
| 88 |
+
type_=sa.String(),
|
| 89 |
+
existing_nullable=True,
|
| 90 |
+
existing_server_default=sa.Computed('normalize_arabic(lower(title))', persisted=True))
|
| 91 |
+
op.drop_index(op.f('idx_materials_status'), table_name='materials')
|
| 92 |
+
op.drop_index(op.f('idx_materials_title_normalized_trgm'), table_name='materials', postgresql_using='gin')
|
| 93 |
+
op.drop_index(op.f('idx_materials_title_trgm'), table_name='materials', postgresql_using='gin')
|
| 94 |
+
op.drop_index(op.f('idx_materials_user_id'), table_name='materials')
|
| 95 |
+
op.drop_constraint(op.f('materials_user_id_title_key'), 'materials', type_='unique')
|
| 96 |
+
op.alter_column('profiles', 'email',
|
| 97 |
+
existing_type=sa.TEXT(),
|
| 98 |
+
type_=sa.String(),
|
| 99 |
+
existing_nullable=True)
|
| 100 |
+
op.alter_column('profiles', 'display_name',
|
| 101 |
+
existing_type=sa.TEXT(),
|
| 102 |
+
type_=sa.String(),
|
| 103 |
+
existing_nullable=True)
|
| 104 |
+
op.alter_column('profiles', 'avatar_url',
|
| 105 |
+
existing_type=sa.TEXT(),
|
| 106 |
+
type_=sa.String(),
|
| 107 |
+
existing_nullable=True)
|
| 108 |
+
op.alter_column('profiles', 'last_request_date',
|
| 109 |
+
existing_type=sa.DATE(),
|
| 110 |
+
type_=sa.String(),
|
| 111 |
+
existing_nullable=True)
|
| 112 |
+
op.alter_column('profiles', 'theme',
|
| 113 |
+
existing_type=sa.TEXT(),
|
| 114 |
+
type_=sa.String(),
|
| 115 |
+
existing_nullable=False,
|
| 116 |
+
existing_server_default=sa.text("'system'::text"))
|
| 117 |
+
op.drop_constraint(op.f('profiles_id_fkey'), 'profiles', type_='foreignkey')
|
| 118 |
+
op.alter_column('quiz_attempts', 'score',
|
| 119 |
+
existing_type=sa.INTEGER(),
|
| 120 |
+
nullable=True)
|
| 121 |
+
op.alter_column('quiz_attempts', 'total',
|
| 122 |
+
existing_type=sa.INTEGER(),
|
| 123 |
+
nullable=True)
|
| 124 |
+
op.alter_column('quiz_attempts', 'results',
|
| 125 |
+
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
| 126 |
+
nullable=True)
|
| 127 |
+
op.drop_index(op.f('idx_quiz_attempts_quiz_id'), table_name='quiz_attempts')
|
| 128 |
+
op.alter_column('quizzes', 'source_type',
|
| 129 |
+
existing_type=sa.TEXT(),
|
| 130 |
+
type_=sa.String(),
|
| 131 |
+
nullable=True)
|
| 132 |
+
op.alter_column('quizzes', 'difficulty',
|
| 133 |
+
existing_type=sa.TEXT(),
|
| 134 |
+
type_=sa.String(),
|
| 135 |
+
nullable=True)
|
| 136 |
+
op.alter_column('quizzes', 'mcq_count',
|
| 137 |
+
existing_type=sa.INTEGER(),
|
| 138 |
+
nullable=True)
|
| 139 |
+
op.alter_column('quizzes', 'tf_count',
|
| 140 |
+
existing_type=sa.INTEGER(),
|
| 141 |
+
nullable=True)
|
| 142 |
+
op.alter_column('quizzes', 'quiz_data',
|
| 143 |
+
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
| 144 |
+
nullable=True)
|
| 145 |
+
op.alter_column('quizzes', 'status',
|
| 146 |
+
existing_type=sa.TEXT(),
|
| 147 |
+
type_=sa.String(),
|
| 148 |
+
existing_nullable=False,
|
| 149 |
+
existing_server_default=sa.text("'completed'::text"))
|
| 150 |
+
op.alter_column('quizzes', 'model_name',
|
| 151 |
+
existing_type=sa.TEXT(),
|
| 152 |
+
type_=sa.String(),
|
| 153 |
+
existing_nullable=True)
|
| 154 |
+
op.alter_column('quizzes', 'error_message',
|
| 155 |
+
existing_type=sa.TEXT(),
|
| 156 |
+
type_=sa.String(),
|
| 157 |
+
existing_nullable=True)
|
| 158 |
+
op.drop_index(op.f('idx_quizzes_material_id'), table_name='quizzes')
|
| 159 |
+
op.drop_index(op.f('idx_quizzes_user_id'), table_name='quizzes')
|
| 160 |
+
op.drop_constraint(op.f('quizzes_material_id_fkey'), 'quizzes', type_='foreignkey')
|
| 161 |
+
op.create_foreign_key(None, 'quizzes', 'materials', ['material_id'], ['id'], ondelete='CASCADE')
|
| 162 |
+
op.alter_column('refresh_tokens', 'token_hash',
|
| 163 |
+
existing_type=sa.TEXT(),
|
| 164 |
+
type_=sa.String(),
|
| 165 |
+
existing_nullable=False)
|
| 166 |
+
op.drop_index(op.f('idx_refresh_tokens_token_hash'), table_name='refresh_tokens')
|
| 167 |
+
op.drop_index(op.f('idx_refresh_tokens_user_id'), table_name='refresh_tokens')
|
| 168 |
+
op.alter_column('summaries', 'status',
|
| 169 |
+
existing_type=sa.TEXT(),
|
| 170 |
+
type_=sa.String(),
|
| 171 |
+
existing_nullable=False,
|
| 172 |
+
existing_server_default=sa.text("'completed'::text"))
|
| 173 |
+
op.alter_column('summaries', 'model_name',
|
| 174 |
+
existing_type=sa.TEXT(),
|
| 175 |
+
type_=sa.String(),
|
| 176 |
+
existing_nullable=True)
|
| 177 |
+
op.alter_column('summaries', 'error_message',
|
| 178 |
+
existing_type=sa.TEXT(),
|
| 179 |
+
type_=sa.String(),
|
| 180 |
+
existing_nullable=True)
|
| 181 |
+
op.drop_index(op.f('idx_summaries_material_id'), table_name='summaries')
|
| 182 |
+
op.drop_constraint(op.f('summaries_material_id_key'), 'summaries', type_='unique')
|
| 183 |
+
# ### end Alembic commands ###
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def downgrade() -> None:
|
| 187 |
+
"""Downgrade schema."""
|
| 188 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 189 |
+
op.create_unique_constraint(op.f('summaries_material_id_key'), 'summaries', ['material_id'], postgresql_nulls_not_distinct=False)
|
| 190 |
+
op.create_index(op.f('idx_summaries_material_id'), 'summaries', ['material_id'], unique=False)
|
| 191 |
+
op.alter_column('summaries', 'error_message',
|
| 192 |
+
existing_type=sa.String(),
|
| 193 |
+
type_=sa.TEXT(),
|
| 194 |
+
existing_nullable=True)
|
| 195 |
+
op.alter_column('summaries', 'model_name',
|
| 196 |
+
existing_type=sa.String(),
|
| 197 |
+
type_=sa.TEXT(),
|
| 198 |
+
existing_nullable=True)
|
| 199 |
+
op.alter_column('summaries', 'status',
|
| 200 |
+
existing_type=sa.String(),
|
| 201 |
+
type_=sa.TEXT(),
|
| 202 |
+
existing_nullable=False,
|
| 203 |
+
existing_server_default=sa.text("'completed'::text"))
|
| 204 |
+
op.create_index(op.f('idx_refresh_tokens_user_id'), 'refresh_tokens', ['user_id'], unique=False)
|
| 205 |
+
op.create_index(op.f('idx_refresh_tokens_token_hash'), 'refresh_tokens', ['token_hash'], unique=False)
|
| 206 |
+
op.alter_column('refresh_tokens', 'token_hash',
|
| 207 |
+
existing_type=sa.String(),
|
| 208 |
+
type_=sa.TEXT(),
|
| 209 |
+
existing_nullable=False)
|
| 210 |
+
op.drop_constraint(None, 'quizzes', type_='foreignkey')
|
| 211 |
+
op.create_foreign_key(op.f('quizzes_material_id_fkey'), 'quizzes', 'materials', ['material_id'], ['id'], ondelete='SET NULL')
|
| 212 |
+
op.create_index(op.f('idx_quizzes_user_id'), 'quizzes', ['user_id'], unique=False)
|
| 213 |
+
op.create_index(op.f('idx_quizzes_material_id'), 'quizzes', ['material_id'], unique=False)
|
| 214 |
+
op.alter_column('quizzes', 'error_message',
|
| 215 |
+
existing_type=sa.String(),
|
| 216 |
+
type_=sa.TEXT(),
|
| 217 |
+
existing_nullable=True)
|
| 218 |
+
op.alter_column('quizzes', 'model_name',
|
| 219 |
+
existing_type=sa.String(),
|
| 220 |
+
type_=sa.TEXT(),
|
| 221 |
+
existing_nullable=True)
|
| 222 |
+
op.alter_column('quizzes', 'status',
|
| 223 |
+
existing_type=sa.String(),
|
| 224 |
+
type_=sa.TEXT(),
|
| 225 |
+
existing_nullable=False,
|
| 226 |
+
existing_server_default=sa.text("'completed'::text"))
|
| 227 |
+
op.alter_column('quizzes', 'quiz_data',
|
| 228 |
+
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
| 229 |
+
nullable=False)
|
| 230 |
+
op.alter_column('quizzes', 'tf_count',
|
| 231 |
+
existing_type=sa.INTEGER(),
|
| 232 |
+
nullable=False)
|
| 233 |
+
op.alter_column('quizzes', 'mcq_count',
|
| 234 |
+
existing_type=sa.INTEGER(),
|
| 235 |
+
nullable=False)
|
| 236 |
+
op.alter_column('quizzes', 'difficulty',
|
| 237 |
+
existing_type=sa.String(),
|
| 238 |
+
type_=sa.TEXT(),
|
| 239 |
+
nullable=False)
|
| 240 |
+
op.alter_column('quizzes', 'source_type',
|
| 241 |
+
existing_type=sa.String(),
|
| 242 |
+
type_=sa.TEXT(),
|
| 243 |
+
nullable=False)
|
| 244 |
+
op.create_index(op.f('idx_quiz_attempts_quiz_id'), 'quiz_attempts', ['quiz_id'], unique=False)
|
| 245 |
+
op.alter_column('quiz_attempts', 'results',
|
| 246 |
+
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
| 247 |
+
nullable=False)
|
| 248 |
+
op.alter_column('quiz_attempts', 'total',
|
| 249 |
+
existing_type=sa.INTEGER(),
|
| 250 |
+
nullable=False)
|
| 251 |
+
op.alter_column('quiz_attempts', 'score',
|
| 252 |
+
existing_type=sa.INTEGER(),
|
| 253 |
+
nullable=False)
|
| 254 |
+
op.create_foreign_key(op.f('profiles_id_fkey'), 'profiles', 'users', ['id'], ['id'], referent_schema='auth', ondelete='CASCADE')
|
| 255 |
+
op.alter_column('profiles', 'theme',
|
| 256 |
+
existing_type=sa.String(),
|
| 257 |
+
type_=sa.TEXT(),
|
| 258 |
+
existing_nullable=False,
|
| 259 |
+
existing_server_default=sa.text("'system'::text"))
|
| 260 |
+
op.alter_column('profiles', 'last_request_date',
|
| 261 |
+
existing_type=sa.String(),
|
| 262 |
+
type_=sa.DATE(),
|
| 263 |
+
existing_nullable=True)
|
| 264 |
+
op.alter_column('profiles', 'avatar_url',
|
| 265 |
+
existing_type=sa.String(),
|
| 266 |
+
type_=sa.TEXT(),
|
| 267 |
+
existing_nullable=True)
|
| 268 |
+
op.alter_column('profiles', 'display_name',
|
| 269 |
+
existing_type=sa.String(),
|
| 270 |
+
type_=sa.TEXT(),
|
| 271 |
+
existing_nullable=True)
|
| 272 |
+
op.alter_column('profiles', 'email',
|
| 273 |
+
existing_type=sa.String(),
|
| 274 |
+
type_=sa.TEXT(),
|
| 275 |
+
existing_nullable=True)
|
| 276 |
+
op.create_unique_constraint(op.f('materials_user_id_title_key'), 'materials', ['user_id', 'title'], postgresql_nulls_not_distinct=False)
|
| 277 |
+
op.create_index(op.f('idx_materials_user_id'), 'materials', ['user_id'], unique=False)
|
| 278 |
+
op.create_index(op.f('idx_materials_title_trgm'), 'materials', ['title'], unique=False, postgresql_using='gin')
|
| 279 |
+
op.create_index(op.f('idx_materials_title_normalized_trgm'), 'materials', ['title_normalized'], unique=False, postgresql_using='gin')
|
| 280 |
+
op.create_index(op.f('idx_materials_status'), 'materials', ['status'], unique=False)
|
| 281 |
+
op.alter_column('materials', 'title_normalized',
|
| 282 |
+
existing_type=sa.String(),
|
| 283 |
+
type_=sa.TEXT(),
|
| 284 |
+
existing_nullable=True,
|
| 285 |
+
existing_server_default=sa.Computed('normalize_arabic(lower(title))', persisted=True))
|
| 286 |
+
op.alter_column('materials', 'vector_store_path',
|
| 287 |
+
existing_type=sa.String(),
|
| 288 |
+
type_=sa.TEXT(),
|
| 289 |
+
existing_nullable=True)
|
| 290 |
+
op.alter_column('materials', 'error_message',
|
| 291 |
+
existing_type=sa.String(),
|
| 292 |
+
type_=sa.TEXT(),
|
| 293 |
+
existing_nullable=True)
|
| 294 |
+
op.alter_column('materials', 'status',
|
| 295 |
+
existing_type=sa.String(),
|
| 296 |
+
type_=sa.TEXT(),
|
| 297 |
+
existing_nullable=False,
|
| 298 |
+
existing_server_default=sa.text("'pending'::text"))
|
| 299 |
+
op.alter_column('materials', 'url',
|
| 300 |
+
existing_type=sa.String(),
|
| 301 |
+
type_=sa.TEXT(),
|
| 302 |
+
existing_nullable=True)
|
| 303 |
+
op.alter_column('materials', 'file_path',
|
| 304 |
+
existing_type=sa.String(),
|
| 305 |
+
type_=sa.TEXT(),
|
| 306 |
+
existing_nullable=True)
|
| 307 |
+
op.alter_column('materials', 'title',
|
| 308 |
+
existing_type=sa.String(),
|
| 309 |
+
type_=sa.TEXT(),
|
| 310 |
+
existing_nullable=False)
|
| 311 |
+
op.alter_column('materials', 'source_type',
|
| 312 |
+
existing_type=sa.String(),
|
| 313 |
+
type_=sa.TEXT(),
|
| 314 |
+
existing_nullable=False)
|
| 315 |
+
op.create_index(op.f('idx_embeddings_vector'), 'material_embeddings', ['embedding'], unique=False, postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 316 |
+
op.create_index(op.f('idx_embeddings_material_id'), 'material_embeddings', ['material_id'], unique=False)
|
| 317 |
+
op.create_index(op.f('idx_embeddings_chunk_id'), 'material_embeddings', ['chunk_id'], unique=False)
|
| 318 |
+
op.alter_column('material_embeddings', 'embedding',
|
| 319 |
+
existing_type=pgvector.sqlalchemy.vector.VECTOR(dim=384),
|
| 320 |
+
nullable=False)
|
| 321 |
+
op.create_unique_constraint(op.f('material_chunks_material_id_chunk_index_key'), 'material_chunks', ['material_id', 'chunk_index'], postgresql_nulls_not_distinct=False)
|
| 322 |
+
op.create_index(op.f('idx_material_chunks_material_id'), 'material_chunks', ['material_id'], unique=False)
|
| 323 |
+
op.drop_constraint(None, 'chat_sessions', type_='foreignkey')
|
| 324 |
+
op.create_foreign_key(op.f('chat_sessions_material_id_fkey'), 'chat_sessions', 'materials', ['material_id'], ['id'], ondelete='SET NULL')
|
| 325 |
+
op.create_index(op.f('idx_chat_sessions_user_id'), 'chat_sessions', ['user_id'], unique=False)
|
| 326 |
+
op.create_index(op.f('idx_chat_sessions_material_id'), 'chat_sessions', ['material_id'], unique=False)
|
| 327 |
+
op.alter_column('chat_sessions', 'title',
|
| 328 |
+
existing_type=sa.String(),
|
| 329 |
+
type_=sa.TEXT(),
|
| 330 |
+
existing_nullable=True)
|
| 331 |
+
op.alter_column('chat_sessions', 'material_id',
|
| 332 |
+
existing_type=sa.UUID(),
|
| 333 |
+
nullable=True)
|
| 334 |
+
op.create_index(op.f('idx_chat_messages_session_id'), 'chat_messages', ['session_id'], unique=False)
|
| 335 |
+
op.alter_column('chat_messages', 'retrieved_chunk_ids',
|
| 336 |
+
existing_type=postgresql.JSONB(astext_type=sa.Text()),
|
| 337 |
+
type_=postgresql.ARRAY(sa.UUID()),
|
| 338 |
+
existing_nullable=True)
|
| 339 |
+
op.alter_column('chat_messages', 'content',
|
| 340 |
+
existing_type=sa.TEXT(),
|
| 341 |
+
nullable=False)
|
| 342 |
+
op.alter_column('chat_messages', 'role',
|
| 343 |
+
existing_type=sa.String(),
|
| 344 |
+
type_=sa.TEXT(),
|
| 345 |
+
nullable=False)
|
| 346 |
+
# ### end Alembic commands ###
|
main.py
CHANGED
|
@@ -28,10 +28,33 @@ from src.config import settings
|
|
| 28 |
async def lifespan(app: FastAPI):
|
| 29 |
# Run pending Alembic database migrations automatically on server startup
|
| 30 |
try:
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
logger.warning(f"Database migration step failed or skipped: {e}")
|
| 37 |
|
|
|
|
| 28 |
async def lifespan(app: FastAPI):
|
| 29 |
# Run pending Alembic database migrations automatically on server startup
|
| 30 |
try:
|
| 31 |
+
db_url = settings.database_url or os.getenv("DATABASE_URL", "")
|
| 32 |
+
if not db_url:
|
| 33 |
+
logger.info("DATABASE_URL not configured; skipping automatic Alembic migrations.")
|
| 34 |
+
else:
|
| 35 |
+
from alembic.config import Config
|
| 36 |
+
from alembic import command
|
| 37 |
+
|
| 38 |
+
candidate_ini = None
|
| 39 |
+
for p in [
|
| 40 |
+
os.path.join(current_dir, "alembic.ini"),
|
| 41 |
+
os.path.join(parent_dir, "alembic.ini"),
|
| 42 |
+
os.path.abspath("alembic.ini"),
|
| 43 |
+
]:
|
| 44 |
+
if os.path.isfile(p):
|
| 45 |
+
candidate_ini = p
|
| 46 |
+
break
|
| 47 |
+
|
| 48 |
+
if candidate_ini:
|
| 49 |
+
logger.info(f"Running database migrations via Alembic ({candidate_ini})...")
|
| 50 |
+
alembic_cfg = Config(candidate_ini)
|
| 51 |
+
script_dir = os.path.join(os.path.dirname(candidate_ini), "alembic")
|
| 52 |
+
if os.path.isdir(script_dir):
|
| 53 |
+
alembic_cfg.set_main_option("script_location", script_dir)
|
| 54 |
+
command.upgrade(alembic_cfg, "head")
|
| 55 |
+
logger.info("Database migrations completed successfully.")
|
| 56 |
+
else:
|
| 57 |
+
logger.warning("alembic.ini not found; skipping database migrations.")
|
| 58 |
except Exception as e:
|
| 59 |
logger.warning(f"Database migration step failed or skipped: {e}")
|
| 60 |
|
rag/rag.py
CHANGED
|
@@ -56,6 +56,10 @@ from .schemas import EmbeddingJob
|
|
| 56 |
|
| 57 |
@lru_cache
|
| 58 |
def get_embedder():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
return HuggingFaceEmbeddings(
|
| 60 |
model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
| 61 |
model_kwargs={"device": "cpu"},
|
|
|
|
| 56 |
|
| 57 |
@lru_cache
|
| 58 |
def get_embedder():
|
| 59 |
+
try:
|
| 60 |
+
import sentence_transformers
|
| 61 |
+
except Exception as exc:
|
| 62 |
+
logger.error(f"Failed to import sentence_transformers directly: {exc}", exc_info=True)
|
| 63 |
return HuggingFaceEmbeddings(
|
| 64 |
model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
|
| 65 |
model_kwargs={"device": "cpu"},
|
requirements.txt
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
torch
|
| 3 |
torchvision
|
|
|
|
| 4 |
|
| 5 |
arxiv==2.2.0
|
| 6 |
wikipedia==1.4.0
|
|
@@ -16,9 +17,16 @@ mistralai
|
|
| 16 |
langchain-openai
|
| 17 |
langchain-huggingface
|
| 18 |
langchain-text-splitters
|
|
|
|
| 19 |
python-dotenv
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
pdfplumber==0.11.4
|
| 24 |
unstructured==0.18.15
|
|
@@ -35,9 +43,11 @@ passlib[bcrypt]>=1.7.4
|
|
| 35 |
supabase
|
| 36 |
redis>=5.0.0
|
| 37 |
transformers
|
|
|
|
| 38 |
sacremoses
|
| 39 |
sentencepiece
|
| 40 |
omegaconf
|
|
|
|
| 41 |
|
| 42 |
# ββ ASR Models ββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
nemo_toolkit[asr]
|
|
|
|
| 1 |
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
torch
|
| 3 |
torchvision
|
| 4 |
+
torchaudio
|
| 5 |
|
| 6 |
arxiv==2.2.0
|
| 7 |
wikipedia==1.4.0
|
|
|
|
| 17 |
langchain-openai
|
| 18 |
langchain-huggingface
|
| 19 |
langchain-text-splitters
|
| 20 |
+
langsmith
|
| 21 |
python-dotenv
|
| 22 |
+
|
| 23 |
+
# ββ Retrieval & Search ββββββββββββββββββββββββββββββββββββ
|
| 24 |
+
sentence-transformers>=3.0.0
|
| 25 |
+
rank-bm25>=0.2.2
|
| 26 |
+
|
| 27 |
+
# ββ Binary Compatibility (NumPy & PyArrow) ββββββββββββββββ
|
| 28 |
+
numpy>=1.26.4,<2.0.0
|
| 29 |
+
pyarrow>=14.0.0,<18.0.0
|
| 30 |
|
| 31 |
pdfplumber==0.11.4
|
| 32 |
unstructured==0.18.15
|
|
|
|
| 43 |
supabase
|
| 44 |
redis>=5.0.0
|
| 45 |
transformers
|
| 46 |
+
einops
|
| 47 |
sacremoses
|
| 48 |
sentencepiece
|
| 49 |
omegaconf
|
| 50 |
+
onnxruntime>=1.17.0
|
| 51 |
|
| 52 |
# ββ ASR Models ββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
nemo_toolkit[asr]
|