feat: revamped project structure
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +13 -25
- README.md +2 -2
- alembic.ini +0 -147
- alembic/README +0 -1
- alembic/env.py +0 -91
- alembic/script.py.mako +0 -28
- alembic/versions/0724098f2d79_add_embeddings_column_with_server_.py +0 -34
- alembic/versions/293bf8cae101_add_conversation_history_table.py +0 -50
- alembic/versions/2bae362756b9_version_9.py +0 -58
- alembic/versions/2cfae3938d63_change_embeddings_table_structure.py +0 -32
- alembic/versions/3bdda54c971f_update_embedding_dimension_to_3072.py +0 -39
- alembic/versions/4de3dfd809bd_add_sources_to_conversation_history.py +0 -40
- alembic/versions/50eb9bd827e5_version_8.py +0 -32
- alembic/versions/51088f15a504_version_5.py +0 -32
- alembic/versions/576915939aff_version_4.py +0 -109
- alembic/versions/5dd0716c12e2_version_2.py +0 -32
- alembic/versions/9c071425ee4a_version_3.py +0 -32
- alembic/versions/af5827742674_change_embedding_column_type.py +0 -38
- alembic/versions/bbbad8eb92e1_version_7.py +0 -44
- alembic/versions/c4f1502f712e_create_embeddings_table_and_vector_.py +0 -78
- alembic/versions/c5e48c9bf293_initial.py +0 -79
- alembic/versions/c85ba4afc960_add_new_users_table.py +0 -50
- alembic/versions/d77bd9f8ce2f_version_10.py +0 -51
- alembic/versions/e817f10d0cc8_rename_metadata_column_on_embeddings_.py +0 -44
- alembic/versions/f8746e269698_version_6.py +0 -70
- kb_docs.py +19 -0
- main.py +79 -0
- onnx_model_optimized/config.json +0 -46
- onnx_model_optimized/ort_config.json +0 -33
- onnx_model_optimized/special_tokens_map.json +0 -51
- onnx_model_optimized/tokenizer.json +0 -0
- onnx_model_optimized/tokenizer_config.json +0 -57
- pytest.ini +0 -2
- requirements.txt +6 -125
- src/app/__init__.py +0 -0
- src/app/celery_app.py +0 -54
- src/app/config.py +0 -122
- src/app/crud.py +0 -783
- src/app/db/base.py +0 -3
- src/app/db/session.py +0 -58
- src/app/limiter.py +0 -4
- src/app/logging_config.py +0 -42
- src/app/main.py +0 -211
- src/app/models.py +0 -329
- src/app/routers/__init__.py +0 -0
- src/app/routers/auth.py +0 -36
- src/app/routers/chats.py +0 -63
- src/app/routers/dashboard.py +0 -83
- src/app/routers/rag.py +0 -120
- src/app/routers/rag_streamed.py +0 -75
Dockerfile
CHANGED
|
@@ -1,19 +1,7 @@
|
|
| 1 |
-
# Use Python 3.10 slim (Standard for HF Spaces)
|
| 2 |
FROM python:3.11-slim
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
# 1. Install system dependencies + Redis Server
|
| 7 |
-
USER root
|
| 8 |
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
-
build-essential \
|
| 10 |
-
redis-server \
|
| 11 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
-
|
| 13 |
-
RUN useradd -m -u 1000 user && \
|
| 14 |
-
mkdir -p /var/run/redis /var/log/redis && \
|
| 15 |
-
chown -R user:user /var/run/redis /var/log/redis /var/lib/redis
|
| 16 |
-
|
| 17 |
USER user
|
| 18 |
ENV HOME=/home/user \
|
| 19 |
PATH=/home/user/.local/bin:$PATH \
|
|
@@ -21,20 +9,20 @@ ENV HOME=/home/user \
|
|
| 21 |
PYTHONDONTWRITEBYTECODE=1 \
|
| 22 |
PYTHONUNBUFFERED=1
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
|
|
|
| 26 |
COPY --chown=user requirements.txt .
|
| 27 |
-
RUN pip install --no-cache-dir --upgrade
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
-
# We copy everything in the current directory (src, onnx_model_optimized, alembic)
|
| 31 |
-
COPY --chown=user . .
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
|
| 36 |
-
#
|
| 37 |
EXPOSE 7860
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
CMD [".
|
|
|
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
# Create a non-root user with UID 1000 as required by HF Spaces
|
| 4 |
+
RUN useradd -m -u 1000 user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
USER user
|
| 6 |
ENV HOME=/home/user \
|
| 7 |
PATH=/home/user/.local/bin:$PATH \
|
|
|
|
| 9 |
PYTHONDONTWRITEBYTECODE=1 \
|
| 10 |
PYTHONUNBUFFERED=1
|
| 11 |
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# Install Python dependencies
|
| 15 |
COPY --chown=user requirements.txt .
|
| 16 |
+
RUN pip install --no-cache-dir --upgrade pip && \
|
| 17 |
+
pip install --no-cache-dir -r requirements.txt
|
| 18 |
|
| 19 |
+
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('all-MiniLM-L6-v2')"
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
# Copy all application files into the container
|
| 22 |
+
COPY --chown=user . .
|
| 23 |
|
| 24 |
+
# Expose the mandatory Hugging Face port
|
| 25 |
EXPOSE 7860
|
| 26 |
|
| 27 |
+
# Run FastAPI directly in the foreground
|
| 28 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -8,5 +8,5 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
#
|
| 12 |
-
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Joule Dynamics RAG Demo
|
| 12 |
+
Backend API for the Amara Home & Kitchen RAG chatbot.
|
alembic.ini
DELETED
|
@@ -1,147 +0,0 @@
|
|
| 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 |
-
|
| 16 |
-
# sys.path path, will be prepended to sys.path if present.
|
| 17 |
-
# defaults to the current working directory. for multiple paths, the path separator
|
| 18 |
-
# is defined by "path_separator" below.
|
| 19 |
-
prepend_sys_path = .
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
# timezone to use when rendering the date within the migration file
|
| 23 |
-
# as well as the filename.
|
| 24 |
-
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
|
| 25 |
-
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
| 26 |
-
# string value is passed to ZoneInfo()
|
| 27 |
-
# leave blank for localtime
|
| 28 |
-
# timezone =
|
| 29 |
-
|
| 30 |
-
# max length of characters to apply to the "slug" field
|
| 31 |
-
# truncate_slug_length = 40
|
| 32 |
-
|
| 33 |
-
# set to 'true' to run the environment during
|
| 34 |
-
# the 'revision' command, regardless of autogenerate
|
| 35 |
-
# revision_environment = false
|
| 36 |
-
|
| 37 |
-
# set to 'true' to allow .pyc and .pyo files without
|
| 38 |
-
# a source .py file to be detected as revisions in the
|
| 39 |
-
# versions/ directory
|
| 40 |
-
# sourceless = false
|
| 41 |
-
|
| 42 |
-
# version location specification; This defaults
|
| 43 |
-
# to <script_location>/versions. When using multiple version
|
| 44 |
-
# directories, initial revisions must be specified with --version-path.
|
| 45 |
-
# The path separator used here should be the separator specified by "path_separator"
|
| 46 |
-
# below.
|
| 47 |
-
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
| 48 |
-
|
| 49 |
-
# path_separator; This indicates what character is used to split lists of file
|
| 50 |
-
# paths, including version_locations and prepend_sys_path within configparser
|
| 51 |
-
# files such as alembic.ini.
|
| 52 |
-
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
| 53 |
-
# to provide os-dependent path splitting.
|
| 54 |
-
#
|
| 55 |
-
# Note that in order to support legacy alembic.ini files, this default does NOT
|
| 56 |
-
# take place if path_separator is not present in alembic.ini. If this
|
| 57 |
-
# option is omitted entirely, fallback logic is as follows:
|
| 58 |
-
#
|
| 59 |
-
# 1. Parsing of the version_locations option falls back to using the legacy
|
| 60 |
-
# "version_path_separator" key, which if absent then falls back to the legacy
|
| 61 |
-
# behavior of splitting on spaces and/or commas.
|
| 62 |
-
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
| 63 |
-
# behavior of splitting on spaces, commas, or colons.
|
| 64 |
-
#
|
| 65 |
-
# Valid values for path_separator are:
|
| 66 |
-
#
|
| 67 |
-
# path_separator = :
|
| 68 |
-
# path_separator = ;
|
| 69 |
-
# path_separator = space
|
| 70 |
-
# path_separator = newline
|
| 71 |
-
#
|
| 72 |
-
# Use os.pathsep. Default configuration used for new projects.
|
| 73 |
-
path_separator = os
|
| 74 |
-
|
| 75 |
-
# set to 'true' to search source files recursively
|
| 76 |
-
# in each "version_locations" directory
|
| 77 |
-
# new in Alembic version 1.10
|
| 78 |
-
# recursive_version_locations = false
|
| 79 |
-
|
| 80 |
-
# the output encoding used when revision files
|
| 81 |
-
# are written from script.py.mako
|
| 82 |
-
# output_encoding = utf-8
|
| 83 |
-
|
| 84 |
-
# database URL. This is consumed by the user-maintained env.py script only.
|
| 85 |
-
# other means of configuring database URLs may be customized within the env.py
|
| 86 |
-
# file.
|
| 87 |
-
sqlalchemy.url =
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
[post_write_hooks]
|
| 91 |
-
# post_write_hooks defines scripts or Python functions that are run
|
| 92 |
-
# on newly generated revision scripts. See the documentation for further
|
| 93 |
-
# detail and examples
|
| 94 |
-
|
| 95 |
-
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 96 |
-
# hooks = black
|
| 97 |
-
# black.type = console_scripts
|
| 98 |
-
# black.entrypoint = black
|
| 99 |
-
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 100 |
-
|
| 101 |
-
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
| 102 |
-
# hooks = ruff
|
| 103 |
-
# ruff.type = module
|
| 104 |
-
# ruff.module = ruff
|
| 105 |
-
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 106 |
-
|
| 107 |
-
# Alternatively, use the exec runner to execute a binary found on your PATH
|
| 108 |
-
# hooks = ruff
|
| 109 |
-
# ruff.type = exec
|
| 110 |
-
# ruff.executable = ruff
|
| 111 |
-
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 112 |
-
|
| 113 |
-
# Logging configuration. This is also consumed by the user-maintained
|
| 114 |
-
# env.py script only.
|
| 115 |
-
[loggers]
|
| 116 |
-
keys = root,sqlalchemy,alembic
|
| 117 |
-
|
| 118 |
-
[handlers]
|
| 119 |
-
keys = console
|
| 120 |
-
|
| 121 |
-
[formatters]
|
| 122 |
-
keys = generic
|
| 123 |
-
|
| 124 |
-
[logger_root]
|
| 125 |
-
level = WARNING
|
| 126 |
-
handlers = console
|
| 127 |
-
qualname =
|
| 128 |
-
|
| 129 |
-
[logger_sqlalchemy]
|
| 130 |
-
level = WARNING
|
| 131 |
-
handlers =
|
| 132 |
-
qualname = sqlalchemy.engine
|
| 133 |
-
|
| 134 |
-
[logger_alembic]
|
| 135 |
-
level = INFO
|
| 136 |
-
handlers =
|
| 137 |
-
qualname = alembic
|
| 138 |
-
|
| 139 |
-
[handler_console]
|
| 140 |
-
class = StreamHandler
|
| 141 |
-
args = (sys.stderr,)
|
| 142 |
-
level = NOTSET
|
| 143 |
-
formatter = generic
|
| 144 |
-
|
| 145 |
-
[formatter_generic]
|
| 146 |
-
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 147 |
-
datefmt = %H:%M:%S
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/README
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Generic single-database configuration.
|
|
|
|
|
|
alembic/env.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
| 1 |
-
from logging.config import fileConfig
|
| 2 |
-
import asyncio
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
from sqlalchemy import engine_from_config
|
| 6 |
-
from sqlalchemy import pool
|
| 7 |
-
from sqlalchemy.engine import Connection
|
| 8 |
-
from sqlalchemy.ext.asyncio import create_async_engine
|
| 9 |
-
|
| 10 |
-
from alembic import context
|
| 11 |
-
|
| 12 |
-
from src.app.db.base import Base
|
| 13 |
-
from src.app.config import settings
|
| 14 |
-
from src.app import models
|
| 15 |
-
# this is the Alembic Config object, which provides
|
| 16 |
-
# access to the values within the .ini file in use.
|
| 17 |
-
config = context.config # type: ignore[attr-defined]
|
| 18 |
-
|
| 19 |
-
# Interpret the config file for Python logging.
|
| 20 |
-
# This line sets up loggers basically.
|
| 21 |
-
if config.config_file_name is not None:
|
| 22 |
-
fileConfig(config.config_file_name)
|
| 23 |
-
|
| 24 |
-
# add your model's MetaData object here
|
| 25 |
-
# for 'autogenerate' support
|
| 26 |
-
# from myapp import mymodel
|
| 27 |
-
# target_metadata = mymodel.Base.metadata
|
| 28 |
-
target_metadata = Base.metadata
|
| 29 |
-
|
| 30 |
-
# other values from the config, defined by the needs of env.py,
|
| 31 |
-
# can be acquired:
|
| 32 |
-
# my_important_option = config.get_main_option("my_important_option")
|
| 33 |
-
# ... etc.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def run_migrations_offline() -> None:
|
| 37 |
-
"""Run migrations in 'offline' mode.
|
| 38 |
-
|
| 39 |
-
This configures the context with just a URL
|
| 40 |
-
and not an Engine, though an Engine is acceptable
|
| 41 |
-
here as well. By skipping the Engine creation
|
| 42 |
-
we don't even need a DBAPI to be available.
|
| 43 |
-
|
| 44 |
-
Calls to context.execute() here emit the given string to the
|
| 45 |
-
script output.
|
| 46 |
-
|
| 47 |
-
"""
|
| 48 |
-
url = str(settings.DATABASE_URL)
|
| 49 |
-
context.configure(
|
| 50 |
-
url=url,
|
| 51 |
-
target_metadata=target_metadata,
|
| 52 |
-
literal_binds=True,
|
| 53 |
-
dialect_opts={"paramstyle": "named"},
|
| 54 |
-
) # type: ignore[attr-defined]
|
| 55 |
-
|
| 56 |
-
with context.begin_transaction():
|
| 57 |
-
context.run_migrations()
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def do_run_migrations(connection: Connection) -> None:
|
| 61 |
-
context.configure(
|
| 62 |
-
connection=connection, target_metadata=target_metadata
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
with context.begin_transaction():
|
| 66 |
-
context.run_migrations()
|
| 67 |
-
|
| 68 |
-
async def run_migrations_online() -> None:
|
| 69 |
-
"""Run migrations in 'online' mode.
|
| 70 |
-
|
| 71 |
-
In this scenario we need to create an Engine
|
| 72 |
-
and associate a connection with the context.
|
| 73 |
-
|
| 74 |
-
"""
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
connectable = create_async_engine(
|
| 78 |
-
str(settings.DATABASE_URL),
|
| 79 |
-
poolclass=pool.NullPool,
|
| 80 |
-
connect_args={"command_timeout": 300},
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
async with connectable.connect() as connection:
|
| 84 |
-
await connection.run_sync(do_run_migrations)
|
| 85 |
-
await connectable.dispose()
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
if context.is_offline_mode():
|
| 89 |
-
run_migrations_offline()
|
| 90 |
-
else:
|
| 91 |
-
asyncio.run(run_migrations_online())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/script.py.mako
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 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 |
-
${imports if imports else ""}
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = ${repr(up_revision)}
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
${upgrades if upgrades else "pass"}
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def downgrade() -> None:
|
| 27 |
-
"""Downgrade schema."""
|
| 28 |
-
${downgrades if downgrades else "pass"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/0724098f2d79_add_embeddings_column_with_server_.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
"""add embeddings column with server default
|
| 2 |
-
|
| 3 |
-
Revision ID: 0724098f2d79
|
| 4 |
-
Revises: 2cfae3938d63
|
| 5 |
-
Create Date: 2025-12-03 10:08:06.156818
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '0724098f2d79'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '2cfae3938d63'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('chats', sa.Column('embeddings_status', sa.String(length=30), server_default='pending', nullable=False))
|
| 25 |
-
op.create_index(op.f('ix_chats_embeddings_status'), 'chats', ['embeddings_status'], unique=False)
|
| 26 |
-
# ### end Alembic commands ###
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def downgrade() -> None:
|
| 30 |
-
"""Downgrade schema."""
|
| 31 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 32 |
-
op.drop_index(op.f('ix_chats_embeddings_status'), table_name='chats')
|
| 33 |
-
op.drop_column('chats', 'embeddings_status')
|
| 34 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/293bf8cae101_add_conversation_history_table.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
"""add conversation history table
|
| 2 |
-
|
| 3 |
-
Revision ID: 293bf8cae101
|
| 4 |
-
Revises: 3bdda54c971f
|
| 5 |
-
Create Date: 2025-11-18 02:56:39.769901
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '293bf8cae101'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '3bdda54c971f'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_table('conversation_history',
|
| 25 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 26 |
-
sa.Column('chat_id', sa.Integer(), nullable=False),
|
| 27 |
-
sa.Column('role', sa.String(length=20), nullable=False),
|
| 28 |
-
sa.Column('content', sa.Text(), nullable=False),
|
| 29 |
-
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
| 30 |
-
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ondelete='CASCADE'),
|
| 31 |
-
sa.PrimaryKeyConstraint('id')
|
| 32 |
-
)
|
| 33 |
-
op.create_index(op.f('ix_conversation_history_chat_id'), 'conversation_history', ['chat_id'], unique=False)
|
| 34 |
-
# op.drop_index(op.f('embeddings_chat_id_idx'), table_name='embeddings')
|
| 35 |
-
# op.drop_index(op.f('embeddings_chat_source_idx'), table_name='embeddings')
|
| 36 |
-
# op.drop_index(op.f('embeddings_embedding_ivfflat'), table_name='embeddings', postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 37 |
-
# op.create_index(op.f('ix_embeddings_chat_id'), 'embeddings', ['chat_id'], unique=False)
|
| 38 |
-
# # ### end Alembic commands ###
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def downgrade() -> None:
|
| 42 |
-
"""Downgrade schema."""
|
| 43 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 44 |
-
# op.drop_index(op.f('ix_embeddings_chat_id'), table_name='embeddings')
|
| 45 |
-
# op.create_index(op.f('embeddings_embedding_ivfflat'), 'embeddings', ['embedding'], unique=False, postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 46 |
-
# op.create_index(op.f('embeddings_chat_source_idx'), 'embeddings', ['chat_id', 'source_table', 'source_id'], unique=False)
|
| 47 |
-
# op.create_index(op.f('embeddings_chat_id_idx'), 'embeddings', ['chat_id'], unique=False)
|
| 48 |
-
# op.drop_index(op.f('ix_conversation_history_chat_id'), table_name='conversation_history')
|
| 49 |
-
op.drop_table('conversation_history')
|
| 50 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/2bae362756b9_version_9.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
"""Version 9
|
| 2 |
-
|
| 3 |
-
Revision ID: 2bae362756b9
|
| 4 |
-
Revises: 50eb9bd827e5
|
| 5 |
-
Create Date: 2025-11-01 08:15:21.483742
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '2bae362756b9'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '50eb9bd827e5'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_index(op.f('ix_chats_created_at'), 'chats', ['created_at'], unique=False)
|
| 25 |
-
op.create_index(op.f('ix_chats_sentiment_status'), 'chats', ['sentiment_status'], unique=False)
|
| 26 |
-
op.create_index(op.f('ix_message_sentiments_analysis_timestamp'), 'message_sentiments', ['analysis_timestamp'], unique=False)
|
| 27 |
-
op.create_index(op.f('ix_messages_chat_id'), 'messages', ['chat_id'], unique=False)
|
| 28 |
-
op.create_index(op.f('ix_messages_participant_id'), 'messages', ['participant_id'], unique=False)
|
| 29 |
-
op.drop_constraint(op.f('messages_participant_id_fkey'), 'messages', type_='foreignkey')
|
| 30 |
-
op.create_foreign_key(None, 'messages', 'participants', ['participant_id'], ['id'], ondelete='SET NULL')
|
| 31 |
-
op.create_index(op.f('ix_participants_chat_id'), 'participants', ['chat_id'], unique=False)
|
| 32 |
-
op.create_index(op.f('ix_participants_created_at'), 'participants', ['created_at'], unique=False)
|
| 33 |
-
op.create_index(op.f('ix_segment_sentiments_analysis_timestamp'), 'segment_sentiments', ['analysis_timestamp'], unique=False)
|
| 34 |
-
op.create_index(op.f('ix_segments_sender_sender_id'), 'segments_sender', ['sender_id'], unique=False)
|
| 35 |
-
op.create_index(op.f('ix_segments_sender_time_segment_id'), 'segments_sender', ['time_segment_id'], unique=False)
|
| 36 |
-
op.create_index(op.f('ix_segments_time_chat_id'), 'segments_time', ['chat_id'], unique=False)
|
| 37 |
-
op.create_index(op.f('ix_segments_time_start_time'), 'segments_time', ['start_time'], unique=False)
|
| 38 |
-
# ### end Alembic commands ###
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def downgrade() -> None:
|
| 42 |
-
"""Downgrade schema."""
|
| 43 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 44 |
-
op.drop_index(op.f('ix_segments_time_start_time'), table_name='segments_time')
|
| 45 |
-
op.drop_index(op.f('ix_segments_time_chat_id'), table_name='segments_time')
|
| 46 |
-
op.drop_index(op.f('ix_segments_sender_time_segment_id'), table_name='segments_sender')
|
| 47 |
-
op.drop_index(op.f('ix_segments_sender_sender_id'), table_name='segments_sender')
|
| 48 |
-
op.drop_index(op.f('ix_segment_sentiments_analysis_timestamp'), table_name='segment_sentiments')
|
| 49 |
-
op.drop_index(op.f('ix_participants_created_at'), table_name='participants')
|
| 50 |
-
op.drop_index(op.f('ix_participants_chat_id'), table_name='participants')
|
| 51 |
-
op.drop_constraint(None, 'messages', type_='foreignkey')
|
| 52 |
-
op.create_foreign_key(op.f('messages_participant_id_fkey'), 'messages', 'participants', ['participant_id'], ['id'])
|
| 53 |
-
op.drop_index(op.f('ix_messages_participant_id'), table_name='messages')
|
| 54 |
-
op.drop_index(op.f('ix_messages_chat_id'), table_name='messages')
|
| 55 |
-
op.drop_index(op.f('ix_message_sentiments_analysis_timestamp'), table_name='message_sentiments')
|
| 56 |
-
op.drop_index(op.f('ix_chats_sentiment_status'), table_name='chats')
|
| 57 |
-
op.drop_index(op.f('ix_chats_created_at'), table_name='chats')
|
| 58 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/2cfae3938d63_change_embeddings_table_structure.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""change embeddings table structure
|
| 2 |
-
|
| 3 |
-
Revision ID: 2cfae3938d63
|
| 4 |
-
Revises: af5827742674
|
| 5 |
-
Create Date: 2025-11-28 18:14:02.462140
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '2cfae3938d63'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'af5827742674'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
pass
|
| 25 |
-
# ### end Alembic commands ###
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def downgrade() -> None:
|
| 29 |
-
"""Downgrade schema."""
|
| 30 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
-
pass
|
| 32 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/3bdda54c971f_update_embedding_dimension_to_3072.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
"""update_embedding_dimension_to_3072
|
| 2 |
-
|
| 3 |
-
Revision ID: 3bdda54c971f
|
| 4 |
-
Revises: e817f10d0cc8
|
| 5 |
-
Create Date: 2025-11-12 14:58:09.216085
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '3bdda54c971f'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'e817f10d0cc8'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
def upgrade() -> None:
|
| 21 |
-
# ### commands auto-generated by Alembic - manually adjusted ###
|
| 22 |
-
print("Altering embedding column type to vector(3072)...")
|
| 23 |
-
|
| 24 |
-
# This command changes the database column to accept the new dimension.
|
| 25 |
-
op.execute("ALTER TABLE public.embeddings ALTER COLUMN embedding TYPE vector(1536);")
|
| 26 |
-
|
| 27 |
-
print("Done.")
|
| 28 |
-
# ### end Alembic commands ###
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def downgrade() -> None:
|
| 32 |
-
# ### commands auto-generated by Alembic - manually adjusted ###
|
| 33 |
-
print("Reverting embedding column type to vector(1536)...")
|
| 34 |
-
|
| 35 |
-
# This reverts the change if you ever need to.
|
| 36 |
-
op.execute("ALTER TABLE public.embeddings ALTER COLUMN embedding TYPE vector(1536);")
|
| 37 |
-
|
| 38 |
-
print("Done.")
|
| 39 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/4de3dfd809bd_add_sources_to_conversation_history.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"""add sources to conversation history
|
| 2 |
-
|
| 3 |
-
Revision ID: 4de3dfd809bd
|
| 4 |
-
Revises: 293bf8cae101
|
| 5 |
-
Create Date: 2025-11-19 02:53:46.860280
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '4de3dfd809bd'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '293bf8cae101'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('conversation_history', sa.Column('sources', sa.JSON(), nullable=True))
|
| 25 |
-
# op.drop_index(op.f('embeddings_chat_id_idx'), table_name='embeddings')
|
| 26 |
-
# op.drop_index(op.f('embeddings_chat_source_idx'), table_name='embeddings')
|
| 27 |
-
# op.drop_index(op.f('embeddings_embedding_ivfflat'), table_name='embeddings', postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 28 |
-
# op.create_index(op.f('ix_embeddings_chat_id'), 'embeddings', ['chat_id'], unique=False)
|
| 29 |
-
# ### end Alembic commands ###
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
def downgrade() -> None:
|
| 33 |
-
"""Downgrade schema."""
|
| 34 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 35 |
-
# op.drop_index(op.f('ix_embeddings_chat_id'), table_name='embeddings')
|
| 36 |
-
# op.create_index(op.f('embeddings_embedding_ivfflat'), 'embeddings', ['embedding'], unique=False, postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 37 |
-
# op.create_index(op.f('embeddings_chat_source_idx'), 'embeddings', ['chat_id', 'source_table', 'source_id'], unique=False)
|
| 38 |
-
# op.create_index(op.f('embeddings_chat_id_idx'), 'embeddings', ['chat_id'], unique=False)
|
| 39 |
-
op.drop_column('conversation_history', 'sources')
|
| 40 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/50eb9bd827e5_version_8.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""Version 8
|
| 2 |
-
|
| 3 |
-
Revision ID: 50eb9bd827e5
|
| 4 |
-
Revises: bbbad8eb92e1
|
| 5 |
-
Create Date: 2025-10-18 02:49:04.517995
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '50eb9bd827e5'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'bbbad8eb92e1'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('chats', sa.Column('cancel_requested', sa.Boolean(), nullable=True))
|
| 25 |
-
# ### end Alembic commands ###
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def downgrade() -> None:
|
| 29 |
-
"""Downgrade schema."""
|
| 30 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
-
op.drop_column('chats', 'cancel_requested')
|
| 32 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/51088f15a504_version_5.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""version 5
|
| 2 |
-
|
| 3 |
-
Revision ID: 51088f15a504
|
| 4 |
-
Revises: 576915939aff
|
| 5 |
-
Create Date: 2025-10-12 02:43:12.573367
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '51088f15a504'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '576915939aff'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('chats', sa.Column('sentiment_status', sa.String(length=30), nullable=False))
|
| 25 |
-
# ### end Alembic commands ###
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def downgrade() -> None:
|
| 29 |
-
"""Downgrade schema."""
|
| 30 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
-
op.drop_column('chats', 'sentiment_status')
|
| 32 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/576915939aff_version_4.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
"""version 4
|
| 2 |
-
|
| 3 |
-
Revision ID: 576915939aff
|
| 4 |
-
Revises: 9c071425ee4a
|
| 5 |
-
Create Date: 2025-10-10 17:57:04.404537
|
| 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 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '576915939aff'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '9c071425ee4a'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_table('segments_time',
|
| 25 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 26 |
-
sa.Column('chat_id', sa.Integer(), nullable=False),
|
| 27 |
-
sa.Column('start_time', sa.DateTime(), nullable=False),
|
| 28 |
-
sa.Column('end_time', sa.DateTime(), nullable=False),
|
| 29 |
-
sa.Column('duration_minutes', sa.Float(), nullable=False),
|
| 30 |
-
sa.Column('message_count', sa.Integer(), nullable=False),
|
| 31 |
-
sa.Column('summary', sa.Text(), nullable=True),
|
| 32 |
-
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ondelete='CASCADE'),
|
| 33 |
-
sa.PrimaryKeyConstraint('id')
|
| 34 |
-
)
|
| 35 |
-
op.create_table('segments_sender',
|
| 36 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 37 |
-
sa.Column('time_segment_id', sa.Integer(), nullable=False),
|
| 38 |
-
sa.Column('sender_id', sa.Integer(), nullable=False),
|
| 39 |
-
sa.Column('message_count', sa.Integer(), nullable=False),
|
| 40 |
-
sa.Column('combined_text', sa.Text(), nullable=False),
|
| 41 |
-
sa.ForeignKeyConstraint(['sender_id'], ['participants.id'], ondelete='CASCADE'),
|
| 42 |
-
sa.ForeignKeyConstraint(['time_segment_id'], ['segments_time.id'], ondelete='CASCADE'),
|
| 43 |
-
sa.PrimaryKeyConstraint('id')
|
| 44 |
-
)
|
| 45 |
-
op.create_table('message_sentiments',
|
| 46 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 47 |
-
sa.Column('message_id', sa.Integer(), nullable=False),
|
| 48 |
-
sa.Column('overall_label', sa.String(length=50), nullable=False),
|
| 49 |
-
sa.Column('score_positive', sa.Float(), nullable=False),
|
| 50 |
-
sa.Column('score_negative', sa.Float(), nullable=False),
|
| 51 |
-
sa.Column('score_neutral', sa.Float(), nullable=False),
|
| 52 |
-
sa.Column('sentences_summary', sa.JSON(), nullable=True),
|
| 53 |
-
sa.Column('opinions', sa.JSON(), nullable=True),
|
| 54 |
-
sa.Column('api_version', sa.String(length=50), nullable=True),
|
| 55 |
-
sa.Column('analysis_timestamp', sa.DateTime(timezone=True), nullable=False),
|
| 56 |
-
sa.Column('error_code', sa.String(length=100), nullable=True),
|
| 57 |
-
sa.Column('error_message', sa.Text(), nullable=True),
|
| 58 |
-
sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'),
|
| 59 |
-
sa.PrimaryKeyConstraint('id')
|
| 60 |
-
)
|
| 61 |
-
op.create_index(op.f('ix_message_sentiments_message_id'), 'message_sentiments', ['message_id'], unique=True)
|
| 62 |
-
op.create_table('segment_sentiments',
|
| 63 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 64 |
-
sa.Column('sender_segment_id', sa.Integer(), nullable=False),
|
| 65 |
-
sa.Column('overall_label', sa.String(length=50), nullable=False),
|
| 66 |
-
sa.Column('score_positive', sa.Float(), nullable=False),
|
| 67 |
-
sa.Column('score_negative', sa.Float(), nullable=False),
|
| 68 |
-
sa.Column('score_neutral', sa.Float(), nullable=False),
|
| 69 |
-
sa.Column('sentences_summary', sa.JSON(), nullable=True),
|
| 70 |
-
sa.Column('opinions', sa.JSON(), nullable=True),
|
| 71 |
-
sa.Column('api_version', sa.String(length=50), nullable=True),
|
| 72 |
-
sa.Column('analysis_timestamp', sa.DateTime(timezone=True), nullable=False),
|
| 73 |
-
sa.Column('error_code', sa.String(length=100), nullable=True),
|
| 74 |
-
sa.Column('error_message', sa.Text(), nullable=True),
|
| 75 |
-
sa.ForeignKeyConstraint(['sender_segment_id'], ['segments_sender.id'], ondelete='CASCADE'),
|
| 76 |
-
sa.PrimaryKeyConstraint('id')
|
| 77 |
-
)
|
| 78 |
-
op.create_index(op.f('ix_segment_sentiments_sender_segment_id'), 'segment_sentiments', ['sender_segment_id'], unique=True)
|
| 79 |
-
op.drop_table('sentiments')
|
| 80 |
-
# ### end Alembic commands ###
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def downgrade() -> None:
|
| 84 |
-
"""Downgrade schema."""
|
| 85 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 86 |
-
op.create_table('sentiments',
|
| 87 |
-
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
| 88 |
-
sa.Column('message_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 89 |
-
sa.Column('overall_label', sa.VARCHAR(length=50), autoincrement=False, nullable=False),
|
| 90 |
-
sa.Column('score_positive', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
|
| 91 |
-
sa.Column('score_negative', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
|
| 92 |
-
sa.Column('score_neutral', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=False),
|
| 93 |
-
sa.Column('sentences_summary', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
|
| 94 |
-
sa.Column('opinions', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True),
|
| 95 |
-
sa.Column('api_version', sa.VARCHAR(length=50), autoincrement=False, nullable=True),
|
| 96 |
-
sa.Column('analysis_timestamp', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
|
| 97 |
-
sa.Column('error_code', sa.VARCHAR(length=100), autoincrement=False, nullable=True),
|
| 98 |
-
sa.Column('error_message', sa.TEXT(), autoincrement=False, nullable=True),
|
| 99 |
-
sa.ForeignKeyConstraint(['message_id'], ['messages.id'], name=op.f('sentiments_message_id_fkey'), ondelete='CASCADE'),
|
| 100 |
-
sa.PrimaryKeyConstraint('id', name=op.f('sentiments_pkey')),
|
| 101 |
-
sa.UniqueConstraint('message_id', name=op.f('sentiments_message_id_key'), postgresql_include=[], postgresql_nulls_not_distinct=False)
|
| 102 |
-
)
|
| 103 |
-
op.drop_index(op.f('ix_segment_sentiments_sender_segment_id'), table_name='segment_sentiments')
|
| 104 |
-
op.drop_table('segment_sentiments')
|
| 105 |
-
op.drop_index(op.f('ix_message_sentiments_message_id'), table_name='message_sentiments')
|
| 106 |
-
op.drop_table('message_sentiments')
|
| 107 |
-
op.drop_table('segments_sender')
|
| 108 |
-
op.drop_table('segments_time')
|
| 109 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/5dd0716c12e2_version_2.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""version 2
|
| 2 |
-
|
| 3 |
-
Revision ID: 5dd0716c12e2
|
| 4 |
-
Revises: c5e48c9bf293
|
| 5 |
-
Create Date: 2025-10-07 02:34:56.649412
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '5dd0716c12e2'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'c5e48c9bf293'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
pass
|
| 25 |
-
# ### end Alembic commands ###
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def downgrade() -> None:
|
| 29 |
-
"""Downgrade schema."""
|
| 30 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
-
pass
|
| 32 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/9c071425ee4a_version_3.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
"""version 3
|
| 2 |
-
|
| 3 |
-
Revision ID: 9c071425ee4a
|
| 4 |
-
Revises: 5dd0716c12e2
|
| 5 |
-
Create Date: 2025-10-07 20:55:00.457665
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = '9c071425ee4a'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '5dd0716c12e2'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
pass
|
| 25 |
-
# ### end Alembic commands ###
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def downgrade() -> None:
|
| 29 |
-
"""Downgrade schema."""
|
| 30 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 31 |
-
pass
|
| 32 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/af5827742674_change_embedding_column_type.py
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
"""change embedding column type
|
| 2 |
-
|
| 3 |
-
Revision ID: af5827742674
|
| 4 |
-
Revises: c85ba4afc960
|
| 5 |
-
Create Date: 2025-11-28 18:03:09.896314
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'af5827742674'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'c85ba4afc960'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
# op.drop_index(op.f('embeddings_chat_id_idx'), table_name='embeddings')
|
| 25 |
-
# op.drop_index(op.f('embeddings_chat_source_idx'), table_name='embeddings')
|
| 26 |
-
# op.drop_index(op.f('embeddings_embedding_ivfflat'), table_name='embeddings', postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 27 |
-
# op.create_index(op.f('ix_embeddings_chat_id'), 'embeddings', ['chat_id'], unique=False)
|
| 28 |
-
# # ### end Alembic commands ###
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def downgrade() -> None:
|
| 32 |
-
"""Downgrade schema."""
|
| 33 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 34 |
-
# op.drop_index(op.f('ix_embeddings_chat_id'), table_name='embeddings')
|
| 35 |
-
# op.create_index(op.f('embeddings_embedding_ivfflat'), 'embeddings', ['embedding'], unique=False, postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 36 |
-
# op.create_index(op.f('embeddings_chat_source_idx'), 'embeddings', ['chat_id', 'source_table', 'source_id'], unique=False)
|
| 37 |
-
# op.create_index(op.f('embeddings_chat_id_idx'), 'embeddings', ['chat_id'], unique=False)
|
| 38 |
-
# # ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/bbbad8eb92e1_version_7.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
"""Version 7
|
| 2 |
-
|
| 3 |
-
Revision ID: bbbad8eb92e1
|
| 4 |
-
Revises: f8746e269698
|
| 5 |
-
Create Date: 2025-10-13 03:43:39.044102
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'bbbad8eb92e1'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'f8746e269698'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_index(op.f('ix_message_sentiments_overall_label'), 'message_sentiments', ['overall_label'], unique=False)
|
| 25 |
-
op.create_index(op.f('ix_message_sentiments_overall_label_score'), 'message_sentiments', ['overall_label_score'], unique=False)
|
| 26 |
-
op.alter_column('messages', 'raw',
|
| 27 |
-
existing_type=sa.TEXT(),
|
| 28 |
-
nullable=True)
|
| 29 |
-
op.create_index(op.f('ix_segment_sentiments_overall_label'), 'segment_sentiments', ['overall_label'], unique=False)
|
| 30 |
-
op.create_index(op.f('ix_segment_sentiments_overall_label_score'), 'segment_sentiments', ['overall_label_score'], unique=False)
|
| 31 |
-
# ### end Alembic commands ###
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def downgrade() -> None:
|
| 35 |
-
"""Downgrade schema."""
|
| 36 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 37 |
-
op.drop_index(op.f('ix_segment_sentiments_overall_label_score'), table_name='segment_sentiments')
|
| 38 |
-
op.drop_index(op.f('ix_segment_sentiments_overall_label'), table_name='segment_sentiments')
|
| 39 |
-
op.alter_column('messages', 'raw',
|
| 40 |
-
existing_type=sa.TEXT(),
|
| 41 |
-
nullable=False)
|
| 42 |
-
op.drop_index(op.f('ix_message_sentiments_overall_label_score'), table_name='message_sentiments')
|
| 43 |
-
op.drop_index(op.f('ix_message_sentiments_overall_label'), table_name='message_sentiments')
|
| 44 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/c4f1502f712e_create_embeddings_table_and_vector_.py
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
"""create embeddings table and vector extension
|
| 2 |
-
|
| 3 |
-
Revision ID: c4f1502f712e
|
| 4 |
-
Revises: d77bd9f8ce2f
|
| 5 |
-
Create Date: 2025-11-11 04:08:57.649328
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'c4f1502f712e'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'd77bd9f8ce2f'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
def upgrade() -> None:
|
| 21 |
-
# ### commands auto-generated by Alembic - manually adjusted ###
|
| 22 |
-
|
| 23 |
-
# Step 1: Create the vector extension if it doesn't exist
|
| 24 |
-
op.execute("CREATE EXTENSION IF NOT EXISTS vector;")
|
| 25 |
-
|
| 26 |
-
# Step 2: Create the embeddings table using the exact DDL [cite: 8-17]
|
| 27 |
-
# Note: We use 1536 for the embedding dimension as specified [cite: 6, 11]
|
| 28 |
-
op.execute("""
|
| 29 |
-
CREATE TABLE public.embeddings (
|
| 30 |
-
id BIGSERIAL PRIMARY KEY,
|
| 31 |
-
chat_id INTEGER NOT NULL,
|
| 32 |
-
source_table TEXT NOT NULL,
|
| 33 |
-
source_id INTEGER NOT NULL,
|
| 34 |
-
embedding vector(1536) NOT NULL,
|
| 35 |
-
text_excerpt TEXT,
|
| 36 |
-
metadata JSONB DEFAULT '{}'::jsonb,
|
| 37 |
-
created_at timestamptz DEFAULT now()
|
| 38 |
-
);
|
| 39 |
-
""")
|
| 40 |
-
|
| 41 |
-
# Step 3: Add the foreign key constraint to your chats table [cite: 18-20]
|
| 42 |
-
# This enables ON DELETE CASCADE, as requested by your 4-hour TTL design.
|
| 43 |
-
op.execute("""
|
| 44 |
-
ALTER TABLE public.embeddings
|
| 45 |
-
ADD CONSTRAINT fk_embeddings_chats
|
| 46 |
-
FOREIGN KEY (chat_id) REFERENCES public.chats (id) ON DELETE CASCADE;
|
| 47 |
-
""")
|
| 48 |
-
|
| 49 |
-
# Step 4: Create the standard index for chat_id lookups [cite: 22]
|
| 50 |
-
op.execute("""
|
| 51 |
-
CREATE INDEX embeddings_chat_id_idx ON public.embeddings (chat_id);
|
| 52 |
-
""")
|
| 53 |
-
|
| 54 |
-
# Step 5: Create the composite index for common lookups [cite: 24]
|
| 55 |
-
op.execute("""
|
| 56 |
-
CREATE INDEX embeddings_chat_source_idx ON public.embeddings (chat_id, source_table, source_id);
|
| 57 |
-
""")
|
| 58 |
-
|
| 59 |
-
# Step 6: Create the ANN index
|
| 60 |
-
# Using 'vector_l2_ops' for Euclidean distance, which the guide's
|
| 61 |
-
# query operator '<=>' relies on[cite: 132, 141].
|
| 62 |
-
op.execute("""
|
| 63 |
-
CREATE INDEX embeddings_embedding_ivfflat ON public.embeddings USING ivfflat (embedding vector_l2_ops) WITH (lists=100);
|
| 64 |
-
""")
|
| 65 |
-
|
| 66 |
-
# ### end Alembic commands ###
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def downgrade() -> None:
|
| 70 |
-
# ### commands auto-generated by Alembic - manually adjusted ###
|
| 71 |
-
|
| 72 |
-
# Drop the table, which also drops its indexes and constraints
|
| 73 |
-
op.execute("DROP TABLE IF EXISTS public.embeddings;")
|
| 74 |
-
|
| 75 |
-
# We'll leave the 'vector' extension in place, as it's harmless
|
| 76 |
-
# and dropping it could affect other tables if you use it elsewhere.
|
| 77 |
-
|
| 78 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/c5e48c9bf293_initial.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
"""initial
|
| 2 |
-
|
| 3 |
-
Revision ID: c5e48c9bf293
|
| 4 |
-
Revises:
|
| 5 |
-
Create Date: 2025-10-06 14:10:49.826036
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'c5e48c9bf293'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = None
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_table('chats',
|
| 25 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
| 26 |
-
sa.Column('title', sa.String(length=255), nullable=True),
|
| 27 |
-
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
| 28 |
-
sa.PrimaryKeyConstraint('id')
|
| 29 |
-
)
|
| 30 |
-
op.create_table('participants',
|
| 31 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
| 32 |
-
sa.Column('chat_id', sa.Integer(), nullable=False),
|
| 33 |
-
sa.Column('name', sa.String(length=255), nullable=False),
|
| 34 |
-
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
| 35 |
-
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ondelete='CASCADE'),
|
| 36 |
-
sa.PrimaryKeyConstraint('id'),
|
| 37 |
-
sa.UniqueConstraint('chat_id', 'name', name='uq_participant_chat_name')
|
| 38 |
-
)
|
| 39 |
-
op.create_table('messages',
|
| 40 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
| 41 |
-
sa.Column('chat_id', sa.Integer(), nullable=False),
|
| 42 |
-
sa.Column('participant_id', sa.Integer(), nullable=True),
|
| 43 |
-
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
| 44 |
-
sa.Column('content', sa.Text(), nullable=False),
|
| 45 |
-
sa.Column('raw', sa.Text(), nullable=False),
|
| 46 |
-
sa.ForeignKeyConstraint(['chat_id'], ['chats.id'], ondelete='CASCADE'),
|
| 47 |
-
sa.ForeignKeyConstraint(['participant_id'], ['participants.id'], ),
|
| 48 |
-
sa.PrimaryKeyConstraint('id')
|
| 49 |
-
)
|
| 50 |
-
op.create_index(op.f('ix_messages_timestamp'), 'messages', ['timestamp'], unique=False)
|
| 51 |
-
op.create_table('sentiments',
|
| 52 |
-
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
| 53 |
-
sa.Column('message_id', sa.Integer(), nullable=False),
|
| 54 |
-
sa.Column('overall_label', sa.String(length=50), nullable=False),
|
| 55 |
-
sa.Column('score_positive', sa.Float(), nullable=False),
|
| 56 |
-
sa.Column('score_negative', sa.Float(), nullable=False),
|
| 57 |
-
sa.Column('score_neutral', sa.Float(), nullable=False),
|
| 58 |
-
sa.Column('sentences_summary', sa.JSON(), nullable=True),
|
| 59 |
-
sa.Column('opinions', sa.JSON(), nullable=True),
|
| 60 |
-
sa.Column('api_version', sa.String(length=50), nullable=True),
|
| 61 |
-
sa.Column('analysis_timestamp', sa.DateTime(timezone=True), nullable=False),
|
| 62 |
-
sa.Column('error_code', sa.String(length=100), nullable=True),
|
| 63 |
-
sa.Column('error_message', sa.Text(), nullable=True),
|
| 64 |
-
sa.ForeignKeyConstraint(['message_id'], ['messages.id'], ondelete='CASCADE'),
|
| 65 |
-
sa.PrimaryKeyConstraint('id'),
|
| 66 |
-
sa.UniqueConstraint('message_id')
|
| 67 |
-
)
|
| 68 |
-
# ### end Alembic commands ###
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def downgrade() -> None:
|
| 72 |
-
"""Downgrade schema."""
|
| 73 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 74 |
-
op.drop_table('sentiments')
|
| 75 |
-
op.drop_index(op.f('ix_messages_timestamp'), table_name='messages')
|
| 76 |
-
op.drop_table('messages')
|
| 77 |
-
op.drop_table('participants')
|
| 78 |
-
op.drop_table('chats')
|
| 79 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/c85ba4afc960_add_new_users_table.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
"""add new users table
|
| 2 |
-
|
| 3 |
-
Revision ID: c85ba4afc960
|
| 4 |
-
Revises: 4de3dfd809bd
|
| 5 |
-
Create Date: 2025-11-23 00:28:21.228614
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'c85ba4afc960'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '4de3dfd809bd'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.create_table('users',
|
| 25 |
-
sa.Column('id', sa.UUID(), nullable=False),
|
| 26 |
-
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
| 27 |
-
sa.PrimaryKeyConstraint('id')
|
| 28 |
-
)
|
| 29 |
-
op.add_column('chats', sa.Column('owner_id', sa.UUID(), nullable=False))
|
| 30 |
-
op.create_index(op.f('ix_chats_owner_id'), 'chats', ['owner_id'], unique=False)
|
| 31 |
-
op.create_foreign_key(None, 'chats', 'users', ['owner_id'], ['id'], ondelete='CASCADE')
|
| 32 |
-
# op.drop_index(op.f('embeddings_chat_id_idx'), table_name='embeddings')
|
| 33 |
-
# op.drop_index(op.f('embeddings_chat_source_idx'), table_name='embeddings')
|
| 34 |
-
# op.drop_index(op.f('embeddings_embedding_ivfflat'), table_name='embeddings', postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 35 |
-
# op.create_index(op.f('ix_embeddings_chat_id'), 'embeddings', ['chat_id'], unique=False)
|
| 36 |
-
# ### end Alembic commands ###
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def downgrade() -> None:
|
| 40 |
-
"""Downgrade schema."""
|
| 41 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 42 |
-
# op.drop_index(op.f('ix_embeddings_chat_id'), table_name='embeddings')
|
| 43 |
-
# op.create_index(op.f('embeddings_embedding_ivfflat'), 'embeddings', ['embedding'], unique=False, postgresql_with={'lists': '100'}, postgresql_using='ivfflat')
|
| 44 |
-
# op.create_index(op.f('embeddings_chat_source_idx'), 'embeddings', ['chat_id', 'source_table', 'source_id'], unique=False)
|
| 45 |
-
# op.create_index(op.f('embeddings_chat_id_idx'), 'embeddings', ['chat_id'], unique=False)
|
| 46 |
-
op.drop_constraint(None, 'chats', type_='foreignkey')
|
| 47 |
-
op.drop_index(op.f('ix_chats_owner_id'), table_name='chats')
|
| 48 |
-
op.drop_column('chats', 'owner_id')
|
| 49 |
-
op.drop_table('users')
|
| 50 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/d77bd9f8ce2f_version_10.py
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
"""Version 10
|
| 2 |
-
|
| 3 |
-
Revision ID: d77bd9f8ce2f
|
| 4 |
-
Revises: 2bae362756b9
|
| 5 |
-
Create Date: 2025-11-02 04:03:43.796576
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
from alembic import op
|
| 10 |
-
import sqlalchemy as sa
|
| 11 |
-
|
| 12 |
-
# revision identifiers, used by Alembic.
|
| 13 |
-
revision: str = 'd77bd9f8ce2f'
|
| 14 |
-
down_revision: Union[str, Sequence[str], None] = '2bae362756b9'
|
| 15 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 16 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 17 |
-
|
| 18 |
-
def upgrade() -> None:
|
| 19 |
-
"""Upgrade schema."""
|
| 20 |
-
# Step 1: Add columns as nullable
|
| 21 |
-
op.add_column('messages', sa.Column('word_count', sa.Integer(), nullable=True))
|
| 22 |
-
op.add_column('messages', sa.Column('emojis_count', sa.Integer(), nullable=True))
|
| 23 |
-
op.add_column('messages', sa.Column('links_count', sa.Integer(), nullable=True))
|
| 24 |
-
op.add_column('messages', sa.Column('is_question', sa.Boolean(), nullable=True))
|
| 25 |
-
op.add_column('messages', sa.Column('is_media', sa.Boolean(), nullable=True))
|
| 26 |
-
|
| 27 |
-
# Step 2: Update existing rows with default values
|
| 28 |
-
op.execute("""
|
| 29 |
-
UPDATE messages
|
| 30 |
-
SET word_count = 0,
|
| 31 |
-
emojis_count = 0,
|
| 32 |
-
links_count = 0,
|
| 33 |
-
is_question = FALSE,
|
| 34 |
-
is_media = FALSE
|
| 35 |
-
WHERE word_count IS NULL
|
| 36 |
-
""")
|
| 37 |
-
|
| 38 |
-
# Step 3: Make columns non-nullable
|
| 39 |
-
op.alter_column('messages', 'word_count', nullable=False)
|
| 40 |
-
op.alter_column('messages', 'emojis_count', nullable=False)
|
| 41 |
-
op.alter_column('messages', 'links_count', nullable=False)
|
| 42 |
-
op.alter_column('messages', 'is_question', nullable=False)
|
| 43 |
-
op.alter_column('messages', 'is_media', nullable=False)
|
| 44 |
-
|
| 45 |
-
def downgrade() -> None:
|
| 46 |
-
"""Downgrade schema."""
|
| 47 |
-
op.drop_column('messages', 'is_media')
|
| 48 |
-
op.drop_column('messages', 'is_question')
|
| 49 |
-
op.drop_column('messages', 'links_count')
|
| 50 |
-
op.drop_column('messages', 'emojis_count')
|
| 51 |
-
op.drop_column('messages', 'word_count')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/e817f10d0cc8_rename_metadata_column_on_embeddings_.py
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
"""rename metadata column on embeddings table
|
| 2 |
-
|
| 3 |
-
Revision ID: e817f10d0cc8
|
| 4 |
-
Revises: c4f1502f712e
|
| 5 |
-
Create Date: 2025-11-12 11:55:54.052188
|
| 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 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'e817f10d0cc8'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = 'c4f1502f712e'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('embeddings', sa.Column('chunk_metadata', sa.JSON(), nullable=True))
|
| 25 |
-
op.alter_column('embeddings', 'created_at',
|
| 26 |
-
existing_type=postgresql.TIMESTAMP(timezone=True),
|
| 27 |
-
nullable=False,
|
| 28 |
-
existing_server_default=sa.text('now()'))
|
| 29 |
-
|
| 30 |
-
op.drop_column('embeddings', 'metadata')
|
| 31 |
-
# ### end Alembic commands ###
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
def downgrade() -> None:
|
| 35 |
-
"""Downgrade schema."""
|
| 36 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 37 |
-
op.add_column('embeddings', sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), server_default=sa.text("'{}'::jsonb"), autoincrement=False, nullable=True))
|
| 38 |
-
|
| 39 |
-
op.alter_column('embeddings', 'created_at',
|
| 40 |
-
existing_type=postgresql.TIMESTAMP(timezone=True),
|
| 41 |
-
nullable=True,
|
| 42 |
-
existing_server_default=sa.text('now()'))
|
| 43 |
-
op.drop_column('embeddings', 'chunk_metadata')
|
| 44 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
alembic/versions/f8746e269698_version_6.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
| 1 |
-
"""Version 6
|
| 2 |
-
|
| 3 |
-
Revision ID: f8746e269698
|
| 4 |
-
Revises: 51088f15a504
|
| 5 |
-
Create Date: 2025-10-12 20:42:52.982015
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from typing import Sequence, Union
|
| 9 |
-
|
| 10 |
-
from alembic import op
|
| 11 |
-
import sqlalchemy as sa
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
# revision identifiers, used by Alembic.
|
| 15 |
-
revision: str = 'f8746e269698'
|
| 16 |
-
down_revision: Union[str, Sequence[str], None] = '51088f15a504'
|
| 17 |
-
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
-
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def upgrade() -> None:
|
| 22 |
-
"""Upgrade schema."""
|
| 23 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 24 |
-
op.add_column('message_sentiments', sa.Column('overall_label_score', sa.Float(), nullable=False))
|
| 25 |
-
op.alter_column('message_sentiments', 'score_positive',
|
| 26 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 27 |
-
nullable=True)
|
| 28 |
-
op.alter_column('message_sentiments', 'score_negative',
|
| 29 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 30 |
-
nullable=True)
|
| 31 |
-
op.alter_column('message_sentiments', 'score_neutral',
|
| 32 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 33 |
-
nullable=True)
|
| 34 |
-
op.add_column('segment_sentiments', sa.Column('overall_label_score', sa.Float(), nullable=False))
|
| 35 |
-
op.alter_column('segment_sentiments', 'score_positive',
|
| 36 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 37 |
-
nullable=True)
|
| 38 |
-
op.alter_column('segment_sentiments', 'score_negative',
|
| 39 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 40 |
-
nullable=True)
|
| 41 |
-
op.alter_column('segment_sentiments', 'score_neutral',
|
| 42 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 43 |
-
nullable=True)
|
| 44 |
-
# ### end Alembic commands ###
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def downgrade() -> None:
|
| 48 |
-
"""Downgrade schema."""
|
| 49 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 50 |
-
op.alter_column('segment_sentiments', 'score_neutral',
|
| 51 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 52 |
-
nullable=False)
|
| 53 |
-
op.alter_column('segment_sentiments', 'score_negative',
|
| 54 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 55 |
-
nullable=False)
|
| 56 |
-
op.alter_column('segment_sentiments', 'score_positive',
|
| 57 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 58 |
-
nullable=False)
|
| 59 |
-
op.drop_column('segment_sentiments', 'overall_label_score')
|
| 60 |
-
op.alter_column('message_sentiments', 'score_neutral',
|
| 61 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 62 |
-
nullable=False)
|
| 63 |
-
op.alter_column('message_sentiments', 'score_negative',
|
| 64 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 65 |
-
nullable=False)
|
| 66 |
-
op.alter_column('message_sentiments', 'score_positive',
|
| 67 |
-
existing_type=sa.DOUBLE_PRECISION(precision=53),
|
| 68 |
-
nullable=False)
|
| 69 |
-
op.drop_column('message_sentiments', 'overall_label_score')
|
| 70 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
kb_docs.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
KB_DOCS = [
|
| 2 |
+
{"id": "shipping_times",
|
| 3 |
+
"text": "Standard delivery takes 3–5 business days within major cities and 5–8 business days for other locations. Express delivery (1–2 business days) is available at checkout for an additional fee. Orders placed before 2pm ship the same day."},
|
| 4 |
+
{"id": "shipping_costs", "text": "Standard shipping is free on orders over ₦50,000. Orders below that threshold have a flat ₦2,500 shipping fee. Express shipping is calculated at checkout based on weight and destination."},
|
| 5 |
+
{"id": "returns", "text": "Items can be returned within 14 days of delivery if unused, in original packaging, with proof of purchase. Perishable items and opened personal-care products are not eligible for return."},
|
| 6 |
+
{"id": "refunds", "text": "Once a return is received and inspected, refunds are processed within 5–7 business days to the original payment method. You'll get an email confirmation when the refund is issued."},
|
| 7 |
+
{"id": "tracking", "text": "A tracking link is emailed automatically once an order ships. Tracking can also be checked by logging into your account under 'My Orders.'"},
|
| 8 |
+
{"id": "payment", "text": "We accept debit/credit cards, bank transfer, and major mobile payment wallets. Payment is processed securely at checkout; we never store full card details."},
|
| 9 |
+
{"id": "warranty",
|
| 10 |
+
"text": "All small appliances (blenders, kettles, toasters, etc.) come with a 12-month manufacturer warranty covering defects. Warranty claims require the original receipt and are handled through the manufacturer's service center."},
|
| 11 |
+
{"id": "cookware_care", "text": "Non-stick cookware should be hand-washed with a soft sponge — no metal utensils or abrasive scrubbers, as these damage the coating. Avoid high heat above medium for non-stick pans to preserve the coating's lifespan."},
|
| 12 |
+
{"id": "wholesale", "text": "For orders over 20 units of the same item, contact our sales team directly for wholesale pricing. Bulk orders typically ship within 7–10 business days depending on stock."},
|
| 13 |
+
{"id": "damaged_item", "text": "If an item arrives damaged, contact support within 48 hours with photos of the damage and packaging. A replacement or full refund will be issued — no need to return the damaged item in most cases."},
|
| 14 |
+
{"id": "cancellation", "text": "Orders can be cancelled free of charge within 1 hour of placing them. After that, the order may already be processing and cancellation isn't guaranteed — contact support immediately if this happens."},
|
| 15 |
+
{"id": "gift_wrapping", "text": "Gift wrapping is available at checkout for a small fee and includes a printed gift note if you add a message. Gift cards are available in fixed denominations and don't expire."},
|
| 16 |
+
{"id": "international", "text": "We currently ship only within the country. International shipping isn't available yet, but you can sign up for restock/expansion notifications on the homepage."},
|
| 17 |
+
{"id": "pickup", "text": "Store pickup isn't currently offered — all orders are delivered directly to the address provided at checkout."},
|
| 18 |
+
{"id": "escalation", "text": "If a question falls outside what I can answer, or you'd prefer to speak with a person directly, I'll flag it for the team and someone will follow up by email within one business day."},
|
| 19 |
+
]
|
main.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
from sentence_transformers import SentenceTransformer
|
| 7 |
+
from groq import Groq
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
from kb_docs import KB_DOCS
|
| 10 |
+
|
| 11 |
+
load_dotenv()
|
| 12 |
+
app = FastAPI()
|
| 13 |
+
|
| 14 |
+
# Allow your Vercel frontend to reach this backend
|
| 15 |
+
app.add_middleware(
|
| 16 |
+
CORSMiddleware,
|
| 17 |
+
allow_origins=["*"],
|
| 18 |
+
allow_methods=["POST", "GET"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
print("Initializing Embedding Model...")
|
| 23 |
+
embed_model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 24 |
+
texts = [d["text"] for d in KB_DOCS]
|
| 25 |
+
kb_embeddings = embed_model.encode(texts, normalize_embeddings=True)
|
| 26 |
+
print("Embeddings loaded into memory.")
|
| 27 |
+
|
| 28 |
+
groq_client = Groq(api_key=os.environ.get("GROQ_API_KEY", ""))
|
| 29 |
+
|
| 30 |
+
SYSTEM_PROMPT = """You are the customer support assistant for Amara Home & Kitchen, an online kitchenware and home-goods retailer.
|
| 31 |
+
Answer ONLY using the information provided below. Keep answers short — 2-4 sentences.
|
| 32 |
+
If the answer isn't in the information provided, say you'll connect them with a human team member rather than guessing.
|
| 33 |
+
Never invent policies, prices, or timelines that aren't in the information.
|
| 34 |
+
|
| 35 |
+
INFORMATION:
|
| 36 |
+
{context}
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class ChatRequest(BaseModel):
|
| 41 |
+
question: str
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def retrieve(question: str, top_k: int = 3):
|
| 45 |
+
q_emb = embed_model.encode([question], normalize_embeddings=True)[0]
|
| 46 |
+
sims = kb_embeddings @ q_emb
|
| 47 |
+
top_idx = np.argsort(sims)[::-1][:top_k]
|
| 48 |
+
return [KB_DOCS[i]["text"] for i in top_idx]
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def ask_groq(question: str, context: str, model: str = "llama-3.3-70b-versatile"):
|
| 52 |
+
resp = groq_client.chat.completions.create(
|
| 53 |
+
model=model,
|
| 54 |
+
messages=[
|
| 55 |
+
{"role": "system", "content": SYSTEM_PROMPT.format(
|
| 56 |
+
context=context)},
|
| 57 |
+
{"role": "user", "content": question},
|
| 58 |
+
],
|
| 59 |
+
temperature=0.2,
|
| 60 |
+
max_tokens=300,
|
| 61 |
+
)
|
| 62 |
+
return resp.choices[0].message.content
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
@app.get("/health")
|
| 66 |
+
def health():
|
| 67 |
+
return {"status": "ok"}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@app.post("/api/rag/chat")
|
| 71 |
+
def chat(req: ChatRequest):
|
| 72 |
+
context_chunks = retrieve(req.question)
|
| 73 |
+
context = "\n".join(context_chunks)
|
| 74 |
+
try:
|
| 75 |
+
answer = ask_groq(req.question, context,
|
| 76 |
+
model="llama-3.3-70b-versatile")
|
| 77 |
+
except Exception:
|
| 78 |
+
answer = ask_groq(req.question, context, model="llama-3.1-8b-instant")
|
| 79 |
+
return {"answer": answer}
|
onnx_model_optimized/config.json
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"adapters": {
|
| 3 |
-
"adapters": {},
|
| 4 |
-
"config_map": {},
|
| 5 |
-
"fusion_config_map": {},
|
| 6 |
-
"fusions": {}
|
| 7 |
-
},
|
| 8 |
-
"architectures": [
|
| 9 |
-
"XLMRobertaForSequenceClassification"
|
| 10 |
-
],
|
| 11 |
-
"attention_probs_dropout_prob": 0.1,
|
| 12 |
-
"bos_token_id": 0,
|
| 13 |
-
"classifier_dropout": null,
|
| 14 |
-
"eos_token_id": 2,
|
| 15 |
-
"gradient_checkpointing": false,
|
| 16 |
-
"hidden_act": "gelu",
|
| 17 |
-
"hidden_dropout_prob": 0.1,
|
| 18 |
-
"hidden_size": 768,
|
| 19 |
-
"id2label": {
|
| 20 |
-
"0": "positive",
|
| 21 |
-
"1": "neutral",
|
| 22 |
-
"2": "negative"
|
| 23 |
-
},
|
| 24 |
-
"initializer_range": 0.02,
|
| 25 |
-
"intermediate_size": 3072,
|
| 26 |
-
"label2id": {
|
| 27 |
-
"negative": 2,
|
| 28 |
-
"neutral": 1,
|
| 29 |
-
"positive": 0
|
| 30 |
-
},
|
| 31 |
-
"layer_norm_eps": 1e-05,
|
| 32 |
-
"max_length": 512,
|
| 33 |
-
"max_position_embeddings": 514,
|
| 34 |
-
"model_type": "xlm-roberta",
|
| 35 |
-
"num_attention_heads": 6,
|
| 36 |
-
"num_hidden_layers": 10,
|
| 37 |
-
"output_past": true,
|
| 38 |
-
"pad_token_id": 1,
|
| 39 |
-
"position_embedding_type": "absolute",
|
| 40 |
-
"problem_type": "single_label_classification",
|
| 41 |
-
"torch_dtype": "float32",
|
| 42 |
-
"transformers_version": "4.55.4",
|
| 43 |
-
"type_vocab_size": 1,
|
| 44 |
-
"use_cache": true,
|
| 45 |
-
"vocab_size": 70006
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onnx_model_optimized/ort_config.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"one_external_file": true,
|
| 3 |
-
"opset": null,
|
| 4 |
-
"optimization": {},
|
| 5 |
-
"quantization": {
|
| 6 |
-
"activations_dtype": "QUInt8",
|
| 7 |
-
"activations_symmetric": false,
|
| 8 |
-
"format": "QOperator",
|
| 9 |
-
"is_static": false,
|
| 10 |
-
"mode": "IntegerOps",
|
| 11 |
-
"nodes_to_exclude": [],
|
| 12 |
-
"nodes_to_quantize": [],
|
| 13 |
-
"operators_to_quantize": [
|
| 14 |
-
"Conv",
|
| 15 |
-
"MatMul",
|
| 16 |
-
"Attention",
|
| 17 |
-
"LSTM",
|
| 18 |
-
"Gather",
|
| 19 |
-
"Transpose",
|
| 20 |
-
"EmbedLayerNormalization"
|
| 21 |
-
],
|
| 22 |
-
"per_channel": true,
|
| 23 |
-
"qdq_add_pair_to_weight": false,
|
| 24 |
-
"qdq_dedicated_pair": false,
|
| 25 |
-
"qdq_op_type_per_channel_support_to_axis": {
|
| 26 |
-
"MatMul": 1
|
| 27 |
-
},
|
| 28 |
-
"reduce_range": false,
|
| 29 |
-
"weights_dtype": "QUInt8",
|
| 30 |
-
"weights_symmetric": true
|
| 31 |
-
},
|
| 32 |
-
"use_external_data_format": false
|
| 33 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onnx_model_optimized/special_tokens_map.json
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"bos_token": {
|
| 3 |
-
"content": "<s>",
|
| 4 |
-
"lstrip": false,
|
| 5 |
-
"normalized": false,
|
| 6 |
-
"rstrip": false,
|
| 7 |
-
"single_word": false
|
| 8 |
-
},
|
| 9 |
-
"cls_token": {
|
| 10 |
-
"content": "<s>",
|
| 11 |
-
"lstrip": false,
|
| 12 |
-
"normalized": false,
|
| 13 |
-
"rstrip": false,
|
| 14 |
-
"single_word": false
|
| 15 |
-
},
|
| 16 |
-
"eos_token": {
|
| 17 |
-
"content": "</s>",
|
| 18 |
-
"lstrip": false,
|
| 19 |
-
"normalized": false,
|
| 20 |
-
"rstrip": false,
|
| 21 |
-
"single_word": false
|
| 22 |
-
},
|
| 23 |
-
"mask_token": {
|
| 24 |
-
"content": "<mask>",
|
| 25 |
-
"lstrip": false,
|
| 26 |
-
"normalized": false,
|
| 27 |
-
"rstrip": false,
|
| 28 |
-
"single_word": false
|
| 29 |
-
},
|
| 30 |
-
"pad_token": {
|
| 31 |
-
"content": "<pad>",
|
| 32 |
-
"lstrip": false,
|
| 33 |
-
"normalized": false,
|
| 34 |
-
"rstrip": false,
|
| 35 |
-
"single_word": false
|
| 36 |
-
},
|
| 37 |
-
"sep_token": {
|
| 38 |
-
"content": "</s>",
|
| 39 |
-
"lstrip": false,
|
| 40 |
-
"normalized": false,
|
| 41 |
-
"rstrip": false,
|
| 42 |
-
"single_word": false
|
| 43 |
-
},
|
| 44 |
-
"unk_token": {
|
| 45 |
-
"content": "<unk>",
|
| 46 |
-
"lstrip": false,
|
| 47 |
-
"normalized": false,
|
| 48 |
-
"rstrip": false,
|
| 49 |
-
"single_word": false
|
| 50 |
-
}
|
| 51 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onnx_model_optimized/tokenizer.json
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
onnx_model_optimized/tokenizer_config.json
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"added_tokens_decoder": {
|
| 3 |
-
"0": {
|
| 4 |
-
"content": "<s>",
|
| 5 |
-
"lstrip": false,
|
| 6 |
-
"normalized": false,
|
| 7 |
-
"rstrip": false,
|
| 8 |
-
"single_word": false,
|
| 9 |
-
"special": true
|
| 10 |
-
},
|
| 11 |
-
"1": {
|
| 12 |
-
"content": "<pad>",
|
| 13 |
-
"lstrip": false,
|
| 14 |
-
"normalized": false,
|
| 15 |
-
"rstrip": false,
|
| 16 |
-
"single_word": false,
|
| 17 |
-
"special": true
|
| 18 |
-
},
|
| 19 |
-
"2": {
|
| 20 |
-
"content": "</s>",
|
| 21 |
-
"lstrip": false,
|
| 22 |
-
"normalized": false,
|
| 23 |
-
"rstrip": false,
|
| 24 |
-
"single_word": false,
|
| 25 |
-
"special": true
|
| 26 |
-
},
|
| 27 |
-
"4": {
|
| 28 |
-
"content": "<unk>",
|
| 29 |
-
"lstrip": false,
|
| 30 |
-
"normalized": false,
|
| 31 |
-
"rstrip": false,
|
| 32 |
-
"single_word": false,
|
| 33 |
-
"special": true
|
| 34 |
-
},
|
| 35 |
-
"70005": {
|
| 36 |
-
"content": "<mask>",
|
| 37 |
-
"lstrip": false,
|
| 38 |
-
"normalized": false,
|
| 39 |
-
"rstrip": false,
|
| 40 |
-
"single_word": false,
|
| 41 |
-
"special": true
|
| 42 |
-
}
|
| 43 |
-
},
|
| 44 |
-
"bos_token": "<s>",
|
| 45 |
-
"clean_up_tokenization_spaces": false,
|
| 46 |
-
"cls_token": "<s>",
|
| 47 |
-
"do_lower_case": false,
|
| 48 |
-
"eos_token": "</s>",
|
| 49 |
-
"extra_special_tokens": {},
|
| 50 |
-
"mask_token": "<mask>",
|
| 51 |
-
"model_max_length": 1000000000000000019884624838656,
|
| 52 |
-
"pad_token": "<pad>",
|
| 53 |
-
"sep_token": "</s>",
|
| 54 |
-
"sp_model_kwargs": {},
|
| 55 |
-
"tokenizer_class": "XLMRobertaTokenizerFast",
|
| 56 |
-
"unk_token": "<unk>"
|
| 57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pytest.ini
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
[pytest]
|
| 2 |
-
pythonpath = .
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,125 +1,6 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
aiosignal
|
| 8 |
-
aiosqlite
|
| 9 |
-
anyio
|
| 10 |
-
asyncpg
|
| 11 |
-
fastapi
|
| 12 |
-
h11
|
| 13 |
-
httpcore
|
| 14 |
-
httpx
|
| 15 |
-
httpx-sse
|
| 16 |
-
starlette
|
| 17 |
-
uvicorn
|
| 18 |
-
python-multipart
|
| 19 |
-
slowapi
|
| 20 |
-
limits
|
| 21 |
-
email-validator
|
| 22 |
-
dnspython
|
| 23 |
-
|
| 24 |
-
# --- Database & Migrations ---
|
| 25 |
-
alembic
|
| 26 |
-
sqlalchemy
|
| 27 |
-
psycopg-binary
|
| 28 |
-
psycopg
|
| 29 |
-
redis
|
| 30 |
-
greenlet
|
| 31 |
-
pgvector
|
| 32 |
-
qdrant-client
|
| 33 |
-
|
| 34 |
-
# --- Auth & Security ---
|
| 35 |
-
PyJWT
|
| 36 |
-
passlib[bcrypt]
|
| 37 |
-
python-jose
|
| 38 |
-
python-dotenv
|
| 39 |
-
|
| 40 |
-
# --- AI, ML & Data Science ---
|
| 41 |
-
torch
|
| 42 |
-
transformers
|
| 43 |
-
optimum[onnxruntime]
|
| 44 |
-
onnxruntime
|
| 45 |
-
onnx
|
| 46 |
-
scikit-learn
|
| 47 |
-
scipy
|
| 48 |
-
numpy
|
| 49 |
-
pandas
|
| 50 |
-
networkx
|
| 51 |
-
tiktoken
|
| 52 |
-
tokenizers
|
| 53 |
-
safetensors
|
| 54 |
-
regex
|
| 55 |
-
pydantic
|
| 56 |
-
pydantic-settings
|
| 57 |
-
pydantic_core
|
| 58 |
-
|
| 59 |
-
# --- Azure & OpenAI ---
|
| 60 |
-
openai
|
| 61 |
-
azure-ai-inference
|
| 62 |
-
azure-ai-textanalytics
|
| 63 |
-
azure-common
|
| 64 |
-
azure-core
|
| 65 |
-
|
| 66 |
-
# --- LangChain Ecosystem ---
|
| 67 |
-
langchain
|
| 68 |
-
langchain-classic
|
| 69 |
-
langchain-community
|
| 70 |
-
langchain-core
|
| 71 |
-
langchain-openai
|
| 72 |
-
langchain-text-splitters
|
| 73 |
-
langgraph
|
| 74 |
-
langgraph-checkpoint
|
| 75 |
-
langgraph-prebuilt
|
| 76 |
-
langgraph-sdk
|
| 77 |
-
langsmith
|
| 78 |
-
|
| 79 |
-
# --- Celery & Queue ---
|
| 80 |
-
celery
|
| 81 |
-
kombu
|
| 82 |
-
amqp
|
| 83 |
-
billiard
|
| 84 |
-
vine
|
| 85 |
-
|
| 86 |
-
# --- Utilities & processing ---
|
| 87 |
-
requests
|
| 88 |
-
requests-toolbelt
|
| 89 |
-
tqdm
|
| 90 |
-
colorama
|
| 91 |
-
coloredlogs
|
| 92 |
-
click
|
| 93 |
-
click-didyoumean
|
| 94 |
-
click-plugins
|
| 95 |
-
click-repl
|
| 96 |
-
cachetools
|
| 97 |
-
charset-normalizer
|
| 98 |
-
idna
|
| 99 |
-
jinja2
|
| 100 |
-
joblib
|
| 101 |
-
marshmallow
|
| 102 |
-
multidict
|
| 103 |
-
packaging
|
| 104 |
-
pillow
|
| 105 |
-
protobuf
|
| 106 |
-
python-dateutil
|
| 107 |
-
pytz
|
| 108 |
-
pyyaml
|
| 109 |
-
six
|
| 110 |
-
tenacity
|
| 111 |
-
typing-extensions
|
| 112 |
-
urllib3
|
| 113 |
-
wrapt
|
| 114 |
-
xxhash
|
| 115 |
-
yarl
|
| 116 |
-
zstandard
|
| 117 |
-
fsspec
|
| 118 |
-
huggingface-hub
|
| 119 |
-
filelock
|
| 120 |
-
frozenlist
|
| 121 |
-
attrs
|
| 122 |
-
annotated-types
|
| 123 |
-
certifi
|
| 124 |
-
soupsieve
|
| 125 |
-
beautifulsoup4
|
|
|
|
| 1 |
+
fastapi==0.115.0
|
| 2 |
+
uvicorn[standard]==0.32.0
|
| 3 |
+
sentence-transformers==3.3.1
|
| 4 |
+
numpy==1.26.4
|
| 5 |
+
groq==0.13.0
|
| 6 |
+
python-dotenv==1.0.1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/__init__.py
DELETED
|
File without changes
|
src/app/celery_app.py
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
# src/app/celery_app.py
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import ssl
|
| 5 |
-
from celery import Celery
|
| 6 |
-
from src.app.config import settings
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
# Priority list for the broker
|
| 10 |
-
BROKER_FAILOVER_LIST = [str(settings.CELERY_BROKER_URL), "redis://localhost:6379/0"]
|
| 11 |
-
|
| 12 |
-
celery_app = Celery(
|
| 13 |
-
"chat_processor",
|
| 14 |
-
broker=BROKER_FAILOVER_LIST, # Celery will try these in order
|
| 15 |
-
include=["src.app.services.sentiment_worker", "src.app.services.embedding_worker"]
|
| 16 |
-
)
|
| 17 |
-
|
| 18 |
-
ssl_options = {
|
| 19 |
-
'ssl_cert_reqs': ssl.CERT_NONE
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
celery_app.conf.update(
|
| 23 |
-
# Broker & Backend Settings
|
| 24 |
-
result_backend=settings.CELERY_RESULT_BACKEND,
|
| 25 |
-
broker_use_ssl=ssl_options,
|
| 26 |
-
redis_backend_use_ssl=ssl_options,
|
| 27 |
-
|
| 28 |
-
# Critical Connection Resilience
|
| 29 |
-
broker_transport_options={
|
| 30 |
-
'visibility_timeout': 3600,
|
| 31 |
-
'socket_timeout': 30,
|
| 32 |
-
'socket_keepalive': True,
|
| 33 |
-
'health_check_interval': 15, # Ping every 15s to keep Upstash alive
|
| 34 |
-
'retry_on_timeout': True,
|
| 35 |
-
},
|
| 36 |
-
|
| 37 |
-
# Task Settings
|
| 38 |
-
task_serializer="json",
|
| 39 |
-
accept_content=["json"],
|
| 40 |
-
result_serializer="json",
|
| 41 |
-
timezone="UTC",
|
| 42 |
-
enable_utc=True,
|
| 43 |
-
task_acks_late=True, # Critical: allows continuation after fallback
|
| 44 |
-
|
| 45 |
-
# Worker Settings
|
| 46 |
-
worker_prefetch_multiplier=1,
|
| 47 |
-
worker_cancel_long_running_tasks_on_connection_loss=True,
|
| 48 |
-
|
| 49 |
-
# Routing
|
| 50 |
-
task_routes={
|
| 51 |
-
'src.app.services.embedding_worker.generate_embeddings_task': {'queue': 'embeddings'},
|
| 52 |
-
'src.app.services.sentiment_worker.analyze_sentiment_task': {'queue': 'sentiment'},
|
| 53 |
-
}
|
| 54 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/config.py
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 1 |
-
from pydantic import AnyUrl, AnyHttpUrl, field_validator, PostgresDsn, model_validator
|
| 2 |
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 3 |
-
from typing import Optional, List
|
| 4 |
-
import secrets
|
| 5 |
-
import logging
|
| 6 |
-
import logging.config
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
class Settings(BaseSettings):
|
| 11 |
-
model_config = SettingsConfigDict(env_file=".env")
|
| 12 |
-
|
| 13 |
-
PROJECT_NAME: str = "SentimentScope API"
|
| 14 |
-
APP_NAME: str = PROJECT_NAME
|
| 15 |
-
ENVIRONMENT: str = "development"
|
| 16 |
-
|
| 17 |
-
DB_USER: str
|
| 18 |
-
DB_PASSWORD: str
|
| 19 |
-
DB_HOST: str
|
| 20 |
-
DB_PORT: int
|
| 21 |
-
DB_NAME: str
|
| 22 |
-
DATABASE_URL: Optional[PostgresDsn] = None
|
| 23 |
-
|
| 24 |
-
# Azure OpenAI configs...
|
| 25 |
-
AZURE_OPENAI_ENDPOINT: AnyHttpUrl
|
| 26 |
-
AZURE_OPENAI_API_KEY: str
|
| 27 |
-
AZURE_OPENAI_DEPLOYMENT: str
|
| 28 |
-
AZURE_OPENAI_API_VERSION: str
|
| 29 |
-
AZURE_OPENAI_ENDPOINT_CONTEXT_4: AnyHttpUrl
|
| 30 |
-
AZURE_OPENAI_DEPLOYMENT_CONTEXT_4: str
|
| 31 |
-
AZURE_OPENAI_API_KEY_CONTEXT_4: str
|
| 32 |
-
AZURE_OPENAI_API_VERSION_CONTEXT_4: str
|
| 33 |
-
AZURE_OPENAI_ENDPOINT_ROUTER: AnyHttpUrl
|
| 34 |
-
AZURE_OPENAI_DEPLOYMENT_ROUTER: str
|
| 35 |
-
AZURE_OPENAI_API_KEY_ROUTER: str
|
| 36 |
-
AZURE_OPENAI_API_VERSION_ROUTER: str
|
| 37 |
-
# AZURE_OPENAI_ENDPOINT_MAIN: AnyHttpUrl
|
| 38 |
-
# AZURE_OPENAI_DEPLOYMENT_MAIN: str
|
| 39 |
-
# AZURE_OPENAI_API_KEY_MAIN: str
|
| 40 |
-
# AZURE_OPENAI_API_VERSION_MAIN: str
|
| 41 |
-
AZURE_OPENAI_ENDPOINT_CONTEXT_4O: AnyHttpUrl
|
| 42 |
-
AZURE_OPENAI_DEPLOYMENT_CONTEXT_4O: str
|
| 43 |
-
AZURE_OPENAI_API_KEY_CONTEXT_4O: str
|
| 44 |
-
AZURE_OPENAI_API_VERSION_CONTEXT_4O: str
|
| 45 |
-
AZURE_OPENAI_ENDPOINT_MAIN_4O: AnyHttpUrl
|
| 46 |
-
AZURE_OPENAI_DEPLOYMENT_MAIN_4O: str
|
| 47 |
-
AZURE_OPENAI_API_KEY_MAIN_4O: str
|
| 48 |
-
AZURE_OPENAI_API_VERSION_MAIN_4O: str
|
| 49 |
-
AZURE_OPENAI_ENDPOINT_MAIN_4O_MINI: AnyHttpUrl
|
| 50 |
-
AZURE_OPENAI_DEPLOYMENT_MAIN_4O_MINI: str
|
| 51 |
-
AZURE_OPENAI_API_KEY_MAIN_4O_MINI: str
|
| 52 |
-
AZURE_OPENAI_API_VERSION_MAIN_4O_MINI: str
|
| 53 |
-
AZURE_LANGUAGE_ENDPOINT: Optional[str] = None
|
| 54 |
-
AZURE_LANGUAGE_KEY: Optional[str] = None
|
| 55 |
-
|
| 56 |
-
SECRET_KEY: str = secrets.token_urlsafe(32)
|
| 57 |
-
IS_CELERY_WORKER: str = "false"
|
| 58 |
-
CELERY_BROKER_URL: Optional[str] = None
|
| 59 |
-
CELERY_RESULT_BACKEND: Optional[str] = None
|
| 60 |
-
QDRANT_URL: str
|
| 61 |
-
QDRANT_API_KEY: str
|
| 62 |
-
CORS_ORIGINS: List[str] = []
|
| 63 |
-
JWT_ALGORITHM: str
|
| 64 |
-
JWT_ACCESS_TOKEN_EXPIRE_DAYS: int
|
| 65 |
-
|
| 66 |
-
CORS_ALLOW_CREDENTIALS: bool = True
|
| 67 |
-
TRUSTED_HOSTS: List[str] = ["*"]
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
DB_CONNECT_RETRIES: int = 3
|
| 71 |
-
DB_CONNECT_BACKOFF_SECONDS: int = 2
|
| 72 |
-
|
| 73 |
-
DEBUG: bool = True
|
| 74 |
-
LOG_LEVEL: str = "INFO"
|
| 75 |
-
|
| 76 |
-
MAX_UPLOAD_SIZE_BYTES: int = 10 * 1024 * 1024 # 10 MB
|
| 77 |
-
|
| 78 |
-
@model_validator(mode="before")
|
| 79 |
-
@classmethod
|
| 80 |
-
def assemble_database_url(cls, values):
|
| 81 |
-
# 1. Check if URL is provided directly
|
| 82 |
-
if values.get("DATABASE_URL"):
|
| 83 |
-
url = str(values.get("DATABASE_URL"))
|
| 84 |
-
# If it starts with postgresql:// but not postgresql+asyncpg://, fix it
|
| 85 |
-
if url.startswith("postgresql://") and "asyncpg" not in url:
|
| 86 |
-
values["DATABASE_URL"] = url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
| 87 |
-
|
| 88 |
-
# 2. If no URL provided, assemble it from parts (your existing logic)
|
| 89 |
-
elif not values.get("DATABASE_URL"):
|
| 90 |
-
user = values.get("DB_USER")
|
| 91 |
-
password = values.get("DB_PASSWORD")
|
| 92 |
-
host = values.get("DB_HOST")
|
| 93 |
-
port = values.get("DB_PORT")
|
| 94 |
-
db = values.get("DB_NAME")
|
| 95 |
-
|
| 96 |
-
values["DATABASE_URL"] = (
|
| 97 |
-
f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{db}"
|
| 98 |
-
)
|
| 99 |
-
|
| 100 |
-
return values
|
| 101 |
-
|
| 102 |
-
settings = Settings()
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def setup_logging():
|
| 107 |
-
"""Configure global logging level and format based on settings."""
|
| 108 |
-
log_level = getattr(logging, settings.LOG_LEVEL.upper(), logging.INFO)
|
| 109 |
-
|
| 110 |
-
logging.basicConfig(
|
| 111 |
-
level=log_level,
|
| 112 |
-
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 113 |
-
datefmt="%Y-%m-%d %H:%M:%S",
|
| 114 |
-
)
|
| 115 |
-
|
| 116 |
-
# Suppress noisy loggers from dependencies
|
| 117 |
-
# logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
| 118 |
-
# logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
| 119 |
-
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
| 120 |
-
# logging.getLogger("asyncio").setLevel(logging.WARNING)
|
| 121 |
-
# logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 122 |
-
logging.getLogger("multipart").setLevel(logging.WARNING)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/crud.py
DELETED
|
@@ -1,783 +0,0 @@
|
|
| 1 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 2 |
-
from sqlalchemy.exc import SQLAlchemyError
|
| 3 |
-
from sqlalchemy.future import select
|
| 4 |
-
from sqlalchemy import insert, func, delete, select
|
| 5 |
-
from sqlalchemy.orm import selectinload, joinedload
|
| 6 |
-
from typing import List, Optional, Dict, Tuple, AsyncGenerator
|
| 7 |
-
from src.app import models, schemas
|
| 8 |
-
from src.app.utils.raw_txt_parser import CleanedMessage
|
| 9 |
-
from src.app.utils.message_analytics import compute_message_analytics
|
| 10 |
-
from datetime import date, datetime, time
|
| 11 |
-
import uuid
|
| 12 |
-
|
| 13 |
-
TIME_PERIOD_BUCKETS = {
|
| 14 |
-
"Morning": [6, 7, 8, 9, 10, 11],
|
| 15 |
-
"Afternoon": [12, 13, 14, 15, 16],
|
| 16 |
-
"Evening": [17, 18, 19, 20],
|
| 17 |
-
"Night": [21, 22, 23, 0, 1, 2, 3, 4, 5]
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
async def create_chat(db: AsyncSession, owner_id: uuid.UUID, title: Optional[str] = None, should_commit: bool = True) -> models.Chat:
|
| 22 |
-
new_chat = models.Chat(title=title, owner_id=owner_id)
|
| 23 |
-
db.add(new_chat)
|
| 24 |
-
await db.flush()
|
| 25 |
-
|
| 26 |
-
if should_commit:
|
| 27 |
-
await db.commit()
|
| 28 |
-
await db.refresh(new_chat)
|
| 29 |
-
return new_chat
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
async def get_chat(db: AsyncSession, chat_id: int) -> Optional[models.Chat]:
|
| 33 |
-
result = await db.execute(select(models.Chat).where(models.Chat.id == chat_id))
|
| 34 |
-
return result.scalars().first()
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
async def get_all_chats(db: AsyncSession) -> List[models.Chat]:
|
| 39 |
-
result = await db.execute(select(models.Chat).order_by(models.Chat.created_at.desc()))
|
| 40 |
-
return result.scalars().all()
|
| 41 |
-
|
| 42 |
-
async def get_chat_by_name(db: AsyncSession, chat_name: str) -> Optional[models.Chat]:
|
| 43 |
-
chat_res = await db.execute(
|
| 44 |
-
select(models.Chat).where(models.Chat.title == chat_name.strip())
|
| 45 |
-
)
|
| 46 |
-
return chat_res.scalars().first()
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
async def delete_chat(db: AsyncSession, chat_id: int, should_commit: bool = True) -> bool:
|
| 52 |
-
chat = await get_chat(db, chat_id=chat_id)
|
| 53 |
-
if chat:
|
| 54 |
-
await db.execute(delete(models.Chat).where(models.Chat.id == chat_id))
|
| 55 |
-
if should_commit:
|
| 56 |
-
await db.commit()
|
| 57 |
-
return True
|
| 58 |
-
return False
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
async def request_chat_cancel(db: AsyncSession, chat_id: int) -> bool:
|
| 63 |
-
result = await db.execute(select(models.Chat).filter(models.Chat.id == chat_id))
|
| 64 |
-
chat = result.scalars().first()
|
| 65 |
-
if chat:
|
| 66 |
-
chat.cancel_requested = True
|
| 67 |
-
await db.commit()
|
| 68 |
-
return True
|
| 69 |
-
return False
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
async def is_chat_cancelled(db: AsyncSession, chat_id: int) -> bool:
|
| 73 |
-
result = await db.execute(select(models.Chat.cancel_requested).filter(models.Chat.id == chat_id))
|
| 74 |
-
cancelled = result.scalar()
|
| 75 |
-
return bool(cancelled)
|
| 76 |
-
|
| 77 |
-
async def update_chat_status(db: AsyncSession, chat_id: int, status: str):
|
| 78 |
-
chat = await get_chat(db, chat_id)
|
| 79 |
-
if chat:
|
| 80 |
-
chat.sentiment_status = status
|
| 81 |
-
db.add(chat)
|
| 82 |
-
await db.commit()
|
| 83 |
-
|
| 84 |
-
async def update_chat_embedding_status(db: AsyncSession, chat_id: int, status: str, should_commit: bool = True):
|
| 85 |
-
chat = await get_chat(db, chat_id)
|
| 86 |
-
if chat:
|
| 87 |
-
chat.embeddings_status = status
|
| 88 |
-
db.add(chat)
|
| 89 |
-
if should_commit:
|
| 90 |
-
await db.commit()
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
async def get_chat_embedding_status(db: AsyncSession, chat_id: int) -> Optional[str]:
|
| 94 |
-
"""
|
| 95 |
-
Quickly get just the embeddings_status of a chat.
|
| 96 |
-
"""
|
| 97 |
-
result = await db.execute(
|
| 98 |
-
select(models.Chat.embeddings_status)
|
| 99 |
-
.where(models.Chat.id == chat_id)
|
| 100 |
-
)
|
| 101 |
-
return result.scalar_one_or_none()
|
| 102 |
-
|
| 103 |
-
async def get_chat_status(db: AsyncSession, chat_id: int) -> Optional[str]:
|
| 104 |
-
"""
|
| 105 |
-
Quickly get just the sentiment_status of a chat.
|
| 106 |
-
"""
|
| 107 |
-
result = await db.execute(
|
| 108 |
-
select(models.Chat.sentiment_status)
|
| 109 |
-
.where(models.Chat.id == chat_id)
|
| 110 |
-
)
|
| 111 |
-
return result.scalar_one_or_none()
|
| 112 |
-
|
| 113 |
-
async def get_conversation_history(
|
| 114 |
-
db: AsyncSession,
|
| 115 |
-
chat_id: int,
|
| 116 |
-
limit: int = 6 # Fetch last 6 turns (3 pairs) to save tokens
|
| 117 |
-
) -> List[models.ConversationHistory]:
|
| 118 |
-
stmt = (
|
| 119 |
-
select(models.ConversationHistory)
|
| 120 |
-
.where(models.ConversationHistory.chat_id == chat_id)
|
| 121 |
-
.order_by(models.ConversationHistory.created_at.desc()) # Get newest first
|
| 122 |
-
.limit(limit)
|
| 123 |
-
)
|
| 124 |
-
result = await db.execute(stmt)
|
| 125 |
-
# Reverse to return chronological order (Oldest -> Newest)
|
| 126 |
-
return result.scalars().all()[::-1]
|
| 127 |
-
|
| 128 |
-
async def add_conversation_turn(
|
| 129 |
-
db: AsyncSession,
|
| 130 |
-
chat_id: int,
|
| 131 |
-
user_q: str,
|
| 132 |
-
ai_a: str,
|
| 133 |
-
sources: Optional[List[dict]] = None
|
| 134 |
-
):
|
| 135 |
-
"""Saves both user query and AI response in a transaction."""
|
| 136 |
-
user_msg = models.ConversationHistory(chat_id=chat_id, role="user", content=user_q, sources=None)
|
| 137 |
-
ai_msg = models.ConversationHistory(chat_id=chat_id, role="assistant", content=ai_a, sources=sources)
|
| 138 |
-
db.add_all([user_msg, ai_msg])
|
| 139 |
-
await db.commit()
|
| 140 |
-
|
| 141 |
-
async def get_messages_batch(
|
| 142 |
-
db: AsyncSession,
|
| 143 |
-
chat_id: int,
|
| 144 |
-
limit: int,
|
| 145 |
-
offset: int,
|
| 146 |
-
min_date: Optional[datetime] = None
|
| 147 |
-
) -> List[models.Message]:
|
| 148 |
-
"""
|
| 149 |
-
Fetches a batch of messages for a chat with optional filtering by minimum date.
|
| 150 |
-
"""
|
| 151 |
-
stmt = (
|
| 152 |
-
select(models.Message)
|
| 153 |
-
.where(models.Message.chat_id == chat_id)
|
| 154 |
-
)
|
| 155 |
-
|
| 156 |
-
# Optional min timestamp
|
| 157 |
-
if min_date:
|
| 158 |
-
stmt = stmt.where(models.Message.timestamp >= min_date)
|
| 159 |
-
|
| 160 |
-
# Pagination + ordering
|
| 161 |
-
stmt = (
|
| 162 |
-
stmt.order_by(models.Message.timestamp.desc())
|
| 163 |
-
.limit(limit)
|
| 164 |
-
.offset(offset)
|
| 165 |
-
)
|
| 166 |
-
|
| 167 |
-
result = await db.execute(stmt)
|
| 168 |
-
return result.scalars().all()
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
async def clear_conversation_history(db: AsyncSession, chat_id: int):
|
| 181 |
-
|
| 182 |
-
await db.execute(
|
| 183 |
-
delete(models.ConversationHistory)
|
| 184 |
-
.where(models.ConversationHistory.chat_id == chat_id)
|
| 185 |
-
)
|
| 186 |
-
await db.commit()
|
| 187 |
-
|
| 188 |
-
async def get_full_conversation_history(
|
| 189 |
-
db: AsyncSession,
|
| 190 |
-
chat_id: int
|
| 191 |
-
) -> List[models.ConversationHistory]:
|
| 192 |
-
"""
|
| 193 |
-
Fetches the complete history for a chat session to restore the UI state.
|
| 194 |
-
Ordered purely chronologically (Oldest -> Newest).
|
| 195 |
-
"""
|
| 196 |
-
stmt = (
|
| 197 |
-
select(models.ConversationHistory)
|
| 198 |
-
.where(models.ConversationHistory.chat_id == chat_id)
|
| 199 |
-
.order_by(models.ConversationHistory.created_at.asc())
|
| 200 |
-
)
|
| 201 |
-
result = await db.execute(stmt)
|
| 202 |
-
return result.scalars().all()
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
async def create_participant(db: AsyncSession, chat_id: int, name: str, should_commit: bool = True) -> models.Participant:
|
| 206 |
-
new_participant = models.Participant(chat_id=chat_id, name=name)
|
| 207 |
-
db.add(new_participant)
|
| 208 |
-
await db.flush()
|
| 209 |
-
if should_commit:
|
| 210 |
-
await db.commit()
|
| 211 |
-
await db.refresh(new_participant)
|
| 212 |
-
return new_participant
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
async def bulk_insert_participants(db: AsyncSession, chat_id: int, names: List[str], should_commit: bool = True) -> List[models.Participant]:
|
| 216 |
-
# Fetch existing participants to avoid duplicates
|
| 217 |
-
existing_q = await db.execute(select(models.Participant).where(models.Participant.chat_id == chat_id))
|
| 218 |
-
already_existing = {p.name: p for p in existing_q.scalars().all()}
|
| 219 |
-
to_create = [models.Participant(chat_id=chat_id, name=name) for name in names if name not in already_existing]
|
| 220 |
-
|
| 221 |
-
if to_create:
|
| 222 |
-
db.add_all(to_create)
|
| 223 |
-
await db.flush()
|
| 224 |
-
|
| 225 |
-
if should_commit:
|
| 226 |
-
await db.commit()
|
| 227 |
-
for p in to_create:
|
| 228 |
-
await db.refresh(p)
|
| 229 |
-
|
| 230 |
-
q = await db.execute(select(models.Participant).where(models.Participant.chat_id == chat_id))
|
| 231 |
-
return q.scalars().all()
|
| 232 |
-
|
| 233 |
-
async def get_participants_by_chat(db: AsyncSession, chat_id: int) -> List[models.Participant]:
|
| 234 |
-
result = await db.execute(select(models.Participant).where(models.Participant.chat_id == chat_id))
|
| 235 |
-
return result.scalars().all()
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
async def create_message(db: AsyncSession, message: schemas.MessageCreate, chat_id: int, participant_id: Optional[int], should_commit: bool = True) -> models.Message:
|
| 240 |
-
# Compute analytics for the message
|
| 241 |
-
analytics = compute_message_analytics(message.content)
|
| 242 |
-
|
| 243 |
-
new_msg = models.Message(
|
| 244 |
-
chat_id=chat_id,
|
| 245 |
-
participant_id=participant_id,
|
| 246 |
-
timestamp=message.timestamp,
|
| 247 |
-
content=message.content,
|
| 248 |
-
raw=getattr(message, "raw", None) or message.content, # Use content if raw is not provided
|
| 249 |
-
word_count=analytics['word_count'],
|
| 250 |
-
emojis_count=analytics['emojis_count'],
|
| 251 |
-
links_count=analytics['links_count'],
|
| 252 |
-
is_question=analytics['is_question'],
|
| 253 |
-
is_media=analytics['is_media']
|
| 254 |
-
)
|
| 255 |
-
db.add(new_msg)
|
| 256 |
-
await db.flush()
|
| 257 |
-
if should_commit:
|
| 258 |
-
await db.commit()
|
| 259 |
-
await db.refresh(new_msg)
|
| 260 |
-
return new_msg
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
async def bulk_insert_messages(
|
| 264 |
-
db: AsyncSession,
|
| 265 |
-
messages: List[dict],
|
| 266 |
-
should_commit: bool = True
|
| 267 |
-
) -> List[int]:
|
| 268 |
-
if not messages:
|
| 269 |
-
return []
|
| 270 |
-
|
| 271 |
-
try:
|
| 272 |
-
CHUNK_SIZE = 2500
|
| 273 |
-
inserted_ids = []
|
| 274 |
-
|
| 275 |
-
for i in range(0, len(messages), CHUNK_SIZE):
|
| 276 |
-
chunk = messages[i:i + CHUNK_SIZE]
|
| 277 |
-
stmt = insert(models.Message).values(chunk).returning(models.Message.id)
|
| 278 |
-
result = await db.execute(stmt)
|
| 279 |
-
inserted_ids.extend(result.scalars().all())
|
| 280 |
-
|
| 281 |
-
if should_commit:
|
| 282 |
-
await db.commit()
|
| 283 |
-
|
| 284 |
-
return inserted_ids
|
| 285 |
-
|
| 286 |
-
except SQLAlchemyError:
|
| 287 |
-
await db.rollback()
|
| 288 |
-
raise
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
async def get_messages_by_chat(db: AsyncSession, chat_id: int) -> List[models.Message]:
|
| 293 |
-
result = await db.execute(select(models.Message).where(models.Message.chat_id == chat_id))
|
| 294 |
-
return result.scalars().all()
|
| 295 |
-
|
| 296 |
-
async def get_all_sender_segments_for_chat(db: AsyncSession, chat_id: int) -> List[models.SenderSegment]:
|
| 297 |
-
stmt = (
|
| 298 |
-
select(models.SenderSegment)
|
| 299 |
-
.join(models.TimeSegment)
|
| 300 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 301 |
-
)
|
| 302 |
-
result = await db.execute(stmt)
|
| 303 |
-
return result.scalars().all()
|
| 304 |
-
|
| 305 |
-
async def get_segments_batch(
|
| 306 |
-
db: AsyncSession,
|
| 307 |
-
chat_id: int,
|
| 308 |
-
limit: int,
|
| 309 |
-
offset: int,
|
| 310 |
-
min_date: Optional[datetime] = None
|
| 311 |
-
) -> List[models.SenderSegment]:
|
| 312 |
-
|
| 313 |
-
stmt = (
|
| 314 |
-
select(models.SenderSegment)
|
| 315 |
-
.join(models.TimeSegment)
|
| 316 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
if min_date:
|
| 320 |
-
stmt = stmt.where(models.TimeSegment.start_time >= min_date)
|
| 321 |
-
|
| 322 |
-
stmt = (
|
| 323 |
-
stmt.order_by(models.TimeSegment.start_time.desc())
|
| 324 |
-
.limit(limit)
|
| 325 |
-
.offset(offset)
|
| 326 |
-
)
|
| 327 |
-
|
| 328 |
-
result = await db.execute(stmt)
|
| 329 |
-
return result.scalars().all()
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
# Issues here to handle later
|
| 333 |
-
async def delete_message(db: AsyncSession, message_id: int, should_commit: bool = True) -> bool:
|
| 334 |
-
|
| 335 |
-
try:
|
| 336 |
-
result = await db.execute(select(models.Message).where(models.Message.id == message_id))
|
| 337 |
-
message = result.scalars().first()
|
| 338 |
-
if not message:
|
| 339 |
-
return False
|
| 340 |
-
|
| 341 |
-
participant_id = message.participant_id
|
| 342 |
-
chat_id = message.chat_id
|
| 343 |
-
|
| 344 |
-
await db.delete(message)
|
| 345 |
-
|
| 346 |
-
if participant_id:
|
| 347 |
-
other_messages_query = await db.execute(
|
| 348 |
-
select(models.Message).where(
|
| 349 |
-
models.Message.chat_id == chat_id,
|
| 350 |
-
models.Message.participant_id == participant_id
|
| 351 |
-
)
|
| 352 |
-
)
|
| 353 |
-
|
| 354 |
-
rem_messages = other_messages_query.scalars().first()
|
| 355 |
-
if not rem_messages:
|
| 356 |
-
participant = await db.get(models.Participant, participant_id)
|
| 357 |
-
if participant:
|
| 358 |
-
await db.delete(participant)
|
| 359 |
-
if should_commit:
|
| 360 |
-
await db.commit()
|
| 361 |
-
|
| 362 |
-
return True
|
| 363 |
-
except Exception:
|
| 364 |
-
if should_commit:
|
| 365 |
-
await db.rollback()
|
| 366 |
-
raise
|
| 367 |
-
|
| 368 |
-
async def create_sender_segment(
|
| 369 |
-
db: AsyncSession,
|
| 370 |
-
time_segment_id: int,
|
| 371 |
-
sender_id: int,
|
| 372 |
-
sender_details: dict,
|
| 373 |
-
should_commit: bool = True
|
| 374 |
-
):
|
| 375 |
-
seg = models.SenderSegment(
|
| 376 |
-
time_segment_id=time_segment_id,
|
| 377 |
-
sender_id=sender_id,
|
| 378 |
-
message_count=sender_details.get("message_count"),
|
| 379 |
-
combined_text=sender_details.get("combined_text", "")
|
| 380 |
-
)
|
| 381 |
-
db.add(seg)
|
| 382 |
-
await db.flush()
|
| 383 |
-
if should_commit:
|
| 384 |
-
await db.commit()
|
| 385 |
-
await db.refresh(seg)
|
| 386 |
-
return seg
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
async def create_time_segment(
|
| 391 |
-
db: AsyncSession,
|
| 392 |
-
chat_id: int,
|
| 393 |
-
time_segment_details: dict,
|
| 394 |
-
should_commit: bool = True
|
| 395 |
-
) -> models.TimeSegment:
|
| 396 |
-
seg = models.TimeSegment(
|
| 397 |
-
chat_id=chat_id,
|
| 398 |
-
start_time=time_segment_details.get("start_time"),
|
| 399 |
-
end_time=time_segment_details.get("end_time"),
|
| 400 |
-
duration_minutes=time_segment_details.get("duration_minutes"),
|
| 401 |
-
message_count=time_segment_details.get("message_count"),
|
| 402 |
-
summary=time_segment_details.get("summary", None)
|
| 403 |
-
)
|
| 404 |
-
db.add(seg)
|
| 405 |
-
await db.flush()
|
| 406 |
-
if should_commit:
|
| 407 |
-
await db.commit()
|
| 408 |
-
await db.refresh(seg)
|
| 409 |
-
return seg
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
async def bulk_insert_segments(
|
| 413 |
-
db: AsyncSession,
|
| 414 |
-
chat_id: int,
|
| 415 |
-
participants_map: dict,
|
| 416 |
-
time_segments_list: List[dict]
|
| 417 |
-
) -> int:
|
| 418 |
-
if not time_segments_list:
|
| 419 |
-
return 0
|
| 420 |
-
|
| 421 |
-
# Build all TimeSegment objects
|
| 422 |
-
time_segments = [
|
| 423 |
-
models.TimeSegment(
|
| 424 |
-
chat_id=chat_id,
|
| 425 |
-
start_time=seg.get("start_time"),
|
| 426 |
-
end_time=seg.get("end_time"),
|
| 427 |
-
duration_minutes=seg.get("duration_minutes"),
|
| 428 |
-
message_count=seg.get("message_count"),
|
| 429 |
-
summary=seg.get("summary")
|
| 430 |
-
)
|
| 431 |
-
for seg in time_segments_list
|
| 432 |
-
]
|
| 433 |
-
|
| 434 |
-
# Bulk add time segments
|
| 435 |
-
db.add_all(time_segments)
|
| 436 |
-
await db.flush() # Assign IDs
|
| 437 |
-
|
| 438 |
-
# Build all SenderSegment objects linked to their TimeSegments
|
| 439 |
-
sender_segments = []
|
| 440 |
-
for seg_obj, seg_dict in zip(time_segments, time_segments_list):
|
| 441 |
-
sender_groups = seg_dict.get("sender_groups", {})
|
| 442 |
-
for sender, details in sender_groups.items():
|
| 443 |
-
sender_id = participants_map.get(sender)
|
| 444 |
-
if not sender_id:
|
| 445 |
-
continue
|
| 446 |
-
sender_segments.append(models.SenderSegment(
|
| 447 |
-
time_segment_id=seg_obj.id,
|
| 448 |
-
sender_id=sender_id,
|
| 449 |
-
message_count=details.get("message_count"),
|
| 450 |
-
combined_text=details.get("combined_text", "")
|
| 451 |
-
))
|
| 452 |
-
|
| 453 |
-
if sender_segments:
|
| 454 |
-
db.add_all(sender_segments)
|
| 455 |
-
await db.flush()
|
| 456 |
-
|
| 457 |
-
return len(time_segments)
|
| 458 |
-
|
| 459 |
-
async def get_sender_segments_by_sender_id(db: AsyncSession, sender_id: int) -> List[models.SenderSegment]:
|
| 460 |
-
result = await db.execute(
|
| 461 |
-
select(models.SenderSegment)
|
| 462 |
-
.where(models.SenderSegment.sender_id == sender_id)
|
| 463 |
-
)
|
| 464 |
-
return result.scalars().all()
|
| 465 |
-
|
| 466 |
-
async def get_all_time_segments(db: AsyncSession, chat_id: int) -> List[models.TimeSegment]:
|
| 467 |
-
result = await db.execute(
|
| 468 |
-
select(models.TimeSegment)
|
| 469 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 470 |
-
)
|
| 471 |
-
return result.scalars().all()
|
| 472 |
-
|
| 473 |
-
async def create_message_sentiment(
|
| 474 |
-
db: AsyncSession,
|
| 475 |
-
msg_id: int,
|
| 476 |
-
payload: dict,
|
| 477 |
-
should_commit: bool = True
|
| 478 |
-
) -> models.MessageSentiment:
|
| 479 |
-
s = models.MessageSentiment(message_id = msg_id, **payload)
|
| 480 |
-
db.add(s)
|
| 481 |
-
await db.flush()
|
| 482 |
-
|
| 483 |
-
if should_commit:
|
| 484 |
-
await db.commit()
|
| 485 |
-
await db.refresh(s)
|
| 486 |
-
return s
|
| 487 |
-
|
| 488 |
-
async def create_segment_sentiment(
|
| 489 |
-
db: AsyncSession,
|
| 490 |
-
sender_segment_id: int,
|
| 491 |
-
payload: dict,
|
| 492 |
-
should_commit: bool = True
|
| 493 |
-
) -> models.SegmentSentiment:
|
| 494 |
-
s = models.SegmentSentiment(sender_segment_id=sender_segment_id, **payload)
|
| 495 |
-
db.add(s)
|
| 496 |
-
await db.flush()
|
| 497 |
-
|
| 498 |
-
if should_commit:
|
| 499 |
-
await db.commit()
|
| 500 |
-
await db.refresh(s)
|
| 501 |
-
return s
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
async def get_sentiment_progress(db: AsyncSession, chat_id: int):
|
| 505 |
-
"""
|
| 506 |
-
Efficiently gets counts using SQL func.count.
|
| 507 |
-
"""
|
| 508 |
-
|
| 509 |
-
# Use func.count for direct SQL COUNT(*), which is instant
|
| 510 |
-
total_messages_q = await db.execute(
|
| 511 |
-
select(func.count(models.Message.id))
|
| 512 |
-
.where(models.Message.chat_id == chat_id)
|
| 513 |
-
)
|
| 514 |
-
total_messages = total_messages_q.scalar_one()
|
| 515 |
-
|
| 516 |
-
scored_msgs_q = await db.execute(
|
| 517 |
-
select(func.count(models.MessageSentiment.id))
|
| 518 |
-
.join(models.Message)
|
| 519 |
-
.where(models.Message.chat_id == chat_id)
|
| 520 |
-
)
|
| 521 |
-
total_messages_scored = scored_msgs_q.scalar_one()
|
| 522 |
-
|
| 523 |
-
total_segments_q = await db.execute(
|
| 524 |
-
select(func.count(models.SenderSegment.id))
|
| 525 |
-
.join(models.TimeSegment)
|
| 526 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 527 |
-
)
|
| 528 |
-
total_segments = total_segments_q.scalar_one()
|
| 529 |
-
|
| 530 |
-
scored_segments_q = await db.execute(
|
| 531 |
-
select(func.count(models.SegmentSentiment.id))
|
| 532 |
-
.join(models.SenderSegment)
|
| 533 |
-
.join(models.TimeSegment)
|
| 534 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 535 |
-
)
|
| 536 |
-
total_segments_scored = scored_segments_q.scalar_one()
|
| 537 |
-
|
| 538 |
-
return {
|
| 539 |
-
"messages_total": total_messages,
|
| 540 |
-
"messages_scored": total_messages_scored,
|
| 541 |
-
"segments_total": total_segments,
|
| 542 |
-
"segments_scored": total_segments_scored
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
async def get_chat_message_sentiments(db: AsyncSession, chat_id: int) -> Optional[List[models.MessageSentiment]]:
|
| 546 |
-
stmt = select(models.MessageSentiment).join(models.Message).where(
|
| 547 |
-
models.Message.chat_id == chat_id
|
| 548 |
-
)
|
| 549 |
-
result = await db.execute(stmt)
|
| 550 |
-
|
| 551 |
-
return result.scalars().all()
|
| 552 |
-
|
| 553 |
-
async def get_segments_for_summarization(
|
| 554 |
-
db: AsyncSession, chat_id: int
|
| 555 |
-
) -> List[models.TimeSegment]:
|
| 556 |
-
"""
|
| 557 |
-
Fetches all TimeSegments for a specific chat that do not
|
| 558 |
-
yet have a summary.
|
| 559 |
-
|
| 560 |
-
This function is optimized to EAGERLY load the related
|
| 561 |
-
'sender_segments' for each TimeSegment in a single batch,
|
| 562 |
-
preventing the N+1 query problem in the summary service.
|
| 563 |
-
"""
|
| 564 |
-
|
| 565 |
-
# 1. Define the query
|
| 566 |
-
stmt = (
|
| 567 |
-
select(models.TimeSegment)
|
| 568 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 569 |
-
.where(models.TimeSegment.summary == None) # <-- Finds work to do
|
| 570 |
-
.options(
|
| 571 |
-
selectinload(models.TimeSegment.sender_segments) # <-- The EFFICIENT part
|
| 572 |
-
)
|
| 573 |
-
.order_by(models.TimeSegment.start_time) # Process segments in order
|
| 574 |
-
)
|
| 575 |
-
|
| 576 |
-
# 2. Execute the query
|
| 577 |
-
result = await db.execute(stmt)
|
| 578 |
-
|
| 579 |
-
# 3. Return the TimeSegment objects
|
| 580 |
-
# .scalars() gets the TimeSegment objects
|
| 581 |
-
# .all() executes and returns the list
|
| 582 |
-
segments = result.scalars().all()
|
| 583 |
-
|
| 584 |
-
return segments
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
async def get_chat_segment_sentiments(db: AsyncSession, chat_id: int) -> Optional[List[models.SegmentSentiment]]:
|
| 588 |
-
stmt = select(models.SegmentSentiment).join(models.SenderSegment).join(
|
| 589 |
-
models.TimeSegment
|
| 590 |
-
).where(
|
| 591 |
-
models.TimeSegment.chat_id == chat_id
|
| 592 |
-
)
|
| 593 |
-
|
| 594 |
-
result = await db.execute(stmt)
|
| 595 |
-
return result.scalars().all()
|
| 596 |
-
|
| 597 |
-
async def get_sentiment_by_message(db: AsyncSession, message_id: int) -> Optional[schemas.SentimentRead]:
|
| 598 |
-
result = await db.execute(select(
|
| 599 |
-
models.MessageSentiment
|
| 600 |
-
)
|
| 601 |
-
.where(models.MessageSentiment.message_id == message_id)
|
| 602 |
-
)
|
| 603 |
-
|
| 604 |
-
if result:
|
| 605 |
-
return result.scalars().first()
|
| 606 |
-
return None
|
| 607 |
-
|
| 608 |
-
async def get_sentiment_by_segment(db: AsyncSession, segment_id: int) -> Optional[schemas.SentimentRead]:
|
| 609 |
-
result = await db.execute(
|
| 610 |
-
select(
|
| 611 |
-
models.SegmentSentiment
|
| 612 |
-
)
|
| 613 |
-
.where(models.SegmentSentiment.sender_segment_id == segment_id)
|
| 614 |
-
)
|
| 615 |
-
if result: return result.scalars().first()
|
| 616 |
-
return None
|
| 617 |
-
|
| 618 |
-
# Ingest Cleaned Chat
|
| 619 |
-
async def ingest_cleaned_chat(
|
| 620 |
-
db: AsyncSession,
|
| 621 |
-
owner_id: uuid.UUID,
|
| 622 |
-
chat_name: str,
|
| 623 |
-
cleaned_messages: List["CleanedMessage"],
|
| 624 |
-
segments_list: List[dict]
|
| 625 |
-
) -> models.Chat:
|
| 626 |
-
try:
|
| 627 |
-
# Create chat
|
| 628 |
-
chat = await create_chat(db, owner_id=owner_id, title=chat_name, should_commit=False)
|
| 629 |
-
|
| 630 |
-
# Insert participants
|
| 631 |
-
all_chat_participants = {m.sender for m in cleaned_messages if m.sender}
|
| 632 |
-
participants = await bulk_insert_participants(
|
| 633 |
-
db=db,
|
| 634 |
-
chat_id=chat.id,
|
| 635 |
-
names=all_chat_participants,
|
| 636 |
-
should_commit=False
|
| 637 |
-
)
|
| 638 |
-
|
| 639 |
-
participants_map = {p.name: p.id for p in participants}
|
| 640 |
-
|
| 641 |
-
# Insert time and sender segments (batched)
|
| 642 |
-
await bulk_insert_segments(
|
| 643 |
-
db=db,
|
| 644 |
-
chat_id=chat.id,
|
| 645 |
-
participants_map=participants_map,
|
| 646 |
-
time_segments_list=segments_list
|
| 647 |
-
)
|
| 648 |
-
# Insert messages in bulk (batched)
|
| 649 |
-
# We must compute analytics for each message first
|
| 650 |
-
message_dicts = []
|
| 651 |
-
for msg in cleaned_messages:
|
| 652 |
-
# Skip messages without a sender (e.g., system messages)
|
| 653 |
-
if not msg.sender:
|
| 654 |
-
continue
|
| 655 |
-
|
| 656 |
-
# Compute analytics, just like in create_message
|
| 657 |
-
analytics = compute_message_analytics(msg.text)
|
| 658 |
-
|
| 659 |
-
message_dicts.append(
|
| 660 |
-
{
|
| 661 |
-
"chat_id": chat.id,
|
| 662 |
-
"participant_id": participants_map.get(msg.sender),
|
| 663 |
-
"timestamp": msg.timestamp,
|
| 664 |
-
"content": msg.text,
|
| 665 |
-
# Match the logic from create_message: use content if raw is missing
|
| 666 |
-
"raw": getattr(msg, "raw", None) or msg.text,
|
| 667 |
-
|
| 668 |
-
# Add the missing analytics fields
|
| 669 |
-
"word_count": analytics['word_count'],
|
| 670 |
-
"emojis_count": analytics['emojis_count'],
|
| 671 |
-
"links_count": analytics['links_count'],
|
| 672 |
-
"is_question": analytics['is_question'],
|
| 673 |
-
"is_media": analytics['is_media']
|
| 674 |
-
}
|
| 675 |
-
)
|
| 676 |
-
|
| 677 |
-
await bulk_insert_messages(db, message_dicts, should_commit=False)
|
| 678 |
-
|
| 679 |
-
# Mark chat status
|
| 680 |
-
chat.sentiment_status = "pending"
|
| 681 |
-
|
| 682 |
-
# Single final commit
|
| 683 |
-
await db.commit()
|
| 684 |
-
await db.refresh(chat)
|
| 685 |
-
return chat
|
| 686 |
-
|
| 687 |
-
except Exception:
|
| 688 |
-
await db.rollback()
|
| 689 |
-
raise
|
| 690 |
-
async def get_unscored_messages(
|
| 691 |
-
db: AsyncSession,
|
| 692 |
-
chat_id: int,
|
| 693 |
-
limit: Optional[int] = None
|
| 694 |
-
):
|
| 695 |
-
q = await db.execute(
|
| 696 |
-
select(models.Message)
|
| 697 |
-
.where(models.Message.chat_id==chat_id)
|
| 698 |
-
.outerjoin(models.MessageSentiment)
|
| 699 |
-
.where(models.MessageSentiment.id == None)
|
| 700 |
-
)
|
| 701 |
-
|
| 702 |
-
res = q.scalars().all()
|
| 703 |
-
|
| 704 |
-
return res if limit is None else res[:limit]
|
| 705 |
-
|
| 706 |
-
async def get_unscored_sender_segments(
|
| 707 |
-
db: AsyncSession,
|
| 708 |
-
chat_id: int
|
| 709 |
-
):
|
| 710 |
-
q = await db.execute(
|
| 711 |
-
select(models.SenderSegment).join(models.TimeSegment)
|
| 712 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 713 |
-
.outerjoin(models.SegmentSentiment)
|
| 714 |
-
.where(models.SegmentSentiment.id == None)
|
| 715 |
-
)
|
| 716 |
-
|
| 717 |
-
res = q.scalars().all()
|
| 718 |
-
|
| 719 |
-
return res
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
def _get_filtered_query(
|
| 723 |
-
chat_id: int,
|
| 724 |
-
date_from: Optional[date] = None,
|
| 725 |
-
date_to: Optional[date] = None,
|
| 726 |
-
participant_names: Optional[List[str]] = None,
|
| 727 |
-
time_period: Optional[str] = None
|
| 728 |
-
):
|
| 729 |
-
stmt = select(models.Message).where(models.Message.chat_id == chat_id)
|
| 730 |
-
|
| 731 |
-
if date_from:
|
| 732 |
-
stmt = stmt.where(func.date(models.Message.timestamp) >= date_from)
|
| 733 |
-
if date_to:
|
| 734 |
-
stmt = stmt.where(func.date(models.Message.timestamp) <= date_to)
|
| 735 |
-
if participant_names:
|
| 736 |
-
stmt = stmt.join(models.Participant).where(models.Participant.name.in_(participant_names))
|
| 737 |
-
if time_period and time_period in TIME_PERIOD_BUCKETS:
|
| 738 |
-
hours = TIME_PERIOD_BUCKETS[time_period]
|
| 739 |
-
stmt = stmt.where(func.extract('hour', models.Message.timestamp).in_(hours))
|
| 740 |
-
|
| 741 |
-
return stmt
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
# --- REFACTORED STREAMING FUNCTION ---
|
| 745 |
-
async def stream_unscored_messages(
|
| 746 |
-
db: AsyncSession,
|
| 747 |
-
chat_id: int
|
| 748 |
-
) -> AsyncGenerator[models.Message, None]:
|
| 749 |
-
"""
|
| 750 |
-
Streams unscored messages one by one using a server-side cursor.
|
| 751 |
-
This avoids loading all messages into memory.
|
| 752 |
-
"""
|
| 753 |
-
stmt = (
|
| 754 |
-
select(models.Message)
|
| 755 |
-
.where(models.Message.chat_id == chat_id)
|
| 756 |
-
.outerjoin(models.MessageSentiment)
|
| 757 |
-
.where(models.MessageSentiment.id == None)
|
| 758 |
-
.order_by(models.Message.timestamp) # Good practice to have an order
|
| 759 |
-
)
|
| 760 |
-
|
| 761 |
-
stream = await db.stream_scalars(stmt)
|
| 762 |
-
async for msg in stream:
|
| 763 |
-
yield msg
|
| 764 |
-
|
| 765 |
-
# --- REFACTORED STREAMING FUNCTION ---
|
| 766 |
-
async def stream_unscored_sender_segments(
|
| 767 |
-
db: AsyncSession,
|
| 768 |
-
chat_id: int
|
| 769 |
-
) -> AsyncGenerator[models.SenderSegment, None]:
|
| 770 |
-
"""
|
| 771 |
-
Streams unscored sender segments one by one.
|
| 772 |
-
"""
|
| 773 |
-
stmt = (
|
| 774 |
-
select(models.SenderSegment)
|
| 775 |
-
.join(models.TimeSegment)
|
| 776 |
-
.where(models.TimeSegment.chat_id == chat_id)
|
| 777 |
-
.outerjoin(models.SegmentSentiment)
|
| 778 |
-
.where(models.SegmentSentiment.id == None)
|
| 779 |
-
)
|
| 780 |
-
|
| 781 |
-
stream = await db.stream_scalars(stmt)
|
| 782 |
-
async for seg in stream:
|
| 783 |
-
yield seg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/db/base.py
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
from sqlalchemy.orm import declarative_base
|
| 2 |
-
|
| 3 |
-
Base = declarative_base()
|
|
|
|
|
|
|
|
|
|
|
|
src/app/db/session.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
| 1 |
-
from typing import AsyncGenerator
|
| 2 |
-
from sqlalchemy.ext.asyncio import (
|
| 3 |
-
AsyncSession,
|
| 4 |
-
create_async_engine,
|
| 5 |
-
async_sessionmaker
|
| 6 |
-
)
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
from sqlalchemy.pool import NullPool, QueuePool
|
| 10 |
-
import os
|
| 11 |
-
|
| 12 |
-
from src.app.config import settings
|
| 13 |
-
import logging
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
| 17 |
-
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
|
| 18 |
-
|
| 19 |
-
DATABASE_URL = str(settings.DATABASE_URL)
|
| 20 |
-
|
| 21 |
-
IS_CELERY_WORKER = os.getenv("IS_CELERY_WORKER", "false").lower() == "true"
|
| 22 |
-
|
| 23 |
-
engine_args = {
|
| 24 |
-
"echo": False,
|
| 25 |
-
"echo_pool": False,
|
| 26 |
-
"logging_name": 'sqlalchemy.engine',
|
| 27 |
-
"pool_pre_ping": True,
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
if IS_CELERY_WORKER:
|
| 31 |
-
engine_args["poolclass"] = NullPool
|
| 32 |
-
else:
|
| 33 |
-
engine_args["pool_size"] = 10
|
| 34 |
-
engine_args["max_overflow"] = 0
|
| 35 |
-
engine_args["pool_timeout"] = 30
|
| 36 |
-
engine_args["pool_recycle"] = 3600
|
| 37 |
-
|
| 38 |
-
engine = create_async_engine(
|
| 39 |
-
DATABASE_URL,
|
| 40 |
-
|
| 41 |
-
**engine_args
|
| 42 |
-
)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
AsyncSessionLocal = async_sessionmaker(
|
| 46 |
-
bind=engine,
|
| 47 |
-
class_=AsyncSession,
|
| 48 |
-
autoflush=False,
|
| 49 |
-
autocommit=False,
|
| 50 |
-
expire_on_commit=False,
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
| 54 |
-
"""
|
| 55 |
-
Dependency injection to get a new async session.
|
| 56 |
-
"""
|
| 57 |
-
async with AsyncSessionLocal() as session:
|
| 58 |
-
yield session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/limiter.py
DELETED
|
@@ -1,4 +0,0 @@
|
|
| 1 |
-
from slowapi import Limiter
|
| 2 |
-
from slowapi.util import get_remote_address
|
| 3 |
-
|
| 4 |
-
limiter = Limiter(key_func=get_remote_address)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/logging_config.py
DELETED
|
@@ -1,42 +0,0 @@
|
|
| 1 |
-
import logging.config
|
| 2 |
-
|
| 3 |
-
LOGGING_CONFIG = {
|
| 4 |
-
"version": 1,
|
| 5 |
-
"disable_existing_loggers": False,
|
| 6 |
-
"formatters": {
|
| 7 |
-
"standard": {
|
| 8 |
-
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
| 9 |
-
},
|
| 10 |
-
},
|
| 11 |
-
"handlers": {
|
| 12 |
-
"default": {
|
| 13 |
-
"level": "INFO",
|
| 14 |
-
"formatter": "standard",
|
| 15 |
-
"class": "logging.StreamHandler",
|
| 16 |
-
"stream": "ext://sys.stdout",
|
| 17 |
-
},
|
| 18 |
-
"sqlalchemy": {
|
| 19 |
-
"level": "WARNING",
|
| 20 |
-
"formatter": "standard",
|
| 21 |
-
"class": "logging.StreamHandler",
|
| 22 |
-
"stream": "ext://sys.stdout",
|
| 23 |
-
}
|
| 24 |
-
},
|
| 25 |
-
"loggers": {
|
| 26 |
-
"": { # Root logger
|
| 27 |
-
"handlers": ["default"],
|
| 28 |
-
"level": "INFO",
|
| 29 |
-
"propagate": True
|
| 30 |
-
},
|
| 31 |
-
"sqlalchemy.engine": {
|
| 32 |
-
"handlers": ["sqlalchemy"],
|
| 33 |
-
"level": "WARNING",
|
| 34 |
-
"propagate": False
|
| 35 |
-
},
|
| 36 |
-
"sqlalchemy.pool": {
|
| 37 |
-
"handlers": ["sqlalchemy"],
|
| 38 |
-
"level": "WARNING",
|
| 39 |
-
"propagate": False
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/main.py
DELETED
|
@@ -1,211 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI, Request, HTTPException
|
| 2 |
-
from fastapi.responses import JSONResponse
|
| 3 |
-
from fastapi.exceptions import RequestValidationError
|
| 4 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
-
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
| 6 |
-
from starlette.middleware.base import BaseHTTPMiddleware
|
| 7 |
-
from starlette.middleware import Middleware
|
| 8 |
-
from slowapi.errors import RateLimitExceeded
|
| 9 |
-
from slowapi import _rate_limit_exceeded_handler
|
| 10 |
-
from contextlib import asynccontextmanager
|
| 11 |
-
from fastapi.responses import JSONResponse
|
| 12 |
-
from fastapi import Request
|
| 13 |
-
from sqlalchemy.exc import SQLAlchemyError
|
| 14 |
-
import asyncio
|
| 15 |
-
import logging
|
| 16 |
-
import socket
|
| 17 |
-
import uuid
|
| 18 |
-
import time
|
| 19 |
-
from typing import Callable
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
from src.app.config import settings, setup_logging
|
| 23 |
-
from src.app.db.session import engine
|
| 24 |
-
from src.app.routers import auth, uploads, chats, sentiment_sse, dashboard, sentiment_dashboard, rag
|
| 25 |
-
from src.app.limiter import limiter
|
| 26 |
-
setup_logging()
|
| 27 |
-
logger = logging.getLogger(__name__)
|
| 28 |
-
APP_VERSION = "v1"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class RequestIDMiddleware(BaseHTTPMiddleware):
|
| 32 |
-
async def dispatch(self, request: Request, call_next: Callable):
|
| 33 |
-
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
|
| 34 |
-
request.state.request_id = request_id
|
| 35 |
-
start = time.time()
|
| 36 |
-
try:
|
| 37 |
-
response = await call_next(request)
|
| 38 |
-
except Exception:
|
| 39 |
-
|
| 40 |
-
logger.exception("Unhandled error during request %s %s", request.method, request.url)
|
| 41 |
-
raise
|
| 42 |
-
process_time = (time.time() - start) * 1000
|
| 43 |
-
response.headers["X-Request-ID"] = request_id
|
| 44 |
-
response.headers["X-Process-Time-ms"] = f"{process_time:.2f}"
|
| 45 |
-
logger.info("%s %s %s -> %s (%.2fms) [rid=%s]",
|
| 46 |
-
request.client.host if request.client else "-",
|
| 47 |
-
request.method, request.url.path,
|
| 48 |
-
getattr(response, "status_code", "-"), process_time, request_id)
|
| 49 |
-
return response
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
async def test_db_connection():
|
| 53 |
-
|
| 54 |
-
async with engine.begin() as conn:
|
| 55 |
-
await conn.run_sync(lambda sync_conn: None)
|
| 56 |
-
|
| 57 |
-
@asynccontextmanager
|
| 58 |
-
async def lifespan(app: FastAPI):
|
| 59 |
-
|
| 60 |
-
logger.info("Starting application: %s", settings.APP_NAME if hasattr(settings, "APP_NAME") else "whatsapp-sentiment")
|
| 61 |
-
|
| 62 |
-
if settings.DEBUG and settings.ENVIRONMENT == "production":
|
| 63 |
-
logger.critical("DEBUG mode is enabled while ENVIRONMENT==production")
|
| 64 |
-
raise RuntimeError("Invalid configuration: DEBUG cannot be True in production")
|
| 65 |
-
|
| 66 |
-
# Validate CORS config early
|
| 67 |
-
if settings.CORS_ORIGINS == ["*"] and settings.CORS_ALLOW_CREDENTIALS:
|
| 68 |
-
|
| 69 |
-
logger.critical("CORS misconfiguration: allow_origins='*' cannot be used with allow_credentials=True")
|
| 70 |
-
raise RuntimeError("Invalid CORS configuration: '*' may not be used with credentials")
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
max_attempts = getattr(settings, "DB_CONNECT_RETRIES", 3)
|
| 74 |
-
backoff = getattr(settings, "DB_CONNECT_BACKOFF_SECONDS", 2)
|
| 75 |
-
connected = False
|
| 76 |
-
last_exc = None
|
| 77 |
-
for attempt in range(1, max_attempts + 1):
|
| 78 |
-
try:
|
| 79 |
-
await test_db_connection()
|
| 80 |
-
connected = True
|
| 81 |
-
logger.info("✅ Database connection successful")
|
| 82 |
-
break
|
| 83 |
-
except Exception as exc:
|
| 84 |
-
last_exc = exc
|
| 85 |
-
logger.warning("Attempt %d/%d - DB connection failed: %s", attempt, max_attempts, exc)
|
| 86 |
-
await asyncio.sleep(backoff * attempt)
|
| 87 |
-
|
| 88 |
-
if not connected:
|
| 89 |
-
logger.critical("Could not connect to the database after %d attempts. Shutting down.", max_attempts)
|
| 90 |
-
|
| 91 |
-
try:
|
| 92 |
-
await engine.dispose()
|
| 93 |
-
except Exception:
|
| 94 |
-
logger.exception("Error while disposing engine after failed startup")
|
| 95 |
-
|
| 96 |
-
raise RuntimeError("Database connection failed")
|
| 97 |
-
yield
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
logger.info("Shutting down application gracefully")
|
| 101 |
-
try:
|
| 102 |
-
await engine.dispose()
|
| 103 |
-
except Exception:
|
| 104 |
-
logger.exception("Failed to dispose DB engine during shutdown")
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
middleware = [
|
| 108 |
-
Middleware(RequestIDMiddleware),
|
| 109 |
-
|
| 110 |
-
Middleware(TrustedHostMiddleware, allowed_hosts=settings.TRUSTED_HOSTS or ["*"]),
|
| 111 |
-
]
|
| 112 |
-
|
| 113 |
-
app = FastAPI(title="WhatsApp Sentiment Analysis API", lifespan=lifespan, middleware=middleware)
|
| 114 |
-
|
| 115 |
-
app.state.limiter = limiter
|
| 116 |
-
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 117 |
-
|
| 118 |
-
app.add_middleware(
|
| 119 |
-
CORSMiddleware,
|
| 120 |
-
allow_origins=settings.CORS_ORIGINS,
|
| 121 |
-
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
| 122 |
-
allow_headers=["*"],
|
| 123 |
-
allow_credentials=getattr(settings, "CORS_ALLOW_CREDENTIALS", True),
|
| 124 |
-
expose_headers=["X-Request-ID", "X-Process-Time-ms"],
|
| 125 |
-
)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
@app.exception_handler(HTTPException)
|
| 129 |
-
async def http_exception_handler(request: Request, exc: HTTPException):
|
| 130 |
-
|
| 131 |
-
logger.warning("HTTPException (%s) - %s %s - %s", exc.status_code, request.method, request.url, exc.detail)
|
| 132 |
-
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
|
| 133 |
-
|
| 134 |
-
@app.exception_handler(RequestValidationError)
|
| 135 |
-
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
| 136 |
-
logger.warning("Validation error for %s %s: %s", request.method, request.url, exc.errors())
|
| 137 |
-
return JSONResponse(status_code=422, content={"detail": exc.errors()})
|
| 138 |
-
|
| 139 |
-
@app.exception_handler(OSError)
|
| 140 |
-
async def os_exception_handler(request: Request, exc: OSError):
|
| 141 |
-
|
| 142 |
-
if isinstance(exc, socket.gaierror):
|
| 143 |
-
logger.exception("DNS/host resolution error while handling request %s %s: %s", request.method, request.url, exc)
|
| 144 |
-
return JSONResponse(status_code=502, content={"detail": "Upstream host resolution failed"})
|
| 145 |
-
logger.exception("OS error while handling request %s %s", request.method, request.url)
|
| 146 |
-
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
| 147 |
-
|
| 148 |
-
@app.exception_handler(Exception)
|
| 149 |
-
async def unhandled_exception_handler(request: Request, exc: Exception):
|
| 150 |
-
|
| 151 |
-
logger.exception("Unhandled exception for request %s %s", request.method, request.url)
|
| 152 |
-
return JSONResponse(status_code=500, content={"detail": "Internal server error"})
|
| 153 |
-
|
| 154 |
-
@app.exception_handler(SQLAlchemyError)
|
| 155 |
-
async def db_exception_handler(request: Request, exc: SQLAlchemyError):
|
| 156 |
-
"""
|
| 157 |
-
Globally catches Database errors (connection pool exhaustion, timeouts).
|
| 158 |
-
Returns a 503 Service Unavailable instead of crashing the server.
|
| 159 |
-
"""
|
| 160 |
-
# Log the full error for your debugging
|
| 161 |
-
# log.error(f"Database error: {str(exc)}")
|
| 162 |
-
|
| 163 |
-
return JSONResponse(
|
| 164 |
-
status_code=503,
|
| 165 |
-
content={
|
| 166 |
-
"detail": "Service is busy. Database connection limit reached. Please try again in a moment."
|
| 167 |
-
},
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
@app.get("/", tags=["health"])
|
| 171 |
-
async def root():
|
| 172 |
-
return {"ok": True, "message": "Welcome to the WhatsApp Sentiment Analysis API"}
|
| 173 |
-
|
| 174 |
-
@app.get("/health/live", tags=["health"])
|
| 175 |
-
async def liveness_probe():
|
| 176 |
-
return {"status": "alive"}
|
| 177 |
-
|
| 178 |
-
@app.get("/health/ready", tags=["health"])
|
| 179 |
-
async def readiness_probe():
|
| 180 |
-
|
| 181 |
-
try:
|
| 182 |
-
async with engine.connect() as conn:
|
| 183 |
-
await conn.execute("SELECT 1")
|
| 184 |
-
except Exception:
|
| 185 |
-
logger.exception("Readiness check failed: DB not available")
|
| 186 |
-
return JSONResponse(status_code=503, content={"status": "unavailable"})
|
| 187 |
-
return {"status": "ready"}
|
| 188 |
-
|
| 189 |
-
# -----------------------------------------------------------------------------
|
| 190 |
-
# Include routers
|
| 191 |
-
# -----------------------------------------------------------------------------
|
| 192 |
-
app.include_router(auth.router, prefix=f"/api/{APP_VERSION}/auth", tags=["auth"])
|
| 193 |
-
app.include_router(uploads.router, prefix=f"/api/{APP_VERSION}/uploads", tags=["uploads"])
|
| 194 |
-
app.include_router(chats.router, prefix=f"/api/{APP_VERSION}/chats", tags=["chats"])
|
| 195 |
-
app.include_router(sentiment_sse.router, prefix=f"/api/{APP_VERSION}/sentiment", tags=["sentiment"])
|
| 196 |
-
app.include_router(dashboard.router, prefix=f"/api/{APP_VERSION}/dashboard", tags=["dashboard"])
|
| 197 |
-
app.include_router(sentiment_dashboard.router, prefix=f"/api/{APP_VERSION}/sentiment-dashboard", tags=["Sentiment Dashboard"])
|
| 198 |
-
app.include_router(rag.router, prefix=f"/api/{APP_VERSION}/rag", tags=["RAG Chat"])
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def _log_startup_banner():
|
| 202 |
-
masked_db = "(hidden)"
|
| 203 |
-
try:
|
| 204 |
-
masked_db = str(settings.DATABASE_URL)[:100] + "..."
|
| 205 |
-
except Exception:
|
| 206 |
-
pass
|
| 207 |
-
|
| 208 |
-
logger.info("Application startup complete")
|
| 209 |
-
logger.info("Environment: %s | Debug: %s | CORS origins: %s", settings.ENVIRONMENT, settings.DEBUG, settings.CORS_ORIGINS)
|
| 210 |
-
logger.info("Database: %s", masked_db)
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/models.py
DELETED
|
@@ -1,329 +0,0 @@
|
|
| 1 |
-
from datetime import datetime, timezone
|
| 2 |
-
from typing import List, Optional, Dict
|
| 3 |
-
from sqlalchemy import (
|
| 4 |
-
JSON, Integer, String, DateTime, Text, ForeignKey, Float,
|
| 5 |
-
UniqueConstraint, Boolean, BigInteger, func, Index
|
| 6 |
-
)
|
| 7 |
-
from sqlalchemy.orm import relationship, Mapped, mapped_column
|
| 8 |
-
import uuid
|
| 9 |
-
from sqlalchemy.dialects.postgresql import UUID
|
| 10 |
-
from src.app.db.base import Base
|
| 11 |
-
from src.app.schemas import SentimentStatusEnum, EmbeddingStatusEnum
|
| 12 |
-
from pgvector.sqlalchemy import Vector
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
class User(Base):
|
| 16 |
-
__tablename__ = 'users'
|
| 17 |
-
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 18 |
-
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
| 19 |
-
chats: Mapped[List["Chat"]] = relationship(
|
| 20 |
-
"Chat",
|
| 21 |
-
back_populates="owner",
|
| 22 |
-
lazy="selectin",
|
| 23 |
-
passive_deletes=True
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
class Chat(Base):
|
| 27 |
-
__tablename__ = "chats"
|
| 28 |
-
|
| 29 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 30 |
-
title: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
|
| 31 |
-
created_at: Mapped[datetime] = mapped_column(
|
| 32 |
-
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True
|
| 33 |
-
)
|
| 34 |
-
sentiment_status: Mapped[SentimentStatusEnum] = mapped_column(
|
| 35 |
-
String(30), nullable=False, default=SentimentStatusEnum.pending.value, index=True
|
| 36 |
-
)
|
| 37 |
-
# In your models.py
|
| 38 |
-
|
| 39 |
-
embeddings_status: Mapped[EmbeddingStatusEnum] = mapped_column(
|
| 40 |
-
String(30),
|
| 41 |
-
nullable=False,
|
| 42 |
-
default=EmbeddingStatusEnum.pending.value, # Keeps Python logic happy
|
| 43 |
-
server_default=EmbeddingStatusEnum.pending.value, # <--- ADDS SQL DEFAULT
|
| 44 |
-
index=True
|
| 45 |
-
)
|
| 46 |
-
cancel_requested: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
|
| 47 |
-
owner_id: Mapped[uuid.UUID] = mapped_column(
|
| 48 |
-
ForeignKey("users.id", ondelete="CASCADE"),
|
| 49 |
-
nullable=False,
|
| 50 |
-
index=True
|
| 51 |
-
)
|
| 52 |
-
messages: Mapped[List["Message"]] = relationship(
|
| 53 |
-
"Message",
|
| 54 |
-
back_populates="chat",
|
| 55 |
-
lazy="selectin",
|
| 56 |
-
passive_deletes=True
|
| 57 |
-
)
|
| 58 |
-
time_segments: Mapped[List["TimeSegment"]] = relationship(
|
| 59 |
-
"TimeSegment",
|
| 60 |
-
back_populates="chat",
|
| 61 |
-
lazy="selectin",
|
| 62 |
-
passive_deletes=True
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
history: Mapped[List["ConversationHistory"]] = relationship(
|
| 66 |
-
"ConversationHistory",
|
| 67 |
-
back_populates="chat",
|
| 68 |
-
lazy="selectin",
|
| 69 |
-
passive_deletes=True
|
| 70 |
-
)
|
| 71 |
-
owner: Mapped["User"] = relationship(
|
| 72 |
-
"User",
|
| 73 |
-
back_populates="chats",
|
| 74 |
-
lazy="selectin",
|
| 75 |
-
passive_deletes=True
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
class Participant(Base):
|
| 80 |
-
__tablename__ = "participants"
|
| 81 |
-
|
| 82 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 83 |
-
chat_id: Mapped[int] = mapped_column(
|
| 84 |
-
ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True
|
| 85 |
-
)
|
| 86 |
-
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
| 87 |
-
created_at: Mapped[datetime] = mapped_column(
|
| 88 |
-
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), index=True
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
__table_args__ = (UniqueConstraint("chat_id", "name", name="uq_participant_chat_name"),)
|
| 92 |
-
|
| 93 |
-
messages: Mapped[List["Message"]] = relationship(
|
| 94 |
-
"Message",
|
| 95 |
-
back_populates="participant",
|
| 96 |
-
lazy="selectin",
|
| 97 |
-
passive_deletes=True
|
| 98 |
-
)
|
| 99 |
-
sender_segments: Mapped[List["SenderSegment"]] = relationship(
|
| 100 |
-
"SenderSegment",
|
| 101 |
-
back_populates="participant",
|
| 102 |
-
lazy="selectin",
|
| 103 |
-
passive_deletes=True
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
class Message(Base):
|
| 108 |
-
__tablename__ = "messages"
|
| 109 |
-
|
| 110 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 111 |
-
chat_id: Mapped[int] = mapped_column(
|
| 112 |
-
ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True
|
| 113 |
-
)
|
| 114 |
-
participant_id: Mapped[Optional[int]] = mapped_column(
|
| 115 |
-
ForeignKey("participants.id", ondelete="SET NULL"), nullable=True, index=True
|
| 116 |
-
)
|
| 117 |
-
timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
| 118 |
-
content: Mapped[str] = mapped_column(Text, nullable=False)
|
| 119 |
-
raw: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
| 120 |
-
|
| 121 |
-
# Analytics fields
|
| 122 |
-
word_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
| 123 |
-
emojis_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
| 124 |
-
links_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
| 125 |
-
is_question: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
| 126 |
-
is_media: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
| 127 |
-
|
| 128 |
-
chat: Mapped["Chat"] = relationship(
|
| 129 |
-
"Chat",
|
| 130 |
-
back_populates="messages",
|
| 131 |
-
lazy="selectin",
|
| 132 |
-
passive_deletes=True
|
| 133 |
-
)
|
| 134 |
-
participant: Mapped[Optional["Participant"]] = relationship(
|
| 135 |
-
"Participant",
|
| 136 |
-
back_populates="messages",
|
| 137 |
-
lazy="selectin"
|
| 138 |
-
)
|
| 139 |
-
sentiment: Mapped[Optional["MessageSentiment"]] = relationship(
|
| 140 |
-
"MessageSentiment",
|
| 141 |
-
back_populates="message",
|
| 142 |
-
uselist=False,
|
| 143 |
-
lazy="joined",
|
| 144 |
-
passive_deletes=True
|
| 145 |
-
)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
class SenderSegment(Base):
|
| 149 |
-
__tablename__ = "segments_sender"
|
| 150 |
-
|
| 151 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 152 |
-
time_segment_id: Mapped[int] = mapped_column(
|
| 153 |
-
ForeignKey("segments_time.id", ondelete="CASCADE"), nullable=False, index=True
|
| 154 |
-
)
|
| 155 |
-
sender_id: Mapped[int] = mapped_column(
|
| 156 |
-
ForeignKey("participants.id", ondelete="CASCADE"), nullable=False, index=True
|
| 157 |
-
)
|
| 158 |
-
message_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 159 |
-
combined_text: Mapped[str] = mapped_column(Text, nullable=False)
|
| 160 |
-
|
| 161 |
-
time_segment: Mapped["TimeSegment"] = relationship(
|
| 162 |
-
"TimeSegment",
|
| 163 |
-
back_populates="sender_segments",
|
| 164 |
-
lazy="selectin",
|
| 165 |
-
passive_deletes=True
|
| 166 |
-
)
|
| 167 |
-
participant: Mapped["Participant"] = relationship(
|
| 168 |
-
"Participant",
|
| 169 |
-
back_populates="sender_segments",
|
| 170 |
-
lazy="selectin",
|
| 171 |
-
passive_deletes=True
|
| 172 |
-
)
|
| 173 |
-
sentiment: Mapped[Optional["SegmentSentiment"]] = relationship(
|
| 174 |
-
"SegmentSentiment",
|
| 175 |
-
back_populates="sender_segment",
|
| 176 |
-
uselist=False,
|
| 177 |
-
lazy="joined",
|
| 178 |
-
passive_deletes=True
|
| 179 |
-
)
|
| 180 |
-
class TimeSegment(Base):
|
| 181 |
-
__tablename__ = "segments_time"
|
| 182 |
-
|
| 183 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 184 |
-
chat_id: Mapped[int] = mapped_column(
|
| 185 |
-
ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True
|
| 186 |
-
)
|
| 187 |
-
start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, index=True)
|
| 188 |
-
end_time: Mapped[datetime] = mapped_column(DateTime, nullable=False)
|
| 189 |
-
duration_minutes: Mapped[float] = mapped_column(Float, nullable=False)
|
| 190 |
-
message_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 191 |
-
summary: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
| 192 |
-
|
| 193 |
-
chat: Mapped["Chat"] = relationship(
|
| 194 |
-
"Chat",
|
| 195 |
-
back_populates="time_segments",
|
| 196 |
-
lazy="selectin",
|
| 197 |
-
passive_deletes=True
|
| 198 |
-
)
|
| 199 |
-
sender_segments: Mapped[List["SenderSegment"]] = relationship(
|
| 200 |
-
"SenderSegment",
|
| 201 |
-
back_populates="time_segment",
|
| 202 |
-
lazy="selectin",
|
| 203 |
-
passive_deletes=True
|
| 204 |
-
)
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
class MessageSentiment(Base):
|
| 208 |
-
__tablename__ = "message_sentiments"
|
| 209 |
-
|
| 210 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 211 |
-
message_id: Mapped[int] = mapped_column(
|
| 212 |
-
ForeignKey("messages.id", ondelete="CASCADE"),
|
| 213 |
-
unique=True,
|
| 214 |
-
nullable=False,
|
| 215 |
-
index=True
|
| 216 |
-
)
|
| 217 |
-
overall_label: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
| 218 |
-
overall_label_score: Mapped[float] = mapped_column(Float, nullable=False, index=True)
|
| 219 |
-
score_positive: Mapped[Optional[float]] = mapped_column(Float)
|
| 220 |
-
score_negative: Mapped[Optional[float]] = mapped_column(Float)
|
| 221 |
-
score_neutral: Mapped[Optional[float]] = mapped_column(Float)
|
| 222 |
-
sentences_summary: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True)
|
| 223 |
-
opinions: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True)
|
| 224 |
-
api_version: Mapped[Optional[str]] = mapped_column(String(50))
|
| 225 |
-
analysis_timestamp: Mapped[datetime] = mapped_column(
|
| 226 |
-
DateTime(timezone=True),
|
| 227 |
-
default=lambda: datetime.now(timezone.utc),
|
| 228 |
-
index=True
|
| 229 |
-
)
|
| 230 |
-
error_code: Mapped[Optional[str]] = mapped_column(String(100))
|
| 231 |
-
error_message: Mapped[Optional[str]] = mapped_column(Text)
|
| 232 |
-
|
| 233 |
-
message: Mapped["Message"] = relationship(
|
| 234 |
-
"Message",
|
| 235 |
-
back_populates="sentiment",
|
| 236 |
-
lazy="joined",
|
| 237 |
-
passive_deletes=True
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
class SegmentSentiment(Base):
|
| 242 |
-
__tablename__ = "segment_sentiments"
|
| 243 |
-
|
| 244 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 245 |
-
sender_segment_id: Mapped[int] = mapped_column(
|
| 246 |
-
ForeignKey("segments_sender.id", ondelete="CASCADE"),
|
| 247 |
-
unique=True,
|
| 248 |
-
nullable=False,
|
| 249 |
-
index=True
|
| 250 |
-
)
|
| 251 |
-
overall_label: Mapped[str] = mapped_column(String(50), nullable=False, index=True)
|
| 252 |
-
overall_label_score: Mapped[float] = mapped_column(Float, nullable=False, index=True)
|
| 253 |
-
score_positive: Mapped[Optional[float]] = mapped_column(Float)
|
| 254 |
-
score_negative: Mapped[Optional[float]] = mapped_column(Float)
|
| 255 |
-
score_neutral: Mapped[Optional[float]] = mapped_column(Float)
|
| 256 |
-
sentences_summary: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True)
|
| 257 |
-
opinions: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True)
|
| 258 |
-
api_version: Mapped[Optional[str]] = mapped_column(String(50))
|
| 259 |
-
analysis_timestamp: Mapped[datetime] = mapped_column(
|
| 260 |
-
DateTime(timezone=True),
|
| 261 |
-
default=lambda: datetime.now(timezone.utc),
|
| 262 |
-
index=True
|
| 263 |
-
)
|
| 264 |
-
error_code: Mapped[Optional[str]] = mapped_column(String(100))
|
| 265 |
-
error_message: Mapped[Optional[str]] = mapped_column(Text)
|
| 266 |
-
|
| 267 |
-
sender_segment: Mapped["SenderSegment"] = relationship(
|
| 268 |
-
"SenderSegment",
|
| 269 |
-
back_populates="sentiment",
|
| 270 |
-
lazy="joined",
|
| 271 |
-
passive_deletes=True
|
| 272 |
-
)
|
| 273 |
-
|
| 274 |
-
# Stub table for reference purpose
|
| 275 |
-
class Embedding(Base):
|
| 276 |
-
__tablename__ = "embeddings"
|
| 277 |
-
|
| 278 |
-
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
| 279 |
-
chat_id: Mapped[int] = mapped_column(
|
| 280 |
-
ForeignKey("chats.id", ondelete="CASCADE"), nullable=False
|
| 281 |
-
)
|
| 282 |
-
source_table: Mapped[str] = mapped_column(Text, nullable=False)
|
| 283 |
-
source_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
| 284 |
-
|
| 285 |
-
embedding: Mapped[List[float]] = mapped_column(Vector(1536), nullable=False)
|
| 286 |
-
|
| 287 |
-
text_excerpt: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
| 288 |
-
chunk_metadata: Mapped[Optional[dict]] = mapped_column(JSON, default={})
|
| 289 |
-
created_at: Mapped[datetime] = mapped_column(
|
| 290 |
-
DateTime(timezone=True), server_default=func.now()
|
| 291 |
-
)
|
| 292 |
-
|
| 293 |
-
__table_args__ = (
|
| 294 |
-
# Match the 'embeddings_chat_id_idx' from your manual migration
|
| 295 |
-
Index('embeddings_chat_id_idx', 'chat_id'),
|
| 296 |
-
|
| 297 |
-
# Match the composite index 'embeddings_chat_source_idx'
|
| 298 |
-
Index('embeddings_chat_source_idx', 'chat_id', 'source_table', 'source_id'),
|
| 299 |
-
|
| 300 |
-
# Match the IVFFlat vector index
|
| 301 |
-
Index(
|
| 302 |
-
'embeddings_embedding_ivfflat',
|
| 303 |
-
'embedding',
|
| 304 |
-
postgresql_using='ivfflat',
|
| 305 |
-
postgresql_with={'lists': 100},
|
| 306 |
-
postgresql_ops={'embedding': 'vector_l2_ops'}
|
| 307 |
-
),
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
class ConversationHistory(Base):
|
| 311 |
-
__tablename__ = "conversation_history"
|
| 312 |
-
|
| 313 |
-
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
| 314 |
-
chat_id: Mapped[int] = mapped_column(
|
| 315 |
-
ForeignKey("chats.id", ondelete="CASCADE"), nullable=False, index=True
|
| 316 |
-
)
|
| 317 |
-
role: Mapped[str] = mapped_column(String(20), nullable=False) # "user" or "assistant"
|
| 318 |
-
content: Mapped[str] = mapped_column(Text, nullable=False)
|
| 319 |
-
sources: Mapped[Optional[List[Dict]]] = mapped_column(JSON, nullable=True)
|
| 320 |
-
created_at: Mapped[datetime] = mapped_column(
|
| 321 |
-
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
| 322 |
-
)
|
| 323 |
-
|
| 324 |
-
chat: Mapped["Chat"] = relationship(
|
| 325 |
-
"Chat",
|
| 326 |
-
back_populates="history",
|
| 327 |
-
lazy="selectin",
|
| 328 |
-
passive_deletes=True
|
| 329 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/routers/__init__.py
DELETED
|
File without changes
|
src/app/routers/auth.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
from fastapi import APIRouter, Depends
|
| 2 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
-
from datetime import datetime, timedelta, timezone
|
| 4 |
-
import jwt
|
| 5 |
-
|
| 6 |
-
from src.app.config import settings
|
| 7 |
-
from src.app.db.session import get_db
|
| 8 |
-
from src.app import models
|
| 9 |
-
from src.app.security import ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
| 10 |
-
|
| 11 |
-
router = APIRouter()
|
| 12 |
-
|
| 13 |
-
@router.post("/guest", response_model=dict)
|
| 14 |
-
async def create_guest_session(db: AsyncSession = Depends(get_db)):
|
| 15 |
-
"""
|
| 16 |
-
Creates a new anonymous user and returns a long-lived JWT.
|
| 17 |
-
The frontend should call this ONCE on first load and store the token.
|
| 18 |
-
"""
|
| 19 |
-
# 1. Create a new Anonymous User
|
| 20 |
-
new_user = models.User()
|
| 21 |
-
db.add(new_user)
|
| 22 |
-
await db.commit()
|
| 23 |
-
await db.refresh(new_user)
|
| 24 |
-
|
| 25 |
-
# 2. GeneratT
|
| 26 |
-
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 27 |
-
|
| 28 |
-
# "sub" holds the User UUID
|
| 29 |
-
to_encode = {"sub": str(new_user.id), "exp": expire}
|
| 30 |
-
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
| 31 |
-
|
| 32 |
-
return {
|
| 33 |
-
"access_token": encoded_jwt,
|
| 34 |
-
"token_type": "bearer",
|
| 35 |
-
"user_id": str(new_user.id)
|
| 36 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/routers/chats.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
| 2 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 3 |
-
from sqlalchemy import select
|
| 4 |
-
import asyncio
|
| 5 |
-
from src.app.config import settings
|
| 6 |
-
from src.app.services.cleanup_service import delete_single_chat_background
|
| 7 |
-
from src.app.db.session import get_db
|
| 8 |
-
from src.app import crud, schemas, models
|
| 9 |
-
from src.app.security import get_current_user
|
| 10 |
-
from src.app.limiter import limiter
|
| 11 |
-
import logging
|
| 12 |
-
log = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
router = APIRouter()
|
| 15 |
-
|
| 16 |
-
@router.delete("/{chat_id}", response_model=dict, status_code=status.HTTP_200_OK)
|
| 17 |
-
@limiter.limit("5/minute")
|
| 18 |
-
async def delete_chat(
|
| 19 |
-
request: Request,
|
| 20 |
-
chat_id: int,
|
| 21 |
-
db: AsyncSession = Depends(get_db),
|
| 22 |
-
current_user: models.User = Depends(get_current_user)
|
| 23 |
-
):
|
| 24 |
-
"""
|
| 25 |
-
Delete a chat and all its related data.
|
| 26 |
-
|
| 27 |
-
"""
|
| 28 |
-
try:
|
| 29 |
-
|
| 30 |
-
stmt = select(models.Chat).where(models.Chat.id == chat_id).with_for_update()
|
| 31 |
-
result = await db.execute(stmt)
|
| 32 |
-
chat = result.scalars().first()
|
| 33 |
-
|
| 34 |
-
if not chat:
|
| 35 |
-
# We must commit/rollback to release the implicit transaction lock if we error out
|
| 36 |
-
await db.rollback()
|
| 37 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 38 |
-
|
| 39 |
-
# 2. Verify Ownership
|
| 40 |
-
if chat.owner_id != current_user.id:
|
| 41 |
-
await db.rollback()
|
| 42 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 43 |
-
|
| 44 |
-
# 3. Check Status
|
| 45 |
-
if chat.sentiment_status == "processing":
|
| 46 |
-
await db.rollback()
|
| 47 |
-
raise HTTPException(
|
| 48 |
-
status_code=409,
|
| 49 |
-
detail="Sentiment analysis is still running. Please cancel the job first."
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
await db.rollback() # Release the lock before starting background task
|
| 53 |
-
|
| 54 |
-
asyncio.create_task(delete_single_chat_background(chat_id))
|
| 55 |
-
|
| 56 |
-
return {"ok": True, "message": f"Chat {chat_id} deleted successfully"}
|
| 57 |
-
|
| 58 |
-
except HTTPException:
|
| 59 |
-
raise
|
| 60 |
-
except Exception as e:
|
| 61 |
-
await db.rollback()
|
| 62 |
-
log.error(f"Error scheduling delete for chat {chat_id}: {e}")
|
| 63 |
-
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/routers/dashboard.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
-
from typing import List, Optional
|
| 3 |
-
from fastapi import APIRouter, Depends, Query, HTTPException, Request
|
| 4 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 5 |
-
|
| 6 |
-
from src.app.schemas import DashboardData, DashboardFilters
|
| 7 |
-
from src.app.db.session import get_db
|
| 8 |
-
from src.app import crud, models
|
| 9 |
-
from src.app.services.dashboard_service import get_dashboard_data
|
| 10 |
-
from src.app.security import get_current_user
|
| 11 |
-
from src.app.limiter import limiter
|
| 12 |
-
|
| 13 |
-
router = APIRouter()
|
| 14 |
-
|
| 15 |
-
def get_dashboard_filters(
|
| 16 |
-
start_date: Optional[datetime] = Query(
|
| 17 |
-
None,
|
| 18 |
-
description="Start date for filtering (ISO 8601 format)"
|
| 19 |
-
),
|
| 20 |
-
end_date: Optional[datetime] = Query(
|
| 21 |
-
None,
|
| 22 |
-
description="End date for filtering (ISO 8601 format)"
|
| 23 |
-
),
|
| 24 |
-
participants: Optional[List[str]] = Query(
|
| 25 |
-
None,
|
| 26 |
-
description="List of participant names to include (can be used multiple times)"
|
| 27 |
-
),
|
| 28 |
-
time_period: Optional[str] = Query(
|
| 29 |
-
None,
|
| 30 |
-
description="Time period to filter by",
|
| 31 |
-
# This enum adds validation and OpenAPI docs for free
|
| 32 |
-
enum=["Morning", "Afternoon", "Evening", "Night"]
|
| 33 |
-
)
|
| 34 |
-
) -> DashboardFilters:
|
| 35 |
-
"""
|
| 36 |
-
Dependency that parses GET query parameters into a
|
| 37 |
-
DashboardFilters Pydantic model.
|
| 38 |
-
"""
|
| 39 |
-
return DashboardFilters(
|
| 40 |
-
start_date=start_date,
|
| 41 |
-
end_date=end_date,
|
| 42 |
-
participants=participants,
|
| 43 |
-
time_period=time_period
|
| 44 |
-
)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# --- Phase 1: Endpoint Definition ---
|
| 48 |
-
|
| 49 |
-
@router.get(
|
| 50 |
-
"/chats/{chat_id}",
|
| 51 |
-
response_model=DashboardData,
|
| 52 |
-
summary="Get Chat Dashboard Analytics"
|
| 53 |
-
)
|
| 54 |
-
@limiter.limit("20/minute")
|
| 55 |
-
async def read_chat_dashboard(
|
| 56 |
-
request: Request,
|
| 57 |
-
chat_id: int,
|
| 58 |
-
db: AsyncSession = Depends(get_db),
|
| 59 |
-
filters: DashboardFilters = Depends(get_dashboard_filters),
|
| 60 |
-
current_user: models.User = Depends(get_current_user)
|
| 61 |
-
):
|
| 62 |
-
"""
|
| 63 |
-
Retrieve a comprehensive analytics dashboard for a specific chat.
|
| 64 |
-
|
| 65 |
-
This endpoint provides all data necessary to populate the dashboard,
|
| 66 |
-
with optional filters for:
|
| 67 |
-
|
| 68 |
-
- **Date Range**: `start_date` and `end_date`
|
| 69 |
-
- **Participants**: `participants` (e.g., ?participants=User+A&participants=User+B)
|
| 70 |
-
- **Time of Day**: `time_period`
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 74 |
-
if not chat or chat.owner_id != current_user.id:
|
| 75 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 76 |
-
|
| 77 |
-
dashboard_data = await get_dashboard_data(chat_id=chat_id, db=db, filters=filters)
|
| 78 |
-
|
| 79 |
-
if not dashboard_data:
|
| 80 |
-
# The service will return None if the chat_id doesn't exist
|
| 81 |
-
raise HTTPException(status_code=404, detail="Chat not found or no data available.")
|
| 82 |
-
|
| 83 |
-
return dashboard_data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/routers/rag.py
DELETED
|
@@ -1,120 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
from fastapi import APIRouter, Depends, HTTPException, Request
|
| 3 |
-
from fastapi.responses import StreamingResponse, JSONResponse
|
| 4 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 5 |
-
from langchain_core.messages import HumanMessage, AIMessage
|
| 6 |
-
from sqlalchemy import select
|
| 7 |
-
from typing import List, Optional
|
| 8 |
-
from src.app.db.session import get_db
|
| 9 |
-
from src.app import crud, models
|
| 10 |
-
from src.app.schemas import RagQueryRequest, ConversationHistoryItem
|
| 11 |
-
from src.app.security import get_current_user
|
| 12 |
-
from src.app.services import router_service
|
| 13 |
-
from src.app.limiter import limiter
|
| 14 |
-
|
| 15 |
-
router = APIRouter()
|
| 16 |
-
log = logging.getLogger(__name__)
|
| 17 |
-
|
| 18 |
-
# --- NEW ENDPOINT START ---
|
| 19 |
-
@router.get("/chat/{chat_id}/status")
|
| 20 |
-
async def get_chat_status(
|
| 21 |
-
chat_id: int,
|
| 22 |
-
db: AsyncSession = Depends(get_db),
|
| 23 |
-
current_user: models.User = Depends(get_current_user)
|
| 24 |
-
):
|
| 25 |
-
# Single query to check ownership AND get status at the same time
|
| 26 |
-
# This prevents double DB hits (one for auth, one for status)
|
| 27 |
-
query = select(models.Chat.embeddings_status).where(
|
| 28 |
-
models.Chat.id == chat_id,
|
| 29 |
-
models.Chat.owner_id == current_user.id
|
| 30 |
-
)
|
| 31 |
-
result = await db.execute(query)
|
| 32 |
-
status = result.scalar_one_or_none()
|
| 33 |
-
|
| 34 |
-
if status is None:
|
| 35 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 36 |
-
|
| 37 |
-
return JSONResponse(content={"status": status})
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@router.post(
|
| 41 |
-
"/chat/{chat_id}/query/streamed"
|
| 42 |
-
)
|
| 43 |
-
@limiter.limit("5/minute")
|
| 44 |
-
async def query_chat_streamed(
|
| 45 |
-
request: Request,
|
| 46 |
-
chat_id: int,
|
| 47 |
-
payload: RagQueryRequest,
|
| 48 |
-
db: AsyncSession = Depends(get_db),
|
| 49 |
-
current_user: models.User = Depends(get_current_user)
|
| 50 |
-
):
|
| 51 |
-
"""
|
| 52 |
-
Main endpoint for asking questions to a chat (Streaming).
|
| 53 |
-
"""
|
| 54 |
-
|
| 55 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 56 |
-
if not chat or chat.owner_id != current_user.id:
|
| 57 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
history_objs = await crud.get_conversation_history(db, chat_id, limit=6)
|
| 62 |
-
chat_history_messages = []
|
| 63 |
-
for h in history_objs:
|
| 64 |
-
if h.role == "user":
|
| 65 |
-
chat_history_messages.append(HumanMessage(content=h.content))
|
| 66 |
-
else:
|
| 67 |
-
chat_history_messages.append(AIMessage(content=h.content))
|
| 68 |
-
|
| 69 |
-
analytics_data = payload.analytics_json or {}
|
| 70 |
-
return StreamingResponse(
|
| 71 |
-
router_service.route_and_process(
|
| 72 |
-
query=payload.question,
|
| 73 |
-
analytics_json=analytics_data,
|
| 74 |
-
chat_id=chat_id,
|
| 75 |
-
db=db,
|
| 76 |
-
chat_history=chat_history_messages
|
| 77 |
-
),
|
| 78 |
-
media_type="text/event-stream"
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
@router.delete("/chat/{chat_id}/history", status_code=204)
|
| 86 |
-
@limiter.limit("10/minute")
|
| 87 |
-
async def clear_chat_history(
|
| 88 |
-
request: Request,
|
| 89 |
-
chat_id: int,
|
| 90 |
-
db: AsyncSession = Depends(get_db),
|
| 91 |
-
current_user: models.User = Depends(get_current_user)
|
| 92 |
-
):
|
| 93 |
-
"""
|
| 94 |
-
Clears the conversation memory for this chat.
|
| 95 |
-
"""
|
| 96 |
-
|
| 97 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 98 |
-
if not chat or chat.owner_id != current_user.id:
|
| 99 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
await crud.clear_conversation_history(db, chat_id)
|
| 103 |
-
|
| 104 |
-
return
|
| 105 |
-
|
| 106 |
-
@router.get(
|
| 107 |
-
"/chat/{chat_id}/history",
|
| 108 |
-
response_model=List[ConversationHistoryItem]
|
| 109 |
-
)
|
| 110 |
-
async def get_chat_history(
|
| 111 |
-
chat_id: int,
|
| 112 |
-
db: AsyncSession = Depends(get_db),
|
| 113 |
-
current_user: models.User = Depends(get_current_user)
|
| 114 |
-
):
|
| 115 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 116 |
-
if not chat or chat.owner_id != current_user.id:
|
| 117 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 118 |
-
|
| 119 |
-
history = await crud.get_full_conversation_history(db, chat_id)
|
| 120 |
-
return history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app/routers/rag_streamed.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
| 1 |
-
# src/app/routers/rag.py
|
| 2 |
-
|
| 3 |
-
import logging
|
| 4 |
-
from fastapi import APIRouter, Depends, HTTPException
|
| 5 |
-
|
| 6 |
-
from sqlalchemy.ext.asyncio import AsyncSession
|
| 7 |
-
from typing import List
|
| 8 |
-
from src.app.db.session import get_db
|
| 9 |
-
from src.app import crud
|
| 10 |
-
from src.app.schemas import RagQueryRequest, RagQueryResponse
|
| 11 |
-
from src.app.services import rag_service_prev
|
| 12 |
-
from src.app.schemas import ConversationHistoryItem
|
| 13 |
-
|
| 14 |
-
router = APIRouter()
|
| 15 |
-
log = logging.getLogger(__name__)
|
| 16 |
-
|
| 17 |
-
@router.post(
|
| 18 |
-
"/chat/{chat_id}/query",
|
| 19 |
-
response_model=RagQueryResponse
|
| 20 |
-
)
|
| 21 |
-
async def query_chat(
|
| 22 |
-
chat_id: int,
|
| 23 |
-
payload: RagQueryRequest,
|
| 24 |
-
db: AsyncSession = Depends(get_db)
|
| 25 |
-
):
|
| 26 |
-
"""
|
| 27 |
-
Main endpoint for asking questions to a chat.
|
| 28 |
-
|
| 29 |
-
"""
|
| 30 |
-
|
| 31 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 32 |
-
if not chat:
|
| 33 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
try:
|
| 37 |
-
response = await rag_service_prev.process_rag_query(chat_id, payload)
|
| 38 |
-
return response
|
| 39 |
-
except Exception as e:
|
| 40 |
-
log.error(f"RAG endpoint failed for chat {chat_id}: {e}", exc_info=True)
|
| 41 |
-
raise HTTPException(status_code=500, detail="Error processing your question.")
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
@router.delete("/chat/{chat_id}/history", status_code=204)
|
| 45 |
-
async def clear_chat_history(
|
| 46 |
-
chat_id: int,
|
| 47 |
-
db: AsyncSession = Depends(get_db)
|
| 48 |
-
):
|
| 49 |
-
"""
|
| 50 |
-
Clears the conversation memory for this chat.
|
| 51 |
-
"""
|
| 52 |
-
|
| 53 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 54 |
-
if not chat:
|
| 55 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
await crud.clear_conversation_history(db, chat_id)
|
| 59 |
-
|
| 60 |
-
return
|
| 61 |
-
|
| 62 |
-
@router.get(
|
| 63 |
-
"/chat/{chat_id}/history",
|
| 64 |
-
response_model=List[ConversationHistoryItem]
|
| 65 |
-
)
|
| 66 |
-
async def get_chat_history(
|
| 67 |
-
chat_id: int,
|
| 68 |
-
db: AsyncSession = Depends(get_db)
|
| 69 |
-
):
|
| 70 |
-
chat = await crud.get_chat(db, chat_id=chat_id)
|
| 71 |
-
if not chat:
|
| 72 |
-
raise HTTPException(status_code=404, detail="Chat not found")
|
| 73 |
-
|
| 74 |
-
history = await crud.get_full_conversation_history(db, chat_id)
|
| 75 |
-
return history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|