build: 打通 FastAPI 与 Vue 最小工程、依赖与数据库连接
后端入口、配置、Redis 与 Alembic 迁移就位;前端 Vite 工程可安装可构建。
This commit is contained in:
1 parent
fb0ddca4de
commit
2aee6286d6
167 files changed
+26082
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
[run]
|
||||||
|
source = app
|
||||||
|
omit =
|
||||||
|
*/tests/*
|
||||||
|
*/__init__.py
|
||||||
|
*/migrations/*
|
||||||
|
*/alembic/*
|
||||||
|
|
||||||
|
[report]
|
||||||
|
exclude_lines =
|
||||||
|
pragma: no cover
|
||||||
|
def __repr__
|
||||||
|
raise NotImplementedError
|
||||||
|
if __name__ == .__main__.:
|
||||||
|
pass
|
||||||
|
raise ImportError
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
[html]
|
||||||
|
directory = coverage_html
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
# Database Configuration
|
||||||
|
DATABASE_URL=postgresql+psycopg://postgres:postgres@db:5432/chattermate
|
||||||
|
|
||||||
|
# CORS Configuration add backend url to allow cors from frontend
|
||||||
|
CORS_ORIGINS=["http://localhost:3000","http://localhost:8000"]
|
||||||
|
|
||||||
|
# Firebase Configuration
|
||||||
|
FIREBASE_CREDENTIALS=app/config/firebase-config.json
|
||||||
|
|
||||||
|
# Widget Configuration # prod image remove :3000 port, since frontend is running on port 80
|
||||||
|
VITE_WIDGET_URL=http://localhost:8000
|
||||||
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
# Public URL of THIS backend. Self-hosters MUST set this to their domain
|
||||||
|
# (e.g. https://support.example.com) — the chat widget derives its API/WS URLs
|
||||||
|
# from it, so if it stays localhost the embedded widget can't reach the backend.
|
||||||
|
BACKEND_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Public help center URL style:
|
||||||
|
# path -> {BACKEND_URL}/help/{slug}, served same-origin (no DNS/TLS/proxy). Default, best for self-host.
|
||||||
|
# subdomain -> https://{slug}.<HELP_CENTER_BASE_DOMAIN> (needs wildcard DNS + TLS; used by cloud).
|
||||||
|
# A verified custom domain always overrides this.
|
||||||
|
HELP_CENTER_PUBLIC_MODE=path
|
||||||
|
|
||||||
|
# JWT Configuration
|
||||||
|
# These sign your auth and conversation tokens - anyone who knows them can forge
|
||||||
|
# tokens for any user, so the placeholders below must be replaced. Outside
|
||||||
|
# ENVIRONMENT=development the app refuses to start while they are still in place.
|
||||||
|
# Generate each (64 hex characters) with:
|
||||||
|
# openssl rand -hex 32
|
||||||
|
JWT_SECRET_KEY=your_jwt_secret_key_here
|
||||||
|
JWT_ALGORITHM=HS256
|
||||||
|
CONVERSATION_SECRET_KEY=your_conversation_secret_key_here
|
||||||
|
|
||||||
|
# Encryption and Redis
|
||||||
|
# REQUIRED outside development: encrypts API keys, provider/OAuth credentials, chat
|
||||||
|
# messages, session summaries and agent memory at rest. Losing or changing it makes
|
||||||
|
# existing data unreadable, so back it up separately from the database.
|
||||||
|
#
|
||||||
|
# The value is a Fernet key that has been base64-encoded a second time: a
|
||||||
|
# 60-character string ending in '='. Generate one with either of:
|
||||||
|
# python -c "import base64;from cryptography.fernet import Fernet;print(base64.b64encode(Fernet.generate_key()).decode())"
|
||||||
|
# openssl rand -base64 32 | tr '+/' '-_' | tr -d '\n' | base64
|
||||||
|
#
|
||||||
|
# Leaving the placeholder below in place means: outside development the app
|
||||||
|
# refuses to start; in development it generates a throwaway key on every boot,
|
||||||
|
# so anything encrypted before a restart becomes unreadable after it.
|
||||||
|
ENCRYPTION_KEY=your_fernet_encryption_key_here
|
||||||
|
|
||||||
|
# Escape hatch for deployments created before ENCRYPTION_KEY had to be set, whose
|
||||||
|
# data is encrypted under the old public default. It downgrades the startup error
|
||||||
|
# to a warning so those instances still boot. Anyone with a copy of the database
|
||||||
|
# can read their conversations and stored credentials, so treat it as temporary.
|
||||||
|
# Moving off the old key means re-encrypting what it wrote, not just swapping the
|
||||||
|
# value - see the key-id rotation note at the top of app/core/encryption.py.
|
||||||
|
# ALLOW_INSECURE_ENCRYPTION_KEY=false
|
||||||
|
REDIS_ENABLED=true
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
ENVIRONMENT=development
|
||||||
|
|
||||||
|
# S3 Configuration
|
||||||
|
S3_FILE_STORAGE=false
|
||||||
|
S3_BUCKET=chattermate-uploads
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
# Leave both blank on EC2/ECS/EKS to use the attached IAM role — boto3 then
|
||||||
|
# resolves credentials from its default chain. Set them only for static keys;
|
||||||
|
# a placeholder value here is treated as a real (invalid) key.
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
# Lifetime of generated presigned URLs, in seconds. They are re-signed on every
|
||||||
|
# response, so this only needs to outlive a page render. AWS rejects SigV4
|
||||||
|
# presigns above 604800 (7 days); larger values are clamped to that.
|
||||||
|
S3_PRESIGN_EXPIRY_SECONDS=3600
|
||||||
|
|
||||||
|
# Jira — create an OAuth 2.0 (3LO) app at developer.atlassian.com. Unlike the
|
||||||
|
# other integrations the redirect URI is configured here rather than derived
|
||||||
|
# from BACKEND_URL, so it must match the callback registered on the Atlassian
|
||||||
|
# app exactly (route: {API_V1_STR}/jira/callback).
|
||||||
|
JIRA_CLIENT_ID=your_jira_client_id_here
|
||||||
|
JIRA_CLIENT_SECRET=your_jira_client_secret_here
|
||||||
|
JIRA_REDIRECT_URI=http://localhost:8000/api/v1/jira/callback
|
||||||
|
|
||||||
|
SHOPIFY_API_KEY=your_shopify_key
|
||||||
|
SHOPIFY_API_SECRET=your_shopify_secret
|
||||||
|
SHOPIFY_API_VERSION=2025-04
|
||||||
|
|
||||||
|
# Slack channel — create an app at api.slack.com/apps. The OAuth redirect URI is
|
||||||
|
# derived from BACKEND_URL, so add {BACKEND_URL}/api/v1/channels/slack/callback
|
||||||
|
# to the app's Redirect URLs. Without SLACK_SIGNING_SECRET every incoming Slack
|
||||||
|
# webhook is rejected with a 403, since signature checks fail closed.
|
||||||
|
SLACK_CLIENT_ID=your_slack_client_id
|
||||||
|
SLACK_CLIENT_SECRET=your_slack_client_secret
|
||||||
|
SLACK_SIGNING_SECRET=your_slack_signing_secret
|
||||||
|
|
||||||
|
# CRM lead push (HubSpot / Pipedrive) — create OAuth apps at
|
||||||
|
# developers.hubspot.com and developers.pipedrive.com. Redirect URIs derive from
|
||||||
|
# BACKEND_URL: add {BACKEND_URL}/api/v1/crm/hubspot/callback and
|
||||||
|
# {BACKEND_URL}/api/v1/crm/pipedrive/callback to the respective apps.
|
||||||
|
HUBSPOT_CLIENT_ID=your_hubspot_client_id
|
||||||
|
HUBSPOT_CLIENT_SECRET=your_hubspot_client_secret
|
||||||
|
PIPEDRIVE_CLIENT_ID=your_pipedrive_client_id
|
||||||
|
PIPEDRIVE_CLIENT_SECRET=your_pipedrive_client_secret
|
||||||
|
|
||||||
|
# Meta channels (WhatsApp Cloud API, Messenger, Instagram) — create an app at
|
||||||
|
# developers.facebook.com. META_WEBHOOK_VERIFY_TOKEN is any random string you pick
|
||||||
|
# and also enter in the App Dashboard webhook config.
|
||||||
|
META_APP_ID=your_meta_app_id
|
||||||
|
META_APP_SECRET=your_meta_app_secret
|
||||||
|
META_WEBHOOK_VERIFY_TOKEN=any_random_string_you_choose
|
||||||
|
META_GRAPH_VERSION=v21.0
|
||||||
|
# Only for cloud-hosted one-click onboarding; leave blank for self-hosting.
|
||||||
|
# META_CONFIG_ID: WhatsApp Embedded Signup. META_MESSENGER_CONFIG_ID: Facebook
|
||||||
|
# Login for Business, for connecting a Page (Messenger + Instagram DM).
|
||||||
|
META_CONFIG_ID=
|
||||||
|
META_MESSENGER_CONFIG_ID=
|
||||||
|
# Instagram API with Instagram Login (no Facebook Page needed). These are the
|
||||||
|
# Instagram app id/secret from the app's Instagram product, NOT the Meta ones.
|
||||||
|
INSTAGRAM_APP_ID=
|
||||||
|
INSTAGRAM_APP_SECRET=
|
||||||
|
# Comma-separated emails allowed to use one-click Meta signup. Empty = everyone.
|
||||||
|
# Set this while the Meta app is in App Review so the flow can be exercised in
|
||||||
|
# production without offering customers a login that would fail for them.
|
||||||
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
|
VERIFY_SSL_CERTIFICATES=true
|
||||||
|
|
||||||
|
# Which addresses the AI base URL, MCP server URLs and ticket database (or its
|
||||||
|
# SSH host) may point at. These are dialled by this server, so:
|
||||||
|
# any -> no restriction. Default; lets a self-hosted install reach its
|
||||||
|
# own network (Ollama on localhost, an internal MCP server, a
|
||||||
|
# private database).
|
||||||
|
# public_only -> refuse private, loopback, link-local and reserved addresses.
|
||||||
|
# Use it when people you don't control can configure these
|
||||||
|
# (a shared or multi-tenant deployment). Checked on the
|
||||||
|
# address actually dialled, redirects included; these
|
||||||
|
# connections then bypass HTTP(S)_PROXY.
|
||||||
|
OUTBOUND_URL_POLICY=any
|
||||||
|
|
||||||
|
# STDIO MCP connectors run a command on this server, typed by an org admin.
|
||||||
|
# Keep true on your own install; set false wherever people you don't control
|
||||||
|
# can add connectors (a shared or multi-tenant deployment).
|
||||||
|
MCP_STDIO_ENABLED=true
|
||||||
|
|
||||||
|
# Enhanced Website Knowledge Base Configuration
|
||||||
|
KB_MAX_DEPTH=5
|
||||||
|
KB_MAX_LINKS=25
|
||||||
|
KB_MIN_CONTENT_LENGTH=100
|
||||||
|
KB_TIMEOUT=30
|
||||||
|
KB_MAX_RETRIES=3
|
||||||
|
KB_MAX_WORKERS=5
|
||||||
|
KB_BATCH_SIZE=5
|
||||||
|
KB_SOURCE_TIME_LIMIT_SECONDS=1800
|
||||||
|
KB_PDF_MAX_PAGES=300
|
||||||
|
|
||||||
|
# Seconds a single agent run (model + tool calls) may take before it is
|
||||||
|
# cancelled and the visitor gets an error reply. Raise it if agents with many
|
||||||
|
# MCP/Shopify tools hit the limit on legitimate answers.
|
||||||
|
AGENT_RUN_TIMEOUT=90
|
||||||
|
|
||||||
|
# agno's own run reporting to api.agno.com. Off unless set to true here; when on,
|
||||||
|
# it runs inside every agent run and counts against AGENT_RUN_TIMEOUT.
|
||||||
|
# AGNO_TELEMETRY=true
|
||||||
|
# AGNO_MONITOR=true
|
||||||
|
|
||||||
|
# Tool calls an agent may make in one turn before it has to answer from what it
|
||||||
|
# already retrieved. Hitting this is not an error — the agent answers without
|
||||||
|
# further tools. Raise it if agents with many knowledge sources or MCP tools are
|
||||||
|
# answering from too little context.
|
||||||
|
AGENT_TOOL_CALL_LIMIT=5
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
|
firebase-credentials.json
|
||||||
|
uploads/
|
||||||
|
# Coverage reports
|
||||||
|
.coverage
|
||||||
|
coverage_html/
|
||||||
|
htmlcov/
|
||||||
|
app/enterprise/
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||||
|
script_location = alembic
|
||||||
|
|
||||||
|
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
||||||
|
# Uncomment the line below if you want the files to be prepended with date and time
|
||||||
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||||
|
# for all available tokens
|
||||||
|
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
||||||
|
|
||||||
|
# sys.path path, will be prepended to sys.path if present.
|
||||||
|
# defaults to the current working directory.
|
||||||
|
prepend_sys_path = .
|
||||||
|
|
||||||
|
# timezone to use when rendering the date within the migration file
|
||||||
|
# as well as the filename.
|
||||||
|
# If specified, requires the python>=3.9 or backports.zoneinfo library.
|
||||||
|
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
|
||||||
|
# string value is passed to ZoneInfo()
|
||||||
|
# leave blank for localtime
|
||||||
|
# timezone =
|
||||||
|
|
||||||
|
# max length of characters to apply to the "slug" field
|
||||||
|
# truncate_slug_length = 40
|
||||||
|
|
||||||
|
# set to 'true' to run the environment during
|
||||||
|
# the 'revision' command, regardless of autogenerate
|
||||||
|
# revision_environment = false
|
||||||
|
|
||||||
|
# set to 'true' to allow .pyc and .pyo files without
|
||||||
|
# a source .py file to be detected as revisions in the
|
||||||
|
# versions/ directory
|
||||||
|
# sourceless = false
|
||||||
|
|
||||||
|
# version location specification; This defaults
|
||||||
|
# to alembic/versions. When using multiple version
|
||||||
|
# directories, initial revisions must be specified with --version-path.
|
||||||
|
# The path separator used here should be the separator specified by "version_path_separator" below.
|
||||||
|
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
|
||||||
|
|
||||||
|
# version path separator; As mentioned above, this is the character used to split
|
||||||
|
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
|
||||||
|
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
|
||||||
|
# Valid values for version_path_separator are:
|
||||||
|
#
|
||||||
|
# version_path_separator = :
|
||||||
|
# version_path_separator = ;
|
||||||
|
# version_path_separator = space
|
||||||
|
# version_path_separator = newline
|
||||||
|
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
|
||||||
|
|
||||||
|
# set to 'true' to search source files recursively
|
||||||
|
# in each "version_locations" directory
|
||||||
|
# new in Alembic version 1.10
|
||||||
|
# recursive_version_locations = false
|
||||||
|
|
||||||
|
# the output encoding used when revision files
|
||||||
|
# are written from script.py.mako
|
||||||
|
# output_encoding = utf-8
|
||||||
|
|
||||||
|
sqlalchemy.url = driver://user:pass@localhost/dbname
|
||||||
|
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
# post_write_hooks defines scripts or Python functions that are run
|
||||||
|
# on newly generated revision scripts. See the documentation for further
|
||||||
|
# detail and examples
|
||||||
|
|
||||||
|
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
||||||
|
# hooks = black
|
||||||
|
# black.type = console_scripts
|
||||||
|
# black.entrypoint = black
|
||||||
|
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
|
||||||
|
# hooks = ruff
|
||||||
|
# ruff.type = exec
|
||||||
|
# ruff.executable = %(here)s/.venv/bin/ruff
|
||||||
|
# ruff.options = --fix REVISION_SCRIPT_FILENAME
|
||||||
|
|
||||||
|
# Logging configuration
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARNING
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARNING
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# alembic/env.py
|
||||||
|
|
||||||
|
import os
|
||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
from alembic import context
|
||||||
|
from app.database import Base
|
||||||
|
from app.models import Organization, User, Role, AIConfig, Agent, Knowledge, KnowledgeToAgent, AIModelType
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# this is the Alembic Config object, which provides access to the values within the .ini file
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
# Override sqlalchemy.url with our database URL from settings
|
||||||
|
|
||||||
|
config.set_main_option('sqlalchemy.url', settings.DATABASE_URL)
|
||||||
|
|
||||||
|
# Interpret the config file for Python logging.
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
# add your model's MetaData object here
|
||||||
|
# for 'autogenerate' support
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_offline():
|
||||||
|
context.configure(
|
||||||
|
url=config.get_main_option("sqlalchemy.url"),
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online():
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(connection=connection,
|
||||||
|
target_metadata=target_metadata)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = ${repr(up_revision)}
|
||||||
|
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||||
|
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_priority_to_knowledge_queue
|
||||||
|
|
||||||
|
Revision ID: 013174e15242
|
||||||
|
Revises: dc827ab30bd4
|
||||||
|
Create Date: 2025-10-06 14:35:40.640506
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '013174e15242'
|
||||||
|
down_revision: Union[str, None] = 'dc827ab30bd4'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add priority column with default value 0 and create index
|
||||||
|
op.add_column('knowledge_queue', sa.Column('priority', sa.Integer(), nullable=False, server_default='0'))
|
||||||
|
op.create_index(op.f('ix_knowledge_queue_priority'), 'knowledge_queue', ['priority'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove index and column
|
||||||
|
op.drop_index(op.f('ix_knowledge_queue_priority'), table_name='knowledge_queue')
|
||||||
|
op.drop_column('knowledge_queue', 'priority')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""merge guardrails and chat initiation
|
||||||
|
|
||||||
|
Revision ID: 0cf34ff05aee
|
||||||
|
Revises: 2037204bbebf, a1b2c3d4e5f6
|
||||||
|
Create Date: 2025-10-31 19:08:54.718682
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '0cf34ff05aee'
|
||||||
|
down_revision: Union[str, None] = ('2037204bbebf', 'a1b2c3d4e5f6')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_teamid_to_agent_sessions
|
||||||
|
|
||||||
|
Revision ID: 0cf5fa1db0cb
|
||||||
|
Revises: 91a148726029
|
||||||
|
Create Date: 2025-03-21 13:57:55.161703
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '0cf5fa1db0cb'
|
||||||
|
down_revision: Union[str, None] = '91a148726029'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Check if 'ai' schema and agent_sessions table exist before attempting to modify
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'"))
|
||||||
|
schema_exists = schema_result.fetchone() is not None
|
||||||
|
|
||||||
|
if not schema_exists:
|
||||||
|
print("Schema 'ai' does not exist, skipping migration")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
table_result = conn.execute(sa.text(
|
||||||
|
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'"
|
||||||
|
))
|
||||||
|
table_exists = table_result.fetchone() is not None
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Add team_id column to ai.agent_sessions table
|
||||||
|
op.add_column('agent_sessions',
|
||||||
|
sa.Column('team_id', sa.UUID(), nullable=True),
|
||||||
|
schema='ai'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Table 'ai.agent_sessions' does not exist, skipping migration")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Check if 'ai' schema and agent_sessions table exist before attempting to modify
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'"))
|
||||||
|
schema_exists = schema_result.fetchone() is not None
|
||||||
|
|
||||||
|
if not schema_exists:
|
||||||
|
print("Schema 'ai' does not exist, skipping migration rollback")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
table_result = conn.execute(sa.text(
|
||||||
|
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'"
|
||||||
|
))
|
||||||
|
table_exists = table_result.fetchone() is not None
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Drop team_id column from ai.agent_sessions table
|
||||||
|
op.drop_column('agent_sessions', 'team_id', schema='ai')
|
||||||
|
else:
|
||||||
|
print("Table 'ai.agent_sessions' does not exist, skipping migration rollback")
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""removed message node specific fields
|
||||||
|
|
||||||
|
Revision ID: 0ee9dbc73d9e
|
||||||
|
Revises: bdaa7a5d6109
|
||||||
|
Create Date: 2025-07-23 12:19:03.629271
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '0ee9dbc73d9e'
|
||||||
|
down_revision: Union[str, None] = 'bdaa7a5d6109'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_column('workflow_nodes', 'message_text')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('workflow_nodes', sa.Column('message_text', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added shopify models
|
||||||
|
|
||||||
|
Revision ID: 1493b8279a2f
|
||||||
|
Revises: 520df92643e3
|
||||||
|
Create Date: 2025-04-10 13:50:54.439561
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '1493b8279a2f'
|
||||||
|
down_revision: Union[str, None] = '520df92643e3'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('shopify_shops',
|
||||||
|
sa.Column('id', sa.String(), nullable=False),
|
||||||
|
sa.Column('shop_domain', sa.String(), nullable=False),
|
||||||
|
sa.Column('access_token', sa.String(), nullable=True),
|
||||||
|
sa.Column('scope', sa.String(), nullable=True),
|
||||||
|
sa.Column('is_installed', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_shopify_shops_shop_domain'), 'shopify_shops', ['shop_domain'], unique=True)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_shopify_shops_shop_domain'), table_name='shopify_shops')
|
||||||
|
op.drop_table('shopify_shops')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added rating table
|
||||||
|
|
||||||
|
Revision ID: 186c65ae9406
|
||||||
|
Revises: 537f2b7d4a7c
|
||||||
|
Create Date: 2025-03-04 12:18:45.004571
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '186c65ae9406'
|
||||||
|
down_revision: Union[str, None] = '537f2b7d4a7c'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('ratings',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('session_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('customer_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('rating', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('feedback', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ratings_id'), 'ratings', ['id'], unique=False)
|
||||||
|
op.alter_column('agents', 'ask_for_rating',
|
||||||
|
existing_type=sa.BOOLEAN(),
|
||||||
|
nullable=True,
|
||||||
|
existing_server_default=sa.text('true'))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('agents', 'ask_for_rating',
|
||||||
|
existing_type=sa.BOOLEAN(),
|
||||||
|
nullable=False,
|
||||||
|
existing_server_default=sa.text('true'))
|
||||||
|
op.drop_index(op.f('ix_ratings_id'), table_name='ratings')
|
||||||
|
op.drop_table('ratings')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add guardrails node type
|
||||||
|
|
||||||
|
Revision ID: 2037204bbebf
|
||||||
|
Revises: 7af9828611e7
|
||||||
|
Create Date: 2025-10-29 11:47:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2037204bbebf'
|
||||||
|
down_revision: Union[str, None] = '7af9828611e7'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add GUARDRAILS to the NodeType enum
|
||||||
|
op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'GUARDRAILS'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# First, delete or convert any existing GUARDRAILS nodes
|
||||||
|
# Option 1: Delete GUARDRAILS nodes (uncomment if you want to delete them)
|
||||||
|
# op.execute("DELETE FROM workflow_nodes WHERE node_type = 'GUARDRAILS'")
|
||||||
|
|
||||||
|
# Option 2: Convert GUARDRAILS nodes to ACTION nodes (safer - preserves data)
|
||||||
|
op.execute("UPDATE workflow_nodes SET node_type = 'ACTION' WHERE node_type = 'GUARDRAILS'")
|
||||||
|
|
||||||
|
# Create a new enum without the guardrails value
|
||||||
|
op.execute("""
|
||||||
|
CREATE TYPE nodetype_new AS ENUM (
|
||||||
|
'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END','LANDING_PAGE','USER_INPUT'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Update the column type to use the new enum
|
||||||
|
op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new")
|
||||||
|
|
||||||
|
# Drop the old enum
|
||||||
|
op.execute("DROP TYPE nodetype")
|
||||||
|
|
||||||
|
# Rename the new enum to the original name
|
||||||
|
op.execute("ALTER TYPE nodetype_new RENAME TO nodetype")
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""merge multiple heads
|
||||||
|
|
||||||
|
Revision ID: 2381319791e8
|
||||||
|
Revises: add_allowed_attachment_types_001, add_require_token_auth_001
|
||||||
|
Create Date: 2026-01-08 10:31:54.013991
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2381319791e8'
|
||||||
|
down_revision: Union[str, None] = ('add_allowed_attachment_types_001', 'add_require_token_auth_001')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_workflow_history_column
|
||||||
|
|
||||||
|
Revision ID: 2fd826882d08
|
||||||
|
Revises: 0ee9dbc73d9e
|
||||||
|
Create Date: 2025-07-24 21:10:23.798939
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2fd826882d08'
|
||||||
|
down_revision: Union[str, None] = '0ee9dbc73d9e'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add workflow_history column to store form submissions and workflow interactions
|
||||||
|
op.add_column('session_to_agents', sa.Column('workflow_history', sa.JSON(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove workflow_history column
|
||||||
|
op.drop_column('session_to_agents', 'workflow_history')
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add jira config
|
||||||
|
|
||||||
|
Revision ID: 30caca239191
|
||||||
|
Revises: fa04f644fc0c
|
||||||
|
Create Date: 2025-03-06 20:27:48.691533
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '30caca239191'
|
||||||
|
down_revision: Union[str, None] = 'fa04f644fc0c'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('jira_tokens',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('access_token', sa.String(), nullable=False),
|
||||||
|
sa.Column('refresh_token', sa.String(), nullable=False),
|
||||||
|
sa.Column('token_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('cloud_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('site_url', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_jira_tokens_id'), 'jira_tokens', ['id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_jira_tokens_id'), table_name='jira_tokens')
|
||||||
|
op.drop_table('jira_tokens')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Added chattermate AI model
|
||||||
|
|
||||||
|
Revision ID: 520df92643e3
|
||||||
|
Revises: 85364d188f15
|
||||||
|
Create Date: 2025-03-28 23:27:11.906265
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '520df92643e3'
|
||||||
|
down_revision: Union[str, None] = '85364d188f15'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# Create a new temporary type with the new value
|
||||||
|
op.execute("ALTER TYPE aimodeltype ADD VALUE IF NOT EXISTS 'CHATTERMATE'")
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# Note: PostgreSQL does not support removing enum values
|
||||||
|
# We can only create a new enum without the value and replace the old one
|
||||||
|
pass
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""make user org and role nullable
|
||||||
|
|
||||||
|
Revision ID: 52e6ca829bb8
|
||||||
|
Revises: b365b8447598
|
||||||
|
Create Date: 2024-02-04 18:54:47.823
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '52e6ca829bb8'
|
||||||
|
down_revision: Union[str, None] = 'b365b8447598'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Make organization_id nullable
|
||||||
|
op.alter_column('users', 'organization_id',
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
# Make role_id nullable
|
||||||
|
op.alter_column('users', 'role_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Revert organization_id to non-nullable
|
||||||
|
op.alter_column('users', 'organization_id',
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
nullable=False)
|
||||||
|
|
||||||
|
# Revert role_id to non-nullable
|
||||||
|
op.alter_column('users', 'role_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
nullable=False)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added end chat support
|
||||||
|
|
||||||
|
Revision ID: 537f2b7d4a7c
|
||||||
|
Revises: 5b5f936cd9e4
|
||||||
|
Create Date: 2025-03-04 12:07:37.679734
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '537f2b7d4a7c'
|
||||||
|
down_revision: Union[str, None] = '5b5f936cd9e4'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create the enum type first
|
||||||
|
end_chat_reason_type = postgresql.ENUM(
|
||||||
|
'ISSUE_RESOLVED',
|
||||||
|
'CUSTOMER_REQUEST',
|
||||||
|
'CONFIRMATION_RECEIVED',
|
||||||
|
'FAREWELL',
|
||||||
|
'THANK_YOU',
|
||||||
|
'NATURAL_CONCLUSION',
|
||||||
|
'TASK_COMPLETED',
|
||||||
|
name='endchatreasontype'
|
||||||
|
)
|
||||||
|
end_chat_reason_type.create(op.get_bind())
|
||||||
|
|
||||||
|
# Add columns
|
||||||
|
op.add_column('agents', sa.Column('ask_for_rating', sa.Boolean(), server_default='true', nullable=False))
|
||||||
|
op.add_column('session_to_agents', sa.Column('end_chat_reason', postgresql.ENUM('ISSUE_RESOLVED', 'CUSTOMER_REQUEST', 'CONFIRMATION_RECEIVED', 'FAREWELL', 'THANK_YOU', 'NATURAL_CONCLUSION', 'TASK_COMPLETED', name='endchatreasontype'), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('end_chat_description', sa.String(), nullable=True))
|
||||||
|
op.drop_column('session_to_agents', 'closed_at')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('session_to_agents', sa.Column('closed_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True))
|
||||||
|
op.drop_column('session_to_agents', 'end_chat_description')
|
||||||
|
op.drop_column('session_to_agents', 'end_chat_reason')
|
||||||
|
op.drop_column('agents', 'ask_for_rating')
|
||||||
|
|
||||||
|
# Drop the enum type
|
||||||
|
op.execute('DROP TYPE endchatreasontype')
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Role remove name alone as unique
|
||||||
|
|
||||||
|
Revision ID: 5b5f936cd9e4
|
||||||
|
Revises: 52e6ca829bb8
|
||||||
|
Create Date: 2025-02-08 07:36:33.283138
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '5b5f936cd9e4'
|
||||||
|
down_revision: Union[str, None] = '52e6ca829bb8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('roles_name_key', 'roles', type_='unique')
|
||||||
|
op.create_unique_constraint('uq_role_name_org', 'roles', ['name', 'organization_id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint('uq_role_name_org', 'roles', type_='unique')
|
||||||
|
op.create_unique_constraint('roles_name_key', 'roles', ['name'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_mcp_tools_tables
|
||||||
|
|
||||||
|
Revision ID: 621095beb234
|
||||||
|
Revises: 7991b4fc00d8
|
||||||
|
Create Date: 2025-07-27 16:45:37.520270
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '621095beb234'
|
||||||
|
down_revision: Union[str, None] = '7991b4fc00d8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
|
||||||
|
op.create_table('mcp_tools',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('transport_type', sa.Enum('STDIO', 'SSE', 'HTTP', name='mcptransporttype'), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('command', sa.String(), nullable=True),
|
||||||
|
sa.Column('args', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('env_vars', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('url', sa.String(), nullable=True),
|
||||||
|
sa.Column('headers', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('timeout', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('sse_read_timeout', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('terminate_on_close', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_mcp_tools_id'), 'mcp_tools', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('mcp_tool_to_agent',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('mcp_tool_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['mcp_tool_id'], ['mcp_tools.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_mcp_tool_to_agent_id'), 'mcp_tool_to_agent', ['id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_mcp_tool_to_agent_id'), table_name='mcp_tool_to_agent')
|
||||||
|
op.drop_table('mcp_tool_to_agent')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_mcp_tools_id'), table_name='mcp_tools')
|
||||||
|
op.drop_table('mcp_tools')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add chat_style and widget_position to agent_customizations
|
||||||
|
|
||||||
|
Revision ID: 6885c9f36712
|
||||||
|
Revises: 9ce623009955
|
||||||
|
Create Date: 2025-01-27 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '6885c9f36712'
|
||||||
|
down_revision: Union[str, None] = '9ce623009955'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create ChatStyle enum
|
||||||
|
chatstyle_enum = sa.Enum('CHATBOT', 'ASK_ANYTHING', name='chatstyle')
|
||||||
|
chatstyle_enum.create(op.get_bind())
|
||||||
|
|
||||||
|
# Create WidgetPosition enum
|
||||||
|
widgetposition_enum = sa.Enum('FLOATING', 'FIXED', name='widgetposition')
|
||||||
|
widgetposition_enum.create(op.get_bind())
|
||||||
|
|
||||||
|
# Add columns to agent_customizations table as nullable first
|
||||||
|
op.add_column('agent_customizations',
|
||||||
|
sa.Column('chat_style', chatstyle_enum, nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column('agent_customizations',
|
||||||
|
sa.Column('widget_position', widgetposition_enum, nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update existing rows with default values
|
||||||
|
op.execute("UPDATE agent_customizations SET chat_style = 'CHATBOT' WHERE chat_style IS NULL")
|
||||||
|
op.execute("UPDATE agent_customizations SET widget_position = 'FLOATING' WHERE widget_position IS NULL")
|
||||||
|
|
||||||
|
# Now make the columns NOT NULL
|
||||||
|
op.alter_column('agent_customizations', 'chat_style', nullable=False)
|
||||||
|
op.alter_column('agent_customizations', 'widget_position', nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop columns
|
||||||
|
op.drop_column('agent_customizations', 'widget_position')
|
||||||
|
op.drop_column('agent_customizations', 'chat_style')
|
||||||
|
|
||||||
|
# Drop enums
|
||||||
|
sa.Enum(name='widgetposition').drop(op.get_bind())
|
||||||
|
sa.Enum(name='chatstyle').drop(op.get_bind())
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added wlecome header and title
|
||||||
|
|
||||||
|
Revision ID: 7991b4fc00d8
|
||||||
|
Revises: 6885c9f36712
|
||||||
|
Create Date: 2025-07-27 16:11:49.392307
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '7991b4fc00d8'
|
||||||
|
down_revision: Union[str, None] = '6885c9f36712'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.add_column('agent_customizations', sa.Column('welcome_title', sa.String(), nullable=True))
|
||||||
|
op.add_column('agent_customizations', sa.Column('welcome_subtitle', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('agent_customizations', 'welcome_subtitle')
|
||||||
|
op.drop_column('agent_customizations', 'welcome_title')
|
||||||
|
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add oauth_state fields to shopify_shops
|
||||||
|
|
||||||
|
Revision ID: 7af9828611e7
|
||||||
|
Revises: d9a4b9902f80
|
||||||
|
Create Date: 2025-10-28 20:28:32.936719
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '7af9828611e7'
|
||||||
|
down_revision: Union[str, None] = 'd9a4b9902f80'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add oauth_state and oauth_state_expiry columns to shopify_shops table
|
||||||
|
op.add_column('shopify_shops', sa.Column('oauth_state', sa.String(), nullable=True))
|
||||||
|
op.add_column('shopify_shops', sa.Column('oauth_state_expiry', sa.DateTime(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove oauth_state and oauth_state_expiry columns from shopify_shops table
|
||||||
|
op.drop_column('shopify_shops', 'oauth_state_expiry')
|
||||||
|
op.drop_column('shopify_shops', 'oauth_state')
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Workflow tables added
|
||||||
|
|
||||||
|
Revision ID: 7fd0447b3d45
|
||||||
|
Revises: e252efacd083
|
||||||
|
Create Date: 2025-07-09 11:12:34.104222
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '7fd0447b3d45'
|
||||||
|
down_revision: Union[str, None] = 'e252efacd083'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('workflows',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('status', sa.Enum('DRAFT', 'PUBLISHED', 'ARCHIVED', name='workflowstatus'), nullable=True),
|
||||||
|
sa.Column('version', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('is_template', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('default_language', sa.String(length=10), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('created_by', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('canvas_data', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
op.create_table('workflow_nodes',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('workflow_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('node_type', sa.Enum('MESSAGE', 'LLM', 'CONDITION', 'FORM', 'ACTION', 'HUMAN_TRANSFER', 'WAIT', 'END', name='nodetype'), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('position_x', sa.Float(), nullable=True),
|
||||||
|
sa.Column('position_y', sa.Float(), nullable=True),
|
||||||
|
sa.Column('config', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('message_text', sa.Text(), nullable=True),
|
||||||
|
sa.Column('system_prompt', sa.Text(), nullable=True),
|
||||||
|
sa.Column('temperature', sa.Float(), nullable=True),
|
||||||
|
sa.Column('model_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('form_fields', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('condition_expression', sa.Text(), nullable=True),
|
||||||
|
sa.Column('action_type', sa.String(), nullable=True),
|
||||||
|
sa.Column('action_config', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('transfer_rules', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('wait_duration', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('wait_until_condition', sa.Text(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['model_id'], ['ai_configs.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['workflow_id'], ['workflows.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('workflow_connections',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('workflow_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('source_node_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('target_node_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('label', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('condition', sa.Text(), nullable=True),
|
||||||
|
sa.Column('priority', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('connection_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['source_node_id'], ['workflow_nodes.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['target_node_id'], ['workflow_nodes.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['workflow_id'], ['workflows.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.add_column('agents', sa.Column('use_workflow', sa.Boolean(), nullable=True))
|
||||||
|
op.add_column('agents', sa.Column('active_workflow_id', sa.UUID(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'agents', 'workflows', ['active_workflow_id'], ['id'])
|
||||||
|
op.add_column('session_to_agents', sa.Column('workflow_id', sa.UUID(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('current_node_id', sa.UUID(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('workflow_state', sa.JSON(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'session_to_agents', 'workflow_nodes', ['current_node_id'], ['id'], ondelete='SET NULL')
|
||||||
|
op.create_foreign_key(None, 'session_to_agents', 'workflows', ['workflow_id'], ['id'], ondelete='SET NULL')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'session_to_agents', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'session_to_agents', type_='foreignkey')
|
||||||
|
op.drop_column('session_to_agents', 'workflow_state')
|
||||||
|
op.drop_column('session_to_agents', 'current_node_id')
|
||||||
|
op.drop_column('session_to_agents', 'workflow_id')
|
||||||
|
op.drop_constraint(None, 'agents', type_='foreignkey')
|
||||||
|
op.drop_column('agents', 'active_workflow_id')
|
||||||
|
op.drop_column('agents', 'use_workflow')
|
||||||
|
op.drop_table('workflow_connections')
|
||||||
|
op.drop_table('workflow_nodes')
|
||||||
|
|
||||||
|
op.drop_table('workflows')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""merge heads
|
||||||
|
|
||||||
|
Revision ID: 85364d188f15
|
||||||
|
Revises: 0cf5fa1db0cb, 89a4f4898aa4
|
||||||
|
Create Date: 2025-03-22 20:43:28.206404
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '85364d188f15'
|
||||||
|
down_revision: Union[str, None] = ('0cf5fa1db0cb', '89a4f4898aa4')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""change datetime to timestamp with timezone
|
||||||
|
|
||||||
|
Revision ID: 89a4f4898aa4
|
||||||
|
Revises: 91a148726029
|
||||||
|
Create Date: 2024-03-21 19:18:36.159906
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '89a4f4898aa4'
|
||||||
|
down_revision: Union[str, None] = '91a148726029'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Change columns from DateTime to TIMESTAMP WITH TIME ZONE in chat_history table
|
||||||
|
op.alter_column('chat_history', 'created_at',
|
||||||
|
existing_type=sa.DateTime(),
|
||||||
|
type_=sa.TIMESTAMP(timezone=True),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
|
||||||
|
op.alter_column('chat_history', 'updated_at',
|
||||||
|
existing_type=sa.DateTime(),
|
||||||
|
type_=sa.TIMESTAMP(timezone=True),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Change columns back to DateTime in chat_history table
|
||||||
|
op.alter_column('chat_history', 'created_at',
|
||||||
|
existing_type=sa.TIMESTAMP(timezone=True),
|
||||||
|
type_=sa.DateTime(),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
|
|
||||||
|
op.alter_column('chat_history', 'updated_at',
|
||||||
|
existing_type=sa.TIMESTAMP(timezone=True),
|
||||||
|
type_=sa.DateTime(),
|
||||||
|
existing_nullable=True,
|
||||||
|
existing_server_default=sa.text('now()'))
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""make_knowledge_queue_user_id_nullable
|
||||||
|
|
||||||
|
Revision ID: 910bfe03518b
|
||||||
|
Revises: 0cf34ff05aee
|
||||||
|
Create Date: 2025-11-04 17:18:35.904635
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '910bfe03518b'
|
||||||
|
down_revision: Union[str, None] = '0cf34ff05aee'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Make user_id nullable in knowledge_queue table
|
||||||
|
op.alter_column('knowledge_queue', 'user_id',
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Revert user_id to not nullable
|
||||||
|
op.alter_column('knowledge_queue', 'user_id',
|
||||||
|
existing_type=sa.UUID(),
|
||||||
|
nullable=False)
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add extra_data to agent_sessions
|
||||||
|
|
||||||
|
Revision ID: 91a148726029
|
||||||
|
Revises: f3dad24474e0
|
||||||
|
Create Date: 2024-03-21 10:57:23.123456
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '91a148726029'
|
||||||
|
down_revision: Union[str, None] = 'f3dad24474e0'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Check if 'ai' schema and agent_sessions table exist before attempting to modify
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'"))
|
||||||
|
schema_exists = schema_result.fetchone() is not None
|
||||||
|
|
||||||
|
if not schema_exists:
|
||||||
|
print("Schema 'ai' does not exist, skipping migration")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
table_result = conn.execute(sa.text(
|
||||||
|
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'"
|
||||||
|
))
|
||||||
|
table_exists = table_result.fetchone() is not None
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Add extra_data column to ai.agent_sessions table
|
||||||
|
op.add_column('agent_sessions',
|
||||||
|
sa.Column('extra_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
||||||
|
schema='ai'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("Table 'ai.agent_sessions' does not exist, skipping migration")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Check if 'ai' schema and agent_sessions table exist before attempting to modify
|
||||||
|
conn = op.get_bind()
|
||||||
|
|
||||||
|
# Check if schema exists
|
||||||
|
schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'"))
|
||||||
|
schema_exists = schema_result.fetchone() is not None
|
||||||
|
|
||||||
|
if not schema_exists:
|
||||||
|
print("Schema 'ai' does not exist, skipping migration rollback")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
table_result = conn.execute(sa.text(
|
||||||
|
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'"
|
||||||
|
))
|
||||||
|
table_exists = table_result.fetchone() is not None
|
||||||
|
|
||||||
|
if table_exists:
|
||||||
|
# Drop extra_data column from ai.agent_sessions table
|
||||||
|
op.drop_column('agent_sessions', 'extra_data', schema='ai')
|
||||||
|
else:
|
||||||
|
print("Table 'ai.agent_sessions' does not exist, skipping migration rollback")
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add agent shopify config with shop relation
|
||||||
|
|
||||||
|
Revision ID: 9a01e6b653b2
|
||||||
|
Revises: 1493b8279a2f
|
||||||
|
Create Date: 2025-04-11 13:43:05.945125
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '9a01e6b653b2'
|
||||||
|
down_revision: Union[str, None] = '1493b8279a2f'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.create_table('agent_shopify_configs',
|
||||||
|
sa.Column('id', sa.String(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('shop_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['shop_id'], ['shopify_shops.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_agent_shopify_configs_agent_id'), 'agent_shopify_configs', ['agent_id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_agent_shopify_configs_agent_id'), table_name='agent_shopify_configs')
|
||||||
|
op.drop_table('agent_shopify_configs')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added new node type user input
|
||||||
|
|
||||||
|
Revision ID: 9ce623009955
|
||||||
|
Revises: 2fd826882d08
|
||||||
|
Create Date: 2025-07-25 13:59:26.571283
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '9ce623009955'
|
||||||
|
down_revision: Union[str, None] = '2fd826882d08'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'USER_INPUT'")
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
# Create a new enum without the landing_page value
|
||||||
|
op.execute("""
|
||||||
|
CREATE TYPE nodetype_new AS ENUM (
|
||||||
|
'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END','LANDING_PAGE'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Update the column type to use the new enum
|
||||||
|
op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new")
|
||||||
|
|
||||||
|
# Drop the old enum
|
||||||
|
op.execute("DROP TYPE nodetype")
|
||||||
|
|
||||||
|
# Rename the new enum to the original name
|
||||||
|
op.execute("ALTER TYPE nodetype_new RENAME TO nodetype")
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add chat initiation messages
|
||||||
|
|
||||||
|
Revision ID: a1b2c3d4e5f6
|
||||||
|
Revises: 7af9828611e7
|
||||||
|
Create Date: 2025-10-29 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a1b2c3d4e5f6'
|
||||||
|
down_revision: Union[str, None] = '7af9828611e7'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('agent_customizations', sa.Column('chat_initiation_messages', postgresql.JSON(astext_type=sa.Text()), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('agent_customizations', 'chat_initiation_messages')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""merge ai-ticketing + agent-queue heads
|
||||||
|
|
||||||
|
Revision ID: a1dbf557bbb0
|
||||||
|
Revises: add_agent_tkt_toggle_001, b7c4e91a2d38
|
||||||
|
Create Date: 2026-07-20 11:02:56.904328
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a1dbf557bbb0'
|
||||||
|
down_revision: Union[str, None] = ('add_agent_tkt_toggle_001', 'b7c4e91a2d38')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""merge crm and chat-session-idx heads
|
||||||
|
|
||||||
|
Revision ID: a8efc6b31aa8
|
||||||
|
Revises: add_chat_session_idx_001, add_crm_tables_001
|
||||||
|
Create Date: 2026-07-28 18:55:32.388594
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'a8efc6b31aa8'
|
||||||
|
down_revision: Union[str, None] = ('add_chat_session_idx_001', 'add_crm_tables_001')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_file_attachments_table
|
||||||
|
|
||||||
|
Revision ID: abc123456789
|
||||||
|
Revises: 0cf34ff05aee
|
||||||
|
Create Date: 2025-10-30 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'abc123456789'
|
||||||
|
down_revision: Union[str, None] = '0cf34ff05aee'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create file_attachments table
|
||||||
|
op.create_table('file_attachments',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('file_url', sa.String(), nullable=False),
|
||||||
|
sa.Column('filename', sa.String(), nullable=False),
|
||||||
|
sa.Column('content_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('file_size', sa.BigInteger(), nullable=False),
|
||||||
|
sa.Column('chat_history_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('uploaded_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('uploaded_by_customer_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['chat_history_id'], ['chat_history.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['uploaded_by_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['uploaded_by_customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_file_attachments_id'), 'file_attachments', ['id'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop the table and index
|
||||||
|
op.drop_index(op.f('ix_file_attachments_id'), table_name='file_attachments')
|
||||||
|
op.drop_table('file_attachments')
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add agents.ai_replies_enabled (human-only agents)
|
||||||
|
|
||||||
|
Revision ID: add_agent_ai_replies_001
|
||||||
|
Revises: purge_mangled_crawl_pages_001
|
||||||
|
Create Date: 2026-08-02
|
||||||
|
|
||||||
|
Off means the AI never answers on this agent: every chat is queued for the team
|
||||||
|
from the customer's first message. Defaults on, so existing agents keep
|
||||||
|
answering exactly as before.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_agent_ai_replies_001'
|
||||||
|
down_revision = 'purge_mangled_crawl_pages_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'agents',
|
||||||
|
sa.Column('ai_replies_enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'ai_replies_enabled')
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add agents.guardrail_prompt and agents.guardrail_enabled
|
||||||
|
|
||||||
|
Revision ID: add_agent_guardrail_prompt_001
|
||||||
|
Revises: add_guardrails_layer_001
|
||||||
|
Create Date: 2026-07-31
|
||||||
|
|
||||||
|
Makes the agent's scope rule tenant-editable. A code-owned topic list is a
|
||||||
|
guess about what a business is NOT — the shipped wording refused maths problems
|
||||||
|
for a maths tutor and algorithm questions for a coding bootcamp, i.e. their core
|
||||||
|
use case. guardrail_prompt NULL means "use the platform default", so the default
|
||||||
|
wording can still be improved centrally without rewriting existing rows.
|
||||||
|
guardrail_enabled defaults true so every existing agent keeps its protection.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_agent_guardrail_prompt_001'
|
||||||
|
down_revision = 'add_guardrails_layer_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('agents', sa.Column('guardrail_prompt', sa.Text(), nullable=True))
|
||||||
|
op.add_column(
|
||||||
|
'agents',
|
||||||
|
sa.Column('guardrail_enabled', sa.Boolean(),
|
||||||
|
server_default=sa.text('true'), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'guardrail_enabled')
|
||||||
|
op.drop_column('agents', 'guardrail_prompt')
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add agents.ticketing_enabled (per-agent native ticketing toggle)
|
||||||
|
|
||||||
|
Revision ID: add_agent_tkt_toggle_001
|
||||||
|
Revises: add_tkt_db_ssh_001
|
||||||
|
Create Date: 2026-07-17
|
||||||
|
|
||||||
|
Per-agent switch for native AI ticketing. Defaults on so paid orgs get
|
||||||
|
ticketing on every agent out of the box; the org's plan gate still applies on
|
||||||
|
top of it.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_agent_tkt_toggle_001'
|
||||||
|
down_revision = 'add_tkt_db_ssh_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'agents',
|
||||||
|
sa.Column('ticketing_enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'ticketing_enabled')
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add agent_customizations.show_ai_disclaimer
|
||||||
|
|
||||||
|
Revision ID: add_ai_disclaimer_001
|
||||||
|
Revises: add_agent_guardrail_prompt_001
|
||||||
|
Create Date: 2026-07-31
|
||||||
|
|
||||||
|
Controls the "AI can make mistakes" line in the widget footer. Defaults true so
|
||||||
|
existing widgets disclose by default; operators who show the disclosure
|
||||||
|
elsewhere on their site can switch it off.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_ai_disclaimer_001'
|
||||||
|
down_revision = 'add_agent_guardrail_prompt_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'agent_customizations',
|
||||||
|
sa.Column('show_ai_disclaimer', sa.Boolean(),
|
||||||
|
server_default=sa.text('true'), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agent_customizations', 'show_ai_disclaimer')
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add allow_attachments to agents table
|
||||||
|
|
||||||
|
Revision ID: add_allow_attachments_001
|
||||||
|
Revises: abc123456789, 910bfe03518b
|
||||||
|
Create Date: 2025-11-08 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_allow_attachments_001'
|
||||||
|
down_revision: Union[str, Sequence[str]] = ('abc123456789', '910bfe03518b')
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('agents',
|
||||||
|
sa.Column('allow_attachments', sa.Boolean(), nullable=False, server_default='false')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'allow_attachments')
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add agent_customizations.allow_new_chat
|
||||||
|
|
||||||
|
Revision ID: add_allow_new_chat_001
|
||||||
|
Revises: repair_agent_role_seed_001
|
||||||
|
Create Date: 2026-08-09
|
||||||
|
|
||||||
|
Shows a "New chat" control in the widget so a visitor can abandon the current
|
||||||
|
conversation and start a clean one. Defaults false: starting a new chat closes
|
||||||
|
the session, which would cut off a human agent mid-handover, so operators opt in.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_allow_new_chat_001'
|
||||||
|
down_revision = 'repair_agent_role_seed_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_column() -> bool:
|
||||||
|
"""Development databases are shared between feature branches, so the column can
|
||||||
|
already exist by the time this runs. Adding it twice would abort the upgrade."""
|
||||||
|
inspector = sa.inspect(op.get_bind())
|
||||||
|
return any(c['name'] == 'allow_new_chat'
|
||||||
|
for c in inspector.get_columns('agent_customizations'))
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
if _has_column():
|
||||||
|
return
|
||||||
|
op.add_column(
|
||||||
|
'agent_customizations',
|
||||||
|
sa.Column('allow_new_chat', sa.Boolean(),
|
||||||
|
server_default=sa.text('false'), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
if _has_column():
|
||||||
|
op.drop_column('agent_customizations', 'allow_new_chat')
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add allowed_attachment_types to agents table
|
||||||
|
|
||||||
|
Revision ID: add_allowed_attachment_types_001
|
||||||
|
Revises: add_slack_integration_001
|
||||||
|
Create Date: 2024-12-17
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_allowed_attachment_types_001'
|
||||||
|
down_revision: Union[str, Sequence[str]] = 'add_slack_integration_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add allowed_attachment_types column as JSON
|
||||||
|
op.add_column('agents',
|
||||||
|
sa.Column('allowed_attachment_types', sa.JSON(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'allowed_attachment_types')
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add omni-channel tables and session channel column
|
||||||
|
|
||||||
|
Revision ID: add_channel_tables_001
|
||||||
|
Revises: e5f6a7b8c9d0
|
||||||
|
Create Date: 2026-07-10 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_channel_tables_001'
|
||||||
|
down_revision: Union[str, None] = 'e5f6a7b8c9d0'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'channel_accounts',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('channel_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('external_account_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('display_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('encrypted_credentials', sa.Text(), nullable=False),
|
||||||
|
sa.Column('webhook_secret', sa.String(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('channel_type', 'external_account_id', name='uq_channel_account_external'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_channel_accounts_organization_id', 'channel_accounts', ['organization_id'])
|
||||||
|
op.create_index('ix_channel_accounts_channel_type', 'channel_accounts', ['channel_type'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'channel_conversations',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('channel_account_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('channel_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('external_conversation_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('external_user_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('session_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('customer_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('last_inbound_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('extra', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['channel_account_id'], ['channel_accounts.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
'channel_account_id', 'external_conversation_id', 'session_id',
|
||||||
|
name='uq_channel_conversation_session'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index('ix_channel_conversations_channel_account_id', 'channel_conversations', ['channel_account_id'])
|
||||||
|
op.create_index('ix_channel_conversations_external_conversation_id', 'channel_conversations', ['external_conversation_id'])
|
||||||
|
op.create_index('ix_channel_conversations_session_id', 'channel_conversations', ['session_id'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'agent_channel_configs',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('channel_account_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['channel_account_id'], ['channel_accounts.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('channel_account_id', name='uq_agent_channel_config_account'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_agent_channel_configs_agent_id', 'agent_channel_configs', ['agent_id'])
|
||||||
|
op.create_index('ix_agent_channel_configs_channel_account_id', 'agent_channel_configs', ['channel_account_id'])
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'session_to_agents',
|
||||||
|
sa.Column('channel', sa.String(), nullable=False, server_default='web')
|
||||||
|
)
|
||||||
|
op.create_index('ix_session_to_agents_channel', 'session_to_agents', ['channel'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_session_to_agents_channel', table_name='session_to_agents')
|
||||||
|
op.drop_column('session_to_agents', 'channel')
|
||||||
|
|
||||||
|
op.drop_index('ix_agent_channel_configs_channel_account_id', table_name='agent_channel_configs')
|
||||||
|
op.drop_index('ix_agent_channel_configs_agent_id', table_name='agent_channel_configs')
|
||||||
|
op.drop_table('agent_channel_configs')
|
||||||
|
|
||||||
|
op.drop_index('ix_channel_conversations_session_id', table_name='channel_conversations')
|
||||||
|
op.drop_index('ix_channel_conversations_external_conversation_id', table_name='channel_conversations')
|
||||||
|
op.drop_index('ix_channel_conversations_channel_account_id', table_name='channel_conversations')
|
||||||
|
op.drop_table('channel_conversations')
|
||||||
|
|
||||||
|
op.drop_index('ix_channel_accounts_channel_type', table_name='channel_accounts')
|
||||||
|
op.drop_index('ix_channel_accounts_organization_id', table_name='channel_accounts')
|
||||||
|
op.drop_table('channel_accounts')
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""index chat_history on (session_id, created_at)
|
||||||
|
|
||||||
|
Revision ID: add_chat_session_idx_001
|
||||||
|
Revises: hc_img_paths_001
|
||||||
|
Create Date: 2026-07-22
|
||||||
|
|
||||||
|
chat_history had no index on session_id — only the primary key — so every lookup of
|
||||||
|
a conversation's messages was a sequential scan. The columns match the ordering
|
||||||
|
every reader now uses, (created_at, id), so the inbox previews and
|
||||||
|
get_session_history are both answered from the index alone.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_chat_session_idx_001'
|
||||||
|
down_revision = 'hc_img_paths_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
INDEX_NAME = 'ix_chat_history_session_created'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_index(
|
||||||
|
INDEX_NAME,
|
||||||
|
'chat_history',
|
||||||
|
['session_id', sa.text('created_at DESC'), sa.text('id DESC')],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index(INDEX_NAME, table_name='chat_history')
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add crm_customer_syncs (per-person CRM link)
|
||||||
|
|
||||||
|
Revision ID: add_crm_customer_syncs_001
|
||||||
|
Revises: a8efc6b31aa8
|
||||||
|
Create Date: 2026-07-28 20:40:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_crm_customer_syncs_001'
|
||||||
|
down_revision: Union[str, None] = 'a8efc6b31aa8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'crm_customer_syncs',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('provider', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('contact_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('secondary_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('record_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('synced_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('customer_id', 'provider', name='uq_crm_customer_sync_customer_provider'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_crm_customer_syncs_organization_id', 'crm_customer_syncs', ['organization_id'])
|
||||||
|
op.create_index('ix_crm_customer_syncs_customer_id', 'crm_customer_syncs', ['customer_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_crm_customer_syncs_customer_id', table_name='crm_customer_syncs')
|
||||||
|
op.drop_index('ix_crm_customer_syncs_organization_id', table_name='crm_customer_syncs')
|
||||||
|
op.drop_table('crm_customer_syncs')
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add crm_connections and crm_sync_jobs (CRM lead push)
|
||||||
|
|
||||||
|
Revision ID: add_crm_tables_001
|
||||||
|
Revises: crm_sync_varchar_001
|
||||||
|
Create Date: 2026-07-22 13:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_crm_tables_001'
|
||||||
|
down_revision: Union[str, None] = 'crm_sync_varchar_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'crm_connections',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('provider', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('external_account_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('display_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('encrypted_credentials', sa.Text(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=16), nullable=False, server_default='active'),
|
||||||
|
sa.Column('last_error', sa.Text(), nullable=True),
|
||||||
|
sa.Column('access_token_expires_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('refresh_token_expires_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('last_refreshed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('connected_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['connected_by_user_id'], ['users.id']),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('organization_id', 'provider', name='uq_crm_connection_org_provider'),
|
||||||
|
sa.UniqueConstraint('provider', 'external_account_id', name='uq_crm_connection_external'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_crm_connections_organization_id', 'crm_connections', ['organization_id'])
|
||||||
|
op.create_index('ix_crm_connections_provider', 'crm_connections', ['provider'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'crm_sync_jobs',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('lead_response_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('provider', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('connection_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('status', sa.String(length=16), nullable=False, server_default='pending'),
|
||||||
|
sa.Column('attempts', sa.Integer(), nullable=False, server_default='0'),
|
||||||
|
sa.Column('next_attempt_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')),
|
||||||
|
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('last_error', sa.Text(), nullable=True),
|
||||||
|
sa.Column('result', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['lead_response_id'], ['lead_capture_responses.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['connection_id'], ['crm_connections.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('lead_response_id', 'provider', name='uq_crm_sync_job_lead_provider'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_crm_sync_jobs_organization_id', 'crm_sync_jobs', ['organization_id'])
|
||||||
|
op.create_index('ix_crm_sync_jobs_claim', 'crm_sync_jobs', ['status', 'next_attempt_at'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_crm_sync_jobs_claim', table_name='crm_sync_jobs')
|
||||||
|
op.drop_index('ix_crm_sync_jobs_organization_id', table_name='crm_sync_jobs')
|
||||||
|
op.drop_table('crm_sync_jobs')
|
||||||
|
op.drop_index('ix_crm_connections_provider', table_name='crm_connections')
|
||||||
|
op.drop_index('ix_crm_connections_organization_id', table_name='crm_connections')
|
||||||
|
op.drop_table('crm_connections')
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
customers.phone: a second identity key beside email, normalized E.164 with a
|
||||||
|
leading '+'. Phone-bearing channels (WhatsApp, SMS, Telegram share-contact)
|
||||||
|
resolve people by it, so it must be unique per org — but only where present,
|
||||||
|
hence a partial unique index rather than a UniqueConstraint (rows without a
|
||||||
|
phone are the overwhelming majority and must never collide). The index is
|
||||||
|
declared in the model's __table_args__ as well as created here — so
|
||||||
|
autogenerate never sees it as unknown and drops it, and so the sqlite test
|
||||||
|
schema enforces the same uniqueness Postgres does. Keep the two in step.
|
||||||
|
|
||||||
|
No backfill: WhatsApp/SMS aren't live yet, so no rows carry a phone anywhere
|
||||||
|
to backfill from.
|
||||||
|
|
||||||
|
Revision ID: add_customer_phone_001
|
||||||
|
Revises: add_hc_faq_feedback_001
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "add_customer_phone_001"
|
||||||
|
down_revision = "add_hc_faq_feedback_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
INDEX = "uix_customer_phone_org"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("customers", sa.Column("phone", sa.String(), nullable=True))
|
||||||
|
op.create_index(
|
||||||
|
INDEX,
|
||||||
|
"customers",
|
||||||
|
["organization_id", "phone"],
|
||||||
|
unique=True,
|
||||||
|
postgresql_where=sa.text("phone IS NOT NULL"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index(INDEX, table_name="customers")
|
||||||
|
op.drop_column("customers", "phone")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""Row-level scoping for the ticket DB connector
|
||||||
|
|
||||||
|
Revision ID: add_tkt_row_scope_001
|
||||||
|
Revises: a1dbf557bbb0
|
||||||
|
Create Date: 2026-07-20
|
||||||
|
|
||||||
|
Adds the per-table customer-column map that restricts a connector's queries to
|
||||||
|
the ticket customer's own rows, and which customer value fills the predicate.
|
||||||
|
Existing connectors get NULL/'email' — no scoping until an org configures it,
|
||||||
|
so behaviour is unchanged on upgrade.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
|
||||||
|
revision = "add_tkt_row_scope_001"
|
||||||
|
down_revision = "a1dbf557bbb0"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"ticket_db_connectors",
|
||||||
|
sa.Column("row_scope", JSON(), nullable=True),
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
"ticket_db_connectors",
|
||||||
|
sa.Column(
|
||||||
|
"row_scope_key",
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
server_default="email",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("ticket_db_connectors", "row_scope_key")
|
||||||
|
op.drop_column("ticket_db_connectors", "row_scope")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""Per-article SEO overrides on FAQs
|
||||||
|
|
||||||
|
Revision ID: add_faq_seo_001
|
||||||
|
Revises: rel_hc_uploads_001
|
||||||
|
Create Date: 2026-07-25
|
||||||
|
|
||||||
|
Adds nullable meta_title / meta_description to faqs so an org can override the
|
||||||
|
public article page's <title> and meta description instead of always deriving
|
||||||
|
them from the question and answer excerpt. NULL keeps today's derived behaviour.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "add_faq_seo_001"
|
||||||
|
down_revision = "rel_hc_uploads_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("faqs", sa.Column("meta_title", sa.String(length=120), nullable=True))
|
||||||
|
op.add_column("faqs", sa.Column("meta_description", sa.String(length=300), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("faqs", "meta_description")
|
||||||
|
op.drop_column("faqs", "meta_title")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add guardrail_events table and agents.topic_scope
|
||||||
|
|
||||||
|
Revision ID: add_guardrails_layer_001
|
||||||
|
Revises: add_crm_customer_syncs_001
|
||||||
|
Create Date: 2026-07-30
|
||||||
|
|
||||||
|
Global guardrail layer for customer-facing agents. guardrail_events records
|
||||||
|
every trigger (injection signals, blocks, prompt-leak replacements, scope
|
||||||
|
refusals) so abuse is measurable and false positives reviewable before any
|
||||||
|
rule is promoted from counting to blocking. agents.topic_scope is the optional
|
||||||
|
per-agent remit override for the code-owned policy's topic-scope line; null
|
||||||
|
falls back to the agent description or the organization name/domain.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_guardrails_layer_001'
|
||||||
|
down_revision = 'add_crm_customer_syncs_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'guardrail_events',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True),
|
||||||
|
sa.ForeignKey('organizations.id', ondelete='CASCADE'), nullable=True),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True),
|
||||||
|
sa.ForeignKey('agents.id', ondelete='SET NULL'), nullable=True),
|
||||||
|
# No FK: a help-center /ask event has no chat session.
|
||||||
|
sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('surface', sa.String(20), nullable=False),
|
||||||
|
sa.Column('layer', sa.String(16), nullable=False),
|
||||||
|
sa.Column('rule', sa.String(64), nullable=False),
|
||||||
|
sa.Column('action', sa.String(16), nullable=False),
|
||||||
|
sa.Column('score', sa.Float(), nullable=True),
|
||||||
|
# Rule ids / short match tokens only — never raw visitor text.
|
||||||
|
sa.Column('matched', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('char_len', sa.Integer(), nullable=True),
|
||||||
|
# Encrypted at rest (EncryptedText decorates a Text column).
|
||||||
|
sa.Column('excerpt', sa.Text(), nullable=True),
|
||||||
|
sa.Column('reviewed', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('false_positive', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()),
|
||||||
|
)
|
||||||
|
op.create_index('ix_guardrail_events_org_created', 'guardrail_events',
|
||||||
|
['organization_id', 'created_at'])
|
||||||
|
op.create_index('ix_guardrail_events_rule_created', 'guardrail_events',
|
||||||
|
['rule', 'created_at'])
|
||||||
|
|
||||||
|
op.add_column('agents', sa.Column('topic_scope', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'topic_scope')
|
||||||
|
op.drop_index('ix_guardrail_events_rule_created', table_name='guardrail_events')
|
||||||
|
op.drop_index('ix_guardrail_events_org_created', table_name='guardrail_events')
|
||||||
|
op.drop_table('guardrail_events')
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""add chat_widget_enabled to help_center_settings
|
||||||
|
|
||||||
|
Splits the public help center's two AI surfaces into independent toggles: the
|
||||||
|
inline AI quick-summary in search (ai_search_enabled) and the embedded chat
|
||||||
|
widget (chat_widget_enabled). Existing rows default to enabled to preserve the
|
||||||
|
prior behaviour, where a mapped agent's widget was shown alongside AI search.
|
||||||
|
|
||||||
|
Revision ID: add_hc_chat_widget_001
|
||||||
|
Revises: f7a1c2d3e4b5
|
||||||
|
Create Date: 2026-07-13
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_hc_chat_widget_001'
|
||||||
|
down_revision = 'f7a1c2d3e4b5'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'help_center_settings',
|
||||||
|
sa.Column('chat_widget_enabled', sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||||
|
)
|
||||||
|
# Drop the server default now that existing rows are backfilled — the model
|
||||||
|
# supplies the default on insert, matching ai_search_enabled.
|
||||||
|
op.alter_column('help_center_settings', 'chat_widget_enabled', server_default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('help_center_settings', 'chat_widget_enabled')
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""add cta_enabled to help_center_settings
|
||||||
|
|
||||||
|
Lets the primary button (CTA) in the public help-center header be toggled on/off
|
||||||
|
independently of whether its text/URL are filled in. Existing rows default to
|
||||||
|
enabled to preserve the prior behaviour (CTA shown when text and URL are set).
|
||||||
|
|
||||||
|
Revision ID: add_hc_cta_enabled_001
|
||||||
|
Revises: add_hc_chat_widget_001
|
||||||
|
Create Date: 2026-07-13
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_hc_cta_enabled_001'
|
||||||
|
down_revision = 'add_hc_chat_widget_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'help_center_settings',
|
||||||
|
sa.Column('cta_enabled', sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||||
|
)
|
||||||
|
op.alter_column('help_center_settings', 'cta_enabled', server_default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('help_center_settings', 'cta_enabled')
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
Per-article "Was this helpful?" counters on faqs: helpful_yes / helpful_no.
|
||||||
|
Votes are aggregated as counts (de-duplicated per IP/day at the API); no
|
||||||
|
per-vote rows are stored.
|
||||||
|
|
||||||
|
Revision ID: add_hc_faq_feedback_001
|
||||||
|
Revises: add_hc_job_meter_001
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "add_hc_faq_feedback_001"
|
||||||
|
down_revision = "add_hc_job_meter_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
TABLE = "faqs"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(TABLE, sa.Column("helpful_yes", sa.Integer(), nullable=False, server_default="0"))
|
||||||
|
op.add_column(TABLE, sa.Column("helpful_no", sa.Integer(), nullable=False, server_default="0"))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column(TABLE, "helpful_no")
|
||||||
|
op.drop_column(TABLE, "helpful_yes")
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""add slug to faqs for public article-page URLs
|
||||||
|
|
||||||
|
FAQs become full help-center articles served at /a/{slug}. Adds a nullable
|
||||||
|
per-org-unique slug column, then backfills existing rows from their question
|
||||||
|
(deduping collisions with -2, -3, … within each org). New/edited FAQs get their
|
||||||
|
slug assigned in the API; this backfill covers everything created before.
|
||||||
|
|
||||||
|
Revision ID: add_hc_faq_slug_001
|
||||||
|
Revises: add_hc_cta_enabled_001
|
||||||
|
Create Date: 2026-07-13
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_hc_faq_slug_001'
|
||||||
|
down_revision = 'add_hc_cta_enabled_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
SLUG_MAX_LENGTH = 80
|
||||||
|
_SLUG_CLEAN_RE = re.compile(r"[^a-z0-9]+")
|
||||||
|
|
||||||
|
|
||||||
|
def _slugify(text: str) -> str:
|
||||||
|
base = _SLUG_CLEAN_RE.sub("-", (text or "").casefold()).strip("-")
|
||||||
|
return base[:SLUG_MAX_LENGTH].strip("-") or "article"
|
||||||
|
|
||||||
|
|
||||||
|
def _backfill(conn) -> None:
|
||||||
|
faqs = conn.execute(
|
||||||
|
sa.text("SELECT id, organization_id, question FROM faqs ORDER BY created_at ASC, id ASC")
|
||||||
|
).fetchall()
|
||||||
|
used: dict = {} # organization_id -> set(slug)
|
||||||
|
for faq_id, org_id, question in faqs:
|
||||||
|
seen = used.setdefault(org_id, set())
|
||||||
|
base = _slugify(question)
|
||||||
|
candidate = base
|
||||||
|
suffix = 2
|
||||||
|
while candidate in seen:
|
||||||
|
tail = f"-{suffix}"
|
||||||
|
candidate = f"{base[:SLUG_MAX_LENGTH - len(tail)]}{tail}"
|
||||||
|
suffix += 1
|
||||||
|
seen.add(candidate)
|
||||||
|
conn.execute(
|
||||||
|
sa.text("UPDATE faqs SET slug = :slug WHERE id = :id"),
|
||||||
|
{"slug": candidate, "id": faq_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('faqs', sa.Column('slug', sa.String(length=80), nullable=True))
|
||||||
|
_backfill(op.get_bind())
|
||||||
|
op.create_index('ix_faqs_org_slug', 'faqs', ['organization_id', 'slug'], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_faqs_org_slug', table_name='faqs')
|
||||||
|
op.drop_column('faqs', 'slug')
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""preserve original article URLs when migrating a help center
|
||||||
|
|
||||||
|
An org importing an existing help center can now keep every article at the URL
|
||||||
|
it already ranks for: faqs.url_path is the root-relative path the article is
|
||||||
|
SERVED at, and /a/{slug} 301s to it. faqs.source_url records where an imported
|
||||||
|
article came from (provenance only, never routed on). The job flag carries the
|
||||||
|
import-time opt-in through to the worker.
|
||||||
|
|
||||||
|
No backfill on purpose — NULL url_path is exactly today's behaviour (/a/{slug}),
|
||||||
|
so nothing already published moves.
|
||||||
|
|
||||||
|
Revision ID: add_hc_faq_url_path_001
|
||||||
|
Revises: repair_agent_role_seed_001
|
||||||
|
Create Date: 2026-08-06
|
||||||
|
"""
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_hc_faq_url_path_001'
|
||||||
|
down_revision = 'repair_agent_role_seed_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
URL_PATH_MAX_LENGTH = 400
|
||||||
|
SOURCE_URL_MAX_LENGTH = 2048
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('faqs', sa.Column('url_path', sa.String(length=URL_PATH_MAX_LENGTH), nullable=True))
|
||||||
|
op.add_column('faqs', sa.Column('source_url', sa.String(length=SOURCE_URL_MAX_LENGTH), nullable=True))
|
||||||
|
# Serves the preserved-path lookup and enforces per-org uniqueness. Every
|
||||||
|
# existing row is NULL here, and Postgres treats NULLs as distinct, so the
|
||||||
|
# unique index applies cleanly without a backfill.
|
||||||
|
op.create_index('ix_faqs_org_url_path', 'faqs', ['organization_id', 'url_path'], unique=True)
|
||||||
|
op.add_column(
|
||||||
|
'faq_generation_jobs',
|
||||||
|
sa.Column(
|
||||||
|
'preserve_source_urls',
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.false(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('faq_generation_jobs', 'preserve_source_urls')
|
||||||
|
op.drop_index('ix_faqs_org_url_path', table_name='faqs')
|
||||||
|
op.drop_column('faqs', 'source_url')
|
||||||
|
op.drop_column('faqs', 'url_path')
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Help center favicon URL
|
||||||
|
|
||||||
|
Revision ID: add_hc_favicon_001
|
||||||
|
Revises: merge_tkt_csat_001
|
||||||
|
Create Date: 2026-07-25
|
||||||
|
|
||||||
|
Adds a nullable favicon_url to help center settings, mirroring logo_url, so orgs
|
||||||
|
can set the browser-tab icon for their public help center from Customization.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "add_hc_favicon_001"
|
||||||
|
down_revision = "merge_tkt_csat_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("help_center_settings", sa.Column("favicon_url", sa.String(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("help_center_settings", "favicon_url")
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
FAQ generation-job metering + targeting columns:
|
||||||
|
- llm_calls / metered: per-job LLM call counter for hosted-model message
|
||||||
|
metering (metered stamped at enqueue from the org's AI config).
|
||||||
|
- knowledge_ids: optional explicit source narrowing for GENERATE_ALL jobs.
|
||||||
|
- source_file_name: original filename for PDF imports.
|
||||||
|
|
||||||
|
Revision ID: add_hc_job_meter_001
|
||||||
|
Revises: add_hc_faq_slug_001
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "add_hc_job_meter_001"
|
||||||
|
down_revision = "add_hc_faq_slug_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
TABLE = "faq_generation_jobs"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(TABLE, sa.Column("llm_calls", sa.Integer(), nullable=False, server_default="0"))
|
||||||
|
op.add_column(TABLE, sa.Column("metered", sa.Boolean(), nullable=False, server_default=sa.text("true")))
|
||||||
|
op.add_column(TABLE, sa.Column("knowledge_ids", sa.JSON(), nullable=True))
|
||||||
|
op.add_column(TABLE, sa.Column("source_file_name", sa.String(length=255), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column(TABLE, "source_file_name")
|
||||||
|
op.drop_column(TABLE, "knowledge_ids")
|
||||||
|
op.drop_column(TABLE, "metered")
|
||||||
|
op.drop_column(TABLE, "llm_calls")
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add investigation glass-box tables (hypotheses, events, RCA documents)
|
||||||
|
|
||||||
|
Revision ID: add_invest_glassbox_001
|
||||||
|
Revises: merge_tkt_phone_001
|
||||||
|
Create Date: 2026-07-17
|
||||||
|
|
||||||
|
Phase 3 of AI-first ticketing: hypothesis-driven investigation with evidence
|
||||||
|
captured as first-class rows (investigation_events) and structured, versioned
|
||||||
|
RCA documents.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_invest_glassbox_001'
|
||||||
|
down_revision = 'merge_tkt_phone_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'investigation_hypotheses',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('idx', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(length=300), nullable=False),
|
||||||
|
sa.Column('rationale', sa.Text(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('confidence', sa.Float(), nullable=True),
|
||||||
|
sa.Column('conclusion', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_investigation_hypotheses_run_id'), 'investigation_hypotheses', ['run_id'])
|
||||||
|
op.create_index(op.f('ix_investigation_hypotheses_ticket_id'), 'investigation_hypotheses', ['ticket_id'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'investigation_events',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('hypothesis_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('seq', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('event_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('label', sa.String(length=300), nullable=True),
|
||||||
|
sa.Column('tool_name', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('connector_name', sa.String(length=200), nullable=True),
|
||||||
|
sa.Column('tool_input', sa.Text(), nullable=True),
|
||||||
|
sa.Column('tool_result', sa.Text(), nullable=True),
|
||||||
|
sa.Column('duration_ms', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('error', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['hypothesis_id'], ['investigation_hypotheses.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_investigation_events_run_id'), 'investigation_events', ['run_id'])
|
||||||
|
op.create_index(op.f('ix_investigation_events_ticket_id'), 'investigation_events', ['ticket_id'])
|
||||||
|
op.create_index('ix_investigation_events_run_seq', 'investigation_events', ['run_id', 'seq'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'rca_documents',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('version', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('summary', sa.Text(), nullable=True),
|
||||||
|
sa.Column('impact', sa.Text(), nullable=True),
|
||||||
|
sa.Column('timeline', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('investigation_log', sa.Text(), nullable=True),
|
||||||
|
sa.Column('contributing_factors', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('conclusion', sa.Text(), nullable=True),
|
||||||
|
sa.Column('remediation', sa.Text(), nullable=True),
|
||||||
|
sa.Column('prevention', sa.Text(), nullable=True),
|
||||||
|
sa.Column('customer_summary', sa.Text(), nullable=True),
|
||||||
|
sa.Column('confidence', sa.Float(), nullable=True),
|
||||||
|
sa.Column('is_partial', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||||
|
sa.Column('generated_by', sa.String(), server_default='ai', nullable=False),
|
||||||
|
sa.Column('reviewed_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['reviewed_by_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('ticket_id', 'version', name='uq_rca_documents_ticket_version'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_rca_documents_ticket_id'), 'rca_documents', ['ticket_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index(op.f('ix_rca_documents_ticket_id'), table_name='rca_documents')
|
||||||
|
op.drop_table('rca_documents')
|
||||||
|
op.drop_index('ix_investigation_events_run_seq', table_name='investigation_events')
|
||||||
|
op.drop_index(op.f('ix_investigation_events_ticket_id'), table_name='investigation_events')
|
||||||
|
op.drop_index(op.f('ix_investigation_events_run_id'), table_name='investigation_events')
|
||||||
|
op.drop_table('investigation_events')
|
||||||
|
op.drop_index(op.f('ix_investigation_hypotheses_ticket_id'), table_name='investigation_hypotheses')
|
||||||
|
op.drop_index(op.f('ix_investigation_hypotheses_run_id'), table_name='investigation_hypotheses')
|
||||||
|
op.drop_table('investigation_hypotheses')
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Add metered flag to investigation_runs
|
||||||
|
|
||||||
|
Revision ID: add_tkt_metered_001
|
||||||
|
Revises: add_tkt_row_scope_001
|
||||||
|
Create Date: 2026-07-21
|
||||||
|
|
||||||
|
Marks whether an investigation run's LLM usage counts against the org's message
|
||||||
|
budget (hosted model only). Existing rows default to false — nothing is
|
||||||
|
retroactively metered on upgrade.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "add_tkt_metered_001"
|
||||||
|
down_revision = "add_tkt_row_scope_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"investigation_runs",
|
||||||
|
sa.Column(
|
||||||
|
"metered",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("false"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# The enterprise message-limit check sums llm_calls of metered runs per
|
||||||
|
# billing window — an index on (org, metered, created_at) keeps that cheap.
|
||||||
|
op.create_index(
|
||||||
|
"ix_investigation_runs_metered_period",
|
||||||
|
"investigation_runs",
|
||||||
|
["organization_id", "metered", "created_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_investigation_runs_metered_period", table_name="investigation_runs")
|
||||||
|
op.drop_column("investigation_runs", "metered")
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add investigation_runs (AI triage/investigation queue)
|
||||||
|
|
||||||
|
Revision ID: add_investigation_runs_001
|
||||||
|
Revises: add_ticketing_core_001
|
||||||
|
Create Date: 2026-07-16
|
||||||
|
|
||||||
|
Phase 2 of AI-first ticketing: the run table doubles as the worker queue
|
||||||
|
(ticket_investigator polls status='pending', mirroring faq_generation_jobs).
|
||||||
|
Budgets and token actuals live on the row for cost tracking.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_investigation_runs_001'
|
||||||
|
down_revision = 'add_ticketing_core_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
ACTIVE_RUN_STATUSES = ('pending', 'running')
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'investigation_runs',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('run_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('trigger', sa.String(), nullable=False),
|
||||||
|
sa.Column('requested_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('context_note', sa.Text(), nullable=True),
|
||||||
|
sa.Column('max_tool_calls', sa.Integer(), server_default='25', nullable=False),
|
||||||
|
sa.Column('max_wall_seconds', sa.Integer(), server_default='600', nullable=False),
|
||||||
|
sa.Column('tool_calls_used', sa.Integer(), server_default='0', nullable=False),
|
||||||
|
sa.Column('llm_calls', sa.Integer(), server_default='0', nullable=False),
|
||||||
|
sa.Column('input_tokens', sa.BigInteger(), server_default='0', nullable=False),
|
||||||
|
sa.Column('output_tokens', sa.BigInteger(), server_default='0', nullable=False),
|
||||||
|
sa.Column('model_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('error', sa.Text(), nullable=True),
|
||||||
|
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['requested_by_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_investigation_runs_ticket_id'), 'investigation_runs', ['ticket_id'])
|
||||||
|
op.create_index(op.f('ix_investigation_runs_organization_id'), 'investigation_runs', ['organization_id'])
|
||||||
|
# Keeps the worker's poll O(active rows) as terminal rows accumulate.
|
||||||
|
op.create_index(
|
||||||
|
'ix_investigation_runs_active',
|
||||||
|
'investigation_runs',
|
||||||
|
['status'],
|
||||||
|
postgresql_where=sa.text(
|
||||||
|
"status IN ({})".format(", ".join(f"'{s}'" for s in ACTIVE_RUN_STATUSES))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
# Closes the enqueue check-then-insert race: at most one active run per
|
||||||
|
# ticket. Lives only in the migration (sqlite tests can't compile the
|
||||||
|
# partial WHERE).
|
||||||
|
op.execute(
|
||||||
|
"CREATE UNIQUE INDEX uq_investigation_runs_one_active ON investigation_runs "
|
||||||
|
"(ticket_id) WHERE status IN ('pending', 'running')"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS uq_investigation_runs_one_active")
|
||||||
|
op.drop_index('ix_investigation_runs_active', table_name='investigation_runs')
|
||||||
|
op.drop_index(op.f('ix_investigation_runs_organization_id'), table_name='investigation_runs')
|
||||||
|
op.drop_index(op.f('ix_investigation_runs_ticket_id'), table_name='investigation_runs')
|
||||||
|
op.drop_table('investigation_runs')
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add mcp_tools.usage_guidance
|
||||||
|
|
||||||
|
Operator-written notes on what an MCP source holds and how to query it.
|
||||||
|
Reaches the model as agno per-toolkit instructions so investigations stop
|
||||||
|
inventing index and field names (#318). Nullable with no server default, so
|
||||||
|
existing rows stay NULL and their prompts are unchanged.
|
||||||
|
|
||||||
|
Revision ID: add_mcp_usage_guidance_001
|
||||||
|
Revises: add_openai_compat_ai_model_001
|
||||||
|
Create Date: 2026-09-08 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_mcp_usage_guidance_001'
|
||||||
|
down_revision: Union[str, None] = 'add_openai_compat_ai_model_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_column() -> bool:
|
||||||
|
"""Development databases are shared between feature branches, so the column
|
||||||
|
can already exist by the time this runs. Adding it twice would abort the
|
||||||
|
upgrade."""
|
||||||
|
inspector = sa.inspect(op.get_bind())
|
||||||
|
return any(c['name'] == 'usage_guidance'
|
||||||
|
for c in inspector.get_columns('mcp_tools'))
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
if _has_column():
|
||||||
|
return
|
||||||
|
op.add_column(
|
||||||
|
'mcp_tools',
|
||||||
|
sa.Column('usage_guidance', sa.Text(), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
if _has_column():
|
||||||
|
op.drop_column('mcp_tools', 'usage_guidance')
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add per-user chat notification preferences
|
||||||
|
|
||||||
|
Revision ID: add_notif_prefs_001
|
||||||
|
Revises: add_faq_seo_001
|
||||||
|
Create Date: 2026-07-22
|
||||||
|
|
||||||
|
Rows are created lazily on first read, so nothing is backfilled here — a user
|
||||||
|
without a row falls back to the column defaults (transfer/assignment on,
|
||||||
|
new-chat off), which keeps today's transfer notifications working unchanged.
|
||||||
|
The revision id is kept short — alembic_version.version_num is varchar(32).
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_notif_prefs_001'
|
||||||
|
down_revision = 'add_faq_seo_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'user_notification_settings',
|
||||||
|
sa.Column('user_id', UUID(as_uuid=True),
|
||||||
|
sa.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
|
||||||
|
sa.Column('notify_new_chat', sa.Boolean(), nullable=False,
|
||||||
|
server_default=sa.false()),
|
||||||
|
sa.Column('notify_chat_transfer', sa.Boolean(), nullable=False,
|
||||||
|
server_default=sa.true()),
|
||||||
|
sa.Column('notify_chat_assigned', sa.Boolean(), nullable=False,
|
||||||
|
server_default=sa.true()),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.func.now()),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('user_notification_settings')
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add OPENAI_COMPATIBLE AI model type
|
||||||
|
|
||||||
|
Revision ID: add_openai_compat_ai_model_001
|
||||||
|
Revises: fcm_tokens_per_device_001
|
||||||
|
Create Date: 2026-09-03 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_openai_compat_ai_model_001'
|
||||||
|
down_revision: Union[str, None] = 'fcm_tokens_per_device_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("ALTER TYPE aimodeltype ADD VALUE IF NOT EXISTS 'OPENAI_COMPATIBLE'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# PostgreSQL does not support removing enum values
|
||||||
|
pass
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add require_token_auth to agents table
|
||||||
|
|
||||||
|
Revision ID: add_require_token_auth_001
|
||||||
|
Revises: add_allow_attachments_001
|
||||||
|
Create Date: 2025-12-01 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_require_token_auth_001'
|
||||||
|
down_revision: Union[str, Sequence[str]] = 'add_allow_attachments_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('agents',
|
||||||
|
sa.Column('require_token_auth', sa.Boolean(), nullable=False, server_default='false')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'require_token_auth')
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add investigation_runs.connector_status
|
||||||
|
|
||||||
|
Revision ID: add_run_connector_status_001
|
||||||
|
Revises: add_ai_disclaimer_001
|
||||||
|
Create Date: 2026-07-31
|
||||||
|
|
||||||
|
Records the MCP connector outcome of each investigation run ({"configured": N,
|
||||||
|
"loaded": M, "failed": [{"name", "error"}]}) so a run that quietly executed
|
||||||
|
with 0 of its configured tools (e.g. npx missing in the image, issue #271) is
|
||||||
|
visible on the run detail instead of only in worker logs.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_run_connector_status_001'
|
||||||
|
down_revision = 'add_ai_disclaimer_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('investigation_runs', sa.Column('connector_status', JSON(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('investigation_runs', 'connector_status')
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add sentiment analysis columns
|
||||||
|
|
||||||
|
Revision ID: add_sentiment_analysis_001
|
||||||
|
Revises: add_widget_apps_001
|
||||||
|
Create Date: 2026-03-16 11:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_sentiment_analysis_001'
|
||||||
|
down_revision: Union[str, None] = 'add_widget_apps_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add sentiment columns to chat_history
|
||||||
|
op.add_column('chat_history', sa.Column('sentiment_label', sa.String(), nullable=True))
|
||||||
|
op.add_column('chat_history', sa.Column('sentiment_score', sa.Float(), nullable=True))
|
||||||
|
|
||||||
|
# Add sentiment columns to session_to_agents
|
||||||
|
op.add_column('session_to_agents', sa.Column('sentiment_label', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('sentiment_score', sa.Float(), nullable=True))
|
||||||
|
|
||||||
|
# Create indexes for efficient sentiment queries
|
||||||
|
op.create_index(
|
||||||
|
'ix_chat_history_sentiment_label',
|
||||||
|
'chat_history',
|
||||||
|
['sentiment_label'],
|
||||||
|
unique=False
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
'ix_session_to_agents_sentiment_label',
|
||||||
|
'session_to_agents',
|
||||||
|
['sentiment_label'],
|
||||||
|
unique=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_session_to_agents_sentiment_label', table_name='session_to_agents')
|
||||||
|
op.drop_index('ix_chat_history_sentiment_label', table_name='chat_history')
|
||||||
|
op.drop_column('session_to_agents', 'sentiment_score')
|
||||||
|
op.drop_column('session_to_agents', 'sentiment_label')
|
||||||
|
op.drop_column('chat_history', 'sentiment_score')
|
||||||
|
op.drop_column('chat_history', 'sentiment_label')
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add Slack integration tables
|
||||||
|
|
||||||
|
Revision ID: add_slack_integration_001
|
||||||
|
Revises: add_allow_attachments_001
|
||||||
|
Create Date: 2025-11-25 10:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_slack_integration_001'
|
||||||
|
down_revision: Union[str, None] = 'add_allow_attachments_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create StorageMode enum type if it doesn't exist
|
||||||
|
connection = op.get_bind()
|
||||||
|
result = connection.execute(sa.text(
|
||||||
|
"SELECT 1 FROM pg_type WHERE typname = 'storagemode'"
|
||||||
|
))
|
||||||
|
if not result.fetchone():
|
||||||
|
op.execute("CREATE TYPE storagemode AS ENUM ('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY')")
|
||||||
|
|
||||||
|
# Create slack_tokens table
|
||||||
|
op.create_table('slack_tokens',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('access_token', sa.String(), nullable=False),
|
||||||
|
sa.Column('bot_user_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('team_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('team_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('authed_user_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('scope', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_slack_tokens_id'), 'slack_tokens', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_tokens_organization_id'), 'slack_tokens', ['organization_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_tokens_team_id'), 'slack_tokens', ['team_id'], unique=False)
|
||||||
|
|
||||||
|
# Create slack_workspace_configs table
|
||||||
|
op.create_table('slack_workspace_configs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('team_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('allowed_channel_ids', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('storage_mode', postgresql.ENUM('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY', name='storagemode', create_type=False), nullable=False),
|
||||||
|
sa.Column('default_agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['default_agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_slack_workspace_configs_id'), 'slack_workspace_configs', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_workspace_configs_organization_id'), 'slack_workspace_configs', ['organization_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_workspace_configs_team_id'), 'slack_workspace_configs', ['team_id'], unique=False)
|
||||||
|
|
||||||
|
# Create agent_slack_configs table
|
||||||
|
op.create_table('agent_slack_configs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('team_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('channel_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('channel_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('respond_to_mentions', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('respond_to_reactions', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('respond_to_commands', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('reaction_emoji', sa.String(), nullable=False, server_default='robot_face'),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('organization_id', 'channel_id', name='uq_agent_slack_config_org_channel')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_agent_slack_configs_id'), 'agent_slack_configs', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_agent_slack_configs_organization_id'), 'agent_slack_configs', ['organization_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_agent_slack_configs_team_id'), 'agent_slack_configs', ['team_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_agent_slack_configs_agent_id'), 'agent_slack_configs', ['agent_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_agent_slack_configs_channel_id'), 'agent_slack_configs', ['channel_id'], unique=False)
|
||||||
|
|
||||||
|
# Create slack_conversations table
|
||||||
|
op.create_table('slack_conversations',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('team_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('channel_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('thread_ts', sa.String(), nullable=False),
|
||||||
|
sa.Column('session_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('slack_user_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('team_id', 'channel_id', 'thread_ts', name='uq_slack_conversation_thread')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_slack_conversations_id'), 'slack_conversations', ['id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_conversations_team_id'), 'slack_conversations', ['team_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_conversations_channel_id'), 'slack_conversations', ['channel_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_conversations_thread_ts'), 'slack_conversations', ['thread_ts'], unique=False)
|
||||||
|
op.create_index(op.f('ix_slack_conversations_session_id'), 'slack_conversations', ['session_id'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop slack_conversations table
|
||||||
|
op.drop_index(op.f('ix_slack_conversations_session_id'), table_name='slack_conversations')
|
||||||
|
op.drop_index(op.f('ix_slack_conversations_thread_ts'), table_name='slack_conversations')
|
||||||
|
op.drop_index(op.f('ix_slack_conversations_channel_id'), table_name='slack_conversations')
|
||||||
|
op.drop_index(op.f('ix_slack_conversations_team_id'), table_name='slack_conversations')
|
||||||
|
op.drop_index(op.f('ix_slack_conversations_id'), table_name='slack_conversations')
|
||||||
|
op.drop_table('slack_conversations')
|
||||||
|
|
||||||
|
# Drop agent_slack_configs table
|
||||||
|
op.drop_index(op.f('ix_agent_slack_configs_channel_id'), table_name='agent_slack_configs')
|
||||||
|
op.drop_index(op.f('ix_agent_slack_configs_agent_id'), table_name='agent_slack_configs')
|
||||||
|
op.drop_index(op.f('ix_agent_slack_configs_team_id'), table_name='agent_slack_configs')
|
||||||
|
op.drop_index(op.f('ix_agent_slack_configs_organization_id'), table_name='agent_slack_configs')
|
||||||
|
op.drop_index(op.f('ix_agent_slack_configs_id'), table_name='agent_slack_configs')
|
||||||
|
op.drop_table('agent_slack_configs')
|
||||||
|
|
||||||
|
# Drop slack_workspace_configs table
|
||||||
|
op.drop_index(op.f('ix_slack_workspace_configs_team_id'), table_name='slack_workspace_configs')
|
||||||
|
op.drop_index(op.f('ix_slack_workspace_configs_organization_id'), table_name='slack_workspace_configs')
|
||||||
|
op.drop_index(op.f('ix_slack_workspace_configs_id'), table_name='slack_workspace_configs')
|
||||||
|
op.drop_table('slack_workspace_configs')
|
||||||
|
|
||||||
|
# Drop slack_tokens table
|
||||||
|
op.drop_index(op.f('ix_slack_tokens_team_id'), table_name='slack_tokens')
|
||||||
|
op.drop_index(op.f('ix_slack_tokens_organization_id'), table_name='slack_tokens')
|
||||||
|
op.drop_index(op.f('ix_slack_tokens_id'), table_name='slack_tokens')
|
||||||
|
op.drop_table('slack_tokens')
|
||||||
|
|
||||||
|
# Drop StorageMode enum type
|
||||||
|
sa.Enum('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY', name='storagemode').drop(op.get_bind(), checkfirst=True)
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""CSAT score linkage on tickets
|
||||||
|
|
||||||
|
Revision ID: add_tkt_csat_001
|
||||||
|
Revises: add_tkt_row_scope_001
|
||||||
|
Create Date: 2026-07-22
|
||||||
|
|
||||||
|
The CSAT ask already goes out on close; nothing carried the answer back. These
|
||||||
|
columns close the loop: when the customer rates the conversation linked to a
|
||||||
|
ticket, the score lands on the ticket itself, so AI-resolved vs human-resolved
|
||||||
|
CSAT is a plain query instead of a join through ratings/sessions.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision = "add_tkt_csat_001"
|
||||||
|
down_revision = "add_tkt_row_scope_001"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("tickets", sa.Column("csat_requested_at", sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column("tickets", sa.Column("csat_score", sa.Integer(), nullable=True))
|
||||||
|
op.add_column("tickets", sa.Column("csat_rating_id", sa.UUID(), nullable=True))
|
||||||
|
op.add_column("tickets", sa.Column("csat_responded_at", sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_tickets_csat_rating_id",
|
||||||
|
"tickets",
|
||||||
|
"ratings",
|
||||||
|
["csat_rating_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_index("ix_tickets_org_csat", "tickets", ["organization_id", "csat_responded_at"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_tickets_org_csat", table_name="tickets")
|
||||||
|
op.drop_constraint("fk_tickets_csat_rating_id", "tickets", type_="foreignkey")
|
||||||
|
op.drop_column("tickets", "csat_responded_at")
|
||||||
|
op.drop_column("tickets", "csat_rating_id")
|
||||||
|
op.drop_column("tickets", "csat_score")
|
||||||
|
op.drop_column("tickets", "csat_requested_at")
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add native AI-first ticketing core tables
|
||||||
|
|
||||||
|
Revision ID: add_ticketing_core_001
|
||||||
|
Revises: add_hc_faq_feedback_001
|
||||||
|
Create Date: 2026-07-16
|
||||||
|
|
||||||
|
New feature: native AI-first ticketing (replaces the Jira-only flow). Tickets
|
||||||
|
are first-class aggregates decoupled from chat sessions; the legacy
|
||||||
|
session_to_agents.ticket_* columns remain as a denormalized mirror.
|
||||||
|
|
||||||
|
Tables:
|
||||||
|
- tickets: the aggregate root (per-org TKT-{n} numbers, triage fields,
|
||||||
|
pgvector embedding for dedup, SLA timestamps, external escalation ref)
|
||||||
|
- ticket_sequences: per-org display-number allocator (SELECT ... FOR UPDATE)
|
||||||
|
- ticket_sessions: ticket <-> chat-session link table
|
||||||
|
- ticket_activities: append-only comments + audit trail
|
||||||
|
- organization_ticket_settings: autonomy level, SLA targets, comms templates,
|
||||||
|
investigation budgets
|
||||||
|
|
||||||
|
Status/priority/source columns are plain strings (mirroring
|
||||||
|
faq_generation_jobs) so new values never need an ALTER TYPE migration.
|
||||||
|
|
||||||
|
Also seeds view_tickets/manage_tickets permissions and attaches them to
|
||||||
|
existing roles that already hold manage_all_chats (new orgs get them from
|
||||||
|
Permission.default_permissions()).
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_ticketing_core_001'
|
||||||
|
down_revision = 'add_hc_faq_feedback_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
TICKET_EMBEDDING_DIM = 384
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# pgvector for the dedup embedding (idempotent; also created by
|
||||||
|
# scripts/init-vector.sql on fresh databases).
|
||||||
|
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'tickets',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_number', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('title', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('original_title', sa.String(length=500), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('priority', sa.String(), nullable=False),
|
||||||
|
sa.Column('severity', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('source', sa.String(), nullable=False),
|
||||||
|
sa.Column('intent', sa.String(), nullable=True),
|
||||||
|
sa.Column('triage_confidence', sa.Float(), nullable=True),
|
||||||
|
sa.Column('ai_summary', sa.Text(), nullable=True),
|
||||||
|
sa.Column('tags', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('assignee_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('group_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('duplicate_of_ticket_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('resolution_outcome', sa.String(), nullable=True),
|
||||||
|
sa.Column('resolution_summary', sa.Text(), nullable=True),
|
||||||
|
sa.Column('customer_resolution_message', sa.Text(), nullable=True),
|
||||||
|
sa.Column('resolved_by_actor', sa.String(), nullable=True),
|
||||||
|
sa.Column('first_response_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('confirmation_requested_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('reopened_count', sa.Integer(), server_default='0', nullable=False),
|
||||||
|
sa.Column('external_ref_type', sa.String(), nullable=True),
|
||||||
|
sa.Column('external_ref_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('external_ref_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['assignee_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['duplicate_of_ticket_id'], ['tickets.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('organization_id', 'ticket_number', name='uq_tickets_org_number'),
|
||||||
|
)
|
||||||
|
# The vector column via raw SQL so the sqlalchemy migration stays free of
|
||||||
|
# the pgvector python type.
|
||||||
|
op.execute(f"ALTER TABLE tickets ADD COLUMN embedding vector({TICKET_EMBEDDING_DIM})")
|
||||||
|
op.create_index(op.f('ix_tickets_organization_id'), 'tickets', ['organization_id'])
|
||||||
|
op.create_index(op.f('ix_tickets_status'), 'tickets', ['status'])
|
||||||
|
op.create_index('ix_tickets_org_status', 'tickets', ['organization_id', 'status'])
|
||||||
|
op.create_index('ix_tickets_org_assignee', 'tickets', ['organization_id', 'assignee_user_id'])
|
||||||
|
op.create_index('ix_tickets_org_created', 'tickets', ['organization_id', 'created_at'])
|
||||||
|
# Approximate-NN index for dedup/similar-ticket recall (cosine).
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX ix_tickets_embedding_hnsw ON tickets "
|
||||||
|
"USING hnsw (embedding vector_cosine_ops)"
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'ticket_sequences',
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('next_number', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('organization_id'),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'ticket_sessions',
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('linked_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('ticket_id', 'session_id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ticket_sessions_session_id'), 'ticket_sessions', ['session_id'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'ticket_activities',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('activity_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('actor_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('actor_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('body', sa.Text(), nullable=True),
|
||||||
|
sa.Column('is_internal', sa.Boolean(), server_default=sa.true(), nullable=False),
|
||||||
|
sa.Column('activity_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['actor_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ticket_activities_ticket_id'), 'ticket_activities', ['ticket_id'])
|
||||||
|
op.create_index('ix_ticket_activities_ticket_created', 'ticket_activities', ['ticket_id', 'created_at'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'organization_ticket_settings',
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('autonomy_level', sa.Integer(), server_default='1', nullable=False),
|
||||||
|
sa.Column('auto_investigate_on_create', sa.Boolean(), server_default=sa.true(), nullable=False),
|
||||||
|
sa.Column('min_confidence_to_auto_resolve', sa.Float(), server_default='0.85', nullable=False),
|
||||||
|
sa.Column('confirmation_timeout_hours', sa.Integer(), server_default='72', nullable=False),
|
||||||
|
sa.Column('csat_enabled', sa.Boolean(), server_default=sa.true(), nullable=False),
|
||||||
|
sa.Column('sla_targets', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_template', sa.Text(), nullable=True),
|
||||||
|
sa.Column('resolved_template', sa.Text(), nullable=True),
|
||||||
|
sa.Column('jira_escalation_enabled', sa.Boolean(), server_default=sa.false(), nullable=False),
|
||||||
|
sa.Column('jira_escalation_priority', sa.String(), nullable=True),
|
||||||
|
sa.Column('investigation_mcp_tool_ids', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('alert_webhook_enabled', sa.Boolean(), server_default=sa.false(), nullable=False),
|
||||||
|
sa.Column('max_tool_calls_per_run', sa.Integer(), server_default='25', nullable=False),
|
||||||
|
sa.Column('max_runs_per_ticket', sa.Integer(), server_default='3', nullable=False),
|
||||||
|
sa.Column('monthly_investigation_cap', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('organization_id'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Seed the new permissions and attach them to existing roles that already
|
||||||
|
# manage chats (new orgs seed via Permission.default_permissions()).
|
||||||
|
op.execute("""
|
||||||
|
INSERT INTO permissions (name, description)
|
||||||
|
SELECT 'view_tickets', 'Can view tickets'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = 'view_tickets')
|
||||||
|
""")
|
||||||
|
op.execute("""
|
||||||
|
INSERT INTO permissions (name, description)
|
||||||
|
SELECT 'manage_tickets', 'Can manage tickets'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = 'manage_tickets')
|
||||||
|
""")
|
||||||
|
op.execute("""
|
||||||
|
INSERT INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p_new.id
|
||||||
|
FROM role_permissions rp
|
||||||
|
JOIN permissions p_old ON p_old.id = rp.permission_id AND p_old.name = 'manage_all_chats'
|
||||||
|
CROSS JOIN permissions p_new
|
||||||
|
WHERE p_new.name IN ('view_tickets', 'manage_tickets')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permissions rp2
|
||||||
|
WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p_new.id
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("""
|
||||||
|
DELETE FROM role_permissions
|
||||||
|
WHERE permission_id IN (
|
||||||
|
SELECT id FROM permissions WHERE name IN ('view_tickets', 'manage_tickets')
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
op.execute("DELETE FROM permissions WHERE name IN ('view_tickets', 'manage_tickets')")
|
||||||
|
op.drop_table('organization_ticket_settings')
|
||||||
|
op.drop_index('ix_ticket_activities_ticket_created', table_name='ticket_activities')
|
||||||
|
op.drop_index(op.f('ix_ticket_activities_ticket_id'), table_name='ticket_activities')
|
||||||
|
op.drop_table('ticket_activities')
|
||||||
|
op.drop_index(op.f('ix_ticket_sessions_session_id'), table_name='ticket_sessions')
|
||||||
|
op.drop_table('ticket_sessions')
|
||||||
|
op.drop_table('ticket_sequences')
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_tickets_embedding_hnsw")
|
||||||
|
op.drop_index('ix_tickets_org_created', table_name='tickets')
|
||||||
|
op.drop_index('ix_tickets_org_assignee', table_name='tickets')
|
||||||
|
op.drop_index('ix_tickets_org_status', table_name='tickets')
|
||||||
|
op.drop_index(op.f('ix_tickets_status'), table_name='tickets')
|
||||||
|
op.drop_index(op.f('ix_tickets_organization_id'), table_name='tickets')
|
||||||
|
op.drop_table('tickets')
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add SSH tunnel fields to ticket_db_connectors
|
||||||
|
|
||||||
|
Revision ID: add_tkt_db_ssh_001
|
||||||
|
Revises: add_tkt_phase4_001
|
||||||
|
Create Date: 2026-07-17
|
||||||
|
|
||||||
|
Production databases are usually reached through a bastion / jump host rather
|
||||||
|
than directly — this adds optional SSH-tunnel config to the guardrailed DB
|
||||||
|
connector (key/password encrypted at rest).
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_tkt_db_ssh_001'
|
||||||
|
down_revision = 'add_tkt_phase4_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('ssh_enabled', sa.Boolean(), server_default=sa.text('false'), nullable=False))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('ssh_host', sa.String(length=500), nullable=True))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('ssh_port', sa.Integer(), server_default='22', nullable=False))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('ssh_username', sa.String(length=200), nullable=True))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_password', sa.Text(), nullable=True))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_private_key', sa.Text(), nullable=True))
|
||||||
|
op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_key_passphrase', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('ticket_db_connectors', 'encrypted_ssh_key_passphrase')
|
||||||
|
op.drop_column('ticket_db_connectors', 'encrypted_ssh_private_key')
|
||||||
|
op.drop_column('ticket_db_connectors', 'encrypted_ssh_password')
|
||||||
|
op.drop_column('ticket_db_connectors', 'ssh_username')
|
||||||
|
op.drop_column('ticket_db_connectors', 'ssh_port')
|
||||||
|
op.drop_column('ticket_db_connectors', 'ssh_host')
|
||||||
|
op.drop_column('ticket_db_connectors', 'ssh_enabled')
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add ticketing Phase 4: guardrailed DB connectors, proposals, webhook secret
|
||||||
|
|
||||||
|
Revision ID: add_tkt_phase4_001
|
||||||
|
Revises: add_invest_glassbox_001
|
||||||
|
Create Date: 2026-07-17
|
||||||
|
|
||||||
|
- ticket_db_connectors: read-only, allowlisted, masked DB access for the
|
||||||
|
investigation agent (creds encrypted at rest)
|
||||||
|
- db_connector_audit_logs: every attempted query (raw + canonical SQL,
|
||||||
|
outcome) — result rows are never stored
|
||||||
|
- ticket_proposals: autonomy L2 propose/approve records
|
||||||
|
- organization_ticket_settings.alert_webhook_secret for the alert intake
|
||||||
|
- seeds approve_ticket_actions (roles with manage_tickets) and
|
||||||
|
manage_ticket_connectors (roles with manage_organization)
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'add_tkt_phase4_001'
|
||||||
|
down_revision = 'add_invest_glassbox_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
NEW_PERMISSIONS = {
|
||||||
|
'approve_ticket_actions': ('Can approve AI-proposed ticket resolutions', 'manage_tickets'),
|
||||||
|
'manage_ticket_connectors': ('Can manage ticket investigation connectors', 'manage_organization'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'ticket_db_connectors',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('engine', sa.String(), nullable=False),
|
||||||
|
sa.Column('host', sa.String(length=500), nullable=False),
|
||||||
|
sa.Column('port', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('database', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('username', sa.String(length=200), nullable=False),
|
||||||
|
sa.Column('encrypted_password', sa.Text(), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||||
|
sa.Column('allowed_tables', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('masked_columns', postgresql.JSON(astext_type=sa.Text()), nullable=True),
|
||||||
|
sa.Column('max_rows', sa.Integer(), server_default='100', nullable=False),
|
||||||
|
sa.Column('statement_timeout_ms', sa.Integer(), server_default='5000', nullable=False),
|
||||||
|
sa.Column('last_test_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('last_test_ok', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ticket_db_connectors_organization_id'), 'ticket_db_connectors', ['organization_id'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'db_connector_audit_logs',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('connector_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('raw_sql', sa.Text(), nullable=False),
|
||||||
|
sa.Column('validated_sql', sa.Text(), nullable=True),
|
||||||
|
sa.Column('outcome', sa.String(), nullable=False),
|
||||||
|
sa.Column('block_reason', sa.Text(), nullable=True),
|
||||||
|
sa.Column('row_count', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('duration_ms', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['connector_id'], ['ticket_db_connectors.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_db_connector_audit_logs_connector_id'), 'db_connector_audit_logs', ['connector_id'])
|
||||||
|
op.create_index(op.f('ix_db_connector_audit_logs_organization_id'), 'db_connector_audit_logs', ['organization_id'])
|
||||||
|
op.create_index('ix_db_connector_audit_logs_created', 'db_connector_audit_logs', ['organization_id', 'created_at'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'ticket_proposals',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('summary', sa.Text(), nullable=False),
|
||||||
|
sa.Column('customer_message', sa.Text(), nullable=True),
|
||||||
|
sa.Column('confidence', sa.Float(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('decided_by_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('decided_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('reject_reason', sa.Text(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['decided_by_user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ticket_proposals_ticket_id'), 'ticket_proposals', ['ticket_id'])
|
||||||
|
op.create_index(op.f('ix_ticket_proposals_organization_id'), 'ticket_proposals', ['organization_id'])
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'organization_ticket_settings',
|
||||||
|
sa.Column('alert_webhook_secret', sa.String(length=64), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Seed the new permissions onto roles holding the anchor permission
|
||||||
|
# (new orgs seed via Permission.default_permissions()).
|
||||||
|
for name, (description, anchor) in NEW_PERMISSIONS.items():
|
||||||
|
op.execute(f"""
|
||||||
|
INSERT INTO permissions (name, description)
|
||||||
|
SELECT '{name}', '{description}'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}')
|
||||||
|
""")
|
||||||
|
op.execute(f"""
|
||||||
|
INSERT INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT rp.role_id, p_new.id
|
||||||
|
FROM role_permissions rp
|
||||||
|
JOIN permissions p_old ON p_old.id = rp.permission_id AND p_old.name = '{anchor}'
|
||||||
|
CROSS JOIN permissions p_new
|
||||||
|
WHERE p_new.name = '{name}'
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permissions rp2
|
||||||
|
WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p_new.id
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
names = "', '".join(NEW_PERMISSIONS)
|
||||||
|
op.execute(f"""
|
||||||
|
DELETE FROM role_permissions
|
||||||
|
WHERE permission_id IN (SELECT id FROM permissions WHERE name IN ('{names}'))
|
||||||
|
""")
|
||||||
|
op.execute(f"DELETE FROM permissions WHERE name IN ('{names}')")
|
||||||
|
op.drop_column('organization_ticket_settings', 'alert_webhook_secret')
|
||||||
|
op.drop_index(op.f('ix_ticket_proposals_organization_id'), table_name='ticket_proposals')
|
||||||
|
op.drop_index(op.f('ix_ticket_proposals_ticket_id'), table_name='ticket_proposals')
|
||||||
|
op.drop_table('ticket_proposals')
|
||||||
|
op.drop_index('ix_db_connector_audit_logs_created', table_name='db_connector_audit_logs')
|
||||||
|
op.drop_index(op.f('ix_db_connector_audit_logs_organization_id'), table_name='db_connector_audit_logs')
|
||||||
|
op.drop_index(op.f('ix_db_connector_audit_logs_connector_id'), table_name='db_connector_audit_logs')
|
||||||
|
op.drop_table('db_connector_audit_logs')
|
||||||
|
op.drop_index(op.f('ix_ticket_db_connectors_organization_id'), table_name='ticket_db_connectors')
|
||||||
|
op.drop_table('ticket_db_connectors')
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add widget_apps table
|
||||||
|
|
||||||
|
Revision ID: add_widget_apps_001
|
||||||
|
Revises: 2381319791e8
|
||||||
|
Create Date: 2026-01-13 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'add_widget_apps_001'
|
||||||
|
down_revision: Union[str, Sequence[str]] = '2381319791e8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Create widget_apps table
|
||||||
|
op.create_table(
|
||||||
|
'widget_apps',
|
||||||
|
sa.Column('id', UUID(as_uuid=True), primary_key=True, nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('organization_id', UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('api_key_hash', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_by', UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id']),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['users.id']),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create indexes
|
||||||
|
op.create_index('ix_widget_apps_id', 'widget_apps', ['id'])
|
||||||
|
op.create_index('ix_widget_apps_organization_id', 'widget_apps', ['organization_id'])
|
||||||
|
op.create_index('ix_widget_apps_is_active', 'widget_apps', ['is_active'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop indexes
|
||||||
|
op.drop_index('ix_widget_apps_is_active', table_name='widget_apps')
|
||||||
|
op.drop_index('ix_widget_apps_organization_id', table_name='widget_apps')
|
||||||
|
op.drop_index('ix_widget_apps_id', table_name='widget_apps')
|
||||||
|
|
||||||
|
# Drop table
|
||||||
|
op.drop_table('widget_apps')
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Let an agent offer reply buttons on messaging channels
|
||||||
|
|
||||||
|
Off by default: an agent shows the AI's choices as tappable buttons on
|
||||||
|
WhatsApp, Messenger, Instagram and Telegram only once someone turns it on.
|
||||||
|
|
||||||
|
Revision ID: agent_quick_replies_001
|
||||||
|
Revises: chat_channel_status_001
|
||||||
|
Create Date: 2026-10-11
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
# Keep revision ids under 32 characters: alembic_version.version_num is
|
||||||
|
# varchar(32).
|
||||||
|
revision: str = 'agent_quick_replies_001'
|
||||||
|
down_revision: Union[str, None] = 'chat_channel_status_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('agents', sa.Column('channel_quick_replies', sa.Boolean(), nullable=False,
|
||||||
|
server_default=sa.false()), if_not_exists=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'channel_quick_replies', if_exists=True)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""auth_sessions table, so sign-out and password changes revoke JWTs
|
||||||
|
|
||||||
|
Revision ID: auth_sessions_001
|
||||||
|
Revises: agent_quick_replies_001
|
||||||
|
Create Date: 2026-10-12
|
||||||
|
|
||||||
|
Tokens issued before this revision carry no session id and stay valid until
|
||||||
|
they expire, so the deploy signs nobody out.
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'auth_sessions_001'
|
||||||
|
down_revision: Union[str, None] = 'agent_quick_replies_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'auth_sessions',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_auth_sessions_user_id', 'auth_sessions', ['user_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_auth_sessions_user_id', table_name='auth_sessions')
|
||||||
|
op.drop_table('auth_sessions')
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add meta_data column to customers
|
||||||
|
|
||||||
|
Adds:
|
||||||
|
- `meta_data` (JSON): arbitrary integrator-supplied fields (e.g. student_name,
|
||||||
|
center_name) passed via POST /generate-token's `custom_data`, surfaced to agents
|
||||||
|
in the chat inbox.
|
||||||
|
|
||||||
|
Revision ID: b2c3d4e5f6a7
|
||||||
|
Revises: f4a5b6c7d8e9
|
||||||
|
Create Date: 2026-07-06 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'b2c3d4e5f6a7'
|
||||||
|
down_revision: Union[str, None] = 'f4a5b6c7d8e9'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'customers',
|
||||||
|
sa.Column('meta_data', sa.JSON(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('customers', 'meta_data')
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'b365b8447598'
|
||||||
|
down_revision: Union[str, None] = None
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('organizations',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('domain', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('timezone', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('business_hours', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('domain')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_organizations_id'), 'organizations', ['id'], unique=False)
|
||||||
|
op.create_table('permissions',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_permissions_id'), 'permissions', ['id'], unique=False)
|
||||||
|
op.create_table('agents',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('display_name', sa.String(length=100), nullable=True),
|
||||||
|
sa.Column('description', sa.Text(), nullable=True),
|
||||||
|
sa.Column('agent_type', sa.Enum('CUSTOMER_SUPPORT', 'SALES', 'TECH_SUPPORT', 'GENERAL', 'CUSTOM', name='agenttype'), nullable=False),
|
||||||
|
sa.Column('instructions', sa.Text(), nullable=False),
|
||||||
|
sa.Column('tools', sa.Text(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('is_default', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('transfer_to_human', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('ai_configs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('model_type', sa.Enum('OPENAI', 'ANTHROPIC', 'DEEPSEEK', 'GOOGLE', 'GOOGLEVERTEX', 'GROQ', 'MISTRAL', 'HUGGINGFACE', 'OLLAMA', 'XAI', name='aimodeltype'), nullable=False),
|
||||||
|
sa.Column('model_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('encrypted_api_key', sa.String(), nullable=False),
|
||||||
|
sa.Column('settings', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_ai_configs_id'), 'ai_configs', ['id'], unique=False)
|
||||||
|
op.create_table('customers',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(), nullable=False),
|
||||||
|
sa.Column('full_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('email', 'organization_id', name='uix_customer_email_org')
|
||||||
|
)
|
||||||
|
op.create_table('groups',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('knowledge',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('source', sa.String(), nullable=False),
|
||||||
|
sa.Column('source_type', sa.Enum('DATABASE', 'FILE', 'API', 'WEBSITE', 'CUSTOM', name='sourcetype'), nullable=False),
|
||||||
|
sa.Column('schema', sa.String(), nullable=True),
|
||||||
|
sa.Column('table_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_knowledge_id'), 'knowledge', ['id'], unique=False)
|
||||||
|
op.create_table('roles',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('is_default', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
|
||||||
|
op.create_table('agent_customizations',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('photo_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('chat_background_color', sa.String(), nullable=True),
|
||||||
|
sa.Column('chat_bubble_color', sa.String(), nullable=True),
|
||||||
|
sa.Column('chat_text_color', sa.String(), nullable=True),
|
||||||
|
sa.Column('icon_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('icon_color', sa.String(), nullable=True),
|
||||||
|
sa.Column('accent_color', sa.String(), nullable=True),
|
||||||
|
sa.Column('font_family', sa.String(), nullable=True),
|
||||||
|
sa.Column('custom_css', sa.Text(), nullable=True),
|
||||||
|
sa.Column('customization_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_agent_customizations_id'), 'agent_customizations', ['id'], unique=False)
|
||||||
|
op.create_table('agent_usergroup',
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('group_id', sa.UUID(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE')
|
||||||
|
)
|
||||||
|
op.create_table('knowledge_to_agents',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('knowledge_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_knowledge_to_agents_id'), 'knowledge_to_agents', ['id'], unique=False)
|
||||||
|
op.create_table('role_permissions',
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('permission_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], )
|
||||||
|
)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(), nullable=True),
|
||||||
|
sa.Column('full_name', sa.String(), nullable=True),
|
||||||
|
sa.Column('hashed_password', sa.String(), nullable=True),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('profile_pic', sa.String(), nullable=True),
|
||||||
|
sa.Column('is_online', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('last_seen', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('role_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('fcm_token_web', sa.String(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||||
|
op.create_table('widgets',
|
||||||
|
sa.Column('id', sa.String(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('knowledge_queue',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('source_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('source', sa.String(), nullable=False),
|
||||||
|
sa.Column('status', sa.String(), nullable=True),
|
||||||
|
sa.Column('error', sa.String(), nullable=True),
|
||||||
|
sa.Column('queue_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_knowledge_queue_id'), 'knowledge_queue', ['id'], unique=False)
|
||||||
|
op.create_table('notifications',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('type', sa.Enum('KNOWLEDGE_PROCESSED', 'KNOWLEDGE_FAILED', 'SYSTEM', 'CHAT', name='notificationtype'), nullable=False),
|
||||||
|
sa.Column('title', sa.String(), nullable=False),
|
||||||
|
sa.Column('message', sa.String(), nullable=False),
|
||||||
|
sa.Column('notification_metadata', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('is_read', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False)
|
||||||
|
op.create_table('session_to_agents',
|
||||||
|
sa.Column('session_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('customer_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('status', sa.Enum('OPEN', 'CLOSED', 'TRANSFERRED', name='sessionstatus'), nullable=False),
|
||||||
|
sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('transfer_reason', sa.String(), nullable=True),
|
||||||
|
sa.Column('transfer_description', sa.String(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('session_id')
|
||||||
|
)
|
||||||
|
op.create_table('user_groups',
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
||||||
|
sa.Column('group_id', sa.UUID(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('user_id', 'group_id')
|
||||||
|
)
|
||||||
|
op.create_table('chat_history',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('customer_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('agent_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('session_id', sa.UUID(), nullable=True),
|
||||||
|
sa.Column('message', sa.String(), nullable=False),
|
||||||
|
sa.Column('message_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('attributes', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_chat_history_id'), 'chat_history', ['id'], unique=False)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_index(op.f('ix_chat_history_id'), table_name='chat_history')
|
||||||
|
op.drop_table('chat_history')
|
||||||
|
op.drop_table('user_groups')
|
||||||
|
op.drop_table('session_to_agents')
|
||||||
|
op.drop_index(op.f('ix_notifications_id'), table_name='notifications')
|
||||||
|
op.drop_table('notifications')
|
||||||
|
op.drop_index(op.f('ix_knowledge_queue_id'), table_name='knowledge_queue')
|
||||||
|
op.drop_table('knowledge_queue')
|
||||||
|
op.drop_table('widgets')
|
||||||
|
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_table('role_permissions')
|
||||||
|
op.drop_index(op.f('ix_knowledge_to_agents_id'), table_name='knowledge_to_agents')
|
||||||
|
op.drop_table('knowledge_to_agents')
|
||||||
|
op.drop_table('agent_usergroup')
|
||||||
|
op.drop_index(op.f('ix_agent_customizations_id'), table_name='agent_customizations')
|
||||||
|
op.drop_table('agent_customizations')
|
||||||
|
op.drop_index(op.f('ix_roles_id'), table_name='roles')
|
||||||
|
op.drop_table('roles')
|
||||||
|
op.drop_index(op.f('ix_knowledge_id'), table_name='knowledge')
|
||||||
|
op.drop_table('knowledge')
|
||||||
|
op.drop_table('groups')
|
||||||
|
op.drop_table('customers')
|
||||||
|
op.drop_index(op.f('ix_ai_configs_id'), table_name='ai_configs')
|
||||||
|
op.drop_table('ai_configs')
|
||||||
|
op.drop_table('agents')
|
||||||
|
op.drop_index(op.f('ix_permissions_id'), table_name='permissions')
|
||||||
|
op.drop_table('permissions')
|
||||||
|
op.drop_index(op.f('ix_organizations_id'), table_name='organizations')
|
||||||
|
op.drop_table('organizations')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add view_unassigned_chats + view_people permissions
|
||||||
|
|
||||||
|
Revision ID: b7c4e91a2d38
|
||||||
|
Revises: add_customer_phone_001
|
||||||
|
Create Date: 2026-07-18
|
||||||
|
|
||||||
|
Agents could previously only see chats already assigned to them or their group,
|
||||||
|
so the AI queue was invisible and "Take over" was unreachable. Two new
|
||||||
|
permissions rather than widening view_assigned_chats, so that granting the
|
||||||
|
queue never also exposes another agent's conversations:
|
||||||
|
|
||||||
|
- view_unassigned_chats: the unclaimed queue (session_to_agents.user_id IS NULL)
|
||||||
|
- view_people: read-only people directory, previously gated on view_all_chats
|
||||||
|
|
||||||
|
Backfill targets roles by CAPABILITY, not by name, so every existing
|
||||||
|
organization is covered including ones whose agent role was renamed or
|
||||||
|
hand-built:
|
||||||
|
|
||||||
|
- any role that can view chats at all (assigned, all, or super_admin) gains
|
||||||
|
view_unassigned_chats
|
||||||
|
- the same roles plus manage_all_chats gain view_people
|
||||||
|
|
||||||
|
This deliberately widens existing roles: without it the feature would appear
|
||||||
|
broken for every deployment created before this release, and an admin who
|
||||||
|
wants an agent kept out can uncheck the permission in Roles. Re-runnable —
|
||||||
|
every insert is guarded by NOT EXISTS.
|
||||||
|
|
||||||
|
Also repairs the seeding bug where BOTH "Admin" and "Agent" were created with
|
||||||
|
is_default=true, which made RoleRepository.get_default_role() return whichever
|
||||||
|
row the database happened to yield first — an invited user could land on Admin.
|
||||||
|
Nothing in the API enforces a single default, so the repair is data-only.
|
||||||
|
|
||||||
|
The repair CLEARS Admin, which is what leaves Agent as the default. Setting
|
||||||
|
is_default=false on Agent instead would do the opposite of what is wanted: it
|
||||||
|
would leave Admin as the sole default and hand every newly invited user full
|
||||||
|
permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'b7c4e91a2d38'
|
||||||
|
down_revision = 'add_customer_phone_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
NEW_PERMISSIONS = (
|
||||||
|
('view_unassigned_chats', 'Can view unassigned AI chats'),
|
||||||
|
('view_people', 'Can view the people directory'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Which existing roles receive each new permission, expressed as the permissions
|
||||||
|
# a role must already hold. Capability, not role name: an org that renamed its
|
||||||
|
# agent role, or built its own, is still covered.
|
||||||
|
BACKFILL = (
|
||||||
|
('view_unassigned_chats', ('view_assigned_chats', 'view_all_chats', 'super_admin')),
|
||||||
|
('view_people', ('view_assigned_chats', 'view_all_chats', 'manage_all_chats', 'super_admin')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_list(values) -> str:
|
||||||
|
return ", ".join(f"'{value}'" for value in values)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
for name, description in NEW_PERMISSIONS:
|
||||||
|
op.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO permissions (name, description)
|
||||||
|
SELECT '{name}', '{description}'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}')
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
for new_permission, held_by in BACKFILL:
|
||||||
|
op.execute(
|
||||||
|
f"""
|
||||||
|
INSERT INTO role_permissions (role_id, permission_id)
|
||||||
|
SELECT DISTINCT r.id, target.id
|
||||||
|
FROM roles r
|
||||||
|
JOIN role_permissions rp ON rp.role_id = r.id
|
||||||
|
JOIN permissions held ON held.id = rp.permission_id
|
||||||
|
CROSS JOIN permissions target
|
||||||
|
WHERE target.name = '{new_permission}'
|
||||||
|
AND held.name IN ({_sql_list(held_by)})
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM role_permissions existing
|
||||||
|
WHERE existing.role_id = r.id
|
||||||
|
AND existing.permission_id = target.id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# One default role per organization. Clearing Admin is what PROMOTES Agent
|
||||||
|
# to sole default — the opposite spelling (clearing Agent) would leave
|
||||||
|
# Admin default and make every invited user an admin.
|
||||||
|
#
|
||||||
|
# The COUNT(*) > 1 guard is load-bearing: it means an org whose only
|
||||||
|
# default is Admin keeps it, rather than being left with no default role
|
||||||
|
# at all. An org that renamed its Admin role keeps both defaults — the
|
||||||
|
# pre-existing ambiguity, not a regression.
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
UPDATE roles SET is_default = false
|
||||||
|
WHERE name = 'Admin'
|
||||||
|
AND is_default = true
|
||||||
|
AND organization_id IN (
|
||||||
|
SELECT organization_id FROM roles
|
||||||
|
WHERE is_default = true
|
||||||
|
GROUP BY organization_id
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Drop the grants first (FK), then the permissions themselves. is_default is
|
||||||
|
# not restored: the pre-migration state was ambiguous by definition.
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM role_permissions
|
||||||
|
WHERE permission_id IN (
|
||||||
|
SELECT id FROM permissions
|
||||||
|
WHERE name IN ('view_unassigned_chats', 'view_people')
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM permissions
|
||||||
|
WHERE name IN ('view_unassigned_chats', 'view_people')
|
||||||
|
"""
|
||||||
|
)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""removed node specific fields
|
||||||
|
|
||||||
|
Revision ID: bdaa7a5d6109
|
||||||
|
Revises: dd0e5ceefe4b
|
||||||
|
Create Date: 2025-07-23 12:06:47.901308
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'bdaa7a5d6109'
|
||||||
|
down_revision: Union[str, None] = 'dd0e5ceefe4b'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_constraint('workflow_nodes_model_id_fkey', 'workflow_nodes', type_='foreignkey')
|
||||||
|
op.drop_column('workflow_nodes', 'action_type')
|
||||||
|
op.drop_column('workflow_nodes', 'form_fields')
|
||||||
|
op.drop_column('workflow_nodes', 'model_id')
|
||||||
|
op.drop_column('workflow_nodes', 'system_prompt')
|
||||||
|
op.drop_column('workflow_nodes', 'condition_expression')
|
||||||
|
op.drop_column('workflow_nodes', 'wait_until_condition')
|
||||||
|
op.drop_column('workflow_nodes', 'action_config')
|
||||||
|
op.drop_column('workflow_nodes', 'temperature')
|
||||||
|
op.drop_column('workflow_nodes', 'wait_duration')
|
||||||
|
op.drop_column('workflow_nodes', 'transfer_rules')
|
||||||
|
op.drop_column('workflow_nodes', 'landing_page_content')
|
||||||
|
op.drop_column('workflow_nodes', 'landing_page_heading')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('workflow_nodes', sa.Column('landing_page_heading', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('landing_page_content', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('transfer_rules', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('wait_duration', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('temperature', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('action_config', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('wait_until_condition', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('condition_expression', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('system_prompt', sa.TEXT(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('form_fields', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('action_type', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||||
|
op.create_foreign_key('workflow_nodes_model_id_fkey', 'workflow_nodes', 'ai_configs', ['model_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add premium chat styles and show_citations to agent_customizations
|
||||||
|
|
||||||
|
Adds the new GLASS/TERMINAL/PLAYFUL/CALM_MINT values to the `chatstyle` enum and a
|
||||||
|
`show_citations` boolean column.
|
||||||
|
|
||||||
|
Revision ID: c1d2e3f4a5b6
|
||||||
|
Revises: add_sentiment_analysis_001
|
||||||
|
Create Date: 2026-06-26 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c1d2e3f4a5b6'
|
||||||
|
down_revision: Union[str, None] = 'add_sentiment_analysis_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
NEW_CHAT_STYLES = ("GLASS", "TERMINAL", "PLAYFUL", "CALM_MINT")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
# Add new values to the native Postgres `chatstyle` enum. ALTER TYPE ... ADD VALUE
|
||||||
|
# cannot run inside a transaction block, so use an autocommit block.
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
for value in NEW_CHAT_STYLES:
|
||||||
|
op.execute(f"ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS '{value}'")
|
||||||
|
|
||||||
|
# Add show_citations column (default OFF for now — citations need more work).
|
||||||
|
# Use IF NOT EXISTS on Postgres so the migration is safe to run on a DB where the
|
||||||
|
# column was already added out-of-band.
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE agent_customizations "
|
||||||
|
"ADD COLUMN IF NOT EXISTS show_citations boolean NOT NULL DEFAULT false"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
op.add_column(
|
||||||
|
'agent_customizations',
|
||||||
|
sa.Column('show_citations', sa.Boolean(), server_default=sa.false(), nullable=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agent_customizations', 'show_citations')
|
||||||
|
# Postgres cannot cleanly drop individual enum values; the added chatstyle
|
||||||
|
# values are intentionally left in place.
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add lead capture (configs, responses, customer lead stage)
|
||||||
|
|
||||||
|
Revision ID: c3d4e5f6a7b8
|
||||||
|
Revises: b2c3d4e5f6a7
|
||||||
|
Create Date: 2026-07-07 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'c3d4e5f6a7b8'
|
||||||
|
down_revision: Union[str, None] = 'b2c3d4e5f6a7'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Enum member NAMES are what SQLAlchemy stores (matches agenttype/sessionstatus convention).
|
||||||
|
# create_type=False + an explicit .create(checkfirst=True) so create_table/add_column
|
||||||
|
# reference the type without emitting a second (empty) CREATE TYPE.
|
||||||
|
assign_col = postgresql.ENUM('NONE', 'SALES_TEAM', 'SPECIFIC_PERSON', 'ROUND_ROBIN', name='leadassignmentmode', create_type=False)
|
||||||
|
crm_col = postgresql.ENUM('NONE', 'HUBSPOT', 'SALESFORCE', name='crmsynctarget', create_type=False)
|
||||||
|
stage_col = postgresql.ENUM('VISITOR', 'LEAD', 'CUSTOMER', name='leadstage', create_type=False)
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
# Create enum types up front (idempotent) so create_table / add_column can reference
|
||||||
|
# them without re-emitting CREATE TYPE.
|
||||||
|
assign_col.create(bind, checkfirst=True)
|
||||||
|
crm_col.create(bind, checkfirst=True)
|
||||||
|
stage_col.create(bind, checkfirst=True)
|
||||||
|
|
||||||
|
# ---- lead_capture_configs (1:1 with agents) ----
|
||||||
|
op.create_table(
|
||||||
|
'lead_capture_configs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column('require_consent', sa.Boolean(), nullable=False, server_default=sa.true()),
|
||||||
|
sa.Column('guidance', sa.Text(), nullable=True),
|
||||||
|
sa.Column('fields', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('assignment_mode', assign_col, nullable=False, server_default='NONE'),
|
||||||
|
sa.Column('assignment_target_user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('crm_sync_target', crm_col, nullable=False, server_default='NONE'),
|
||||||
|
sa.Column('slack_notify_enabled', sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['assignment_target_user_id'], ['users.id']),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('agent_id', name='uix_lead_capture_config_agent'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_lead_capture_configs_id'), 'lead_capture_configs', ['id'], unique=False)
|
||||||
|
|
||||||
|
# ---- lead_capture_responses (one row per capture attempt) ----
|
||||||
|
op.create_table(
|
||||||
|
'lead_capture_responses',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('field_values', sa.JSON(), nullable=True),
|
||||||
|
sa.Column('summary', sa.Text(), nullable=True),
|
||||||
|
sa.Column('consent', sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column('qualified', sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id']),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id']),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_lead_capture_responses_customer_id'), 'lead_capture_responses', ['customer_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_lead_capture_responses_agent_id'), 'lead_capture_responses', ['agent_id'], unique=False)
|
||||||
|
|
||||||
|
# ---- customers: lead lifecycle columns ----
|
||||||
|
# Add lead_stage nullable first, backfill, then enforce NOT NULL (template pattern).
|
||||||
|
op.add_column('customers', sa.Column('lead_stage', stage_col, nullable=True))
|
||||||
|
op.add_column('customers', sa.Column('lead_source', sa.JSON(), nullable=True))
|
||||||
|
op.add_column('customers', sa.Column('lead_qualified_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.execute("UPDATE customers SET lead_stage = 'VISITOR' WHERE lead_stage IS NULL")
|
||||||
|
op.alter_column('customers', 'lead_stage', nullable=False, server_default='VISITOR')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('customers', 'lead_qualified_at')
|
||||||
|
op.drop_column('customers', 'lead_source')
|
||||||
|
op.drop_column('customers', 'lead_stage')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_lead_capture_responses_agent_id'), table_name='lead_capture_responses')
|
||||||
|
op.drop_index(op.f('ix_lead_capture_responses_customer_id'), table_name='lead_capture_responses')
|
||||||
|
op.drop_table('lead_capture_responses')
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_lead_capture_configs_id'), table_name='lead_capture_configs')
|
||||||
|
op.drop_table('lead_capture_configs')
|
||||||
|
|
||||||
|
bind = op.get_bind()
|
||||||
|
stage_col.drop(bind, checkfirst=True)
|
||||||
|
crm_col.drop(bind, checkfirst=True)
|
||||||
|
assign_col.drop(bind, checkfirst=True)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Track channel delivery receipts on chat_history
|
||||||
|
|
||||||
|
WhatsApp reports sent / delivered / read / failed for every message we send,
|
||||||
|
keyed by its platform message id (wamid). Storing that id on the row is what
|
||||||
|
lets a receipt find its message; channel_status holds the latest receipt.
|
||||||
|
|
||||||
|
channel_status is deliberately separate from attributes.delivery_status: the
|
||||||
|
inbox reads any delivery_status as "not delivered", so a 'read' written there
|
||||||
|
would show as a failure.
|
||||||
|
|
||||||
|
Revision ID: chat_channel_status_001
|
||||||
|
Revises: idx_kq_org_status_001
|
||||||
|
Create Date: 2026-10-11
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
# Keep revision ids under 32 characters: alembic_version.version_num is
|
||||||
|
# varchar(32).
|
||||||
|
revision: str = 'chat_channel_status_001'
|
||||||
|
down_revision: Union[str, None] = 'idx_kq_org_status_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
INDEX_NAME = 'ix_chat_history_org_external_message'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# if_not_exists throughout: the columns commit before the index builds
|
||||||
|
# (CONCURRENTLY can't share their transaction), so a start that dies
|
||||||
|
# mid-build must be able to run this again.
|
||||||
|
op.add_column('chat_history', sa.Column('external_message_id', sa.String(), nullable=True),
|
||||||
|
if_not_exists=True)
|
||||||
|
op.add_column('chat_history', sa.Column('channel_status', sa.String(16), nullable=True),
|
||||||
|
if_not_exists=True)
|
||||||
|
op.add_column('chat_history', sa.Column('channel_status_at', sa.TIMESTAMP(timezone=True),
|
||||||
|
nullable=True),
|
||||||
|
if_not_exists=True)
|
||||||
|
# Partial: only channel messages carry an id, so widget rows stay out of it.
|
||||||
|
# Built CONCURRENTLY so a large chat_history keeps taking writes while it
|
||||||
|
# builds — migrations run on backend start. That can't run in a
|
||||||
|
# transaction, hence the autocommit block. A build that fails part-way
|
||||||
|
# leaves an INVALID index that IF NOT EXISTS would then skip: if that
|
||||||
|
# happens, DROP INDEX ix_chat_history_org_external_message and restart.
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.create_index(
|
||||||
|
INDEX_NAME, 'chat_history', ['organization_id', 'external_message_id'],
|
||||||
|
unique=False,
|
||||||
|
postgresql_where=sa.text('external_message_id IS NOT NULL'),
|
||||||
|
postgresql_concurrently=True,
|
||||||
|
if_not_exists=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.drop_index(INDEX_NAME, table_name='chat_history',
|
||||||
|
postgresql_concurrently=True, if_exists=True)
|
||||||
|
op.drop_column('chat_history', 'channel_status_at', if_exists=True)
|
||||||
|
op.drop_column('chat_history', 'channel_status', if_exists=True)
|
||||||
|
op.drop_column('chat_history', 'external_message_id', if_exists=True)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""convert crm_sync_target to varchar so new CRMs need no migration
|
||||||
|
|
||||||
|
Revision ID: crm_sync_varchar_001
|
||||||
|
Revises: merge_tkt_csat_001
|
||||||
|
Create Date: 2026-07-22 12:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'crm_sync_varchar_001'
|
||||||
|
down_revision: Union[str, None] = 'merge_tkt_csat_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
# The original crmsynctarget enum stored member NAMES; the varchar column stores
|
||||||
|
# lowercase enum VALUES ('none', 'hubspot', ...) to match ChannelType convention.
|
||||||
|
_ORIGINAL_ENUM_LABELS = ('NONE', 'HUBSPOT', 'SALESFORCE')
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target DROP DEFAULT")
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE lead_capture_configs "
|
||||||
|
"ALTER COLUMN crm_sync_target TYPE varchar(32) "
|
||||||
|
"USING lower(crm_sync_target::text)"
|
||||||
|
)
|
||||||
|
op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target SET DEFAULT 'none'")
|
||||||
|
op.execute("DROP TYPE IF EXISTS crmsynctarget")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
labels = ", ".join(f"'{label}'" for label in _ORIGINAL_ENUM_LABELS)
|
||||||
|
op.execute(f"CREATE TYPE crmsynctarget AS ENUM ({labels})")
|
||||||
|
op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target DROP DEFAULT")
|
||||||
|
# Values unknown to the original enum (e.g. 'pipedrive') fold to 'NONE'.
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE lead_capture_configs "
|
||||||
|
"ALTER COLUMN crm_sync_target TYPE crmsynctarget "
|
||||||
|
f"USING (CASE WHEN upper(crm_sync_target) IN ({labels}) "
|
||||||
|
"THEN upper(crm_sync_target) ELSE 'NONE' END)::crmsynctarget"
|
||||||
|
)
|
||||||
|
op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target SET DEFAULT 'NONE'")
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add contact-collection columns (collect_email + handoff_collect_email/name)
|
||||||
|
|
||||||
|
Adds:
|
||||||
|
- agent_customizations.collect_email (default false) -> optional pre-chat email gate
|
||||||
|
- agents.handoff_collect_email (default true) -> ask email on human handoff
|
||||||
|
- agents.handoff_collect_name (default true) -> ask name on human handoff
|
||||||
|
|
||||||
|
Revision ID: d2e3f4a5b6c7
|
||||||
|
Revises: c1d2e3f4a5b6
|
||||||
|
Create Date: 2026-06-29 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'd2e3f4a5b6c7'
|
||||||
|
down_revision: Union[str, None] = 'c1d2e3f4a5b6'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
# IF NOT EXISTS so the migration is safe on a DB where columns were added out-of-band
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE agent_customizations "
|
||||||
|
"ADD COLUMN IF NOT EXISTS collect_email boolean NOT NULL DEFAULT false"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE agents "
|
||||||
|
"ADD COLUMN IF NOT EXISTS handoff_collect_email boolean NOT NULL DEFAULT true"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE agents "
|
||||||
|
"ADD COLUMN IF NOT EXISTS handoff_collect_name boolean NOT NULL DEFAULT true"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
op.add_column('agent_customizations', sa.Column('collect_email', sa.Boolean(), server_default=sa.false(), nullable=False))
|
||||||
|
op.add_column('agents', sa.Column('handoff_collect_email', sa.Boolean(), server_default=sa.true(), nullable=False))
|
||||||
|
op.add_column('agents', sa.Column('handoff_collect_name', sa.Boolean(), server_default=sa.true(), nullable=False))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agents', 'handoff_collect_name')
|
||||||
|
op.drop_column('agents', 'handoff_collect_email')
|
||||||
|
op.drop_column('agent_customizations', 'collect_email')
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"""add customers.merged_into_customer_id for visitor→customer merge
|
||||||
|
|
||||||
|
Revision ID: d4e5f6a7b8c9
|
||||||
|
Revises: c3d4e5f6a7b8
|
||||||
|
Create Date: 2026-07-08
|
||||||
|
|
||||||
|
When an anonymous widget visitor shares an email that already belongs to an
|
||||||
|
existing customer in the org, the visitor row is merged into that customer.
|
||||||
|
The old row keeps a pointer so stale device tokens resolve to the target.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'd4e5f6a7b8c9'
|
||||||
|
down_revision = 'c3d4e5f6a7b8'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'customers',
|
||||||
|
sa.Column('merged_into_customer_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
'fk_customers_merged_into', 'customers', 'customers',
|
||||||
|
['merged_into_customer_id'], ['id'],
|
||||||
|
)
|
||||||
|
op.create_index('ix_customers_merged_into', 'customers', ['merged_into_customer_id'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_customers_merged_into', table_name='customers')
|
||||||
|
op.drop_constraint('fk_customers_merged_into', 'customers', type_='foreignkey')
|
||||||
|
op.drop_column('customers', 'merged_into_customer_id')
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_attempts_to_otp_table
|
||||||
|
|
||||||
|
Revision ID: d9a4b9902f80
|
||||||
|
Revises: 013174e15242
|
||||||
|
Create Date: 2025-10-07 19:24:50.790908
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'd9a4b9902f80'
|
||||||
|
down_revision: Union[str, None] = '013174e15242'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Check if enterprise schema exists before adding column
|
||||||
|
# In open-source version, enterprise schema doesn't exist
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# Check if enterprise schema exists
|
||||||
|
schema_exists = connection.execute(
|
||||||
|
sa.text("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'enterprise')")
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
if schema_exists:
|
||||||
|
# Add attempts column to otps table in enterprise schema
|
||||||
|
op.add_column('otps', sa.Column('attempts', sa.Integer(), nullable=False, server_default='0'), schema='enterprise')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Check if enterprise schema exists before removing column
|
||||||
|
# In open-source version, enterprise schema doesn't exist
|
||||||
|
connection = op.get_bind()
|
||||||
|
|
||||||
|
# Check if enterprise schema exists
|
||||||
|
schema_exists = connection.execute(
|
||||||
|
sa.text("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'enterprise')")
|
||||||
|
).scalar()
|
||||||
|
|
||||||
|
if schema_exists:
|
||||||
|
# Remove attempts column from otps table in enterprise schema
|
||||||
|
op.drop_column('otps', 'attempts', schema='enterprise')
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_progress_tracking_to_knowledge_queue
|
||||||
|
|
||||||
|
Revision ID: dc827ab30bd4
|
||||||
|
Revises: 621095beb234
|
||||||
|
Create Date: 2025-08-05 16:28:47.201062
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'dc827ab30bd4'
|
||||||
|
down_revision: Union[str, None] = '621095beb234'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# Add new progress tracking columns to knowledge_queue table
|
||||||
|
op.add_column('knowledge_queue', sa.Column('processing_stage', sa.String(), nullable=True, server_default='not_started'))
|
||||||
|
op.add_column('knowledge_queue', sa.Column('progress_percentage', sa.Float(), nullable=True, server_default='0.0'))
|
||||||
|
op.add_column('knowledge_queue', sa.Column('total_items', sa.Integer(), nullable=True, server_default='0'))
|
||||||
|
op.add_column('knowledge_queue', sa.Column('processed_items', sa.Integer(), nullable=True, server_default='0'))
|
||||||
|
op.add_column('knowledge_queue', sa.Column('crawled_urls', sa.JSON(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Remove the progress tracking columns
|
||||||
|
op.drop_column('knowledge_queue', 'crawled_urls')
|
||||||
|
op.drop_column('knowledge_queue', 'processing_stage')
|
||||||
|
op.drop_column('knowledge_queue', 'progress_percentage')
|
||||||
|
op.drop_column('knowledge_queue', 'total_items')
|
||||||
|
op.drop_column('knowledge_queue', 'processed_items')
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added new node type
|
||||||
|
|
||||||
|
Revision ID: dd0e5ceefe4b
|
||||||
|
Revises: 7fd0447b3d45
|
||||||
|
Create Date: 2025-07-18 12:33:17.762285
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'dd0e5ceefe4b'
|
||||||
|
down_revision: Union[str, None] = '7fd0447b3d45'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
# Update the NodeType enum to include the new 'landing_page' type
|
||||||
|
op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'LANDING_PAGE'")
|
||||||
|
|
||||||
|
# Add new columns for landing page node type
|
||||||
|
op.add_column('workflow_nodes', sa.Column('landing_page_heading', sa.Text(), nullable=True))
|
||||||
|
op.add_column('workflow_nodes', sa.Column('landing_page_content', sa.Text(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
# First drop the columns that use the enum
|
||||||
|
op.drop_column('workflow_nodes', 'landing_page_content')
|
||||||
|
op.drop_column('workflow_nodes', 'landing_page_heading')
|
||||||
|
|
||||||
|
# Create a new enum without the landing_page value
|
||||||
|
op.execute("""
|
||||||
|
CREATE TYPE nodetype_new AS ENUM (
|
||||||
|
'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END'
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Update the column type to use the new enum
|
||||||
|
op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new")
|
||||||
|
|
||||||
|
# Drop the old enum
|
||||||
|
op.execute("DROP TYPE nodetype")
|
||||||
|
|
||||||
|
# Rename the new enum to the original name
|
||||||
|
op.execute("ALTER TYPE nodetype_new RENAME TO nodetype")
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Drop legacy Slack tables — Slack now runs on the unified channel stack
|
||||||
|
|
||||||
|
Revision ID: drop_slack_tables_001
|
||||||
|
Revises: add_channel_tables_001
|
||||||
|
Create Date: 2026-07-10 22:00:00.000000
|
||||||
|
|
||||||
|
The old integration (slack_tokens with plaintext tokens, per-thread
|
||||||
|
slack_conversations, per-channel agent_slack_configs, workspace storage modes)
|
||||||
|
is replaced by channel_accounts / channel_conversations / agent_channel_configs.
|
||||||
|
The old integration had no production users, so no data is migrated. Downgrade
|
||||||
|
recreates empty tables via the original add_slack_integration_001 shapes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'drop_slack_tables_001'
|
||||||
|
down_revision: Union[str, None] = 'add_channel_tables_001'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
_TABLES = ('slack_conversations', 'agent_slack_configs', 'slack_workspace_configs', 'slack_tokens')
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
for table in _TABLES:
|
||||||
|
op.execute(f'DROP TABLE IF EXISTS {table} CASCADE')
|
||||||
|
op.execute('DROP TYPE IF EXISTS storagemode')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Recreate empty legacy tables by re-running the original migration's DDL.
|
||||||
|
# Version scripts aren't importable by module name — load by file path.
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
legacy_path = os.path.join(os.path.dirname(__file__), 'add_slack_integration_001.py')
|
||||||
|
spec = importlib.util.spec_from_file_location('add_slack_integration_001', legacy_path)
|
||||||
|
legacy = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(legacy)
|
||||||
|
legacy.upgrade()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Agent created date added
|
||||||
|
|
||||||
|
Revision ID: e252efacd083
|
||||||
|
Revises: 9a01e6b653b2
|
||||||
|
Create Date: 2025-07-09 10:30:11.970669
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e252efacd083'
|
||||||
|
down_revision: Union[str, None] = '9a01e6b653b2'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('agents', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True))
|
||||||
|
op.add_column('agents', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True))
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_column('agents', 'updated_at')
|
||||||
|
op.drop_column('agents', 'created_at')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add AURORA chat style to agent_customizations
|
||||||
|
|
||||||
|
Adds the new AURORA value to the `chatstyle` enum (a dark "ask-me-anything"
|
||||||
|
design with a gradient orb avatar). The orb-vs-photo avatar choice is stored in
|
||||||
|
the existing `customization_metadata` JSON column, so no schema change is needed
|
||||||
|
for that.
|
||||||
|
|
||||||
|
Revision ID: e3f4a5b6c7d8
|
||||||
|
Revises: d2e3f4a5b6c7
|
||||||
|
Create Date: 2026-06-29 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e3f4a5b6c7d8'
|
||||||
|
down_revision: Union[str, None] = 'd2e3f4a5b6c7'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
# Add the new value to the native Postgres `chatstyle` enum. ALTER TYPE ...
|
||||||
|
# ADD VALUE cannot run inside a transaction block, so use an autocommit block.
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.execute("ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS 'AURORA'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# Postgres cannot cleanly drop individual enum values; the added chatstyle
|
||||||
|
# value is intentionally left in place.
|
||||||
|
pass
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
"""add customers.is_authenticated to mark integration-authenticated people
|
||||||
|
|
||||||
|
Revision ID: e5f6a7b8c9d0
|
||||||
|
Revises: d4e5f6a7b8c9
|
||||||
|
Create Date: 2026-07-09
|
||||||
|
|
||||||
|
People who were identified via POST /generate-token (the embedding app asserted a
|
||||||
|
known customer_email) are the business's existing customers, not organic leads, and
|
||||||
|
must be excluded from the People/leads views. This was previously inferred from
|
||||||
|
meta_data being NULL, but SQLAlchemy stores an empty JSON value as JSON null rather
|
||||||
|
than SQL NULL, so `meta_data IS NULL` matched nothing and hid every visitor/lead.
|
||||||
|
Replace that heuristic with an explicit boolean flag.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e5f6a7b8c9d0'
|
||||||
|
down_revision = 'd4e5f6a7b8c9'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
'customers',
|
||||||
|
sa.Column('is_authenticated', sa.Boolean(), nullable=False, server_default='false'),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('customers', 'is_authenticated')
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add jira agent config
|
||||||
|
|
||||||
|
Revision ID: e65e1b8e7e3c
|
||||||
|
Revises: 30caca239191
|
||||||
|
Create Date: 2025-03-06 21:51:54.214580
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'e65e1b8e7e3c'
|
||||||
|
down_revision: Union[str, None] = '30caca239191'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('agent_jira_configs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('agent_id', sa.String(), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('project_key', sa.String(), nullable=True),
|
||||||
|
sa.Column('issue_type_id', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_agent_jira_configs_agent_id'), 'agent_jira_configs', ['agent_id'], unique=False)
|
||||||
|
op.create_index(op.f('ix_agent_jira_configs_id'), 'agent_jira_configs', ['id'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.drop_index(op.f('ix_agent_jira_configs_id'), table_name='agent_jira_configs')
|
||||||
|
op.drop_index(op.f('ix_agent_jira_configs_agent_id'), table_name='agent_jira_configs')
|
||||||
|
op.drop_table('agent_jira_configs')
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""Add rate limiting fields to Agent model
|
||||||
|
|
||||||
|
Revision ID: f3dad24474e0
|
||||||
|
Revises: ff48f854bcbf
|
||||||
|
Create Date: 2025-03-15 20:44:17.465349
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'f3dad24474e0'
|
||||||
|
down_revision: Union[str, None] = 'ff48f854bcbf'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.add_column('agents', sa.Column('enable_rate_limiting', sa.Boolean(), nullable=True, default=False))
|
||||||
|
op.add_column('agents', sa.Column('overall_limit_per_ip', sa.Integer(), nullable=True, default=100))
|
||||||
|
op.add_column('agents', sa.Column('requests_per_sec', sa.Float(), nullable=True, default=1))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('agents', 'requests_per_sec')
|
||||||
|
op.drop_column('agents', 'overall_limit_per_ip')
|
||||||
|
op.drop_column('agents', 'enable_rate_limiting')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add SUNRISE chat style + welcome_message and quick_actions columns
|
||||||
|
|
||||||
|
Adds:
|
||||||
|
- SUNRISE value to the `chatstyle` enum (a light theme).
|
||||||
|
- `welcome_message` (Text): first in-conversation agent bubble shown on open.
|
||||||
|
- `quick_actions` (JSON): predefined quick-action button labels.
|
||||||
|
|
||||||
|
Revision ID: f4a5b6c7d8e9
|
||||||
|
Revises: e3f4a5b6c7d8
|
||||||
|
Create Date: 2026-06-30 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'f4a5b6c7d8e9'
|
||||||
|
down_revision: Union[str, None] = 'e3f4a5b6c7d8'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
|
||||||
|
# Add the new value to the native Postgres `chatstyle` enum. ALTER TYPE ...
|
||||||
|
# ADD VALUE cannot run inside a transaction block, so use an autocommit block.
|
||||||
|
if bind.dialect.name == "postgresql":
|
||||||
|
with op.get_context().autocommit_block():
|
||||||
|
op.execute("ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS 'SUNRISE'")
|
||||||
|
|
||||||
|
op.add_column(
|
||||||
|
'agent_customizations',
|
||||||
|
sa.Column('welcome_message', sa.Text(), nullable=True)
|
||||||
|
)
|
||||||
|
op.add_column(
|
||||||
|
'agent_customizations',
|
||||||
|
sa.Column('quick_actions', sa.JSON(), nullable=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column('agent_customizations', 'quick_actions')
|
||||||
|
op.drop_column('agent_customizations', 'welcome_message')
|
||||||
|
# Postgres cannot cleanly drop individual enum values; the added chatstyle
|
||||||
|
# value is intentionally left in place.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add FAQ + public help center tables
|
||||||
|
|
||||||
|
Revision ID: f7a1c2d3e4b5
|
||||||
|
Revises: drop_slack_tables_001
|
||||||
|
Create Date: 2026-07-12
|
||||||
|
|
||||||
|
New feature: AI-generated FAQs reviewed/published by the org and served as a
|
||||||
|
public help center on {slug}.chattermate.help or a verified custom domain.
|
||||||
|
|
||||||
|
Tables:
|
||||||
|
- faq_generation_jobs: async generate/import jobs (worker + UI polling)
|
||||||
|
- help_center_settings: per-org branding, slug, agent mapping, custom domain
|
||||||
|
- faqs: the question/answer pairs (draft -> published workflow)
|
||||||
|
- help_center_queries: public "Ask AI" log for metering/analytics
|
||||||
|
|
||||||
|
Status/stage columns are plain strings (mirroring knowledge_queue) so new
|
||||||
|
values never need an ALTER TYPE migration.
|
||||||
|
|
||||||
|
Also extends the notificationtype enum with FAQ job outcomes. Labels are the
|
||||||
|
uppercase member names, matching how the enum was created in
|
||||||
|
b365b8447598_initial_version. ALTER TYPE ... ADD VALUE requires PostgreSQL 12+
|
||||||
|
to run inside a transaction; added values cannot be removed on downgrade.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'f7a1c2d3e4b5'
|
||||||
|
down_revision = 'drop_slack_tables_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'faq_generation_jobs',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('job_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('knowledge_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('source_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('stage', sa.String(), nullable=False),
|
||||||
|
sa.Column('progress_percentage', sa.Float(), nullable=False),
|
||||||
|
sa.Column('faqs_created', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('error', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_faq_generation_jobs_id'), 'faq_generation_jobs', ['id'])
|
||||||
|
op.create_index(op.f('ix_faq_generation_jobs_organization_id'), 'faq_generation_jobs', ['organization_id'])
|
||||||
|
op.create_index(op.f('ix_faq_generation_jobs_knowledge_id'), 'faq_generation_jobs', ['knowledge_id'])
|
||||||
|
# The worker polls for pending/processing rows forever; keep that lookup
|
||||||
|
# O(active) as terminal rows accumulate.
|
||||||
|
op.create_index(
|
||||||
|
'ix_faq_generation_jobs_active',
|
||||||
|
'faq_generation_jobs',
|
||||||
|
['status'],
|
||||||
|
postgresql_where=sa.text("status IN ('pending', 'processing')"),
|
||||||
|
)
|
||||||
|
# DB-enforced one-active-job guard per (org, type, source): the API's
|
||||||
|
# check-then-insert would otherwise race on concurrent /generate calls.
|
||||||
|
# COALESCE keeps per-source auto jobs for different sources parallel.
|
||||||
|
op.create_index(
|
||||||
|
'uq_faq_generation_jobs_one_active',
|
||||||
|
'faq_generation_jobs',
|
||||||
|
['organization_id', 'job_type', sa.text('COALESCE(knowledge_id, -1)')],
|
||||||
|
unique=True,
|
||||||
|
postgresql_where=sa.text("status IN ('pending', 'processing')"),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'help_center_settings',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('slug', sa.String(length=63), nullable=True),
|
||||||
|
sa.Column('title', sa.String(length=120), nullable=True),
|
||||||
|
sa.Column('description', sa.String(length=300), nullable=True),
|
||||||
|
sa.Column('logo_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('brand_color', sa.String(length=9), nullable=False),
|
||||||
|
sa.Column('header_links', sa.JSON(), nullable=False),
|
||||||
|
sa.Column('cta_text', sa.String(length=40), nullable=True),
|
||||||
|
sa.Column('cta_url', sa.String(), nullable=True),
|
||||||
|
sa.Column('auto_generate', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('ai_search_enabled', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('custom_domain', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('domain_verification_token', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('txt_record_verified', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('cname_record_verified', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('ssl_status', sa.String(), nullable=False),
|
||||||
|
sa.Column('domain_verified_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('organization_id'),
|
||||||
|
sa.UniqueConstraint('slug'),
|
||||||
|
sa.UniqueConstraint('custom_domain'),
|
||||||
|
sa.CheckConstraint('slug = lower(slug)', name='ck_help_center_slug_lowercase'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_help_center_settings_slug'), 'help_center_settings', ['slug'])
|
||||||
|
op.create_index(op.f('ix_help_center_settings_custom_domain'), 'help_center_settings', ['custom_domain'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'faqs',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('question', sa.Text(), nullable=False),
|
||||||
|
sa.Column('answer', sa.Text(), nullable=False),
|
||||||
|
sa.Column('category', sa.String(length=100), nullable=False),
|
||||||
|
sa.Column('status', sa.String(), nullable=False),
|
||||||
|
sa.Column('knowledge_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('source_label', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('generation_job_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column('sort_order', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['generation_job_id'], ['faq_generation_jobs.id'], ondelete='SET NULL'),
|
||||||
|
sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
# Composite indexes lead with organization_id, so no single-column org index.
|
||||||
|
op.create_index('ix_faqs_org_status', 'faqs', ['organization_id', 'status'])
|
||||||
|
op.create_index('ix_faqs_org_category', 'faqs', ['organization_id', 'category'])
|
||||||
|
# FK indexes so knowledge/job deletions don't scan faqs to enforce SET NULL.
|
||||||
|
op.create_index(op.f('ix_faqs_knowledge_id'), 'faqs', ['knowledge_id'])
|
||||||
|
op.create_index(op.f('ix_faqs_generation_job_id'), 'faqs', ['generation_job_id'])
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
'help_center_queries',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('query', sa.Text(), nullable=False),
|
||||||
|
sa.Column('answered', sa.Boolean(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_help_center_queries_organization_id'), 'help_center_queries', ['organization_id'])
|
||||||
|
op.create_index(op.f('ix_help_center_queries_created_at'), 'help_center_queries', ['created_at'])
|
||||||
|
|
||||||
|
op.execute("ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'FAQ_GENERATED'")
|
||||||
|
op.execute("ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'FAQ_GENERATION_FAILED'")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# The enum values added in upgrade() cannot be removed; delete the rows
|
||||||
|
# that reference them so the reverted application enum can still load
|
||||||
|
# every remaining notification.
|
||||||
|
op.execute("DELETE FROM notifications WHERE type IN ('FAQ_GENERATED', 'FAQ_GENERATION_FAILED')")
|
||||||
|
op.drop_table('help_center_queries')
|
||||||
|
op.drop_table('faqs')
|
||||||
|
op.drop_table('help_center_settings')
|
||||||
|
op.drop_table('faq_generation_jobs')
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""added user id in rating table
|
||||||
|
|
||||||
|
Revision ID: fa04f644fc0c
|
||||||
|
Revises: 186c65ae9406
|
||||||
|
Create Date: 2025-03-05 15:38:12.395283
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'fa04f644fc0c'
|
||||||
|
down_revision: Union[str, None] = '186c65ae9406'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.add_column('ratings', sa.Column('user_id', sa.UUID(), nullable=True))
|
||||||
|
op.create_foreign_key(None, 'ratings', 'users', ['user_id'], ['id'], ondelete='SET NULL')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'ratings', type_='foreignkey')
|
||||||
|
op.drop_column('ratings', 'user_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""per-device fcm_tokens table, replacing users.fcm_token_web
|
||||||
|
|
||||||
|
Revision ID: fcm_tokens_per_device_001
|
||||||
|
Revises: add_agent_ai_replies_001
|
||||||
|
Create Date: 2026-08-20
|
||||||
|
|
||||||
|
The old single column held one push token for the whole account, so signing in
|
||||||
|
on a second device overwrote the first one's token and signing out anywhere
|
||||||
|
cleared it for every device. Existing tokens are carried over, one row each, so
|
||||||
|
already-registered browsers keep receiving pushes across the deploy.
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'fcm_tokens_per_device_001'
|
||||||
|
down_revision = 'add_agent_ai_replies_001'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
'fcm_tokens',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('token', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text('now()'), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('token', name='uq_fcm_tokens_token'),
|
||||||
|
)
|
||||||
|
op.create_index('ix_fcm_tokens_user_id', 'fcm_tokens', ['user_id'])
|
||||||
|
|
||||||
|
# Carry over the tokens already registered. DISTINCT ON keeps the unique
|
||||||
|
# constraint safe in the unlikely case two accounts share a token — the
|
||||||
|
# most recently updated row wins, which is the one FCM would deliver to.
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO fcm_tokens (id, user_id, token)
|
||||||
|
SELECT DISTINCT ON (fcm_token_web) gen_random_uuid(), id, fcm_token_web
|
||||||
|
FROM users
|
||||||
|
WHERE fcm_token_web IS NOT NULL AND fcm_token_web <> ''
|
||||||
|
ORDER BY fcm_token_web, updated_at DESC NULLS LAST
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
op.drop_column('users', 'fcm_token_web')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.add_column('users', sa.Column(
|
||||||
|
'fcm_token_web', sa.String(), nullable=True))
|
||||||
|
# Only one token per user fits in the old column; keep the newest.
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users u
|
||||||
|
SET fcm_token_web = t.token
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT ON (user_id) user_id, token
|
||||||
|
FROM fcm_tokens
|
||||||
|
ORDER BY user_id, created_at DESC
|
||||||
|
) t
|
||||||
|
WHERE t.user_id = u.id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.drop_index('ix_fcm_tokens_user_id', table_name='fcm_tokens')
|
||||||
|
op.drop_table('fcm_tokens')
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""
|
||||||
|
Copyright 2024-2026 ChatterMate
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""add_ticket_fields_to_session
|
||||||
|
|
||||||
|
Revision ID: ff48f854bcbf
|
||||||
|
Revises: e65e1b8e7e3c
|
||||||
|
Create Date: 2025-03-07 11:09:21.933786
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'ff48f854bcbf'
|
||||||
|
down_revision: Union[str, None] = 'e65e1b8e7e3c'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
|
||||||
|
op.add_column('session_to_agents', sa.Column('ticket_id', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('ticket_status', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('ticket_summary', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('ticket_description', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('integration_type', sa.String(), nullable=True))
|
||||||
|
op.add_column('session_to_agents', sa.Column('ticket_priority', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('session_to_agents', 'ticket_priority')
|
||||||
|
op.drop_column('session_to_agents', 'integration_type')
|
||||||
|
op.drop_column('session_to_agents', 'ticket_description')
|
||||||
|
op.drop_column('session_to_agents', 'ticket_summary')
|
||||||
|
op.drop_column('session_to_agents', 'ticket_status')
|
||||||
|
op.drop_column('session_to_agents', 'ticket_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user