mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-18 23:57:20 +00:00
feat(agent-runner): enforce 4.x host-owned execution
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""baseline: stamp existing schema (db version 25)
|
||||
"""baseline: stamp the supported 4.x schema
|
||||
|
||||
This is a no-op migration that marks the starting point for Alembic.
|
||||
All tables already exist via create_all() + legacy DBMigration system.
|
||||
Current tables already exist via SQLAlchemy metadata create_all().
|
||||
|
||||
Revision ID: 0001_baseline
|
||||
Revises: None
|
||||
@@ -15,8 +15,7 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# No-op: existing schema is already at database_version=25
|
||||
# This revision serves as the Alembic baseline.
|
||||
# No-op: this revision serves as the Alembic baseline.
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ ssereadtimeout) live in ``extra_args`` and are left untouched — the
|
||||
auto-detecting remote transport consumes them regardless.
|
||||
|
||||
Revision ID: 0006_normalize_mcp_remote_mode
|
||||
Revises: 8d3a1f2c4b6e
|
||||
Revises: 0005_add_llm_context_length
|
||||
Create Date: 2026-06-21
|
||||
"""
|
||||
|
||||
@@ -18,7 +18,7 @@ import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = '0006_normalize_mcp_remote_mode'
|
||||
down_revision = '8d3a1f2c4b6e'
|
||||
down_revision = '0005_add_llm_context_length'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ Revises: 0006_normalize_mcp_remote_mode
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
@@ -14,19 +16,64 @@ branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_BOT_ADMINS = sa.table(
|
||||
'bot_admins',
|
||||
sa.column('bot_uuid', sa.String(255)),
|
||||
sa.column('launcher_type', sa.String(64)),
|
||||
sa.column('launcher_id', sa.String(255)),
|
||||
)
|
||||
|
||||
|
||||
def _upsert_admin(conn: sa.Connection, *, bot_uuid: str, launcher_type: str, launcher_id: str) -> None:
|
||||
values = {
|
||||
'bot_uuid': bot_uuid,
|
||||
'launcher_type': launcher_type,
|
||||
'launcher_id': launcher_id,
|
||||
}
|
||||
conflict_columns = [
|
||||
_BOT_ADMINS.c.bot_uuid,
|
||||
_BOT_ADMINS.c.launcher_type,
|
||||
_BOT_ADMINS.c.launcher_id,
|
||||
]
|
||||
|
||||
if conn.dialect.name == 'postgresql':
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
|
||||
statement = insert(_BOT_ADMINS).values(**values).on_conflict_do_nothing(index_elements=conflict_columns)
|
||||
elif conn.dialect.name == 'sqlite':
|
||||
from sqlalchemy.dialects.sqlite import insert
|
||||
|
||||
statement = insert(_BOT_ADMINS).values(**values).on_conflict_do_nothing(index_elements=conflict_columns)
|
||||
else:
|
||||
existing = conn.execute(
|
||||
sa.select(sa.literal(1))
|
||||
.select_from(_BOT_ADMINS)
|
||||
.where(
|
||||
_BOT_ADMINS.c.bot_uuid == bot_uuid,
|
||||
_BOT_ADMINS.c.launcher_type == launcher_type,
|
||||
_BOT_ADMINS.c.launcher_id == launcher_id,
|
||||
)
|
||||
.limit(1)
|
||||
).first()
|
||||
if existing is not None:
|
||||
return
|
||||
statement = sa.insert(_BOT_ADMINS).values(**values)
|
||||
|
||||
conn.execute(statement)
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
if 'bot_admins' in sa.inspect(conn).get_table_names():
|
||||
return
|
||||
op.create_table(
|
||||
'bot_admins',
|
||||
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column('bot_uuid', sa.String(255), nullable=False),
|
||||
sa.Column('launcher_type', sa.String(64), nullable=False),
|
||||
sa.Column('launcher_id', sa.String(255), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint('bot_uuid', 'launcher_type', 'launcher_id', name='uq_bot_admin'),
|
||||
)
|
||||
if 'bot_admins' not in sa.inspect(conn).get_table_names():
|
||||
op.create_table(
|
||||
'bot_admins',
|
||||
sa.Column('id', sa.Integer, primary_key=True, autoincrement=True),
|
||||
sa.Column('bot_uuid', sa.String(255), nullable=False),
|
||||
sa.Column('launcher_type', sa.String(64), nullable=False),
|
||||
sa.Column('launcher_id', sa.String(255), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.func.now()),
|
||||
sa.UniqueConstraint('bot_uuid', 'launcher_type', 'launcher_id', name='uq_bot_admin'),
|
||||
)
|
||||
|
||||
# Migrate old config-based admins into the first bot (best-effort)
|
||||
inspector = sa.inspect(conn)
|
||||
@@ -48,28 +95,27 @@ def upgrade() -> None:
|
||||
if meta_row is None:
|
||||
return
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
cfg = json.loads(meta_row[0])
|
||||
except Exception:
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return
|
||||
|
||||
admins = cfg.get('admins', [])
|
||||
if not isinstance(admins, list):
|
||||
return
|
||||
for entry in admins:
|
||||
if not isinstance(entry, str):
|
||||
continue
|
||||
parts = entry.split('_', 1)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
launcher_type, launcher_id = parts
|
||||
try:
|
||||
conn.execute(
|
||||
sa.text(
|
||||
'INSERT OR IGNORE INTO bot_admins (bot_uuid, launcher_type, launcher_id) VALUES (:bu, :lt, :li)'
|
||||
),
|
||||
{'bu': first_bot_uuid, 'lt': launcher_type, 'li': launcher_id},
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
_upsert_admin(
|
||||
conn,
|
||||
bot_uuid=first_bot_uuid,
|
||||
launcher_type=launcher_type,
|
||||
launcher_id=launcher_id,
|
||||
)
|
||||
|
||||
# Remove admins key from stored config
|
||||
if 'admins' in cfg:
|
||||
@@ -81,4 +127,5 @@ def upgrade() -> None:
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('bot_admins')
|
||||
if 'bot_admins' in sa.inspect(op.get_bind()).get_table_names():
|
||||
op.drop_table('bot_admins')
|
||||
|
||||
+17
-13
@@ -3,7 +3,7 @@
|
||||
Bindings keep referencing the original Pipeline UUIDs. This migration never
|
||||
creates Agent rows or copies Pipeline runner configuration.
|
||||
|
||||
Revision ID: 0009_migrate_routing_to_event_bindings
|
||||
Revision ID: 0009_migrate_event_bindings
|
||||
Revises: 0008_agent_product_surface
|
||||
Create Date: 2026-06-26
|
||||
"""
|
||||
@@ -14,7 +14,7 @@ import uuid
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = '0009_migrate_routing_to_event_bindings'
|
||||
revision = '0009_migrate_event_bindings'
|
||||
down_revision = '0008_agent_product_surface'
|
||||
depends_on = None
|
||||
|
||||
@@ -30,24 +30,28 @@ def _column_exists(table_name: str, column_name: str) -> bool:
|
||||
|
||||
|
||||
def _rule_to_filters(rule: dict) -> list[dict] | None:
|
||||
"""Convert a pipeline_routing_rule to event_binding filters (best effort).
|
||||
|
||||
Rules that don't map cleanly (message_content, message_has_element) are
|
||||
skipped — callers should handle None as "cannot migrate".
|
||||
"""
|
||||
"""Convert one legacy Pipeline routing rule to an EBA event filter."""
|
||||
rule_type = rule.get('type')
|
||||
operator = rule.get('operator', 'eq')
|
||||
value = rule.get('value', '')
|
||||
|
||||
if rule_type == 'launcher_type':
|
||||
if value == 'group':
|
||||
return [{'field': 'group', 'operator': 'neq', 'value': None}]
|
||||
if value == 'person':
|
||||
return [{'field': 'group', 'operator': 'eq', 'value': None}]
|
||||
elif rule_type == 'launcher_id':
|
||||
return [{'field': 'chat_type', 'operator': operator, 'value': value}]
|
||||
if rule_type == 'launcher_id':
|
||||
return [{'field': 'chat_id', 'operator': operator, 'value': value}]
|
||||
if rule_type == 'message_content':
|
||||
return [{'field': 'message_text', 'operator': operator, 'value': value}]
|
||||
if rule_type == 'message_has_element':
|
||||
element_operator = {
|
||||
'eq': 'contains',
|
||||
'neq': 'not_contains',
|
||||
}.get(operator)
|
||||
if element_operator is None:
|
||||
# The legacy matcher treated every other operator as non-matching.
|
||||
element_operator = 'unsupported_legacy_operator'
|
||||
return [{'field': 'message_element_types', 'operator': element_operator, 'value': value}]
|
||||
|
||||
return None # message_content / message_has_element: no clean mapping
|
||||
return None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
@@ -0,0 +1,19 @@
|
||||
"""merge mcp resource and agent product migration heads
|
||||
|
||||
Revision ID: 0010_merge_mcp_agent_heads
|
||||
Revises: 0008_mcp_resource_prefs, 0009_migrate_event_bindings
|
||||
Create Date: 2026-06-30
|
||||
"""
|
||||
|
||||
revision = '0010_merge_mcp_agent_heads'
|
||||
down_revision = ('0008_mcp_resource_prefs', '0009_migrate_event_bindings')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1,19 +0,0 @@
|
||||
"""merge mcp resource and agent product migration heads
|
||||
|
||||
Revision ID: 0010_merge_mcp_resource_agent_heads
|
||||
Revises: 0008_mcp_resource_prefs, 0009_migrate_routing_to_event_bindings
|
||||
Create Date: 2026-06-30
|
||||
"""
|
||||
|
||||
revision = '0010_merge_mcp_resource_agent_heads'
|
||||
down_revision = ('0008_mcp_resource_prefs', '0009_migrate_routing_to_event_bindings')
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1,7 +1,7 @@
|
||||
"""drop legacy bot pipeline routing columns
|
||||
|
||||
Revision ID: 0011_drop_legacy_bot_routing
|
||||
Revises: 0010_merge_mcp_resource_agent_heads
|
||||
Revises: 0010_merge_mcp_agent_heads
|
||||
Create Date: 2026-07-01
|
||||
"""
|
||||
|
||||
@@ -10,7 +10,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '0011_drop_legacy_bot_routing'
|
||||
down_revision = '0010_merge_mcp_resource_agent_heads'
|
||||
down_revision = '0010_merge_mcp_agent_heads'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""add monitoring tool calls
|
||||
|
||||
Revision ID: 0012_monitoring_tool_calls
|
||||
Revises: 0011_drop_legacy_bot_routing
|
||||
Create Date: 2026-07-12
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = '0012_monitoring_tool_calls'
|
||||
down_revision = '0011_drop_legacy_bot_routing'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
_INDEXES = {
|
||||
'ix_monitoring_tool_calls_timestamp': ['timestamp'],
|
||||
'ix_monitoring_tool_calls_bot_id': ['bot_id'],
|
||||
'ix_monitoring_tool_calls_pipeline_id': ['pipeline_id'],
|
||||
'ix_monitoring_tool_calls_session_id': ['session_id'],
|
||||
'ix_monitoring_tool_calls_message_id': ['message_id'],
|
||||
}
|
||||
|
||||
|
||||
def _table_exists() -> bool:
|
||||
return 'monitoring_tool_calls' in sa.inspect(op.get_bind()).get_table_names()
|
||||
|
||||
|
||||
def _index_names() -> set[str]:
|
||||
if not _table_exists():
|
||||
return set()
|
||||
return {index['name'] for index in sa.inspect(op.get_bind()).get_indexes('monitoring_tool_calls')}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
if not _table_exists():
|
||||
op.create_table(
|
||||
'monitoring_tool_calls',
|
||||
sa.Column('id', sa.String(255), primary_key=True),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('tool_name', sa.String(255), nullable=False),
|
||||
sa.Column('tool_source', sa.String(50), nullable=False),
|
||||
sa.Column('duration', sa.Integer(), nullable=False),
|
||||
sa.Column('status', sa.String(50), nullable=False),
|
||||
sa.Column('bot_id', sa.String(255), nullable=False),
|
||||
sa.Column('bot_name', sa.String(255), nullable=False),
|
||||
sa.Column('pipeline_id', sa.String(255), nullable=False),
|
||||
sa.Column('pipeline_name', sa.String(255), nullable=False),
|
||||
sa.Column('session_id', sa.String(255), nullable=True),
|
||||
sa.Column('message_id', sa.String(255), nullable=True),
|
||||
sa.Column('arguments', sa.Text(), nullable=True),
|
||||
sa.Column('result', sa.Text(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
)
|
||||
|
||||
existing_indexes = _index_names()
|
||||
for index_name, columns in _INDEXES.items():
|
||||
if index_name not in existing_indexes:
|
||||
op.create_index(index_name, 'monitoring_tool_calls', columns, unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
if _table_exists():
|
||||
op.drop_table('monitoring_tool_calls')
|
||||
@@ -7,15 +7,14 @@ import typing
|
||||
import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
|
||||
import sqlalchemy
|
||||
|
||||
from . import database, migration
|
||||
from ..entity.persistence import base, metadata, model as persistence_model
|
||||
from . import database
|
||||
from ..entity.persistence import base, model as persistence_model
|
||||
from ..entity import persistence
|
||||
from ..core import app
|
||||
from ..utils import constants, importutil
|
||||
from . import databases, migrations
|
||||
from ..utils import importutil
|
||||
from . import databases
|
||||
|
||||
importutil.import_modules_in_pkg(databases)
|
||||
importutil.import_modules_in_pkg(migrations)
|
||||
importutil.import_modules_in_pkg(persistence)
|
||||
|
||||
|
||||
@@ -43,40 +42,6 @@ class PersistenceManager:
|
||||
break
|
||||
|
||||
await self.create_tables()
|
||||
|
||||
# run migrations
|
||||
database_version = await self.execute_async(
|
||||
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version')
|
||||
)
|
||||
|
||||
database_version = int(database_version.fetchone()[1])
|
||||
required_database_version = constants.required_database_version
|
||||
|
||||
if database_version < required_database_version:
|
||||
migrations = migration.preregistered_db_migrations
|
||||
migrations.sort(key=lambda x: x.number)
|
||||
|
||||
last_migration_number = database_version
|
||||
|
||||
for migration_cls in migrations:
|
||||
migration_instance = migration_cls(self.ap)
|
||||
|
||||
if (
|
||||
migration_instance.number > database_version
|
||||
and migration_instance.number <= required_database_version
|
||||
):
|
||||
await migration_instance.upgrade()
|
||||
await self.execute_async(
|
||||
sqlalchemy.update(metadata.Metadata)
|
||||
.where(metadata.Metadata.key == 'database_version')
|
||||
.values({'value': str(migration_instance.number)})
|
||||
)
|
||||
last_migration_number = migration_instance.number
|
||||
self.ap.logger.info(f'Migration {migration_instance.number} completed.')
|
||||
|
||||
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
|
||||
|
||||
# Run Alembic migrations (new migration system)
|
||||
await self._run_alembic_migrations()
|
||||
|
||||
await self.write_space_model_providers()
|
||||
@@ -88,19 +53,6 @@ class PersistenceManager:
|
||||
|
||||
await conn.commit()
|
||||
|
||||
# ======= write initial data =======
|
||||
|
||||
# write initial metadata
|
||||
self.ap.logger.info('Creating initial metadata...')
|
||||
for item in metadata.initial_metadata:
|
||||
# check if the item exists
|
||||
result = await self.execute_async(
|
||||
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == item['key'])
|
||||
)
|
||||
row = result.first()
|
||||
if row is None:
|
||||
await self.execute_async(sqlalchemy.insert(metadata.Metadata).values(item))
|
||||
|
||||
async def write_space_model_providers(self):
|
||||
space_models_gateway_api_url = self.ap.instance_config.data.get('space', {}).get(
|
||||
'models_gateway_api_url', 'https://api.langbot.cloud/v1'
|
||||
@@ -139,7 +91,7 @@ class PersistenceManager:
|
||||
# =================================
|
||||
|
||||
async def _run_alembic_migrations(self):
|
||||
"""Run Alembic-based migrations after legacy migrations complete."""
|
||||
"""Run Alembic migrations for supported 4.x databases."""
|
||||
from . import alembic_runner
|
||||
|
||||
engine = self.get_db_engine()
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import abc
|
||||
|
||||
from ..core import app
|
||||
|
||||
|
||||
preregistered_db_migrations: list[typing.Type[DBMigration]] = []
|
||||
|
||||
|
||||
def migration_class(number: int):
|
||||
"""Migration class decorator"""
|
||||
|
||||
def wrapper(cls: typing.Type[DBMigration]) -> typing.Type[DBMigration]:
|
||||
cls.number = number
|
||||
preregistered_db_migrations.append(cls)
|
||||
return cls
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class DBMigration(abc.ABC):
|
||||
"""Database migration"""
|
||||
|
||||
number: int
|
||||
"""Migration number"""
|
||||
|
||||
def __init__(self, ap: app.Application):
|
||||
self.ap = ap
|
||||
|
||||
@abc.abstractmethod
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,36 +0,0 @@
|
||||
# Legacy migrations (DEPRECATED — do not add new files here)
|
||||
|
||||
This directory holds the **legacy 3.x database migration system**
|
||||
(`DBMigration` subclasses in `dbmXXX_*.py`, registered via
|
||||
`@migration.migration_class(N)` and run from `pkg/persistence/mgr.py`).
|
||||
|
||||
**This system is frozen. Do not add new `dbmXXX_*.py` migrations.**
|
||||
|
||||
The chain is capped at version 25 (`required_database_version = 25` in
|
||||
`pkg/utils/constants.py`). These files exist only to upgrade pre-existing
|
||||
3.x databases up to the Alembic baseline (`0001_baseline`). Removing them
|
||||
would break in-place upgrades from old installations, so they are kept
|
||||
read-only.
|
||||
|
||||
## All new schema changes use Alembic
|
||||
|
||||
Migrations now live in `pkg/persistence/alembic/versions/`. To create one:
|
||||
|
||||
```bash
|
||||
uv run python -m langbot.pkg.persistence.alembic_runner autogenerate "description of your change"
|
||||
```
|
||||
|
||||
(requires `data/config.yaml` to exist). Review and edit the generated
|
||||
script before committing — Alembic migrations run automatically on startup
|
||||
and must be idempotent and guard against missing tables (the test suite
|
||||
runs them against empty databases).
|
||||
|
||||
### Rules for Alembic revision ids
|
||||
|
||||
- Keep the revision id **≤ 32 characters** — PostgreSQL stores
|
||||
`alembic_version.version_num` as `varchar(32)` and will raise
|
||||
`StringDataRightTruncationError` on overflow.
|
||||
- Guard every `op` call against a missing table / missing column
|
||||
(`inspector.get_table_names()` / `inspector.get_columns()`); fresh
|
||||
installs create the schema via `create_all()` and stamp the baseline,
|
||||
so migrations may run against tables that already match or do not exist.
|
||||
@@ -1,218 +0,0 @@
|
||||
from .. import migration
|
||||
from copy import deepcopy
|
||||
import uuid
|
||||
import os
|
||||
import sqlalchemy
|
||||
import shutil
|
||||
|
||||
from ...config import manager as config_manager
|
||||
from ...entity.persistence import (
|
||||
model as persistence_model,
|
||||
pipeline as persistence_pipeline,
|
||||
bot as persistence_bot,
|
||||
)
|
||||
|
||||
|
||||
@migration.migration_class(1)
|
||||
class DBMigrateV3Config(migration.DBMigration):
|
||||
"""Migrate v3 config to v4 database"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
"""
|
||||
Migrate all config files under data/config.
|
||||
After migration, all previous config files are saved under data/legacy/config.
|
||||
After migration, all config files under data/metadata/ are saved under data/legacy/metadata.
|
||||
"""
|
||||
|
||||
if self.ap.provider_cfg is None:
|
||||
return
|
||||
|
||||
# ======= Migrate model =======
|
||||
# Only migrate the currently selected model
|
||||
model_name = self.ap.provider_cfg.data.get('model', 'gpt-4o')
|
||||
|
||||
model_requester = 'openai-chat-completions'
|
||||
model_requester_config = {}
|
||||
model_api_keys = ['sk-proj-1234567890']
|
||||
model_abilities = []
|
||||
model_extra_args = {}
|
||||
|
||||
if os.path.exists('data/metadata/llm-models.json'):
|
||||
_llm_model_meta = await config_manager.load_json_config('data/metadata/llm-models.json', completion=False)
|
||||
|
||||
for item in _llm_model_meta.data.get('list', []):
|
||||
if item.get('name') == model_name:
|
||||
if 'model_name' in item:
|
||||
model_name = item['model_name']
|
||||
if 'requester' in item:
|
||||
model_requester = item['requester']
|
||||
if 'token_mgr' in item:
|
||||
_token_mgr = item['token_mgr']
|
||||
|
||||
if _token_mgr in self.ap.provider_cfg.data.get('keys', {}):
|
||||
model_api_keys = self.ap.provider_cfg.data.get('keys', {})[_token_mgr]
|
||||
|
||||
if 'tool_call_supported' in item and item['tool_call_supported']:
|
||||
model_abilities.append('func_call')
|
||||
|
||||
if 'vision_supported' in item and item['vision_supported']:
|
||||
model_abilities.append('vision')
|
||||
|
||||
if (
|
||||
model_requester in self.ap.provider_cfg.data.get('requester', {})
|
||||
and 'args' in self.ap.provider_cfg.data.get('requester', {})[model_requester]
|
||||
):
|
||||
model_extra_args = self.ap.provider_cfg.data.get('requester', {})[model_requester]['args']
|
||||
|
||||
if model_requester in self.ap.provider_cfg.data.get('requester', {}):
|
||||
model_requester_config = self.ap.provider_cfg.data.get('requester', {})[model_requester]
|
||||
model_requester_config = {
|
||||
'base_url': model_requester_config['base-url'],
|
||||
'timeout': model_requester_config['timeout'],
|
||||
}
|
||||
|
||||
break
|
||||
|
||||
model_uuid = str(uuid.uuid4())
|
||||
|
||||
llm_model_data = {
|
||||
'uuid': model_uuid,
|
||||
'name': model_name,
|
||||
'description': '由 LangBot v3 迁移而来',
|
||||
'requester': model_requester,
|
||||
'requester_config': model_requester_config,
|
||||
'api_keys': model_api_keys,
|
||||
'abilities': model_abilities,
|
||||
'extra_args': model_extra_args,
|
||||
}
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.insert(persistence_model.LLMModel).values(**llm_model_data)
|
||||
)
|
||||
|
||||
# ======= Migrate pipeline config =======
|
||||
# Modify to default pipeline
|
||||
default_pipeline = [
|
||||
self.ap.persistence_mgr.serialize_model(persistence_pipeline.LegacyPipeline, pipeline)
|
||||
for pipeline in (
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_pipeline.LegacyPipeline).where(
|
||||
persistence_pipeline.LegacyPipeline.is_default == True
|
||||
)
|
||||
)
|
||||
).all()
|
||||
][0]
|
||||
|
||||
pipeline_uuid = str(uuid.uuid4())
|
||||
pipeline_name = 'ChatPipeline'
|
||||
|
||||
if default_pipeline:
|
||||
pipeline_name = default_pipeline['name']
|
||||
pipeline_uuid = default_pipeline['uuid']
|
||||
|
||||
pipeline_config = default_pipeline['config']
|
||||
|
||||
# trigger
|
||||
pipeline_config['trigger']['group-respond-rules'] = self.ap.pipeline_cfg.data['respond-rules']['default']
|
||||
pipeline_config['trigger']['access-control'] = self.ap.pipeline_cfg.data['access-control']
|
||||
pipeline_config['trigger']['ignore-rules'] = self.ap.pipeline_cfg.data['ignore-rules']
|
||||
|
||||
# safety
|
||||
pipeline_config['safety']['content-filter'] = {
|
||||
'scope': 'all',
|
||||
'check-sensitive-words': self.ap.pipeline_cfg.data['check-sensitive-words'],
|
||||
}
|
||||
pipeline_config['safety']['rate-limit'] = {
|
||||
'window-length': self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['window-size'],
|
||||
'limitation': self.ap.pipeline_cfg.data['rate-limit']['fixwin']['default']['limit'],
|
||||
'strategy': self.ap.pipeline_cfg.data['rate-limit']['strategy'],
|
||||
}
|
||||
|
||||
# output
|
||||
pipeline_config['output']['long-text-processing'] = self.ap.platform_cfg.data['long-text-process']
|
||||
pipeline_config['output']['force-delay'] = self.ap.platform_cfg.data['force-delay']
|
||||
pipeline_config['output']['misc'] = {
|
||||
'hide-exception': self.ap.platform_cfg.data['hide-exception-info'],
|
||||
'quote-origin': self.ap.platform_cfg.data['quote-origin'],
|
||||
'at-sender': self.ap.platform_cfg.data['at-sender'],
|
||||
'track-function-calls': self.ap.platform_cfg.data['track-function-calls'],
|
||||
}
|
||||
|
||||
default_pipeline['description'] = default_pipeline['description'] + ' [已迁移 LangBot v3 配置]'
|
||||
default_pipeline['config'] = pipeline_config
|
||||
default_pipeline.pop('created_at')
|
||||
default_pipeline.pop('updated_at')
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(persistence_pipeline.LegacyPipeline)
|
||||
.values(default_pipeline)
|
||||
.where(persistence_pipeline.LegacyPipeline.uuid == default_pipeline['uuid'])
|
||||
)
|
||||
|
||||
# ======= Migrate bot =======
|
||||
# Only migrate enabled bots
|
||||
for adapter in self.ap.platform_cfg.data.get('platform-adapters', []):
|
||||
if not adapter.get('enable'):
|
||||
continue
|
||||
|
||||
args = deepcopy(adapter)
|
||||
args.pop('adapter')
|
||||
args.pop('enable')
|
||||
|
||||
bot_data = {
|
||||
'uuid': str(uuid.uuid4()),
|
||||
'name': adapter.get('adapter'),
|
||||
'description': '由 LangBot v3 迁移而来',
|
||||
'adapter': adapter.get('adapter'),
|
||||
'adapter_config': args,
|
||||
'enable': True,
|
||||
'use_pipeline_uuid': pipeline_uuid,
|
||||
'use_pipeline_name': pipeline_name,
|
||||
}
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.insert(persistence_bot.Bot).values(**bot_data))
|
||||
|
||||
# ======= Migrate system settings =======
|
||||
self.ap.instance_config.data['admins'] = self.ap.system_cfg.data['admin-sessions']
|
||||
self.ap.instance_config.data['api']['port'] = self.ap.system_cfg.data['http-api']['port']
|
||||
self.ap.instance_config.data['command'] = {
|
||||
'prefix': self.ap.command_cfg.data['command-prefix'],
|
||||
'enable': self.ap.command_cfg.data['command-enable']
|
||||
if 'command-enable' in self.ap.command_cfg.data
|
||||
else True,
|
||||
'privilege': self.ap.command_cfg.data['privilege'],
|
||||
}
|
||||
self.ap.instance_config.data['concurrency']['pipeline'] = self.ap.system_cfg.data['pipeline-concurrency']
|
||||
self.ap.instance_config.data['concurrency']['session'] = self.ap.system_cfg.data['session-concurrency'][
|
||||
'default'
|
||||
]
|
||||
self.ap.instance_config.data['mcp'] = self.ap.provider_cfg.data['mcp']
|
||||
self.ap.instance_config.data['proxy'] = self.ap.system_cfg.data['network-proxies']
|
||||
await self.ap.instance_config.dump_config()
|
||||
|
||||
# ======= move files =======
|
||||
# Migrate all config files under data/config
|
||||
all_legacy_dir_name = [
|
||||
'config',
|
||||
# 'metadata',
|
||||
'prompts',
|
||||
'scenario',
|
||||
]
|
||||
|
||||
def move_legacy_files(dir_name: str):
|
||||
if not os.path.exists(f'data/legacy/{dir_name}'):
|
||||
os.makedirs(f'data/legacy/{dir_name}')
|
||||
|
||||
if os.path.exists(f'data/{dir_name}'):
|
||||
for file in os.listdir(f'data/{dir_name}'):
|
||||
if file.endswith('.json'):
|
||||
shutil.move(f'data/{dir_name}/{file}', f'data/legacy/{dir_name}/{file}')
|
||||
|
||||
os.rmdir(f'data/{dir_name}')
|
||||
|
||||
for dir_name in all_legacy_dir_name:
|
||||
move_legacy_files(dir_name)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
@@ -1,55 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(2)
|
||||
class DBMigrateCombineQuoteMsgConfig(migration.DBMigration):
|
||||
"""Combine quote message config"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure 'trigger' exists
|
||||
if 'trigger' not in config:
|
||||
config['trigger'] = {}
|
||||
|
||||
# Ensure 'misc' exists in 'trigger'
|
||||
if 'misc' not in config['trigger']:
|
||||
config['trigger']['misc'] = {}
|
||||
|
||||
# Add 'combine-quote-message' if not exists
|
||||
if 'combine-quote-message' not in config['trigger']['misc']:
|
||||
config['trigger']['misc']['combine-quote-message'] = False
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,62 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(3)
|
||||
class DBMigrateN8nConfig(migration.DBMigration):
|
||||
"""N8n config"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure 'ai' exists
|
||||
if 'ai' not in config:
|
||||
config['ai'] = {}
|
||||
|
||||
# Add 'n8n-service-api' if not exists
|
||||
if 'n8n-service-api' not in config['ai']:
|
||||
config['ai']['n8n-service-api'] = {
|
||||
'webhook-url': 'http://your-n8n-webhook-url',
|
||||
'auth-type': 'none',
|
||||
'basic-username': '',
|
||||
'basic-password': '',
|
||||
'jwt-secret': '',
|
||||
'jwt-algorithm': 'HS256',
|
||||
'header-name': '',
|
||||
'header-value': '',
|
||||
'timeout': 120,
|
||||
'output-key': 'response',
|
||||
}
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,53 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(4)
|
||||
class DBMigrateRAGKBUUID(migration.DBMigration):
|
||||
"""RAG知识库UUID"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""升级"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure nested structure exists
|
||||
if 'ai' not in config:
|
||||
config['ai'] = {}
|
||||
if 'local-agent' not in config['ai']:
|
||||
config['ai']['local-agent'] = {}
|
||||
|
||||
# Add 'knowledge-base' if not exists
|
||||
if 'knowledge-base' not in config['ai']['local-agent']:
|
||||
config['ai']['local-agent']['knowledge-base'] = ''
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""降级"""
|
||||
pass
|
||||
@@ -1,53 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(5)
|
||||
class DBMigratePipelineRemoveCotConfig(migration.DBMigration):
|
||||
"""Pipeline remove cot config"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure nested structure exists
|
||||
if 'output' not in config:
|
||||
config['output'] = {}
|
||||
if 'misc' not in config['output']:
|
||||
config['output']['misc'] = {}
|
||||
|
||||
# Add 'remove-think' if not exists
|
||||
if 'remove-think' not in config['output']['misc']:
|
||||
config['output']['misc']['remove-think'] = False
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,58 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(6)
|
||||
class DBMigrateLangflowApiConfig(migration.DBMigration):
|
||||
"""Langflow API config"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure 'ai' exists
|
||||
if 'ai' not in config:
|
||||
config['ai'] = {}
|
||||
|
||||
# Add 'langflow-api' if not exists
|
||||
if 'langflow-api' not in config['ai']:
|
||||
config['ai']['langflow-api'] = {
|
||||
'base-url': 'http://localhost:7860',
|
||||
'api-key': 'your-api-key',
|
||||
'flow-id': 'your-flow-id',
|
||||
'input-type': 'chat',
|
||||
'output-type': 'chat',
|
||||
'tweaks': '{}',
|
||||
}
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,44 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(7)
|
||||
class DBMigratePluginInstallSource(migration.DBMigration):
|
||||
"""插件安装来源"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""升级"""
|
||||
# 查询表结构获取所有列名(异步执行 SQL)
|
||||
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
"SELECT column_name FROM information_schema.columns WHERE table_name = 'plugin_settings';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(plugin_settings);'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# 检查并添加 install_source 列
|
||||
if 'install_source' not in columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
"ALTER TABLE plugin_settings ADD COLUMN install_source VARCHAR(255) NOT NULL DEFAULT 'github'"
|
||||
)
|
||||
)
|
||||
|
||||
# 检查并添加 install_info 列
|
||||
if 'install_info' not in columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("ALTER TABLE plugin_settings ADD COLUMN install_info JSON NOT NULL DEFAULT '{}'")
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""降级"""
|
||||
pass
|
||||
@@ -1,22 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(8)
|
||||
class DBMigratePluginConfig(migration.DBMigration):
|
||||
"""插件配置"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""升级"""
|
||||
|
||||
if 'plugin' not in self.ap.instance_config.data:
|
||||
self.ap.instance_config.data['plugin'] = {
|
||||
'runtime_ws_url': 'ws://langbot_plugin_runtime:5400/control/ws',
|
||||
'enable_marketplace': True,
|
||||
'cloud_service_url': 'https://space.langbot.app',
|
||||
}
|
||||
|
||||
await self.ap.instance_config.dump_config()
|
||||
|
||||
async def downgrade(self):
|
||||
"""降级"""
|
||||
pass
|
||||
@@ -1,20 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(9)
|
||||
class DBMigratePipelineExtensionPreferences(migration.DBMigration):
|
||||
"""Pipeline extension preferences"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
|
||||
sql_text = sqlalchemy.text(
|
||||
"ALTER TABLE legacy_pipelines ADD COLUMN extensions_preferences JSON NOT NULL DEFAULT '{}'"
|
||||
)
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
sql_text = sqlalchemy.text('ALTER TABLE legacy_pipelines DROP COLUMN extensions_preferences')
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
@@ -1,105 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(10)
|
||||
class DBMigratePipelineMultiKnowledgeBase(migration.DBMigration):
|
||||
"""Pipeline support multiple knowledge base binding"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Convert knowledge-base from string to array
|
||||
if 'ai' in config and 'local-agent' in config['ai']:
|
||||
current_kb = config['ai']['local-agent'].get('knowledge-base', '')
|
||||
|
||||
# If it's already a list, skip
|
||||
if isinstance(current_kb, list):
|
||||
continue
|
||||
|
||||
# Convert string to list
|
||||
if current_kb and current_kb != '__none__':
|
||||
config['ai']['local-agent']['knowledge-bases'] = [current_kb]
|
||||
else:
|
||||
config['ai']['local-agent']['knowledge-bases'] = []
|
||||
|
||||
# Remove old field
|
||||
if 'knowledge-base' in config['ai']['local-agent']:
|
||||
del config['ai']['local-agent']['knowledge-base']
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Convert knowledge-bases from array back to string
|
||||
if 'ai' in config and 'local-agent' in config['ai']:
|
||||
current_kbs = config['ai']['local-agent'].get('knowledge-bases', [])
|
||||
|
||||
# If it's already a string, skip
|
||||
if isinstance(current_kbs, str):
|
||||
continue
|
||||
|
||||
# Convert list to string (take first one or empty)
|
||||
if current_kbs and len(current_kbs) > 0:
|
||||
config['ai']['local-agent']['knowledge-base'] = current_kbs[0]
|
||||
else:
|
||||
config['ai']['local-agent']['knowledge-base'] = ''
|
||||
|
||||
# Remove new field
|
||||
if 'knowledge-bases' in config['ai']['local-agent']:
|
||||
del config['ai']['local-agent']['knowledge-bases']
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(11)
|
||||
class DBMigrateDifyApiConfig(migration.DBMigration):
|
||||
"""Dify base prompt config"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
# Ensure nested structure exists
|
||||
if 'ai' not in config:
|
||||
config['ai'] = {}
|
||||
if 'dify-service-api' not in config['ai']:
|
||||
config['ai']['dify-service-api'] = {}
|
||||
|
||||
# Add 'base-prompt' if not exists
|
||||
if 'base-prompt' not in config['ai']['dify-service-api']:
|
||||
config['ai']['dify-service-api']['base-prompt'] = (
|
||||
'When the file content is readable, please read the content of this file. When the file is an image, describe the content of this image.',
|
||||
)
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,73 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(12)
|
||||
class DBMigratePipelineExtensionsEnableAll(migration.DBMigration):
|
||||
"""Pipeline extensions enable all"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Read all pipelines using raw SQL
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, extensions_preferences FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
extensions_preferences = (
|
||||
json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
)
|
||||
|
||||
# Ensure extensions_preferences is a dict
|
||||
if extensions_preferences is None:
|
||||
extensions_preferences = {}
|
||||
|
||||
# Add 'enable_all_plugins' if not exists
|
||||
if 'enable_all_plugins' not in extensions_preferences:
|
||||
if 'plugins' in extensions_preferences:
|
||||
extensions_preferences['enable_all_plugins'] = False
|
||||
else:
|
||||
extensions_preferences['enable_all_plugins'] = True
|
||||
extensions_preferences['plugins'] = []
|
||||
|
||||
# Add 'enable_all_mcp_servers' if not exists
|
||||
if 'enable_all_mcp_servers' not in extensions_preferences:
|
||||
if 'mcp_servers' in extensions_preferences:
|
||||
extensions_preferences['enable_all_mcp_servers'] = False
|
||||
else:
|
||||
extensions_preferences['enable_all_mcp_servers'] = True
|
||||
extensions_preferences['mcp_servers'] = []
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET extensions_preferences = :extensions_preferences::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{
|
||||
'extensions_preferences': json.dumps(extensions_preferences),
|
||||
'for_version': current_version,
|
||||
'uuid': uuid,
|
||||
},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET extensions_preferences = :extensions_preferences, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{
|
||||
'extensions_preferences': json.dumps(extensions_preferences),
|
||||
'for_version': current_version,
|
||||
'uuid': uuid,
|
||||
},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,49 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(13)
|
||||
class DBMigrateKnowledgeBaseUpdatedAt(migration.DBMigration):
|
||||
"""Add updated_at field to knowledge_bases table"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Get all column names from the table
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
"SELECT column_name FROM information_schema.columns WHERE table_name = 'knowledge_bases';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(knowledge_bases);'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# Check and add updated_at column
|
||||
if 'updated_at' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'ALTER TABLE knowledge_bases ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP'
|
||||
)
|
||||
)
|
||||
else:
|
||||
# SQLite doesn't support DEFAULT CURRENT_TIMESTAMP in ALTER TABLE
|
||||
# Add column without default first
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE knowledge_bases ADD COLUMN updated_at DATETIME')
|
||||
)
|
||||
|
||||
# Set initial updated_at values to created_at for existing records
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('UPDATE knowledge_bases SET updated_at = created_at WHERE updated_at IS NULL')
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,94 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(14)
|
||||
class DBMigrateSpaceAccountSupport(migration.DBMigration):
|
||||
"""Add Space account support fields to users table"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Get all column names from the users table
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("SELECT column_name FROM information_schema.columns WHERE table_name = 'users';")
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('PRAGMA table_info(users);'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# Add account_type column
|
||||
if 'account_type' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("ALTER TABLE users ADD COLUMN account_type VARCHAR(32) DEFAULT 'local' NOT NULL")
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("ALTER TABLE users ADD COLUMN account_type VARCHAR(32) DEFAULT 'local' NOT NULL")
|
||||
)
|
||||
|
||||
# Add space_account_uuid column
|
||||
if 'space_account_uuid' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_account_uuid VARCHAR(255)')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_account_uuid VARCHAR(255)')
|
||||
)
|
||||
|
||||
# Add space_access_token column
|
||||
if 'space_access_token' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token TEXT')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token TEXT')
|
||||
)
|
||||
|
||||
# Add space_refresh_token column
|
||||
if 'space_refresh_token' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_refresh_token TEXT')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_refresh_token TEXT')
|
||||
)
|
||||
|
||||
# Add space_access_token_expires_at column
|
||||
if 'space_access_token_expires_at' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token_expires_at TIMESTAMP')
|
||||
)
|
||||
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_access_token_expires_at DATETIME')
|
||||
)
|
||||
|
||||
# Add space_api_key column
|
||||
if 'space_api_key' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_api_key VARCHAR(255)')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE users ADD COLUMN space_api_key VARCHAR(255)')
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
|
||||
# this is a deprecated migration
|
||||
@migration.migration_class(15)
|
||||
class DBMigrateModelSourceTracking(migration.DBMigration):
|
||||
"""Add source tracking fields to models tables for Space integration"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
pass
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,305 +0,0 @@
|
||||
import uuid as uuid_lib
|
||||
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(16)
|
||||
class DBMigrateModelProviderRefactor(migration.DBMigration):
|
||||
"""Refactor model structure: create providers from existing models and update references"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Step 1: Create model_providers table if not exists
|
||||
await self._create_providers_table()
|
||||
|
||||
# Step 2: Migrate existing models to use providers
|
||||
await self._migrate_llm_models()
|
||||
await self._migrate_embedding_models()
|
||||
|
||||
# Step 3: Remove deprecated columns
|
||||
await self._cleanup_columns()
|
||||
|
||||
async def _create_providers_table(self):
|
||||
"""Create model_providers table"""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("""
|
||||
CREATE TABLE IF NOT EXISTS model_providers (
|
||||
uuid VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
requester VARCHAR(255) NOT NULL,
|
||||
base_url VARCHAR(512) NOT NULL,
|
||||
api_keys JSONB NOT NULL DEFAULT '[]',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("""
|
||||
CREATE TABLE IF NOT EXISTS model_providers (
|
||||
uuid VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
requester VARCHAR(255) NOT NULL,
|
||||
base_url VARCHAR(512) NOT NULL,
|
||||
api_keys JSON NOT NULL DEFAULT '[]',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
)
|
||||
|
||||
async def _migrate_llm_models(self):
|
||||
"""Migrate LLM models to use providers"""
|
||||
llm_columns = await self._get_columns('llm_models')
|
||||
|
||||
# Add provider_uuid column if not exists
|
||||
if 'provider_uuid' not in llm_columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN provider_uuid VARCHAR(255)')
|
||||
)
|
||||
|
||||
# Add prefered_ranking column if not exists
|
||||
if 'prefered_ranking' not in llm_columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0')
|
||||
)
|
||||
|
||||
# Only migrate if old columns exist
|
||||
if 'requester' not in llm_columns:
|
||||
return
|
||||
|
||||
# Get all LLM models with old structure
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, name, requester, requester_config, api_keys FROM llm_models')
|
||||
)
|
||||
models = result.fetchall()
|
||||
|
||||
# Create providers and update models
|
||||
provider_cache = {} # (requester, base_url, api_keys_str) -> provider_uuid
|
||||
|
||||
for model in models:
|
||||
model_uuid, model_name, requester, requester_config, api_keys = model
|
||||
|
||||
# Extract base_url from requester_config
|
||||
base_url = ''
|
||||
if requester_config:
|
||||
if isinstance(requester_config, str):
|
||||
import json
|
||||
|
||||
requester_config = json.loads(requester_config)
|
||||
base_url = requester_config.get('base_url', '') or requester_config.get('base-url', '')
|
||||
|
||||
# Parse api_keys if it's a string
|
||||
if isinstance(api_keys, str):
|
||||
import json
|
||||
|
||||
try:
|
||||
api_keys = json.loads(api_keys)
|
||||
except Exception:
|
||||
api_keys = []
|
||||
if not api_keys:
|
||||
api_keys = []
|
||||
|
||||
# Create cache key
|
||||
api_keys_str = str(sorted(api_keys)) if api_keys else '[]'
|
||||
cache_key = (requester, base_url, api_keys_str)
|
||||
|
||||
if cache_key in provider_cache:
|
||||
provider_uuid = provider_cache[cache_key]
|
||||
else:
|
||||
# Create new provider
|
||||
provider_uuid = str(uuid_lib.uuid4())
|
||||
provider_name = f'{requester}'
|
||||
if base_url:
|
||||
# Extract domain for name
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(base_url)
|
||||
provider_name = parsed.netloc or requester
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import json
|
||||
|
||||
api_keys_json = json.dumps(api_keys) if api_keys else '[]'
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("""
|
||||
INSERT INTO model_providers (uuid, name, requester, base_url, api_keys)
|
||||
VALUES (:uuid, :name, :requester, :base_url, :api_keys)
|
||||
"""),
|
||||
{
|
||||
'uuid': provider_uuid,
|
||||
'name': provider_name,
|
||||
'requester': requester,
|
||||
'base_url': base_url,
|
||||
'api_keys': api_keys_json,
|
||||
},
|
||||
)
|
||||
provider_cache[cache_key] = provider_uuid
|
||||
|
||||
# Update model with provider_uuid
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('UPDATE llm_models SET provider_uuid = :provider_uuid WHERE uuid = :uuid'),
|
||||
{'provider_uuid': provider_uuid, 'uuid': model_uuid},
|
||||
)
|
||||
|
||||
async def _migrate_embedding_models(self):
|
||||
"""Migrate embedding models to use providers"""
|
||||
embedding_columns = await self._get_columns('embedding_models')
|
||||
|
||||
# Add provider_uuid column if not exists
|
||||
if 'provider_uuid' not in embedding_columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN provider_uuid VARCHAR(255)')
|
||||
)
|
||||
|
||||
# Add prefered_ranking column if not exists
|
||||
if 'prefered_ranking' not in embedding_columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0')
|
||||
)
|
||||
|
||||
# Only migrate if old columns exist
|
||||
if 'requester' not in embedding_columns:
|
||||
return
|
||||
|
||||
# Get all embedding models with old structure
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, name, requester, requester_config, api_keys FROM embedding_models')
|
||||
)
|
||||
models = result.fetchall()
|
||||
|
||||
# Get existing providers
|
||||
provider_result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, requester, base_url, api_keys FROM model_providers')
|
||||
)
|
||||
existing_providers = provider_result.fetchall()
|
||||
|
||||
provider_cache = {}
|
||||
for p in existing_providers:
|
||||
p_uuid, p_requester, p_base_url, p_api_keys = p
|
||||
api_keys_str = str(sorted(p_api_keys)) if p_api_keys else '[]'
|
||||
provider_cache[(p_requester, p_base_url, api_keys_str)] = p_uuid
|
||||
|
||||
for model in models:
|
||||
model_uuid, model_name, requester, requester_config, api_keys = model
|
||||
|
||||
base_url = ''
|
||||
if requester_config:
|
||||
if isinstance(requester_config, str):
|
||||
import json
|
||||
|
||||
requester_config = json.loads(requester_config)
|
||||
base_url = requester_config.get('base_url', '') or requester_config.get('base-url', '')
|
||||
|
||||
# Parse api_keys if it's a string
|
||||
if isinstance(api_keys, str):
|
||||
import json
|
||||
|
||||
try:
|
||||
api_keys = json.loads(api_keys)
|
||||
except Exception:
|
||||
api_keys = []
|
||||
if not api_keys:
|
||||
api_keys = []
|
||||
|
||||
api_keys_str = str(sorted(api_keys)) if api_keys else '[]'
|
||||
cache_key = (requester, base_url, api_keys_str)
|
||||
|
||||
if cache_key in provider_cache:
|
||||
provider_uuid = provider_cache[cache_key]
|
||||
else:
|
||||
provider_uuid = str(uuid_lib.uuid4())
|
||||
provider_name = f'{requester}'
|
||||
if base_url:
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(base_url)
|
||||
provider_name = parsed.netloc or requester
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
import json
|
||||
|
||||
api_keys_json = json.dumps(api_keys) if api_keys else '[]'
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("""
|
||||
INSERT INTO model_providers (uuid, name, requester, base_url, api_keys)
|
||||
VALUES (:uuid, :name, :requester, :base_url, :api_keys)
|
||||
"""),
|
||||
{
|
||||
'uuid': provider_uuid,
|
||||
'name': provider_name,
|
||||
'requester': requester,
|
||||
'base_url': base_url,
|
||||
'api_keys': api_keys_json,
|
||||
},
|
||||
)
|
||||
provider_cache[cache_key] = provider_uuid
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('UPDATE embedding_models SET provider_uuid = :provider_uuid WHERE uuid = :uuid'),
|
||||
{'provider_uuid': provider_uuid, 'uuid': model_uuid},
|
||||
)
|
||||
|
||||
async def _cleanup_columns(self):
|
||||
"""Remove deprecated columns from model tables"""
|
||||
|
||||
llm_columns = await self._get_columns('llm_models')
|
||||
deprecated_llm_cols = ['requester', 'requester_config', 'api_keys', 'description', 'source', 'space_model_id']
|
||||
for col in deprecated_llm_cols:
|
||||
if col in llm_columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE llm_models DROP COLUMN IF EXISTS {col}')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE llm_models DROP COLUMN {col}')
|
||||
)
|
||||
|
||||
embedding_columns = await self._get_columns('embedding_models')
|
||||
deprecated_embedding_cols = [
|
||||
'requester',
|
||||
'requester_config',
|
||||
'api_keys',
|
||||
'description',
|
||||
'source',
|
||||
'space_model_id',
|
||||
]
|
||||
for col in deprecated_embedding_cols:
|
||||
if col in embedding_columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE embedding_models DROP COLUMN IF EXISTS {col}')
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE embedding_models DROP COLUMN {col}')
|
||||
)
|
||||
|
||||
async def _get_columns(self, table_name: str) -> list:
|
||||
"""Get column names for a table"""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
return [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
all_result = result.fetchall()
|
||||
return [row[1] for row in all_result]
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,25 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(17)
|
||||
class MoveCloudServiceUrl(migration.DBMigration):
|
||||
"""迁移云服务 URL 配置"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""升级"""
|
||||
if 'space' not in self.ap.instance_config.data:
|
||||
self.ap.instance_config.data['space'] = {
|
||||
'url': 'https://space.langbot.app',
|
||||
'models_gateway_api_url': 'https://api.langbot.cloud/v1',
|
||||
'oauth_authorize_url': 'https://space.langbot.app/auth/authorize',
|
||||
'disable_models_service': False,
|
||||
}
|
||||
|
||||
if 'plugin' in self.ap.instance_config.data:
|
||||
self.ap.instance_config.data['plugin'].pop('cloud_service_url', None)
|
||||
|
||||
await self.ap.instance_config.dump_config()
|
||||
|
||||
async def downgrade(self):
|
||||
"""降级"""
|
||||
pass
|
||||
@@ -1,58 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(18)
|
||||
class DBMigrateAddEmojiSupport(migration.DBMigration):
|
||||
"""Add emoji field to knowledge_bases, external_knowledge_bases and legacy_pipelines tables"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
# Add emoji field to knowledge_bases
|
||||
await self._add_emoji_to_table('knowledge_bases', '📚')
|
||||
|
||||
# Add emoji field to external_knowledge_bases
|
||||
await self._add_emoji_to_table('external_knowledge_bases', '🔗')
|
||||
|
||||
# Add emoji field to legacy_pipelines
|
||||
await self._add_emoji_to_table('legacy_pipelines', '⚙️')
|
||||
|
||||
async def _add_emoji_to_table(self, table_name: str, default_emoji: str):
|
||||
"""Add emoji column to specified table if it doesn't exist"""
|
||||
# Get all column names from the table
|
||||
columns = []
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';"
|
||||
)
|
||||
)
|
||||
all_result = result.fetchall()
|
||||
columns = [row[0] for row in all_result]
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
all_result = result.fetchall()
|
||||
columns = [row[1] for row in all_result]
|
||||
|
||||
# Check and add emoji column
|
||||
if 'emoji' not in columns:
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10) DEFAULT '{default_emoji}'")
|
||||
)
|
||||
else:
|
||||
# SQLite doesn't support DEFAULT with emoji directly in ALTER TABLE
|
||||
# Add column without default first
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE {table_name} ADD COLUMN emoji VARCHAR(10)')
|
||||
)
|
||||
|
||||
# Set default emoji value for existing records
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f"UPDATE {table_name} SET emoji = '{default_emoji}' WHERE emoji IS NULL")
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,24 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(19)
|
||||
class DBMigrateMonitoringMessageRole(migration.DBMigration):
|
||||
"""Add role column to monitoring_messages table"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
try:
|
||||
sql_text = sqlalchemy.text("ALTER TABLE monitoring_messages ADD COLUMN role VARCHAR(50) DEFAULT 'user'")
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
except Exception:
|
||||
# Column may already exist
|
||||
pass
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
try:
|
||||
sql_text = sqlalchemy.text('ALTER TABLE monitoring_messages DROP COLUMN role')
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
except Exception:
|
||||
pass
|
||||
-161
@@ -1,161 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(20)
|
||||
class DBMigrateKnowledgeEnginePluginArchitecture(migration.DBMigration):
|
||||
"""Migrate to unified Knowledge Engine plugin architecture.
|
||||
|
||||
Changes:
|
||||
- Backup existing knowledge_bases data to knowledge_bases_backup
|
||||
- Clear knowledge_bases table and add new plugin architecture columns
|
||||
- Drop old columns (PostgreSQL only; SQLite leaves them unmapped)
|
||||
- Preserve external_knowledge_bases table as-is for future migration
|
||||
- Set rag_plugin_migration_needed flag in metadata if old data exists
|
||||
"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
has_internal_data = await self._backup_knowledge_bases()
|
||||
has_external_data = await self._check_external_knowledge_bases()
|
||||
await self._clear_knowledge_bases()
|
||||
await self._add_columns_to_knowledge_bases()
|
||||
await self._drop_old_columns()
|
||||
if has_internal_data or has_external_data:
|
||||
await self._set_migration_flag()
|
||||
|
||||
async def _get_table_columns(self, table_name: str) -> list[str]:
|
||||
"""Get column names from a table (works for both SQLite and PostgreSQL)."""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'SELECT column_name FROM information_schema.columns WHERE table_name = :table_name;'
|
||||
).bindparams(table_name=table_name)
|
||||
)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
else:
|
||||
# SQLite PRAGMA does not support bind parameters; validate identifier.
|
||||
if not table_name.isidentifier():
|
||||
raise ValueError(f'Invalid table name: {table_name}')
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
return [row[1] for row in result.fetchall()]
|
||||
|
||||
async def _table_exists(self, table_name: str) -> bool:
|
||||
"""Check if a table exists."""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = :table_name);'
|
||||
).bindparams(table_name=table_name)
|
||||
)
|
||||
return result.scalar()
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name;").bindparams(
|
||||
table_name=table_name
|
||||
)
|
||||
)
|
||||
return result.first() is not None
|
||||
|
||||
async def _backup_knowledge_bases(self) -> bool:
|
||||
"""Backup knowledge_bases data. Returns True if data was backed up."""
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text('SELECT COUNT(*) FROM knowledge_bases;'))
|
||||
count = result.scalar()
|
||||
if count == 0:
|
||||
return False
|
||||
|
||||
# Drop backup table if it already exists (from a previous failed migration)
|
||||
if await self._table_exists('knowledge_bases_backup'):
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.text('DROP TABLE knowledge_bases_backup;'))
|
||||
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('CREATE TABLE knowledge_bases_backup AS SELECT * FROM knowledge_bases;')
|
||||
)
|
||||
self.ap.logger.info(
|
||||
'Backed up %d knowledge base(s) to knowledge_bases_backup table.',
|
||||
count,
|
||||
)
|
||||
return True
|
||||
|
||||
async def _check_external_knowledge_bases(self) -> bool:
|
||||
"""Check if external_knowledge_bases table exists and has data.
|
||||
|
||||
The table is preserved as-is (not dropped) for future migration.
|
||||
"""
|
||||
if not await self._table_exists('external_knowledge_bases'):
|
||||
return False
|
||||
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT COUNT(*) FROM external_knowledge_bases;')
|
||||
)
|
||||
count = result.scalar()
|
||||
if count > 0:
|
||||
self.ap.logger.info(
|
||||
'Found %d external knowledge base(s) in external_knowledge_bases table. '
|
||||
'Table preserved for future migration.',
|
||||
count,
|
||||
)
|
||||
return count > 0
|
||||
|
||||
async def _clear_knowledge_bases(self):
|
||||
"""Clear all rows from knowledge_bases table (preserve table structure)."""
|
||||
await self.ap.persistence_mgr.execute_async(sqlalchemy.text('DELETE FROM knowledge_bases;'))
|
||||
|
||||
async def _add_columns_to_knowledge_bases(self):
|
||||
"""Add new RAG plugin architecture columns to knowledge_bases table."""
|
||||
columns = await self._get_table_columns('knowledge_bases')
|
||||
|
||||
new_columns = {
|
||||
'knowledge_engine_plugin_id': 'VARCHAR',
|
||||
'collection_id': 'VARCHAR',
|
||||
'creation_settings': 'TEXT', # JSON stored as TEXT for SQLite compatibility
|
||||
'retrieval_settings': 'TEXT',
|
||||
}
|
||||
|
||||
for col_name, col_type in new_columns.items():
|
||||
if col_name not in columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE knowledge_bases ADD COLUMN {col_name} {col_type};')
|
||||
)
|
||||
|
||||
async def _drop_old_columns(self):
|
||||
"""Drop embedding_model_uuid and top_k columns (PostgreSQL only).
|
||||
|
||||
SQLite does not support DROP COLUMN in older versions, so we leave the
|
||||
columns in place — the SQLAlchemy entity simply won't map them.
|
||||
"""
|
||||
if self.ap.persistence_mgr.db.name != 'postgresql':
|
||||
return
|
||||
|
||||
columns = await self._get_table_columns('knowledge_bases')
|
||||
|
||||
if 'embedding_model_uuid' in columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE knowledge_bases DROP COLUMN embedding_model_uuid;')
|
||||
)
|
||||
|
||||
if 'top_k' in columns:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('ALTER TABLE knowledge_bases DROP COLUMN top_k;')
|
||||
)
|
||||
|
||||
async def _set_migration_flag(self):
|
||||
"""Set rag_plugin_migration_needed flag in metadata table."""
|
||||
# Check if the key already exists
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("SELECT value FROM metadata WHERE key = 'rag_plugin_migration_needed';")
|
||||
)
|
||||
row = result.first()
|
||||
if row is not None:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("UPDATE metadata SET value = 'true' WHERE key = 'rag_plugin_migration_needed';")
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("INSERT INTO metadata (key, value) VALUES ('rag_plugin_migration_needed', 'true');")
|
||||
)
|
||||
self.ap.logger.info('Set rag_plugin_migration_needed=true in metadata.')
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,74 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(21)
|
||||
class DBMigrateMergeExceptionHandling(migration.DBMigration):
|
||||
"""Merge hide-exception and block-failed-request-output into a single exception-handling select option,
|
||||
and add failure-hint field.
|
||||
|
||||
Conversion logic:
|
||||
- block-failed-request-output=true -> exception-handling: hide
|
||||
- hide-exception=true -> exception-handling: show-hint
|
||||
- hide-exception=false -> exception-handling: show-error
|
||||
"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
if 'output' not in config:
|
||||
config['output'] = {}
|
||||
if 'misc' not in config['output']:
|
||||
config['output']['misc'] = {}
|
||||
|
||||
misc = config['output']['misc']
|
||||
|
||||
# Determine new exception-handling value from legacy fields
|
||||
hide_exception = misc.get('hide-exception', True)
|
||||
block_failed = misc.get('block-failed-request-output', False)
|
||||
|
||||
if block_failed:
|
||||
exception_handling = 'hide'
|
||||
elif hide_exception:
|
||||
exception_handling = 'show-hint'
|
||||
else:
|
||||
exception_handling = 'show-error'
|
||||
|
||||
misc['exception-handling'] = exception_handling
|
||||
|
||||
# Add failure-hint with default value
|
||||
misc['failure-hint'] = 'Request failed.'
|
||||
|
||||
# Remove legacy fields
|
||||
misc.pop('hide-exception', None)
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,73 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(22)
|
||||
class DBMigrateMonitoringUserId(migration.DBMigration):
|
||||
"""Add user_id and user_name columns to monitoring_sessions table
|
||||
|
||||
This migration adds the missing user_id column and also ensures user_name
|
||||
column exists (in case migration 21 failed or was skipped).
|
||||
"""
|
||||
|
||||
async def _table_exists(self, table_name: str) -> bool:
|
||||
"""Check if a table exists (works for both SQLite and PostgreSQL)."""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = :table_name);'
|
||||
).bindparams(table_name=table_name)
|
||||
)
|
||||
return bool(result.scalar())
|
||||
else:
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("SELECT name FROM sqlite_master WHERE type='table' AND name=:table_name;").bindparams(
|
||||
table_name=table_name
|
||||
)
|
||||
)
|
||||
return result.first() is not None
|
||||
|
||||
async def _get_table_columns(self, table_name: str) -> list[str]:
|
||||
"""Get column names from a table (works for both SQLite and PostgreSQL)."""
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'SELECT column_name FROM information_schema.columns WHERE table_name = :table_name;'
|
||||
).bindparams(table_name=table_name)
|
||||
)
|
||||
return [row[0] for row in result.fetchall()]
|
||||
else:
|
||||
if not table_name.isidentifier():
|
||||
raise ValueError(f'Invalid table name: {table_name}')
|
||||
result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});'))
|
||||
return [row[1] for row in result.fetchall()]
|
||||
|
||||
async def _add_column_if_not_exists(self, table_name: str, column_name: str, column_type: str):
|
||||
"""Add a column to a table if it does not already exist."""
|
||||
columns = await self._get_table_columns(table_name)
|
||||
if column_name in columns:
|
||||
self.ap.logger.debug('%s column already exists in %s.', column_name, table_name)
|
||||
return
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(f'ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type};')
|
||||
)
|
||||
self.ap.logger.info('Added %s column to %s table.', column_name, table_name)
|
||||
|
||||
async def upgrade(self):
|
||||
# Check if monitoring_sessions table exists
|
||||
if not await self._table_exists('monitoring_sessions'):
|
||||
self.ap.logger.warning('monitoring_sessions table does not exist, skipping migration.')
|
||||
return
|
||||
|
||||
# Add user_id column to monitoring_sessions table
|
||||
await self._add_column_if_not_exists('monitoring_sessions', 'user_id', 'VARCHAR(255)')
|
||||
|
||||
# Add user_name column to monitoring_sessions table (in case migration 21 failed)
|
||||
await self._add_column_if_not_exists('monitoring_sessions', 'user_name', 'VARCHAR(255)')
|
||||
|
||||
# Add user_name column to monitoring_messages table (in case migration 21 failed)
|
||||
if await self._table_exists('monitoring_messages'):
|
||||
await self._add_column_if_not_exists('monitoring_messages', 'user_name', 'VARCHAR(255)')
|
||||
|
||||
async def downgrade(self):
|
||||
pass
|
||||
@@ -1,102 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(23)
|
||||
class DBMigrateModelFallbackConfig(migration.DBMigration):
|
||||
"""Convert model field from plain UUID string to object with primary/fallbacks"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
if 'ai' not in config or 'local-agent' not in config['ai']:
|
||||
continue
|
||||
|
||||
local_agent = config['ai']['local-agent']
|
||||
changed = False
|
||||
|
||||
# Convert model from string to object
|
||||
model_value = local_agent.get('model', '')
|
||||
if isinstance(model_value, str):
|
||||
local_agent['model'] = {
|
||||
'primary': model_value,
|
||||
'fallbacks': [],
|
||||
}
|
||||
changed = True
|
||||
|
||||
# Remove leftover fallback-models field if present
|
||||
if 'fallback-models' in local_agent:
|
||||
del local_agent['fallback-models']
|
||||
changed = True
|
||||
|
||||
if not changed:
|
||||
continue
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('SELECT uuid, config FROM legacy_pipelines')
|
||||
)
|
||||
pipelines = result.fetchall()
|
||||
|
||||
current_version = self.ap.ver_mgr.get_current_version()
|
||||
|
||||
for pipeline_row in pipelines:
|
||||
uuid = pipeline_row[0]
|
||||
config = json.loads(pipeline_row[1]) if isinstance(pipeline_row[1], str) else pipeline_row[1]
|
||||
|
||||
if 'ai' not in config or 'local-agent' not in config['ai']:
|
||||
continue
|
||||
|
||||
local_agent = config['ai']['local-agent']
|
||||
|
||||
# Convert model from object back to string
|
||||
model_value = local_agent.get('model', '')
|
||||
if isinstance(model_value, dict):
|
||||
local_agent['model'] = model_value.get('primary', '')
|
||||
else:
|
||||
continue
|
||||
|
||||
# Update using raw SQL with compatibility for both SQLite and PostgreSQL
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config::jsonb, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text(
|
||||
'UPDATE legacy_pipelines SET config = :config, for_version = :for_version WHERE uuid = :uuid'
|
||||
),
|
||||
{'config': json.dumps(config), 'for_version': current_version, 'uuid': uuid},
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
from .. import migration
|
||||
|
||||
import sqlalchemy
|
||||
import json
|
||||
|
||||
|
||||
@migration.migration_class(24)
|
||||
class DBMigrateWecomBotWebSocketMode(migration.DBMigration):
|
||||
"""Add enable-webhook field to existing wecombot adapter configs.
|
||||
|
||||
Existing wecombot bots were all using webhook mode, so we set
|
||||
enable-webhook=true to preserve their behavior after the new
|
||||
WebSocket long connection mode is introduced as default.
|
||||
"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text("SELECT uuid, adapter_config FROM bots WHERE adapter = 'wecombot'")
|
||||
)
|
||||
bots = result.fetchall()
|
||||
|
||||
for bot_row in bots:
|
||||
bot_uuid = bot_row[0]
|
||||
adapter_config = json.loads(bot_row[1]) if isinstance(bot_row[1], str) else bot_row[1]
|
||||
|
||||
if 'enable-webhook' in adapter_config:
|
||||
continue
|
||||
|
||||
# Determine mode based on existing config: if webhook fields are present, keep webhook mode
|
||||
has_webhook_config = bool(
|
||||
adapter_config.get('Token') and adapter_config.get('EncodingAESKey') and adapter_config.get('Corpid')
|
||||
)
|
||||
adapter_config['enable-webhook'] = has_webhook_config
|
||||
|
||||
if self.ap.persistence_mgr.db.name == 'postgresql':
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('UPDATE bots SET adapter_config = :config::jsonb WHERE uuid = :uuid'),
|
||||
{'config': json.dumps(adapter_config), 'uuid': bot_uuid},
|
||||
)
|
||||
else:
|
||||
await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.text('UPDATE bots SET adapter_config = :config WHERE uuid = :uuid'),
|
||||
{'config': json.dumps(adapter_config), 'uuid': bot_uuid},
|
||||
)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
pass
|
||||
@@ -1,15 +0,0 @@
|
||||
import sqlalchemy
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(25)
|
||||
class DBMigrateBotPipelineRoutingRules(migration.DBMigration):
|
||||
"""Add pipeline_routing_rules column to bots table"""
|
||||
|
||||
async def upgrade(self):
|
||||
sql_text = sqlalchemy.text("ALTER TABLE bots ADD COLUMN pipeline_routing_rules JSON NOT NULL DEFAULT '[]'")
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
|
||||
async def downgrade(self):
|
||||
sql_text = sqlalchemy.text('ALTER TABLE bots DROP COLUMN pipeline_routing_rules')
|
||||
await self.ap.persistence_mgr.execute_async(sql_text)
|
||||
@@ -1,17 +0,0 @@
|
||||
from langbot.pkg.entity.persistence import monitoring as persistence_monitoring
|
||||
from .. import migration
|
||||
|
||||
|
||||
@migration.migration_class(26)
|
||||
class DBMigrateMonitoringToolCalls(migration.DBMigration):
|
||||
"""Add monitoring_tool_calls table"""
|
||||
|
||||
async def upgrade(self):
|
||||
"""Upgrade"""
|
||||
async with self.ap.persistence_mgr.get_db_engine().begin() as conn:
|
||||
await conn.run_sync(persistence_monitoring.MonitoringToolCall.__table__.create, checkfirst=True)
|
||||
|
||||
async def downgrade(self):
|
||||
"""Downgrade"""
|
||||
async with self.ap.persistence_mgr.get_db_engine().begin() as conn:
|
||||
await conn.run_sync(persistence_monitoring.MonitoringToolCall.__table__.drop, checkfirst=True)
|
||||
Reference in New Issue
Block a user