mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-28 04:16:50 +08:00
Merge remote-tracking branch 'origin/dev/4.11.x' into dev/4.11.x
# Conflicts: # uv.lock # web/src/i18n/locales/en-US.ts # web/src/i18n/locales/ja-JP.ts # web/src/i18n/locales/zh-Hans.ts # web/src/i18n/locales/zh-Hant.ts
This commit is contained in:
@@ -148,6 +148,21 @@ class TestEmbedWidgetEndpoint:
|
||||
assert 'javascript' in response.content_type
|
||||
fake_embed_app.platform_mgr.resolve_public_bot.assert_any_await('a1b2c3d4-5678-90ab-cdef-123456789abc')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
('query', 'expected_base'),
|
||||
[('', 'https://public.example/bot'), ('?preview=wizard', 'http://localhost')],
|
||||
)
|
||||
async def test_widget_preview_uses_current_backend(
|
||||
self, quart_test_client, fake_embed_app, monkeypatch, query, expected_base
|
||||
):
|
||||
monkeypatch.setitem(fake_embed_app.instance_config.data['api'], 'webhook_prefix', 'https://public.example/bot')
|
||||
response = await quart_test_client.get('/api/v1/embed/a1b2c3d4-5678-90ab-cdef-123456789abc/widget.js' + query)
|
||||
assert response.status_code == 200
|
||||
body = await response.get_data(as_text=True)
|
||||
assert f'baseUrl: "{expected_base}"' in body
|
||||
assert f'logoUrl: "{expected_base}"' in body
|
||||
|
||||
def test_widget_template_cache_reloads_after_file_change(self, monkeypatch, tmp_path):
|
||||
"""Development edits to widget.js take effect without restarting the backend."""
|
||||
import langbot.pkg.api.http.controller.groups.pipelines.embed as embed
|
||||
|
||||
@@ -9,7 +9,7 @@ Run: uv run pytest tests/integration/api/test_monitoring.py -q
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, Mock
|
||||
from unittest.mock import MagicMock, AsyncMock, Mock, patch
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tests.factories import FakeApp
|
||||
@@ -242,6 +242,22 @@ class TestMonitoringSessionsEndpoint:
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_sessions_forwards_user_search_and_page_window(self, quart_test_client, fake_monitoring_app):
|
||||
fake_monitoring_app.monitoring_service.get_sessions.reset_mock()
|
||||
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/sessions?botId=bot-1&userQuery=alice&limit=20&offset=40',
|
||||
headers={'Authorization': 'Bearer test_token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
kwargs = fake_monitoring_app.monitoring_service.get_sessions.await_args.kwargs
|
||||
assert kwargs['bot_ids'] == ['bot-1']
|
||||
assert kwargs['user_query'] == 'alice'
|
||||
assert kwargs['limit'] == 20
|
||||
assert kwargs['offset'] == 40
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
class TestMonitoringErrorsEndpoint:
|
||||
@@ -264,13 +280,20 @@ class TestMonitoringAllDataEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_data_success(self, quart_test_client):
|
||||
"""GET /api/v1/monitoring/data returns all data."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/data', headers={'Authorization': 'Bearer test_token'}
|
||||
)
|
||||
traffic = {'series': [], 'truncated': False}
|
||||
with patch(
|
||||
'langbot.pkg.api.http.controller.groups.monitoring.get_traffic_series',
|
||||
new=AsyncMock(return_value=traffic),
|
||||
) as get_traffic:
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/data', headers={'Authorization': 'Bearer test_token'}
|
||||
)
|
||||
get_traffic.assert_awaited_once()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert 'overview' in data['data']
|
||||
assert data['data']['traffic'] == traffic
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('mock_circular_import_chain')
|
||||
@@ -278,13 +301,19 @@ class TestMonitoringDetailsEndpoints:
|
||||
"""Tests for detail endpoints."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_session_analysis(self, quart_test_client):
|
||||
async def test_get_session_analysis(self, quart_test_client, fake_monitoring_app):
|
||||
"""GET /api/v1/monitoring/sessions/{id}/analysis."""
|
||||
response = await quart_test_client.get(
|
||||
'/api/v1/monitoring/sessions/sess-1/analysis', headers={'Authorization': 'Bearer test_token'}
|
||||
'/api/v1/monitoring/sessions/sess-1/analysis'
|
||||
'?startTime=2026-08-31T16%3A00%3A00.000Z'
|
||||
'&endTime=2026-09-01T15%3A59%3A59.999Z',
|
||||
headers={'Authorization': 'Bearer test_token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
kwargs = fake_monitoring_app.monitoring_service.get_session_analysis.await_args.kwargs
|
||||
assert kwargs['start_time'].isoformat() == '2026-08-31T16:00:00'
|
||||
assert kwargs['end_time'].isoformat() == '2026-09-01T15:59:59.999000'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_message_details(self, quart_test_client):
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from quart import Quart
|
||||
|
||||
from langbot.pkg.api.http.controller.groups import user as user_module
|
||||
from langbot.pkg.api.http.controller.groups.user import UserRouterGroup
|
||||
from langbot.pkg.api.http.service.user import UserService
|
||||
from langbot.pkg.core.stages.genkeys import GenKeysStage
|
||||
from langbot.pkg.persistence.mgr import PersistenceManager
|
||||
from langbot.pkg.utils import constants
|
||||
from langbot.pkg.workspace.collaboration import WorkspaceCollaborationService
|
||||
from langbot.pkg.workspace.service import WorkspaceService
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
||||
|
||||
|
||||
async def test_generated_recovery_code_resets_real_sqlite_account(tmp_path, monkeypatch):
|
||||
"""Exercise generation, reset, and old/new password login without mocked user services."""
|
||||
monkeypatch.setattr(constants, 'instance_id', 'recovery-journey')
|
||||
monkeypatch.setattr(user_module, '_reset_password_state', {'window_started_at': 0.0, 'attempts': 0})
|
||||
monkeypatch.setattr(user_module, 'asyncio', SimpleNamespace(sleep=AsyncMock()))
|
||||
application = SimpleNamespace(
|
||||
logger=logging.getLogger('recovery-password-journey'),
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'database': {'use': 'sqlite', 'sqlite': {'path': str(tmp_path / 'recovery.db')}},
|
||||
'system': {
|
||||
'jwt': {'secret': 'recovery-journey-test-secret-only', 'expire': 3600},
|
||||
'recovery_key': '',
|
||||
},
|
||||
},
|
||||
dump_config=AsyncMock(),
|
||||
),
|
||||
)
|
||||
await GenKeysStage().run(application)
|
||||
key = application.instance_config.data['system']['recovery_key']
|
||||
assert len(key) == 8
|
||||
assert set(key) <= set('23456789ABCDEFGHJKLMNPQRSTUVWXYZ')
|
||||
persistence = PersistenceManager(application)
|
||||
application.persistence_mgr = persistence
|
||||
try:
|
||||
await persistence.initialize()
|
||||
application.workspace_service = WorkspaceService(application, instance_uuid='recovery-journey')
|
||||
application.workspace_collaboration_service = WorkspaceCollaborationService(
|
||||
application, application.workspace_service
|
||||
)
|
||||
application.user_service = UserService(application)
|
||||
quart_app = Quart(__name__)
|
||||
await UserRouterGroup(application, quart_app).initialize()
|
||||
client = quart_app.test_client()
|
||||
|
||||
initial = await client.post(
|
||||
'/api/v1/user/init', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
|
||||
)
|
||||
assert initial.status_code == 200
|
||||
assert (await initial.get_json())['code'] == 0
|
||||
|
||||
payload = {'user': 'owner@example.com', 'recovery_key': 'WRONG', 'new_password': 'RecoveredPass1!'}
|
||||
wrong = await client.post('/api/v1/user/reset-password', json=payload)
|
||||
assert wrong.status_code == 403
|
||||
unchanged = await client.post(
|
||||
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
|
||||
)
|
||||
assert (await unchanged.get_json())['code'] == 0
|
||||
|
||||
reset = await client.post('/api/v1/user/reset-password', json={**payload, 'recovery_key': key})
|
||||
assert reset.status_code == 200
|
||||
assert (await reset.get_json())['code'] == 0
|
||||
old_login = await client.post(
|
||||
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'OriginalPass1!'}
|
||||
)
|
||||
assert (await old_login.get_json())['code'] != 0
|
||||
new_login = await client.post(
|
||||
'/api/v1/user/auth', json={'user': 'owner@example.com', 'password': 'RecoveredPass1!'}
|
||||
)
|
||||
new_data = await new_login.get_json()
|
||||
assert new_data['code'] == 0
|
||||
assert new_data['data']['token']
|
||||
finally:
|
||||
await persistence.get_db_engine().dispose()
|
||||
@@ -310,6 +310,8 @@ class TestUserInitEndpoint:
|
||||
'invitation_registration_enabled': True,
|
||||
'password_login_enabled': True,
|
||||
'space_login_enabled': False,
|
||||
'passkey_login_enabled': True,
|
||||
'passkey_supported': True,
|
||||
}
|
||||
fake_api_app.user_service.get_login_capabilities.assert_awaited_once_with()
|
||||
fake_api_app.user_service.get_first_user.assert_not_awaited()
|
||||
@@ -334,6 +336,8 @@ class TestUserInitEndpoint:
|
||||
'invitation_registration_enabled': False,
|
||||
'password_login_enabled': False,
|
||||
'space_login_enabled': True,
|
||||
'passkey_login_enabled': True,
|
||||
'passkey_supported': True,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -355,6 +359,8 @@ class TestUserInitEndpoint:
|
||||
'invitation_registration_enabled': True,
|
||||
'password_login_enabled': False,
|
||||
'space_login_enabled': True,
|
||||
'passkey_login_enabled': True,
|
||||
'passkey_supported': True,
|
||||
}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Integration smoke tests for Passkey API endpoints.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.factories import FakeApp
|
||||
from tests.utils.import_isolation import isolated_sys_modules, MockLifecycleControlScope
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures('mock_circular_import_chain')]
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
def mock_circular_import_chain():
|
||||
class FakeMinimalApplication:
|
||||
pass
|
||||
|
||||
mock_app = Mock()
|
||||
mock_app.Application = FakeMinimalApplication
|
||||
|
||||
mock_entities = Mock()
|
||||
mock_entities.LifecycleControlScope = MockLifecycleControlScope
|
||||
|
||||
clear = [
|
||||
'langbot.pkg.api.http.controller.group',
|
||||
'langbot.pkg.api.http.controller.groups',
|
||||
'langbot.pkg.api.http.controller.groups.system',
|
||||
'langbot.pkg.api.http.controller.groups.user',
|
||||
'langbot.pkg.api.http.controller.main',
|
||||
]
|
||||
|
||||
with isolated_sys_modules(
|
||||
mocks={
|
||||
'langbot.pkg.core.app': mock_app,
|
||||
'langbot.pkg.core.entities': mock_entities,
|
||||
},
|
||||
clear=clear,
|
||||
):
|
||||
import langbot.pkg.api.http.controller.groups.user as _user_group # noqa: E402, F401
|
||||
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_api_app():
|
||||
app = FakeApp()
|
||||
app.instance_config.data.update(
|
||||
{
|
||||
'api': {'port': 5300},
|
||||
'system': {'allow_modify_login_info': True},
|
||||
}
|
||||
)
|
||||
app.user_service = Mock()
|
||||
app.user_service.verify_jwt_token = AsyncMock(side_effect=ValueError('Invalid token'))
|
||||
app.user_service.get_user_by_email = AsyncMock(return_value=Mock())
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def quart_test_client(fake_api_app, http_controller_cls):
|
||||
controller = http_controller_cls(fake_api_app)
|
||||
await controller.initialize()
|
||||
|
||||
client = controller.quart_app.test_client()
|
||||
yield client
|
||||
|
||||
|
||||
class TestPasskeyPublicEndpoints:
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_options_endpoint(self, quart_test_client, fake_api_app):
|
||||
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
|
||||
return_value=({'challenge': 'test_chal', 'rpId': 'localhost'}, 'token_123')
|
||||
)
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/options',
|
||||
json={'origin': 'http://localhost:3000'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['challenge_token'] == 'token_123'
|
||||
assert data['data']['options']['rpId'] == 'localhost'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_verify_missing_payload(self, quart_test_client, fake_api_app):
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/verify',
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['code'] != 0
|
||||
assert 'Missing challenge_token or credential' in data['msg']
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_verify_success(self, quart_test_client, fake_api_app):
|
||||
fake_api_app.user_service.verify_passkey_authentication = AsyncMock(
|
||||
return_value=('jwt_token_abc', Mock(user='user@example.com'))
|
||||
)
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/verify',
|
||||
json={'challenge_token': 'token_123', 'credential': {'id': 'cred_id'}},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
assert data['data']['token'] == 'jwt_token_abc'
|
||||
assert data['data']['user'] == 'user@example.com'
|
||||
|
||||
|
||||
class TestPasskeyProtectedEndpoints:
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_options_requires_auth(self, quart_test_client):
|
||||
response = await quart_test_client.post('/api/v1/user/passkey/register/options', json={})
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passkeys_list_requires_auth(self, quart_test_client):
|
||||
response = await quart_test_client.get('/api/v1/user/passkeys')
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestPasskeyReverseProxyScenarios:
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_options_respects_custom_origin_body_behind_proxy(self, quart_test_client, fake_api_app):
|
||||
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
|
||||
return_value=({'challenge': 'test_chal', 'rpId': 'proxy.company.com'}, 'token_proxy')
|
||||
)
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/options',
|
||||
json={'origin': 'https://proxy.company.com:8443'},
|
||||
headers={'Host': '127.0.0.1:5300'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = await response.get_json()
|
||||
assert data['code'] == 0
|
||||
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
|
||||
rp_id='proxy.company.com',
|
||||
origin='https://proxy.company.com:8443',
|
||||
email=None,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_options_falls_back_to_origin_header(self, quart_test_client, fake_api_app):
|
||||
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
|
||||
return_value=({'challenge': 'test_chal', 'rpId': 'bot.example.com'}, 'token_header')
|
||||
)
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/options',
|
||||
json={},
|
||||
headers={'Origin': 'https://bot.example.com'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
|
||||
rp_id='bot.example.com',
|
||||
origin='https://bot.example.com',
|
||||
email=None,
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_auth_options_falls_back_to_referer_header(self, quart_test_client, fake_api_app):
|
||||
fake_api_app.user_service.generate_passkey_authentication_options = AsyncMock(
|
||||
return_value=({'challenge': 'test_chal', 'rpId': 'bot.example.com'}, 'token_referer')
|
||||
)
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/user/passkey/auth/options',
|
||||
json={},
|
||||
headers={'Referer': 'https://bot.example.com:9000/login'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
fake_api_app.user_service.generate_passkey_authentication_options.assert_awaited_once_with(
|
||||
rp_id='bot.example.com',
|
||||
origin='https://bot.example.com:9000',
|
||||
email=None,
|
||||
)
|
||||
@@ -27,7 +27,8 @@ async def space_oauth_api():
|
||||
execution=SimpleNamespace(instance_uuid='instance-a', placement_generation=1),
|
||||
)
|
||||
application = Mock()
|
||||
application.deployment = SimpleNamespace(multi_workspace_enabled=False)
|
||||
application.deployment = SimpleNamespace(multi_workspace_enabled=False, mode='oss')
|
||||
application.directory_projection_service = None
|
||||
application.persistence_mgr = None
|
||||
application.user_service.get_authenticated_account = AsyncMock(return_value=account)
|
||||
application.user_service.issue_space_oauth_state = AsyncMock(
|
||||
@@ -125,6 +126,26 @@ async def test_cloud_launch_state_is_server_issued_and_workspace_bound(space_oau
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_login_entry_uses_normal_stateful_oauth(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
|
||||
response = await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={
|
||||
'redirect_uri': 'http://localhost/auth/space/callback',
|
||||
'cloud_entry': '1',
|
||||
},
|
||||
headers={'Origin': 'http://localhost'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
authorize_url = (await response.get_json())['data']['authorize_url']
|
||||
assert authorize_url.startswith('https://space.example/authorize?state=')
|
||||
application.user_service.issue_space_oauth_state.assert_awaited_once_with('login')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_public_login_rejects_caller_supplied_state(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
@@ -249,10 +270,14 @@ async def test_server_side_webhook_origin_supports_bundled_ui(space_oauth_api):
|
||||
async def test_login_callback_requires_and_consumes_server_state(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
|
||||
missing = await client.post('/api/v1/user/space/callback', json={'code': 'oauth-code'})
|
||||
missing = await client.post('/api/v1/user/space/callback', json={'code': 'v4_oauth-code'})
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'oauth-code', 'state': 'opaque-login-state'},
|
||||
json={
|
||||
'code': 'v4_oauth-code',
|
||||
'state': 'opaque-login-state',
|
||||
'redirect_uri': 'https://oss.example/auth/space/callback',
|
||||
},
|
||||
)
|
||||
|
||||
assert (await missing.get_json())['code'] == 1
|
||||
@@ -260,12 +285,146 @@ async def test_login_callback_requires_and_consumes_server_state(space_oauth_api
|
||||
assert (await response.get_json())['data']['token'] == 'space-login-token'
|
||||
application.user_service.consume_space_oauth_state_details.assert_awaited_once_with('opaque-login-state', 'login')
|
||||
application.space_service.exchange_oauth_code.assert_awaited_once_with(
|
||||
'oauth-code',
|
||||
'v4_oauth-code',
|
||||
[WORKSPACE_UUID],
|
||||
{WORKSPACE_UUID: int(WORKSPACE_CREATED_AT.timestamp())},
|
||||
redirect_uri='https://oss.example/auth/space/callback',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_callback_rejects_downgraded_legacy_code(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v2_legacy-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
payload = await response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert payload['code'] == 1
|
||||
assert 'code contract' in payload['msg']
|
||||
application.space_service.exchange_oauth_code.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_login_callback_reconciles_authorized_workspace_before_local_authentication(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
calls: list[str] = []
|
||||
application.directory_projection_service = SimpleNamespace(
|
||||
reconcile_workspaces=AsyncMock(side_effect=lambda _workspace_uuids: calls.append('reconcile'))
|
||||
)
|
||||
application.space_service.exchange_oauth_code.return_value = {
|
||||
'access_token': 'space-access-token',
|
||||
'refresh_token': 'space-refresh-token',
|
||||
'expires_in': 3600,
|
||||
'cloud_workspace_uuid': WORKSPACE_UUID,
|
||||
}
|
||||
|
||||
authenticated_account = application.user_service.authenticate_space_user.return_value[1]
|
||||
|
||||
async def authenticate(*_args):
|
||||
calls.append('authenticate')
|
||||
return 'space-login-token', authenticated_account
|
||||
|
||||
application.user_service.authenticate_space_user.side_effect = authenticate
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
|
||||
assert calls == ['reconcile', 'authenticate']
|
||||
application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_login_callback_fails_closed_without_workspace_binding(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
payload = await response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert payload['code'] == 1
|
||||
assert 'Cloud Workspace binding' in payload['msg']
|
||||
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
|
||||
application.user_service.authenticate_space_user.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_login_callback_requires_code_binding_for_launch_state(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
|
||||
launch_workspace_uuid=WORKSPACE_UUID
|
||||
)
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
payload = await response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert payload['code'] == 1
|
||||
assert 'Workspace binding' in payload['msg']
|
||||
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
|
||||
application.user_service.authenticate_space_user.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_login_callback_rejects_conflicting_state_and_code_workspace_bindings(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.deployment.mode = 'cloud'
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
application.user_service.consume_space_oauth_state_details.return_value = SimpleNamespace(
|
||||
launch_workspace_uuid=WORKSPACE_UUID
|
||||
)
|
||||
application.space_service.exchange_oauth_code.return_value = {
|
||||
'access_token': 'space-access-token',
|
||||
'refresh_token': 'space-refresh-token',
|
||||
'expires_in': 3600,
|
||||
'cloud_workspace_uuid': 'workspace-from-another-flow',
|
||||
}
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
payload = await response.get_json()
|
||||
assert response.status_code == 200
|
||||
assert payload['code'] == 1
|
||||
assert 'Workspace binding' in payload['msg']
|
||||
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
|
||||
application.user_service.authenticate_space_user.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_oss_login_callback_does_not_request_cloud_reconciliation(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
application.directory_projection_service.reconcile_workspaces.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_callback_launch_state_selects_asserted_workspace(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
@@ -276,7 +435,7 @@ async def test_login_callback_launch_state_selects_asserted_workspace(space_oaut
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={'code': 'oauth-code', 'state': 'opaque-login-state'},
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-login-state'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -375,18 +534,22 @@ async def test_bind_callback_uses_opaque_state_and_never_treats_it_as_jwt(space_
|
||||
|
||||
rejected = await client.post(
|
||||
'/api/v1/user/bind-space',
|
||||
json={'code': 'attacker-code', 'state': 'jwt.must-not-be-used'},
|
||||
json={'code': 'v4_attacker-code', 'state': 'jwt.must-not-be-used'},
|
||||
)
|
||||
response = await client.post(
|
||||
'/api/v1/user/bind-space',
|
||||
json={'code': 'oauth-code', 'state': 'opaque-bind-state'},
|
||||
json={'code': 'v4_oauth-code', 'state': 'opaque-bind-state'},
|
||||
)
|
||||
|
||||
assert rejected.status_code == 401
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data']['token'] == 'rotated-account-token'
|
||||
application.user_service.verify_jwt_token.assert_not_awaited()
|
||||
application.user_service.bind_space_account.assert_awaited_once_with('owner@example.com', 'oauth-code')
|
||||
application.user_service.bind_space_account.assert_awaited_once_with(
|
||||
'owner@example.com',
|
||||
'v4_oauth-code',
|
||||
redirect_uri='http://localhost/auth/space/callback?mode=bind',
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -394,6 +557,7 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
|
||||
application, client = space_oauth_api
|
||||
application.user_service.consume_space_oauth_state.reset_mock()
|
||||
application.space_service.exchange_oauth_code.reset_mock()
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
@@ -414,3 +578,29 @@ async def test_direct_launch_assertion_does_not_consume_normal_oauth_state(space
|
||||
)
|
||||
application.user_service.consume_space_oauth_state.assert_not_awaited()
|
||||
application.space_service.exchange_oauth_code.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_direct_launch_reconciles_exact_workspace_before_resolving_access(space_oauth_api):
|
||||
application, client = space_oauth_api
|
||||
projected_account = SimpleNamespace(
|
||||
uuid='account-a',
|
||||
user='owner@example.com',
|
||||
account_type='space',
|
||||
status='active',
|
||||
)
|
||||
application.user_service.get_user_by_uuid = AsyncMock(return_value=projected_account)
|
||||
application.directory_projection_service = SimpleNamespace(reconcile_workspaces=AsyncMock())
|
||||
|
||||
response = await client.post(
|
||||
'/api/v1/user/space/callback',
|
||||
json={
|
||||
'workspace_uuid': WORKSPACE_UUID,
|
||||
'launch_assertion': 'signed-launch-token',
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data']['workspace_uuid'] == WORKSPACE_UUID
|
||||
application.directory_projection_service.reconcile_workspaces.assert_awaited_once_with((WORKSPACE_UUID,))
|
||||
application.user_service.get_user_by_uuid.assert_awaited_once_with('account-a')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from types import SimpleNamespace
|
||||
@@ -22,6 +23,7 @@ from langbot.pkg.api.http.service.apikey import ApiKeyService
|
||||
from langbot.pkg.api.http.service.user import ControlPlaneDirectoryRequiredError, UserService
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.entity.persistence.metadata import WorkspaceMetadata
|
||||
from langbot.pkg.entity.persistence import apikey
|
||||
from langbot.pkg.entity.persistence.user import User
|
||||
from langbot.pkg.entity.persistence.workspace import (
|
||||
Workspace,
|
||||
@@ -440,6 +442,264 @@ async def test_api_key_secret_is_one_time_and_viewer_cannot_manage_keys(workspac
|
||||
assert (await forbidden.get_json())['code'] == 'permission_denied'
|
||||
|
||||
|
||||
async def test_api_key_context_returns_bound_identity_without_workspace_permission(workspace_api):
|
||||
application, client, _, owner_token = workspace_api
|
||||
current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token))
|
||||
workspace_uuid = (await current_response.get_json())['data']['workspace']['uuid']
|
||||
|
||||
create_response = await client.post(
|
||||
'/api/v1/apikeys',
|
||||
headers=_auth(owner_token, workspace_uuid),
|
||||
json={'name': 'Context probe', 'scopes': []},
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
created = (await create_response.get_json())['data']['key']
|
||||
|
||||
missing_auth = await client.get('/api/v1/system/context')
|
||||
assert missing_auth.status_code == 401
|
||||
|
||||
invalid_auth = await client.get(
|
||||
'/api/v1/system/context',
|
||||
headers={'X-API-Key': 'lbk_invalid'},
|
||||
)
|
||||
assert invalid_auth.status_code == 401
|
||||
|
||||
invalid_capabilities = await client.get(
|
||||
'/api/v1/system/capabilities',
|
||||
headers={'X-API-Key': 'lbk_invalid'},
|
||||
)
|
||||
assert invalid_capabilities.status_code == 401
|
||||
|
||||
response = await client.get(
|
||||
'/api/v1/system/context',
|
||||
headers={
|
||||
'X-API-Key': created['key'],
|
||||
'X-Workspace-Id': 'caller-selected-workspace-must-be-ignored',
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data'] == {
|
||||
'instance_uuid': application.workspace_service.instance_uuid,
|
||||
'workspace_uuid': workspace_uuid,
|
||||
'api_key_id': created['uuid'],
|
||||
'permissions': [],
|
||||
}
|
||||
|
||||
capabilities_response = await client.get(
|
||||
'/api/v1/system/capabilities',
|
||||
headers={
|
||||
'X-API-Key': created['key'],
|
||||
'X-Workspace-Id': 'caller-selected-workspace-must-be-ignored',
|
||||
},
|
||||
)
|
||||
assert capabilities_response.status_code == 200
|
||||
capabilities = (await capabilities_response.get_json())['data']
|
||||
assert capabilities['schema_version'] == 1
|
||||
assert sorted(capabilities['operations']) == sorted(
|
||||
[
|
||||
'bot.list',
|
||||
'bot.get',
|
||||
'bot.create',
|
||||
'bot.update',
|
||||
'bot.delete',
|
||||
'pipeline.list',
|
||||
'pipeline.get',
|
||||
'pipeline.create',
|
||||
'pipeline.update',
|
||||
'pipeline.delete',
|
||||
'pipeline.copy',
|
||||
'task.list',
|
||||
'task.get',
|
||||
'knowledge_base.list',
|
||||
'knowledge_base.get',
|
||||
'knowledge_base.create',
|
||||
'knowledge_base.update',
|
||||
'knowledge_base.delete',
|
||||
'knowledge_base.file.list',
|
||||
'knowledge_base.file.store',
|
||||
'knowledge_base.file.delete',
|
||||
'knowledge_base.retrieve',
|
||||
'file.document.upload',
|
||||
'plugin.install.github',
|
||||
'plugin.install.marketplace',
|
||||
'plugin.install.local',
|
||||
'plugin.upgrade',
|
||||
'plugin.get',
|
||||
'plugin.list',
|
||||
'plugin.config.get',
|
||||
'plugin.config.update',
|
||||
'plugin.logs',
|
||||
'plugin.delete',
|
||||
'provider.list',
|
||||
'provider.get',
|
||||
'provider.create',
|
||||
'provider.update',
|
||||
'provider.delete',
|
||||
'provider.scan_models',
|
||||
'model.llm.list',
|
||||
'model.llm.get',
|
||||
'model.llm.create',
|
||||
'model.llm.update',
|
||||
'model.llm.delete',
|
||||
'model.llm.test',
|
||||
'model.embedding.list',
|
||||
'model.embedding.get',
|
||||
'model.embedding.create',
|
||||
'model.embedding.update',
|
||||
'model.embedding.delete',
|
||||
'model.embedding.test',
|
||||
'model.rerank.list',
|
||||
'model.rerank.get',
|
||||
'model.rerank.create',
|
||||
'model.rerank.update',
|
||||
'model.rerank.delete',
|
||||
'model.rerank.test',
|
||||
'skill.list',
|
||||
'skill.get',
|
||||
'skill.create',
|
||||
'skill.update',
|
||||
'skill.delete',
|
||||
'skill.files.list',
|
||||
'skill.files.read',
|
||||
'skill.files.write',
|
||||
'skill.preview',
|
||||
'skill.install.github',
|
||||
'skill.install.upload',
|
||||
'mcp_server.list',
|
||||
'mcp_server.get',
|
||||
'mcp_server.create',
|
||||
'mcp_server.update',
|
||||
'mcp_server.delete',
|
||||
'mcp_server.resources',
|
||||
'mcp_server.resource_templates',
|
||||
'mcp_server.resource_read',
|
||||
'mcp_server.logs',
|
||||
'mcp_server.test',
|
||||
]
|
||||
)
|
||||
assert all(item == {'supported': True} for item in capabilities['operations'].values())
|
||||
assert created['key'] not in await capabilities_response.get_data(as_text=True)
|
||||
|
||||
bearer_response = await client.get(
|
||||
'/api/v1/system/context',
|
||||
headers={'Authorization': f'Bearer {created["key"]}'},
|
||||
)
|
||||
assert bearer_response.status_code == 200
|
||||
assert (await bearer_response.get_json())['data']['api_key_id'] == created['uuid']
|
||||
|
||||
jwt_response = await client.get(
|
||||
'/api/v1/system/context',
|
||||
headers={'Authorization': f'Bearer {owner_token}'},
|
||||
)
|
||||
assert jwt_response.status_code == 401
|
||||
|
||||
await application.persistence_mgr.execute_async(
|
||||
sqlalchemy.update(apikey.ApiKey)
|
||||
.where(apikey.ApiKey.uuid == created['uuid'])
|
||||
.values(expires_at=datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - datetime.timedelta(seconds=1))
|
||||
)
|
||||
expired_capabilities = await client.get(
|
||||
'/api/v1/system/capabilities',
|
||||
headers={'X-API-Key': created['key']},
|
||||
)
|
||||
assert expired_capabilities.status_code == 401
|
||||
|
||||
revoke_response = await client.delete(
|
||||
f'/api/v1/apikeys/{created["id"]}',
|
||||
headers=_auth(owner_token, workspace_uuid),
|
||||
)
|
||||
assert revoke_response.status_code == 200
|
||||
|
||||
revoked_response = await client.get(
|
||||
'/api/v1/system/context',
|
||||
headers={'X-API-Key': created['key']},
|
||||
)
|
||||
assert revoked_response.status_code == 401
|
||||
revoked_capabilities = await client.get(
|
||||
'/api/v1/system/capabilities',
|
||||
headers={'X-API-Key': created['key']},
|
||||
)
|
||||
assert revoked_capabilities.status_code == 401
|
||||
|
||||
|
||||
async def test_api_key_can_query_tasks_with_public_contract_and_resource_permission(workspace_api):
|
||||
application, client, _, owner_token = workspace_api
|
||||
task_query = {}
|
||||
task_lookup = {}
|
||||
fake_task = SimpleNamespace(
|
||||
to_public_dict=lambda: {'id': 7, 'status': 'running', 'error': None, 'result': None},
|
||||
to_dict=lambda: {'id': 7, 'runtime': {'state': 'PENDING'}},
|
||||
)
|
||||
|
||||
def get_tasks_dict(*args, **kwargs):
|
||||
task_query.update(kwargs)
|
||||
if kwargs.get('public'):
|
||||
return {'tasks': []}
|
||||
return {'tasks': [], 'id_index': 1}
|
||||
|
||||
def get_task_by_id(*args, **kwargs):
|
||||
task_lookup.update(kwargs)
|
||||
return fake_task if args and args[0] == 7 else None
|
||||
|
||||
application.task_mgr = SimpleNamespace(
|
||||
get_tasks_dict=get_tasks_dict,
|
||||
get_task_by_id=get_task_by_id,
|
||||
)
|
||||
current_response = await client.get('/api/v1/workspaces/current', headers=_auth(owner_token))
|
||||
workspace_uuid = (await current_response.get_json())['data']['workspace']['uuid']
|
||||
create_response = await client.post(
|
||||
'/api/v1/apikeys',
|
||||
headers=_auth(owner_token, workspace_uuid),
|
||||
json={'name': 'Task reader', 'scopes': ['resource.view']},
|
||||
)
|
||||
assert create_response.status_code == 200
|
||||
key = (await create_response.get_json())['data']['key']['key']
|
||||
|
||||
listing = await client.get('/api/v1/system/tasks', headers={'X-API-Key': key})
|
||||
assert listing.status_code == 200
|
||||
assert (await listing.get_json())['data'] == {'tasks': []}
|
||||
assert task_query['instance_uuid'] == application.workspace_service.instance_uuid
|
||||
assert task_query['workspace_uuid'] == workspace_uuid
|
||||
assert task_query['placement_generation'] == 1
|
||||
assert task_query['public'] is True
|
||||
|
||||
bearer_listing = await client.get('/api/v1/system/tasks', headers=_auth(owner_token, workspace_uuid))
|
||||
assert bearer_listing.status_code == 200
|
||||
assert (await bearer_listing.get_json())['data'] == {'tasks': [], 'id_index': 1}
|
||||
|
||||
public_task = await client.get('/api/v1/system/tasks/7', headers={'X-API-Key': key})
|
||||
assert public_task.status_code == 200
|
||||
assert (await public_task.get_json())['data'] == {
|
||||
'id': 7,
|
||||
'status': 'running',
|
||||
'error': None,
|
||||
'result': None,
|
||||
}
|
||||
assert task_lookup == {
|
||||
'instance_uuid': application.workspace_service.instance_uuid,
|
||||
'workspace_uuid': workspace_uuid,
|
||||
'placement_generation': 1,
|
||||
}
|
||||
|
||||
legacy_task = await client.get('/api/v1/system/tasks/7', headers=_auth(owner_token, workspace_uuid))
|
||||
assert legacy_task.status_code == 200
|
||||
assert (await legacy_task.get_json())['data'] == {'id': 7, 'runtime': {'state': 'PENDING'}}
|
||||
|
||||
missing = await client.get('/api/v1/system/tasks/not-an-id', headers={'X-API-Key': key})
|
||||
assert missing.status_code == 404
|
||||
|
||||
no_permission_response = await client.post(
|
||||
'/api/v1/apikeys',
|
||||
headers=_auth(owner_token, workspace_uuid),
|
||||
json={'name': 'Task denied', 'scopes': []},
|
||||
)
|
||||
assert no_permission_response.status_code == 200
|
||||
no_permission_key = (await no_permission_response.get_json())['data']['key']['key']
|
||||
denied = await client.get('/api/v1/system/tasks', headers={'X-API-Key': no_permission_key})
|
||||
assert denied.status_code == 403
|
||||
|
||||
|
||||
async def test_cloud_projection_is_selected_explicitly_and_collaboration_runs_in_core(
|
||||
workspace_api,
|
||||
):
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# Historical schema fixtures
|
||||
|
||||
These JSON files contain **DDL compiled from historical ORM modules**, not the
|
||||
current models with a historical revision stamped over them. `source_commit`,
|
||||
`source_path`, and `revision` in each file record provenance.
|
||||
|
||||
- `baseline_schema.json`: the first Alembic baseline (database version 25),
|
||||
commit `9cd3544d59600fcb88700d05e4b211f59ac00445`.
|
||||
- `master_schema.json`: commit `9b7ba0d64708496ace30a82866f6dbc185f089dc`,
|
||||
published head `0024_passkey_credentials`.
|
||||
- `beta_schema.json`: commit `03854b5d33d8b66fec4c86a77714e0e7512a31bd`,
|
||||
published head `0025_bot_plugin_processors`.
|
||||
|
||||
`test_migration_branch_convergence.py` loads the appropriate SQLite/PostgreSQL
|
||||
DDL into an empty database, executes the real ancestors to the exact historical
|
||||
revision, seeds representative rows, then upgrades to the unified head. There
|
||||
is no version stamping in these regression tests. Historical ORM startup used
|
||||
`create_all` before Alembic; executing ancestors also installs PostgreSQL RLS
|
||||
and pgvector objects absent from the ORM snapshots.
|
||||
|
||||
The baseline PostgreSQL case follows the application's staged legacy startup:
|
||||
upgrade to `0010_scope_resources`, create deferred tenant tables, then upgrade
|
||||
to head. Bare Alembic against a completely empty PostgreSQL database is **not**
|
||||
the product's fresh-install contract; the fresh case explicitly starts empty,
|
||||
creates current ORM metadata, and executes all migrations. Existing-table
|
||||
columns and populated baseline data still undergo real migrations.
|
||||
|
||||
The data probes preserve account/owner membership, bot routes, pipelines,
|
||||
providers, messages, colliding bot sessions, beta Agent/Runner state and plugin
|
||||
processor subscriptions, and master Codex/passkey rows. PostgreSQL checks RLS
|
||||
flags/policies; SQLite checks foreign-key integrity. This is representative
|
||||
migration coverage, not a production database clone or exhaustive data fuzzing.
|
||||
|
||||
PostgreSQL tests require `TEST_POSTGRES_URL` to an expendable test service. Each
|
||||
case creates and drops only its own UUID-named schema. Prefer a disposable
|
||||
`pgvector/pgvector:pg16` container with loopback-only port binding, CPU/memory
|
||||
limits, and tmpfs storage. Do not point the suite at production.
|
||||
|
||||
The only downgrade exercised with populated branch data removes the no-op
|
||||
merge and leaves **both** parent heads. Older feature downgrades can destroy
|
||||
credentials, agents, or colliding bot sessions and are not claimed safe.
|
||||
|
||||
## Regeneration
|
||||
|
||||
Run `generate_schema_fixtures.py` with the repository's locked Python environment
|
||||
and `PYTHONPATH=src` from the repository root. It reads historical objects with
|
||||
`git show` into a temporary package and compiles each historical metadata set
|
||||
using SQLAlchemy's SQLite/PostgreSQL dialects; it never modifies migrations or
|
||||
connects to a database. Keep these fixed historical snapshots when adding new
|
||||
migrations; do not regenerate from current models.
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"source_commit": "9cd3544d59600fcb88700d05e4b211f59ac00445",
|
||||
"source_path": "src/langbot/pkg/entity/persistence/",
|
||||
"revision": "0001_baseline",
|
||||
"dialects": {
|
||||
"sqlite": [
|
||||
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tUNIQUE (\"key\")\n)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE binary_storages (\n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (unique_key)\n)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (session_id)\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE UNIQUE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE plugin_settings (\n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (plugin_author, plugin_name)\n)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id)\n)"
|
||||
],
|
||||
"postgresql": [
|
||||
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tUNIQUE (key)\n)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE binary_storages (\n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (unique_key)\n)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (session_id)\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE UNIQUE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE plugin_settings (\n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (plugin_author, plugin_name)\n)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id)\n)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
{
|
||||
"source_commit": "03854b5d33d8b66fec4c86a77714e0e7512a31bd",
|
||||
"source_path": "src/langbot/pkg/entity/persistence/",
|
||||
"revision": "0025_bot_plugin_processors",
|
||||
"dialects": {
|
||||
"sqlite": [
|
||||
"CREATE TABLE agent_interaction (\n\tid INTEGER NOT NULL, \n\tinteraction_id VARCHAR(255) NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tbinding_id VARCHAR(255) NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tprocessor_type VARCHAR(50) NOT NULL, \n\tprocessor_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\trequest_json TEXT NOT NULL, \n\tdelivery_target_json TEXT, \n\tdelivery_result_json TEXT, \n\treplaces_interaction_id VARCHAR(255), \n\tcallback_token_hash VARCHAR(64) NOT NULL, \n\texpires_at DATETIME, \n\tsubmitted_at DATETIME, \n\tsubmission_json TEXT, \n\tstatus_reason TEXT, \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_interaction_run_interaction UNIQUE (run_id, interaction_id)\n)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_id ON agent_interaction (processor_id)",
|
||||
"CREATE INDEX ix_agent_interaction_binding_id ON agent_interaction (binding_id)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_type ON agent_interaction (processor_type)",
|
||||
"CREATE INDEX ix_agent_interaction_scope_status ON agent_interaction (bot_id, conversation_id, actor_id, status)",
|
||||
"CREATE UNIQUE INDEX ix_agent_interaction_callback_token_hash ON agent_interaction (callback_token_hash)",
|
||||
"CREATE INDEX ix_agent_interaction_expires_at ON agent_interaction (expires_at)",
|
||||
"CREATE INDEX ix_agent_interaction_actor_id ON agent_interaction (actor_id)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_status ON agent_interaction (processor_type, processor_id, status)",
|
||||
"CREATE INDEX ix_agent_interaction_runner_id ON agent_interaction (runner_id)",
|
||||
"CREATE INDEX ix_agent_interaction_bot_id ON agent_interaction (bot_id)",
|
||||
"CREATE INDEX ix_agent_interaction_status ON agent_interaction (status)",
|
||||
"CREATE INDEX ix_agent_interaction_run_id ON agent_interaction (run_id)",
|
||||
"CREATE INDEX ix_agent_interaction_conversation_id ON agent_interaction (conversation_id)",
|
||||
"CREATE TABLE agent_run (\n\tid INTEGER NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255), \n\tagent_id VARCHAR(255), \n\tbinding_id VARCHAR(255), \n\trunner_id VARCHAR(255) NOT NULL, \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tbot_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\tstatus_reason TEXT, \n\tqueue_name VARCHAR(255), \n\tpriority INTEGER NOT NULL, \n\trequested_runtime_id VARCHAR(255), \n\tclaimed_by_runtime_id VARCHAR(255), \n\tclaim_token VARCHAR(255), \n\tclaim_lease_expires_at DATETIME, \n\tdispatch_attempts INTEGER NOT NULL, \n\tlast_claimed_at DATETIME, \n\tcreated_at DATETIME NOT NULL, \n\tstarted_at DATETIME, \n\tfinished_at DATETIME, \n\tupdated_at DATETIME NOT NULL, \n\tdeadline_at DATETIME, \n\tcancel_requested_at DATETIME, \n\tusage_json TEXT, \n\tcost_json TEXT, \n\tauthorization_json TEXT, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_agent_run_queue_claim ON agent_run (queue_name, status, priority, id)",
|
||||
"CREATE INDEX ix_agent_run_binding_id ON agent_run (binding_id)",
|
||||
"CREATE INDEX ix_agent_run_bot_id ON agent_run (bot_id)",
|
||||
"CREATE INDEX ix_agent_run_requested_runtime_id ON agent_run (requested_runtime_id)",
|
||||
"CREATE INDEX ix_agent_run_claim_lease_expires_at ON agent_run (claim_lease_expires_at)",
|
||||
"CREATE INDEX ix_agent_run_runner_id ON agent_run (runner_id)",
|
||||
"CREATE INDEX ix_agent_run_status ON agent_run (status)",
|
||||
"CREATE INDEX ix_agent_run_event_id ON agent_run (event_id)",
|
||||
"CREATE INDEX ix_agent_run_scope_status ON agent_run (bot_id, workspace_id, conversation_id, thread_id, status)",
|
||||
"CREATE INDEX ix_agent_run_claimed_by_runtime_id ON agent_run (claimed_by_runtime_id)",
|
||||
"CREATE INDEX ix_agent_run_conversation_id ON agent_run (conversation_id)",
|
||||
"CREATE INDEX ix_agent_run_runner_status ON agent_run (runner_id, status)",
|
||||
"CREATE INDEX ix_agent_run_queue_name ON agent_run (queue_name)",
|
||||
"CREATE UNIQUE INDEX ix_agent_run_run_id ON agent_run (run_id)",
|
||||
"CREATE INDEX ix_agent_run_claim_token ON agent_run (claim_token)",
|
||||
"CREATE TABLE agent_runtime (\n\tid INTEGER NOT NULL, \n\truntime_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tdisplay_name VARCHAR(255), \n\tendpoint VARCHAR(1024), \n\tversion VARCHAR(255), \n\tcapabilities_json TEXT, \n\tlabels_json TEXT, \n\tmetadata_json TEXT, \n\tlast_heartbeat_at DATETIME, \n\theartbeat_deadline_at DATETIME, \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_agent_runtime_heartbeat_deadline_at ON agent_runtime (heartbeat_deadline_at)",
|
||||
"CREATE INDEX ix_agent_runtime_status ON agent_runtime (status)",
|
||||
"CREATE UNIQUE INDEX ix_agent_runtime_runtime_id ON agent_runtime (runtime_id)",
|
||||
"CREATE INDEX ix_agent_runtime_last_heartbeat_at ON agent_runtime (last_heartbeat_at)",
|
||||
"CREATE TABLE agent_run_event (\n\tid INTEGER NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tsequence INTEGER NOT NULL, \n\ttype VARCHAR(100) NOT NULL, \n\tdata_json TEXT, \n\tusage_json TEXT, \n\tcreated_at DATETIME NOT NULL, \n\tsource VARCHAR(50), \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_run_event_run_sequence UNIQUE (run_id, sequence)\n)",
|
||||
"CREATE INDEX ix_agent_run_event_run_id ON agent_run_event (run_id)",
|
||||
"CREATE INDEX ix_agent_run_event_run_sequence ON agent_run_event (run_id, sequence)",
|
||||
"CREATE INDEX ix_agent_run_event_type ON agent_run_event (type)",
|
||||
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at DATETIME NOT NULL, \n\tlease_expires_at DATETIME, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
|
||||
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tapplied_at DATETIME, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
|
||||
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
|
||||
"CREATE TABLE event_log (\n\tid INTEGER NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tevent_type VARCHAR(100) NOT NULL, \n\tevent_time DATETIME, \n\tsource VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tactor_name VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tinput_summary TEXT, \n\tinput_json TEXT, \n\traw_ref VARCHAR(255), \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE UNIQUE INDEX ix_event_log_event_id ON event_log (event_id)",
|
||||
"CREATE INDEX ix_event_log_event_type ON event_log (event_type)",
|
||||
"CREATE INDEX ix_event_log_conversation_id ON event_log (conversation_id)",
|
||||
"CREATE INDEX ix_event_log_run_id ON event_log (run_id)",
|
||||
"CREATE INDEX ix_event_log_bot_id ON event_log (bot_id)",
|
||||
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
|
||||
"CREATE TABLE runner_state (\n\tid INTEGER NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tbinding_identity VARCHAR(255) NOT NULL, \n\tscope VARCHAR(50) NOT NULL, \n\tscope_key VARCHAR(512) NOT NULL, \n\tstate_key VARCHAR(255) NOT NULL, \n\tvalue_json TEXT, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tupdated_at DATETIME NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_runner_state_scope_key_state_key UNIQUE (scope_key, state_key)\n)",
|
||||
"CREATE INDEX ix_runner_state_conversation_id ON runner_state (conversation_id)",
|
||||
"CREATE INDEX ix_runner_state_scope ON runner_state (scope)",
|
||||
"CREATE INDEX ix_runner_state_runner_id ON runner_state (runner_id)",
|
||||
"CREATE INDEX ix_runner_state_actor_id ON runner_state (actor_id)",
|
||||
"CREATE INDEX ix_runner_state_runner_binding ON runner_state (runner_id, binding_identity)",
|
||||
"CREATE INDEX ix_runner_state_binding_identity ON runner_state (binding_identity)",
|
||||
"CREATE INDEX ix_runner_state_bot_id ON runner_state (bot_id)",
|
||||
"CREATE INDEX ix_runner_state_scope_key_lookup ON runner_state (scope_key)",
|
||||
"CREATE TABLE transcript (\n\tid INTEGER NOT NULL, \n\ttranscript_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255) NOT NULL, \n\tthread_id VARCHAR(255), \n\trole VARCHAR(50) NOT NULL, \n\titem_type VARCHAR(50) NOT NULL, \n\tcontent TEXT, \n\tcontent_json TEXT, \n\tattachment_refs_json TEXT, \n\tseq INTEGER NOT NULL, \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at DATETIME NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_transcript_conversation_id ON transcript (conversation_id)",
|
||||
"CREATE UNIQUE INDEX ix_transcript_transcript_id ON transcript (transcript_id)",
|
||||
"CREATE INDEX ix_transcript_event_id ON transcript (event_id)",
|
||||
"CREATE INDEX ix_transcript_conversation_seq ON transcript (conversation_id, seq)",
|
||||
"CREATE INDEX ix_transcript_bot_id ON transcript (bot_id)",
|
||||
"CREATE INDEX ix_transcript_conversation_created ON transcript (conversation_id, created_at)",
|
||||
"CREATE INDEX ix_transcript_scope_seq ON transcript (bot_id, workspace_id, conversation_id, thread_id, seq)",
|
||||
"CREATE INDEX ix_transcript_run_id ON transcript (run_id)",
|
||||
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
|
||||
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
|
||||
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
|
||||
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
|
||||
"CREATE TABLE agents (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tkind VARCHAR(50) NOT NULL, \n\tcomponent_ref VARCHAR(255), \n\tconfig JSON NOT NULL, \n\tsupported_event_patterns JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE INDEX ix_agents_workspace_updated ON agents (workspace_uuid, updated_at)",
|
||||
"CREATE INDEX ix_agents_workspace_name ON agents (workspace_uuid, name)",
|
||||
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at DATETIME, \n\tlast_used_at DATETIME, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
|
||||
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
|
||||
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tevent_bindings JSON DEFAULT '[]' NOT NULL, \n\tplugin_processors JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
|
||||
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
|
||||
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, \"key\"), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
|
||||
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
|
||||
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT 0 NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
|
||||
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at DATETIME NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\trevoked_at DATETIME, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
|
||||
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
|
||||
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at DATETIME, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
|
||||
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
|
||||
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\taccepted_at DATETIME, \n\trevoked_at DATETIME, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
|
||||
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT 0 NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
|
||||
"CREATE TABLE bot_admins (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
|
||||
],
|
||||
"postgresql": [
|
||||
"CREATE TABLE agent_interaction (\n\tid SERIAL NOT NULL, \n\tinteraction_id VARCHAR(255) NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tbinding_id VARCHAR(255) NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tprocessor_type VARCHAR(50) NOT NULL, \n\tprocessor_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\trequest_json TEXT NOT NULL, \n\tdelivery_target_json TEXT, \n\tdelivery_result_json TEXT, \n\treplaces_interaction_id VARCHAR(255), \n\tcallback_token_hash VARCHAR(64) NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tsubmitted_at TIMESTAMP WITHOUT TIME ZONE, \n\tsubmission_json TEXT, \n\tstatus_reason TEXT, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_interaction_run_interaction UNIQUE (run_id, interaction_id)\n)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_id ON agent_interaction (processor_id)",
|
||||
"CREATE INDEX ix_agent_interaction_binding_id ON agent_interaction (binding_id)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_type ON agent_interaction (processor_type)",
|
||||
"CREATE INDEX ix_agent_interaction_scope_status ON agent_interaction (bot_id, conversation_id, actor_id, status)",
|
||||
"CREATE UNIQUE INDEX ix_agent_interaction_callback_token_hash ON agent_interaction (callback_token_hash)",
|
||||
"CREATE INDEX ix_agent_interaction_expires_at ON agent_interaction (expires_at)",
|
||||
"CREATE INDEX ix_agent_interaction_actor_id ON agent_interaction (actor_id)",
|
||||
"CREATE INDEX ix_agent_interaction_processor_status ON agent_interaction (processor_type, processor_id, status)",
|
||||
"CREATE INDEX ix_agent_interaction_runner_id ON agent_interaction (runner_id)",
|
||||
"CREATE INDEX ix_agent_interaction_bot_id ON agent_interaction (bot_id)",
|
||||
"CREATE INDEX ix_agent_interaction_status ON agent_interaction (status)",
|
||||
"CREATE INDEX ix_agent_interaction_run_id ON agent_interaction (run_id)",
|
||||
"CREATE INDEX ix_agent_interaction_conversation_id ON agent_interaction (conversation_id)",
|
||||
"CREATE TABLE agent_run (\n\tid SERIAL NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255), \n\tagent_id VARCHAR(255), \n\tbinding_id VARCHAR(255), \n\trunner_id VARCHAR(255) NOT NULL, \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tbot_id VARCHAR(255), \n\tstatus VARCHAR(50) NOT NULL, \n\tstatus_reason TEXT, \n\tqueue_name VARCHAR(255), \n\tpriority INTEGER NOT NULL, \n\trequested_runtime_id VARCHAR(255), \n\tclaimed_by_runtime_id VARCHAR(255), \n\tclaim_token VARCHAR(255), \n\tclaim_lease_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tdispatch_attempts INTEGER NOT NULL, \n\tlast_claimed_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tdeadline_at TIMESTAMP WITHOUT TIME ZONE, \n\tcancel_requested_at TIMESTAMP WITHOUT TIME ZONE, \n\tusage_json TEXT, \n\tcost_json TEXT, \n\tauthorization_json TEXT, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_agent_run_queue_claim ON agent_run (queue_name, status, priority, id)",
|
||||
"CREATE INDEX ix_agent_run_binding_id ON agent_run (binding_id)",
|
||||
"CREATE INDEX ix_agent_run_bot_id ON agent_run (bot_id)",
|
||||
"CREATE INDEX ix_agent_run_requested_runtime_id ON agent_run (requested_runtime_id)",
|
||||
"CREATE INDEX ix_agent_run_claim_lease_expires_at ON agent_run (claim_lease_expires_at)",
|
||||
"CREATE INDEX ix_agent_run_runner_id ON agent_run (runner_id)",
|
||||
"CREATE INDEX ix_agent_run_status ON agent_run (status)",
|
||||
"CREATE INDEX ix_agent_run_event_id ON agent_run (event_id)",
|
||||
"CREATE INDEX ix_agent_run_scope_status ON agent_run (bot_id, workspace_id, conversation_id, thread_id, status)",
|
||||
"CREATE INDEX ix_agent_run_claimed_by_runtime_id ON agent_run (claimed_by_runtime_id)",
|
||||
"CREATE INDEX ix_agent_run_conversation_id ON agent_run (conversation_id)",
|
||||
"CREATE INDEX ix_agent_run_runner_status ON agent_run (runner_id, status)",
|
||||
"CREATE INDEX ix_agent_run_queue_name ON agent_run (queue_name)",
|
||||
"CREATE UNIQUE INDEX ix_agent_run_run_id ON agent_run (run_id)",
|
||||
"CREATE INDEX ix_agent_run_claim_token ON agent_run (claim_token)",
|
||||
"CREATE TABLE agent_runtime (\n\tid SERIAL NOT NULL, \n\truntime_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tdisplay_name VARCHAR(255), \n\tendpoint VARCHAR(1024), \n\tversion VARCHAR(255), \n\tcapabilities_json TEXT, \n\tlabels_json TEXT, \n\tmetadata_json TEXT, \n\tlast_heartbeat_at TIMESTAMP WITHOUT TIME ZONE, \n\theartbeat_deadline_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_agent_runtime_heartbeat_deadline_at ON agent_runtime (heartbeat_deadline_at)",
|
||||
"CREATE INDEX ix_agent_runtime_status ON agent_runtime (status)",
|
||||
"CREATE UNIQUE INDEX ix_agent_runtime_runtime_id ON agent_runtime (runtime_id)",
|
||||
"CREATE INDEX ix_agent_runtime_last_heartbeat_at ON agent_runtime (last_heartbeat_at)",
|
||||
"CREATE TABLE agent_run_event (\n\tid SERIAL NOT NULL, \n\trun_id VARCHAR(255) NOT NULL, \n\tsequence INTEGER NOT NULL, \n\ttype VARCHAR(100) NOT NULL, \n\tdata_json TEXT, \n\tusage_json TEXT, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tsource VARCHAR(50), \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_agent_run_event_run_sequence UNIQUE (run_id, sequence)\n)",
|
||||
"CREATE INDEX ix_agent_run_event_run_id ON agent_run_event (run_id)",
|
||||
"CREATE INDEX ix_agent_run_event_run_sequence ON agent_run_event (run_id, sequence)",
|
||||
"CREATE INDEX ix_agent_run_event_type ON agent_run_event (type)",
|
||||
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at TIMESTAMP WITH TIME ZONE NOT NULL, \n\tlease_expires_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
|
||||
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, \n\tapplied_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
|
||||
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
|
||||
"CREATE TABLE event_log (\n\tid SERIAL NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tevent_type VARCHAR(100) NOT NULL, \n\tevent_time TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tactor_name VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tinput_summary TEXT, \n\tinput_json TEXT, \n\traw_ref VARCHAR(255), \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE UNIQUE INDEX ix_event_log_event_id ON event_log (event_id)",
|
||||
"CREATE INDEX ix_event_log_event_type ON event_log (event_type)",
|
||||
"CREATE INDEX ix_event_log_conversation_id ON event_log (conversation_id)",
|
||||
"CREATE INDEX ix_event_log_run_id ON event_log (run_id)",
|
||||
"CREATE INDEX ix_event_log_bot_id ON event_log (bot_id)",
|
||||
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
|
||||
"CREATE TABLE runner_state (\n\tid SERIAL NOT NULL, \n\trunner_id VARCHAR(255) NOT NULL, \n\tbinding_identity VARCHAR(255) NOT NULL, \n\tscope VARCHAR(50) NOT NULL, \n\tscope_key VARCHAR(512) NOT NULL, \n\tstate_key VARCHAR(255) NOT NULL, \n\tvalue_json TEXT, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255), \n\tthread_id VARCHAR(255), \n\tactor_type VARCHAR(50), \n\tactor_id VARCHAR(255), \n\tsubject_type VARCHAR(50), \n\tsubject_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_runner_state_scope_key_state_key UNIQUE (scope_key, state_key)\n)",
|
||||
"CREATE INDEX ix_runner_state_conversation_id ON runner_state (conversation_id)",
|
||||
"CREATE INDEX ix_runner_state_scope ON runner_state (scope)",
|
||||
"CREATE INDEX ix_runner_state_runner_id ON runner_state (runner_id)",
|
||||
"CREATE INDEX ix_runner_state_actor_id ON runner_state (actor_id)",
|
||||
"CREATE INDEX ix_runner_state_runner_binding ON runner_state (runner_id, binding_identity)",
|
||||
"CREATE INDEX ix_runner_state_binding_identity ON runner_state (binding_identity)",
|
||||
"CREATE INDEX ix_runner_state_bot_id ON runner_state (bot_id)",
|
||||
"CREATE INDEX ix_runner_state_scope_key_lookup ON runner_state (scope_key)",
|
||||
"CREATE TABLE transcript (\n\tid SERIAL NOT NULL, \n\ttranscript_id VARCHAR(255) NOT NULL, \n\tevent_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255), \n\tworkspace_id VARCHAR(255), \n\tconversation_id VARCHAR(255) NOT NULL, \n\tthread_id VARCHAR(255), \n\trole VARCHAR(50) NOT NULL, \n\titem_type VARCHAR(50) NOT NULL, \n\tcontent TEXT, \n\tcontent_json TEXT, \n\tattachment_refs_json TEXT, \n\tseq INTEGER NOT NULL, \n\trun_id VARCHAR(255), \n\trunner_id VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmetadata_json TEXT, \n\tPRIMARY KEY (id)\n)",
|
||||
"CREATE INDEX ix_transcript_conversation_id ON transcript (conversation_id)",
|
||||
"CREATE UNIQUE INDEX ix_transcript_transcript_id ON transcript (transcript_id)",
|
||||
"CREATE INDEX ix_transcript_event_id ON transcript (event_id)",
|
||||
"CREATE INDEX ix_transcript_conversation_seq ON transcript (conversation_id, seq)",
|
||||
"CREATE INDEX ix_transcript_bot_id ON transcript (bot_id)",
|
||||
"CREATE INDEX ix_transcript_conversation_created ON transcript (conversation_id, created_at)",
|
||||
"CREATE INDEX ix_transcript_scope_seq ON transcript (bot_id, workspace_id, conversation_id, thread_id, seq)",
|
||||
"CREATE INDEX ix_transcript_run_id ON transcript (run_id)",
|
||||
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
|
||||
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
|
||||
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
|
||||
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
|
||||
"CREATE TABLE agents (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tuuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tkind VARCHAR(50) NOT NULL, \n\tcomponent_ref VARCHAR(255), \n\tconfig JSON NOT NULL, \n\tsupported_event_patterns JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid)\n)",
|
||||
"CREATE INDEX ix_agents_workspace_updated ON agents (workspace_uuid, updated_at)",
|
||||
"CREATE INDEX ix_agents_workspace_name ON agents (workspace_uuid, name)",
|
||||
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
|
||||
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
|
||||
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tevent_bindings JSON DEFAULT '[]' NOT NULL, \n\tplugin_processors JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
|
||||
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
|
||||
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
|
||||
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
|
||||
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT false NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
|
||||
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
|
||||
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
|
||||
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
|
||||
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
|
||||
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\taccepted_at TIMESTAMP WITHOUT TIME ZONE, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
|
||||
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT false NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
|
||||
"CREATE TABLE bot_admins (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Reproduce immutable historical ORM DDL snapshots without a database."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from langbot.pkg.utils import constants
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[4]
|
||||
OUTPUT = Path(__file__).resolve().parent
|
||||
SOURCE = 'src/langbot/pkg/entity/persistence/'
|
||||
REFS = {
|
||||
'baseline': ('9cd3544d59600fcb88700d05e4b211f59ac00445', '0001_baseline'),
|
||||
'master': ('9b7ba0d64708496ace30a82866f6dbc185f089dc', '0024_passkey_credentials'),
|
||||
'beta': ('03854b5d33d8b66fec4c86a77714e0e7512a31bd', '0025_bot_plugin_processors'),
|
||||
}
|
||||
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.check_output(['git', *args], cwd=ROOT, text=True)
|
||||
|
||||
|
||||
def generate() -> None:
|
||||
# Only the baseline module's initial_metadata seed list reads this removed
|
||||
# constant. It does not affect any table, column, constraint or generated DDL.
|
||||
constants.required_database_version = 25
|
||||
for name, (commit, revision) in REFS.items():
|
||||
paths = git('ls-tree', '-r', '--name-only', commit, SOURCE).splitlines()
|
||||
with tempfile.TemporaryDirectory(prefix='langbot-historical-schema-') as directory:
|
||||
package = Path(directory)
|
||||
for path in paths:
|
||||
if path.endswith('.py'):
|
||||
(package / Path(path).name).write_text(git('show', f'{commit}:{path}'))
|
||||
# Keep ...utils imports working but give each snapshot its own Base.
|
||||
package_name = f'langbot.pkg.entity.historical_{name}'
|
||||
module = types.ModuleType(package_name)
|
||||
module.__path__ = [directory]
|
||||
sys.modules[package_name] = module
|
||||
for path in sorted(package.glob('*.py')):
|
||||
importlib.import_module(f'{package_name}.{path.stem}')
|
||||
metadata = importlib.import_module(f'{package_name}.base').Base.metadata
|
||||
dialects = {}
|
||||
for dialect in ('sqlite', 'postgresql'):
|
||||
statements = []
|
||||
engine = sa.create_mock_engine(
|
||||
f'{dialect}://',
|
||||
lambda sql, *args, **kwargs: statements.append(str(sql.compile(dialect=engine.dialect)).strip()),
|
||||
)
|
||||
metadata.create_all(engine)
|
||||
dialects[dialect] = statements
|
||||
payload = {'source_commit': commit, 'source_path': SOURCE, 'revision': revision, 'dialects': dialects}
|
||||
(OUTPUT / f'{name}_schema.json').write_text(json.dumps(payload, indent=2) + '\n')
|
||||
print(f'{name}: {commit}, {len(metadata.tables)} historical ORM tables')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
generate()
|
||||
@@ -0,0 +1,259 @@
|
||||
{
|
||||
"source_commit": "9b7ba0d64708496ace30a82866f6dbc185f089dc",
|
||||
"source_path": "src/langbot/pkg/entity/persistence/",
|
||||
"revision": "0024_passkey_credentials",
|
||||
"dialects": {
|
||||
"sqlite": [
|
||||
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at DATETIME NOT NULL, \n\tlease_expires_at DATETIME, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
|
||||
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tapplied_at DATETIME, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
|
||||
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
|
||||
"CREATE TABLE metadata (\n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (\"key\")\n)",
|
||||
"CREATE TABLE users (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tuser VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at DATETIME, \n\tspace_api_key VARCHAR(255), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
|
||||
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
|
||||
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
|
||||
"CREATE TABLE passkey_credentials (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tcredential_id VARCHAR(255) NOT NULL, \n\tpublic_key TEXT NOT NULL, \n\tsign_count INTEGER NOT NULL, \n\taaguid VARCHAR(64), \n\ttransports VARCHAR(255), \n\tbacked_up BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_passkey_credentials_cred_id ON passkey_credentials (credential_id)",
|
||||
"CREATE UNIQUE INDEX uq_passkey_credentials_uuid ON passkey_credentials (uuid)",
|
||||
"CREATE INDEX ix_passkey_credentials_account ON passkey_credentials (account_uuid)",
|
||||
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
|
||||
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
|
||||
"CREATE TABLE api_keys (\n\tid INTEGER NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at DATETIME, \n\tlast_used_at DATETIME, \n\tdescription VARCHAR(512), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
|
||||
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
|
||||
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BLOB NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
|
||||
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\t\"key\" VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, \"key\"), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time DATETIME NOT NULL, \n\tlast_activity DATETIME NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, bot_id, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp DATETIME NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
|
||||
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
|
||||
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at DATETIME, \n\tupdated_at DATETIME, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT 0 NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
|
||||
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at DATETIME NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\trevoked_at DATETIME, \n\tlast_used_at DATETIME, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
|
||||
"CREATE TABLE webhooks (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
|
||||
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at DATETIME, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
|
||||
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at DATETIME NOT NULL, \n\taccepted_at DATETIME, \n\trevoked_at DATETIME, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
|
||||
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT 0 NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
|
||||
"CREATE TABLE bot_admins (\n\tid INTEGER NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE codex_credentials (\n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpayload JSON NOT NULL, \n\tversion INTEGER NOT NULL, \n\tlease_owner VARCHAR(64), \n\tlease_until FLOAT NOT NULL, \n\tPRIMARY KEY (provider_uuid), \n\tCONSTRAINT fk_codex_credentials_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_codex_credentials_workspace ON codex_credentials (workspace_uuid)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tupdated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, \n\tstarted_at DATETIME NOT NULL, \n\tfinished_at DATETIME NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at DATETIME, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
|
||||
],
|
||||
"postgresql": [
|
||||
"CREATE TABLE directory_projection_states (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tcursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_coverage_cursor BIGINT DEFAULT '0' NOT NULL, \n\tsnapshot_fingerprint TEXT NOT NULL, \n\tlast_applied_at TIMESTAMP WITH TIME ZONE NOT NULL, \n\tlease_expires_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid), \n\tCONSTRAINT ck_directory_projection_state_cursor CHECK (cursor >= 0), \n\tCONSTRAINT ck_directory_projection_state_snapshot_coverage CHECK (snapshot_coverage_cursor >= 0 AND snapshot_coverage_cursor <= cursor), \n\tCONSTRAINT ck_directory_projection_state_fingerprint CHECK (length(snapshot_fingerprint) = 64)\n)",
|
||||
"CREATE TABLE directory_projection_inbox (\n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tevent_uuid VARCHAR(36) NOT NULL, \n\tcursor BIGINT NOT NULL, \n\tevent_type VARCHAR(128) NOT NULL, \n\trevision BIGINT NOT NULL, \n\tfingerprint TEXT NOT NULL, \n\treceived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, \n\tapplied_at TIMESTAMP WITH TIME ZONE, \n\tPRIMARY KEY (instance_uuid, event_uuid), \n\tCONSTRAINT uq_directory_projection_inbox_cursor UNIQUE (instance_uuid, cursor), \n\tCONSTRAINT ck_directory_projection_inbox_cursor CHECK (cursor > 0), \n\tCONSTRAINT ck_directory_projection_inbox_revision CHECK (revision > 0), \n\tCONSTRAINT ck_directory_projection_inbox_fingerprint CHECK (length(fingerprint) = 64)\n)",
|
||||
"CREATE INDEX ix_directory_projection_inbox_pending ON directory_projection_inbox (instance_uuid, applied_at, cursor)",
|
||||
"CREATE TABLE metadata (\n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (key)\n)",
|
||||
"CREATE TABLE users (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\t\"user\" VARCHAR(255) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\tpassword VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\taccount_type VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tspace_account_uuid VARCHAR(255), \n\tspace_access_token TEXT, \n\tspace_refresh_token TEXT, \n\tspace_access_token_expires_at TIMESTAMP WITHOUT TIME ZONE, \n\tspace_api_key VARCHAR(255), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_users_normalized_email CHECK (normalized_email = trim(normalized_email) AND length(normalized_email) > 0 AND length(normalized_email) <= 320), \n\tCONSTRAINT ck_users_status CHECK (status IN ('active', 'disabled', 'deleted')), \n\tCONSTRAINT ck_users_source CHECK (source IN ('local', 'cloud_projection'))\n)",
|
||||
"CREATE UNIQUE INDEX uq_users_uuid ON users (uuid)",
|
||||
"CREATE UNIQUE INDEX uq_users_normalized_email ON users (normalized_email)",
|
||||
"CREATE TABLE passkey_credentials (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tcredential_id VARCHAR(255) NOT NULL, \n\tpublic_key TEXT NOT NULL, \n\tsign_count INTEGER NOT NULL, \n\taaguid VARCHAR(64), \n\ttransports VARCHAR(255), \n\tbacked_up BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_passkey_credentials_cred_id ON passkey_credentials (credential_id)",
|
||||
"CREATE UNIQUE INDEX uq_passkey_credentials_uuid ON passkey_credentials (uuid)",
|
||||
"CREATE INDEX ix_passkey_credentials_account ON passkey_credentials (account_uuid)",
|
||||
"CREATE TABLE workspaces (\n\tuuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tslug VARCHAR(255) NOT NULL, \n\ttype VARCHAR(32) DEFAULT 'team' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspaces_instance_slug UNIQUE (instance_uuid, slug), \n\tCONSTRAINT ck_workspaces_type CHECK (type IN ('personal', 'team')), \n\tCONSTRAINT ck_workspaces_status CHECK (status IN ('provisioning', 'active', 'suspended', 'archived', 'deleted')), \n\tCONSTRAINT ck_workspaces_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspaces_local_instance ON workspaces (instance_uuid) WHERE source = 'local'",
|
||||
"CREATE INDEX ix_workspaces_instance_status ON workspaces (instance_uuid, status)",
|
||||
"CREATE TABLE api_keys (\n\tid SERIAL NOT NULL, \n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tcreated_by_account_uuid VARCHAR(36), \n\tname VARCHAR(255) NOT NULL, \n\tkey_hash VARCHAR(64) NOT NULL, \n\tscopes JSON DEFAULT '[]' NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tdescription VARCHAR(512), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT ck_api_keys_status CHECK (status IN ('active', 'revoked')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_api_keys_workspace_name ON api_keys (workspace_uuid, name)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_key_hash ON api_keys (key_hash)",
|
||||
"CREATE INDEX ix_api_keys_workspace_status ON api_keys (workspace_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_api_keys_uuid ON api_keys (uuid)",
|
||||
"CREATE TABLE bots (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\tadapter VARCHAR(255) NOT NULL, \n\tadapter_config JSON NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tuse_pipeline_name VARCHAR(255), \n\tuse_pipeline_uuid VARCHAR(255), \n\tpipeline_routing_rules JSON DEFAULT '[]' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_bots_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_bots_workspace_name ON bots (workspace_uuid, name)",
|
||||
"CREATE TABLE binary_storages (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tunique_key VARCHAR(255) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\towner_type VARCHAR(255) NOT NULL, \n\towner VARCHAR(255) NOT NULL, \n\tvalue BYTEA NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, unique_key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_binary_storages_workspace_owner ON binary_storages (workspace_uuid, owner_type, owner)",
|
||||
"CREATE TABLE mcp_servers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tenable BOOLEAN NOT NULL, \n\tmode VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\treadme TEXT DEFAULT '' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_mcp_servers_workspace_name UNIQUE (workspace_uuid, name), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_mcp_servers_workspace_enable ON mcp_servers (workspace_uuid, enable)",
|
||||
"CREATE TABLE workspace_metadata (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkey VARCHAR(255) NOT NULL, \n\tvalue VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, key), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE model_providers (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\trequester VARCHAR(255) NOT NULL, \n\tbase_url VARCHAR(512) NOT NULL, \n\tapi_keys JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_model_providers_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_model_providers_workspace_name ON model_providers (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_model_providers_workspace_requester ON model_providers (workspace_uuid, requester)",
|
||||
"CREATE TABLE monitoring_messages (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_content TEXT NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tlevel VARCHAR(50) NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\trunner_name VARCHAR(255), \n\tvariables TEXT, \n\trole VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_messages_bot_id ON monitoring_messages (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_timestamp ON monitoring_messages (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_pipeline_id ON monitoring_messages (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_timestamp ON monitoring_messages (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_messages_workspace_session ON monitoring_messages (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_messages_session_id ON monitoring_messages (session_id)",
|
||||
"CREATE TABLE monitoring_llm_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tinput_tokens INTEGER NOT NULL, \n\toutput_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tcost FLOAT, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\terror_message TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_session ON monitoring_llm_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_timestamp ON monitoring_llm_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_bot_id ON monitoring_llm_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_message_id ON monitoring_llm_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_workspace_timestamp ON monitoring_llm_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_llm_calls_pipeline_id ON monitoring_llm_calls (pipeline_id)",
|
||||
"CREATE TABLE monitoring_tool_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\ttool_name VARCHAR(255) NOT NULL, \n\ttool_source VARCHAR(50) NOT NULL, \n\tduration INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\targuments TEXT, \n\tresult TEXT, \n\terror_message TEXT, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_bot_id ON monitoring_tool_calls (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_message_id ON monitoring_tool_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_pipeline_id ON monitoring_tool_calls (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_timestamp ON monitoring_tool_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_session_id ON monitoring_tool_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_workspace_session ON monitoring_tool_calls (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_tool_calls_timestamp ON monitoring_tool_calls (timestamp)",
|
||||
"CREATE TABLE monitoring_sessions (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tmessage_count INTEGER NOT NULL, \n\tstart_time TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tlast_activity TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tis_active BOOLEAN NOT NULL, \n\tplatform VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tuser_name VARCHAR(255), \n\tPRIMARY KEY (workspace_uuid, bot_id, session_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_sessions_start_time ON monitoring_sessions (start_time)",
|
||||
"CREATE INDEX ix_monitoring_sessions_bot_id ON monitoring_sessions (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_activity ON monitoring_sessions (workspace_uuid, last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_pipeline_id ON monitoring_sessions (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_sessions_last_activity ON monitoring_sessions (last_activity)",
|
||||
"CREATE INDEX ix_monitoring_sessions_workspace_active ON monitoring_sessions (workspace_uuid, is_active)",
|
||||
"CREATE INDEX ix_monitoring_sessions_is_active ON monitoring_sessions (is_active)",
|
||||
"CREATE TABLE monitoring_errors (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\terror_type VARCHAR(255) NOT NULL, \n\terror_message TEXT NOT NULL, \n\tbot_id VARCHAR(255) NOT NULL, \n\tbot_name VARCHAR(255) NOT NULL, \n\tpipeline_id VARCHAR(255) NOT NULL, \n\tpipeline_name VARCHAR(255) NOT NULL, \n\tsession_id VARCHAR(255), \n\tstack_trace TEXT, \n\tmessage_id VARCHAR(255), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_timestamp ON monitoring_errors (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_message_id ON monitoring_errors (message_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_bot_id ON monitoring_errors (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_workspace_session ON monitoring_errors (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_errors_timestamp ON monitoring_errors (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_errors_pipeline_id ON monitoring_errors (pipeline_id)",
|
||||
"CREATE TABLE monitoring_embedding_calls (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tmodel_name VARCHAR(255) NOT NULL, \n\tprompt_tokens INTEGER NOT NULL, \n\ttotal_tokens INTEGER NOT NULL, \n\tduration INTEGER NOT NULL, \n\tinput_count INTEGER NOT NULL, \n\tstatus VARCHAR(50) NOT NULL, \n\terror_message TEXT, \n\tknowledge_base_id VARCHAR(255), \n\tquery_text TEXT, \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tcall_type VARCHAR(50), \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_timestamp ON monitoring_embedding_calls (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_session_id ON monitoring_embedding_calls (session_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_workspace_kb ON monitoring_embedding_calls (workspace_uuid, knowledge_base_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_timestamp ON monitoring_embedding_calls (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_message_id ON monitoring_embedding_calls (message_id)",
|
||||
"CREATE INDEX ix_monitoring_embedding_calls_knowledge_base_id ON monitoring_embedding_calls (knowledge_base_id)",
|
||||
"CREATE TABLE monitoring_feedback (\n\tid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\ttimestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfeedback_id VARCHAR(255) NOT NULL, \n\tfeedback_type INTEGER NOT NULL, \n\tfeedback_content TEXT, \n\tinaccurate_reasons TEXT, \n\tbot_id VARCHAR(255), \n\tbot_name VARCHAR(255), \n\tpipeline_id VARCHAR(255), \n\tpipeline_name VARCHAR(255), \n\tsession_id VARCHAR(255), \n\tmessage_id VARCHAR(255), \n\tstream_id VARCHAR(255), \n\tuser_id VARCHAR(255), \n\tplatform VARCHAR(255), \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_monitoring_feedback_workspace_feedback_id UNIQUE (workspace_uuid, feedback_id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_timestamp ON monitoring_feedback (workspace_uuid, timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_pipeline_id ON monitoring_feedback (pipeline_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_stream_id ON monitoring_feedback (stream_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_workspace_session ON monitoring_feedback (workspace_uuid, session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_timestamp ON monitoring_feedback (timestamp)",
|
||||
"CREATE INDEX ix_monitoring_feedback_session_id ON monitoring_feedback (session_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_bot_id ON monitoring_feedback (bot_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_message_id ON monitoring_feedback (message_id)",
|
||||
"CREATE INDEX ix_monitoring_feedback_feedback_id ON monitoring_feedback (feedback_id)",
|
||||
"CREATE TABLE legacy_pipelines (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tdescription VARCHAR(255) NOT NULL, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tfor_version VARCHAR(255) NOT NULL, \n\tis_default BOOLEAN NOT NULL, \n\tstages JSON NOT NULL, \n\tconfig JSON NOT NULL, \n\textensions_preferences JSON NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_legacy_pipelines_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_name ON legacy_pipelines (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_legacy_pipelines_workspace_default ON legacy_pipelines (workspace_uuid, is_default)",
|
||||
"CREATE TABLE plugin_settings (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tplugin_author VARCHAR(255) NOT NULL, \n\tplugin_name VARCHAR(255) NOT NULL, \n\tinstallation_uuid VARCHAR(36) NOT NULL, \n\tartifact_digest VARCHAR(64) NOT NULL, \n\truntime_revision INTEGER NOT NULL, \n\tenabled BOOLEAN NOT NULL, \n\tpriority INTEGER NOT NULL, \n\tconfig JSON NOT NULL, \n\tinstall_source VARCHAR(255) NOT NULL, \n\tinstall_info JSON NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid, plugin_author, plugin_name), \n\tCONSTRAINT uq_plugin_settings_installation_uuid UNIQUE (installation_uuid), \n\tCONSTRAINT ck_plugin_settings_runtime_revision_positive CHECK (runtime_revision >= 1), \n\tCONSTRAINT ck_plugin_settings_artifact_digest_length CHECK (length(artifact_digest) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_plugin_settings_workspace_enabled ON plugin_settings (workspace_uuid, enabled)",
|
||||
"CREATE UNIQUE INDEX ix_plugin_settings_workspace_installation ON plugin_settings (workspace_uuid, installation_uuid)",
|
||||
"CREATE TABLE knowledge_bases (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR, \n\tdescription TEXT, \n\temoji VARCHAR(10), \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE, \n\tknowledge_engine_plugin_id VARCHAR, \n\tcollection_id VARCHAR, \n\tlegacy_vector_collection BOOLEAN DEFAULT false NOT NULL, \n\tcreation_settings JSON, \n\tretrieval_settings JSON, \n\tembedding_dimension INTEGER, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_bases_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT ck_knowledge_bases_embedding_dimension_positive CHECK (embedding_dimension IS NULL OR embedding_dimension > 0), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_knowledge_bases_workspace_collection ON knowledge_bases (workspace_uuid, collection_id) WHERE collection_id IS NOT NULL",
|
||||
"CREATE INDEX ix_knowledge_bases_workspace_name ON knowledge_bases (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_knowledge_bases_name ON knowledge_bases (name)",
|
||||
"CREATE TABLE support_admin_temporary_sessions (\n\tgrant_jti_hash VARCHAR(64) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tactor_account_uuid VARCHAR(36) NOT NULL, \n\tissued_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tlast_used_at TIMESTAMP WITHOUT TIME ZONE, \n\tPRIMARY KEY (grant_jti_hash), \n\tCONSTRAINT ck_support_admin_sessions_grant_jti_hash CHECK (length(grant_jti_hash) = 64), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_support_admin_sessions_workspace_expiry ON support_admin_temporary_sessions (workspace_uuid, expires_at)",
|
||||
"CREATE TABLE webhooks (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\turl VARCHAR(1024) NOT NULL, \n\tdescription VARCHAR(512), \n\tenabled BOOLEAN NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_webhooks_workspace_name ON webhooks (workspace_uuid, name)",
|
||||
"CREATE TABLE workspace_memberships (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\taccount_uuid VARCHAR(36) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'active' NOT NULL, \n\tinvited_by_account_uuid VARCHAR(36), \n\tjoined_at TIMESTAMP WITHOUT TIME ZONE, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tprojection_revision BIGINT DEFAULT '0' NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_workspace_membership_account UNIQUE (workspace_uuid, account_uuid), \n\tCONSTRAINT ck_workspace_memberships_role CHECK (role IN ('owner', 'admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_memberships_status CHECK (status IN ('active', 'disabled', 'removed')), \n\tCONSTRAINT ck_workspace_memberships_source CHECK (source IN ('local', 'cloud_projection')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(account_uuid) REFERENCES users (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(invited_by_account_uuid) REFERENCES users (uuid) ON DELETE SET NULL\n)",
|
||||
"CREATE INDEX ix_workspace_memberships_account_status ON workspace_memberships (account_uuid, status)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_memberships_one_active_owner ON workspace_memberships (workspace_uuid) WHERE role = 'owner' AND status = 'active'",
|
||||
"CREATE TABLE workspace_invitations (\n\tuuid VARCHAR(36) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tnormalized_email VARCHAR(320) NOT NULL, \n\trole VARCHAR(32) NOT NULL, \n\ttoken_hash VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(32) DEFAULT 'pending' NOT NULL, \n\texpires_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\taccepted_at TIMESTAMP WITHOUT TIME ZONE, \n\trevoked_at TIMESTAMP WITHOUT TIME ZONE, \n\tcreated_by_account_uuid VARCHAR(36) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT ck_workspace_invitations_role CHECK (role IN ('admin', 'developer', 'operator', 'viewer')), \n\tCONSTRAINT ck_workspace_invitations_status CHECK (status IN ('pending', 'accepted', 'revoked', 'expired')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(created_by_account_uuid) REFERENCES users (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_token_hash ON workspace_invitations (token_hash)",
|
||||
"CREATE UNIQUE INDEX uq_workspace_invitations_pending_email ON workspace_invitations (workspace_uuid, normalized_email) WHERE status = 'pending'",
|
||||
"CREATE TABLE workspace_execution_states (\n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tinstance_uuid VARCHAR(255) NOT NULL, \n\tactive_generation BIGINT DEFAULT '1' NOT NULL, \n\tstate VARCHAR(32) DEFAULT 'active' NOT NULL, \n\twrite_fenced BOOLEAN DEFAULT false NOT NULL, \n\tsource VARCHAR(32) DEFAULT 'local' NOT NULL, \n\tdesired_state_revision BIGINT DEFAULT '0' NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (workspace_uuid), \n\tCONSTRAINT ck_workspace_execution_generation CHECK (active_generation > 0), \n\tCONSTRAINT ck_workspace_execution_state CHECK (state IN ('provisioning', 'active', 'migrating', 'draining', 'inactive')), \n\tCONSTRAINT ck_workspace_execution_source CHECK (source IN ('local', 'cloud')), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_workspace_execution_states_instance_state ON workspace_execution_states (instance_uuid, state)",
|
||||
"CREATE TABLE bot_admins (\n\tid SERIAL NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tbot_uuid VARCHAR(255) NOT NULL, \n\tlauncher_type VARCHAR(64) NOT NULL, \n\tlauncher_id VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (id), \n\tCONSTRAINT uq_bot_admin UNIQUE (workspace_uuid, bot_uuid, launcher_type, launcher_id), \n\tCONSTRAINT fk_bot_admins_workspace_bot FOREIGN KEY(workspace_uuid, bot_uuid) REFERENCES bots (workspace_uuid, uuid) ON DELETE CASCADE, \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE TABLE codex_credentials (\n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpayload JSON NOT NULL, \n\tversion INTEGER NOT NULL, \n\tlease_owner VARCHAR(64), \n\tlease_until FLOAT NOT NULL, \n\tPRIMARY KEY (provider_uuid), \n\tCONSTRAINT fk_codex_credentials_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_codex_credentials_workspace ON codex_credentials (workspace_uuid)",
|
||||
"CREATE TABLE llm_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\tabilities JSON NOT NULL, \n\tcontext_length INTEGER, \n\treasoning_config JSON DEFAULT '{\"level\":\"provider_default\"}' NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_llm_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_llm_models_workspace_name ON llm_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_llm_models_workspace_provider ON llm_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE embedding_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_embedding_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_name ON embedding_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_embedding_models_workspace_provider ON embedding_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE rerank_models (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tname VARCHAR(255) NOT NULL, \n\tprovider_uuid VARCHAR(255) NOT NULL, \n\textra_args JSON NOT NULL, \n\tprefered_ranking INTEGER NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_rerank_models_workspace_provider FOREIGN KEY(workspace_uuid, provider_uuid) REFERENCES model_providers (workspace_uuid, uuid), \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_name ON rerank_models (workspace_uuid, name)",
|
||||
"CREATE INDEX ix_rerank_models_workspace_provider ON rerank_models (workspace_uuid, provider_uuid)",
|
||||
"CREATE TABLE pipeline_run_records (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tpipeline_uuid VARCHAR(255) NOT NULL, \n\tstatus VARCHAR(255) NOT NULL, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tupdated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL, \n\tstarted_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tfinished_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, \n\tresult JSON NOT NULL, \n\tknowledge_base_uuid VARCHAR(255), \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_pipeline_run_records_workspace_pipeline FOREIGN KEY(workspace_uuid, pipeline_uuid) REFERENCES legacy_pipelines (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_created ON pipeline_run_records (workspace_uuid, created_at)",
|
||||
"CREATE INDEX ix_pipeline_run_records_workspace_pipeline ON pipeline_run_records (workspace_uuid, pipeline_uuid)",
|
||||
"CREATE TABLE knowledge_base_files (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tkb_id VARCHAR(255), \n\tfile_name VARCHAR, \n\textension VARCHAR, \n\tcreated_at TIMESTAMP WITHOUT TIME ZONE, \n\tstatus VARCHAR, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT uq_knowledge_base_files_workspace_uuid UNIQUE (workspace_uuid, uuid), \n\tCONSTRAINT fk_knowledge_base_files_workspace_kb FOREIGN KEY(workspace_uuid, kb_id) REFERENCES knowledge_bases (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_files_workspace_kb ON knowledge_base_files (workspace_uuid, kb_id)",
|
||||
"CREATE TABLE knowledge_base_chunks (\n\tuuid VARCHAR(255) NOT NULL, \n\tworkspace_uuid VARCHAR(36) NOT NULL, \n\tfile_id VARCHAR(255), \n\ttext TEXT, \n\tPRIMARY KEY (uuid), \n\tCONSTRAINT fk_knowledge_base_chunks_workspace_file FOREIGN KEY(workspace_uuid, file_id) REFERENCES knowledge_base_files (workspace_uuid, uuid) ON DELETE CASCADE, \n\tUNIQUE (uuid), \n\tFOREIGN KEY(workspace_uuid) REFERENCES workspaces (uuid) ON DELETE CASCADE\n)",
|
||||
"CREATE INDEX ix_knowledge_base_chunks_workspace_file ON knowledge_base_chunks (workspace_uuid, file_id)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -193,6 +193,22 @@ async def create_legacy_resource_schema(engine, *, instance_uuid: str) -> None:
|
||||
sa.Column('message_id', sa.String(255), nullable=True),
|
||||
)
|
||||
|
||||
# Include historical monitoring columns consumed by later migrations.
|
||||
for table_name in ('monitoring_messages', 'monitoring_sessions'):
|
||||
table = monitoring_tables[table_name]
|
||||
for name, value in (('bot_name', 'bot'), ('pipeline_id', 'pipeline-1'), ('pipeline_name', 'pipeline')):
|
||||
table.append_column(sa.Column(name, sa.String(255), nullable=False, default=value))
|
||||
for name in ('platform', 'user_id', 'user_name'):
|
||||
table.append_column(sa.Column(name, sa.String(255)))
|
||||
if table_name == 'monitoring_messages':
|
||||
table.append_column(sa.Column('bot_id', sa.String(255), nullable=False, default='bot-1'))
|
||||
table.append_column(sa.Column('role', sa.String(50)))
|
||||
else:
|
||||
table.append_column(sa.Column('message_count', sa.Integer, nullable=False, default=1))
|
||||
table.append_column(
|
||||
sa.Column('start_time', sa.DateTime, nullable=False, default=datetime.datetime(2026, 1, 1))
|
||||
)
|
||||
|
||||
now = datetime.datetime(2026, 1, 1)
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(metadata.create_all)
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
"""Deterministic OAuth tests using real SQLite CAS writes, never live credentials."""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import time
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.api.http.authz import Permission
|
||||
from langbot.pkg.api.http.context import PrincipalContext, PrincipalType, RequestContext, WorkspaceContext
|
||||
from langbot.pkg.entity.persistence.model import CodexCredential
|
||||
from langbot.pkg.persistence.alembic_runner import run_alembic_stamp, run_alembic_upgrade
|
||||
from langbot.pkg.provider.modelmgr.codex_auth import CodexAuth, _tokens, validate_config
|
||||
from langbot.pkg.workspace.errors import WorkspaceNotFoundError
|
||||
|
||||
|
||||
def context(workspace='w', user='u', principal=PrincipalType.ACCOUNT, permitted=True):
|
||||
return RequestContext(
|
||||
'i',
|
||||
0,
|
||||
'r',
|
||||
'user_token',
|
||||
PrincipalContext(principal, account_uuid=user),
|
||||
WorkspaceContext(
|
||||
workspace, 'm', 'owner', frozenset({Permission.PROVIDER_SECRET_MANAGE} if permitted else set())
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def jwt(**claims):
|
||||
return 'test.' + base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=') + '.test'
|
||||
|
||||
|
||||
def token_response(**extra):
|
||||
return {
|
||||
'access_token': jwt(**{'https://api.openai.com/auth': {'chatgpt_account_id': 'account'}}),
|
||||
'refresh_token': 'refresh-secret',
|
||||
'expires_in': 3600,
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth(tmp_path):
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "codex.db"}')
|
||||
|
||||
@sa.event.listens_for(engine.sync_engine, 'connect')
|
||||
def foreign_keys(connection, _):
|
||||
connection.execute('PRAGMA foreign_keys=ON')
|
||||
|
||||
async with engine.begin() as conn:
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
'CREATE TABLE model_providers (uuid VARCHAR(255) PRIMARY KEY, workspace_uuid VARCHAR(36) NOT NULL, requester TEXT, UNIQUE(workspace_uuid, uuid))'
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
"INSERT INTO model_providers VALUES ('p','w','openai-codex'), ('other','other','openai-codex'), ('api','w','openai-chat-completions')"
|
||||
)
|
||||
)
|
||||
await run_alembic_stamp(engine, '0021_merge_reasoning_config')
|
||||
await run_alembic_upgrade(engine, '0022_codex_credentials')
|
||||
|
||||
async def execute(statement):
|
||||
async with engine.begin() as conn:
|
||||
return await conn.execute(statement)
|
||||
|
||||
service = CodexAuth(SimpleNamespace(persistence_mgr=SimpleNamespace(execute_async=execute)))
|
||||
service.engine = engine
|
||||
await execute(
|
||||
sa.insert(CodexCredential).values(provider_uuid='p', workspace_uuid='w', payload={}, version=0, lease_until=0)
|
||||
)
|
||||
try:
|
||||
yield service
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
async def seed(auth, payload):
|
||||
await auth.ap.persistence_mgr.execute_async(sa.update(CodexCredential).values(payload=payload))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_upgrade_repeat_fk_cascade(auth):
|
||||
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
|
||||
await run_alembic_stamp(auth.engine, '0021_merge_reasoning_config')
|
||||
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
|
||||
with pytest.raises(sa.exc.IntegrityError):
|
||||
await auth.ap.persistence_mgr.execute_async(
|
||||
sa.insert(CodexCredential).values(
|
||||
provider_uuid='other', workspace_uuid='w', payload={}, version=0, lease_until=0
|
||||
)
|
||||
)
|
||||
await auth.ap.persistence_mgr.execute_async(sa.text("DELETE FROM model_providers WHERE uuid='p'"))
|
||||
assert await auth._read('w', 'p') is None
|
||||
from langbot.pkg.persistence.alembic_runner import run_alembic_downgrade
|
||||
|
||||
await run_alembic_downgrade(auth.engine, '0021_merge_reasoning_config')
|
||||
async with auth.engine.connect() as conn:
|
||||
assert 'codex_credentials' not in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
|
||||
await run_alembic_upgrade(auth.engine, '0022_codex_credentials')
|
||||
async with auth.engine.connect() as conn:
|
||||
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_pacing_exchange_secrecy_and_user_binding(auth):
|
||||
auth._post = AsyncMock(
|
||||
side_effect=[
|
||||
httpx.Response(200, json={'device_auth_id': 'device-secret', 'usercode': 'CODE', 'interval': '5'}),
|
||||
httpx.Response(200, json={'authorization_code': 'code-secret', 'code_verifier': 'verifier-secret'}),
|
||||
httpx.Response(200, json=token_response()),
|
||||
]
|
||||
)
|
||||
start = await auth.start(context(), 'p')
|
||||
assert set(start) == {'authorization_id', 'user_code', 'interval', 'expires_at', 'verification_uri'}
|
||||
assert 'device-secret' not in json.dumps(start)
|
||||
attempt = start['authorization_id']
|
||||
with pytest.raises(WorkspaceNotFoundError):
|
||||
await auth.poll(context(user='attacker'), 'p', attempt)
|
||||
assert (await auth.poll(context(), 'p', attempt))['status'] == 'pending'
|
||||
assert auth._post.await_count == 1
|
||||
row = await auth._read('w', 'p')
|
||||
row['payload']['pending']['next_poll_at'] = 0
|
||||
await seed(auth, row['payload'])
|
||||
assert await auth.poll(context(), 'p', attempt) == {'status': 'connected'}
|
||||
assert await auth.poll(context(), 'p', attempt) == {'status': 'connected'}
|
||||
exchange = auth._post.call_args.kwargs['data']
|
||||
assert exchange['grant_type'] == 'authorization_code'
|
||||
assert exchange['redirect_uri'] == 'https://auth.openai.com/deviceauth/callback'
|
||||
assert exchange['code_verifier'] == 'verifier-secret'
|
||||
status = await auth.status(context(), 'p')
|
||||
assert set(status) == {'status', 'connected', 'expires_at'}
|
||||
assert 'secret' not in json.dumps(status)
|
||||
await auth.disconnect(context(), 'p')
|
||||
assert (await auth._read('w', 'p'))['payload'] == {}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
'ctx,provider,error',
|
||||
[
|
||||
(context('other'), 'p', WorkspaceNotFoundError),
|
||||
(context(principal=PrincipalType.API_KEY), 'p', ValueError),
|
||||
(context(permitted=False), 'p', ValueError),
|
||||
(context(), 'api', ValueError),
|
||||
],
|
||||
)
|
||||
async def test_auth_tenant_principal_permission_guards(auth, ctx, provider, error):
|
||||
auth._post = AsyncMock()
|
||||
with pytest.raises(error):
|
||||
await auth.start(ctx, provider)
|
||||
auth._post.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_cross_instance_single_flight_and_rotation(auth):
|
||||
old = _tokens(token_response())
|
||||
old['expires_at'] = 0
|
||||
await seed(auth, {'tokens': old})
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
|
||||
async def refresh(*args, **kwargs):
|
||||
entered.set()
|
||||
await release.wait()
|
||||
return httpx.Response(200, json=token_response(refresh_token='rotated-secret'))
|
||||
|
||||
auth._post = AsyncMock(side_effect=refresh)
|
||||
other = CodexAuth(auth.ap)
|
||||
other._post = auth._post
|
||||
first = asyncio.create_task(auth.access('w', 'p'))
|
||||
await entered.wait()
|
||||
second = asyncio.create_task(other.access('w', 'p'))
|
||||
release.set()
|
||||
a, b = await asyncio.gather(first, second)
|
||||
assert a == b
|
||||
assert a['refresh_token'] == 'rotated-secret'
|
||||
assert auth._post.await_count == 1
|
||||
assert (await auth._read('w', 'p'))['payload']['tokens'] == a
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
'status,error,invalid',
|
||||
[
|
||||
(400, 'invalid_grant', True),
|
||||
(401, 'refresh_token_reused', True),
|
||||
(429, 'limited', False),
|
||||
(500, 'secret-upstream-body', False),
|
||||
(403, 'permission_denied', False),
|
||||
],
|
||||
)
|
||||
async def test_refresh_errors_are_safe_and_transient_preserves_tokens(auth, status, error, invalid):
|
||||
old = _tokens(token_response())
|
||||
old['expires_at'] = 0
|
||||
await seed(auth, {'tokens': old})
|
||||
auth._post = AsyncMock(
|
||||
return_value=httpx.Response(status, json={'error': error, 'access_token': 'secret-upstream-body'})
|
||||
)
|
||||
with pytest.raises(ValueError) as caught:
|
||||
await auth.access('w', 'p')
|
||||
assert 'secret' not in str(caught.value)
|
||||
payload = (await auth._read('w', 'p'))['payload']
|
||||
assert bool(payload.get('invalid')) == invalid
|
||||
assert ('tokens' not in payload) if invalid else payload['tokens'] == old
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('cancel', [False, True])
|
||||
async def test_disconnect_or_cancel_fences_inflight_exchange(auth, cancel):
|
||||
old = _tokens(token_response())
|
||||
await seed(
|
||||
auth,
|
||||
{
|
||||
'tokens': old,
|
||||
'pending': {
|
||||
'authorization_id': 'attempt',
|
||||
'account_uuid': 'u',
|
||||
'expires_at': time.time() + 100,
|
||||
'next_poll_at': 0,
|
||||
'interval': 5,
|
||||
'device_auth_id': 'device',
|
||||
'user_code': 'code',
|
||||
},
|
||||
},
|
||||
)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
|
||||
async def post(path, **kwargs):
|
||||
if path.endswith('/token') and path != '/oauth/token':
|
||||
return httpx.Response(200, json={'authorization_code': 'code', 'code_verifier': 'verifier'})
|
||||
entered.set()
|
||||
await release.wait()
|
||||
return httpx.Response(200, json=token_response())
|
||||
|
||||
auth._post = post
|
||||
task = asyncio.create_task(auth.poll(context(), 'p', 'attempt'))
|
||||
await entered.wait()
|
||||
if cancel:
|
||||
await auth.cancel(context(), 'p', 'attempt')
|
||||
else:
|
||||
await auth.disconnect(context(), 'p')
|
||||
release.set()
|
||||
with pytest.raises(ValueError, match='cancelled or replaced'):
|
||||
await task
|
||||
payload = (await auth._read('w', 'p'))['payload']
|
||||
assert payload == ({'tokens': old} if cancel else {})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('status,interval', [(403, 5), (404, 5), (429, 10)])
|
||||
async def test_device_pending_and_backoff(auth, status, interval):
|
||||
await seed(
|
||||
auth,
|
||||
{
|
||||
'pending': {
|
||||
'authorization_id': 'attempt',
|
||||
'account_uuid': 'u',
|
||||
'expires_at': time.time() + 100,
|
||||
'next_poll_at': 0,
|
||||
'interval': 5,
|
||||
'device_auth_id': 'device',
|
||||
'user_code': 'code',
|
||||
}
|
||||
},
|
||||
)
|
||||
auth._post = AsyncMock(return_value=httpx.Response(status))
|
||||
assert await auth.poll(context(), 'p', 'attempt') == {'status': 'pending', 'interval': interval}
|
||||
assert await auth.poll(context(), 'p', 'attempt') == {'status': 'pending', 'interval': interval}
|
||||
assert auth._post.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_replacement_expiry_and_idempotent_cancel(auth):
|
||||
auth._post = AsyncMock(return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE'}))
|
||||
first = await auth.start(context(), 'p')
|
||||
second = await auth.start(context(), 'p')
|
||||
assert first['authorization_id'] != second['authorization_id']
|
||||
assert await auth.poll(context(), 'p', first['authorization_id']) == {'status': 'expired'}
|
||||
await auth.cancel(context(), 'p', first['authorization_id'])
|
||||
payload = (await auth._read('w', 'p'))['payload']
|
||||
assert payload['pending']['authorization_id'] == second['authorization_id']
|
||||
payload['pending']['expires_at'] = 0
|
||||
await seed(auth, payload)
|
||||
assert await auth.poll(context(), 'p', second['authorization_id']) == {'status': 'expired'}
|
||||
assert 'pending' not in (await auth._read('w', 'p'))['payload']
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_accepts_issuer_iso_expiry(auth):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
expires = datetime.fromtimestamp(time.time() + 600, timezone.utc).isoformat().replace('+00:00', 'Z')
|
||||
auth._post = AsyncMock(
|
||||
return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE', 'expires_at': expires})
|
||||
)
|
||||
result = await auth.start(context(), 'p')
|
||||
assert time.time() < result['expires_at'] < time.time() + 900
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_device_expires_in_fallback(auth):
|
||||
auth._post = AsyncMock(
|
||||
return_value=httpx.Response(200, json={'device_auth_id': 'device', 'user_code': 'CODE', 'expires_in': 60})
|
||||
)
|
||||
result = await auth.start(context(), 'p')
|
||||
assert time.time() < result['expires_at'] <= time.time() + 60
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('cancel_reads_before_refresh', [False, True])
|
||||
async def test_cancel_pending_relogin_waits_for_existing_refresh(auth, cancel_reads_before_refresh):
|
||||
old = _tokens(token_response())
|
||||
old['expires_at'] = 0
|
||||
await seed(auth, {'tokens': old, 'pending': {'authorization_id': 'attempt', 'account_uuid': 'u'}})
|
||||
entered, release, cancel_read = asyncio.Event(), asyncio.Event(), asyncio.Event()
|
||||
|
||||
async def refresh(*args, **kwargs):
|
||||
entered.set()
|
||||
await release.wait()
|
||||
return httpx.Response(200, json=token_response(refresh_token='rotated-secret'))
|
||||
|
||||
other = CodexAuth(auth.ap)
|
||||
original_read = other._read
|
||||
|
||||
async def read(workspace, provider):
|
||||
row = await original_read(workspace, provider)
|
||||
cancel_read.set()
|
||||
if cancel_reads_before_refresh:
|
||||
await entered.wait()
|
||||
return row
|
||||
|
||||
other._read = read
|
||||
auth._post = refresh
|
||||
if cancel_reads_before_refresh:
|
||||
cancelling = asyncio.create_task(other.cancel(context(), 'p', 'attempt'))
|
||||
await cancel_read.wait()
|
||||
refreshing = asyncio.create_task(auth.access('w', 'p'))
|
||||
await entered.wait()
|
||||
if not cancel_reads_before_refresh:
|
||||
cancelling = asyncio.create_task(other.cancel(context(), 'p', 'attempt'))
|
||||
await cancel_read.wait()
|
||||
await asyncio.sleep(0.05)
|
||||
try:
|
||||
assert not cancelling.done(), 'Cancellation must not revoke the refresh lease'
|
||||
finally:
|
||||
release.set()
|
||||
results = await asyncio.gather(refreshing, cancelling, return_exceptions=True)
|
||||
assert not any(isinstance(result, Exception) for result in results)
|
||||
payload = (await auth._read('w', 'p'))['payload']
|
||||
assert payload['tokens']['refresh_token'] == 'rotated-secret'
|
||||
assert 'pending' not in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('operation', ['save', 'cancel', 'disconnect', 'acquire', 'release', 'read'])
|
||||
async def test_credential_database_errors_never_expose_secrets(auth, operation):
|
||||
import traceback
|
||||
|
||||
markers = ['ACCESS-MARKER', 'REFRESH-MARKER', 'DEVICE-MARKER', 'VERIFIER-MARKER']
|
||||
payload = {
|
||||
'tokens': {'access_token': markers[0], 'refresh_token': markers[1]},
|
||||
'pending': {
|
||||
'authorization_id': 'attempt',
|
||||
'account_uuid': 'u',
|
||||
'device_auth_id': markers[2],
|
||||
'code_verifier': markers[3],
|
||||
},
|
||||
}
|
||||
await seed(auth, payload)
|
||||
if operation == 'read':
|
||||
auth.ap.persistence_mgr.execute_async = AsyncMock(
|
||||
side_effect=sa.exc.StatementError(
|
||||
'failure', 'SELECT credentials', {'payload': payload}, RuntimeError(markers[0])
|
||||
)
|
||||
)
|
||||
else:
|
||||
column = 'payload' if operation in ('save', 'cancel', 'disconnect') else 'lease_owner'
|
||||
condition = ' WHEN NEW.lease_owner IS NULL' if operation == 'release' else ''
|
||||
# Trigger errors can themselves contain secrets, even for parameter-free writes.
|
||||
await auth.ap.persistence_mgr.execute_async(
|
||||
sa.text(
|
||||
f'CREATE TRIGGER reject_write BEFORE UPDATE OF {column} ON codex_credentials{condition} '
|
||||
f"BEGIN SELECT RAISE(ABORT, '{' '.join(markers)}'); END"
|
||||
)
|
||||
)
|
||||
with pytest.raises(ValueError, match='credential storage') as caught:
|
||||
if operation == 'save':
|
||||
async with auth._lease('w', 'p') as owner:
|
||||
await auth._save('w', 'p', owner, payload)
|
||||
elif operation == 'cancel':
|
||||
await auth.cancel(context(), 'p', 'attempt')
|
||||
elif operation == 'disconnect':
|
||||
await auth.disconnect(context(), 'p')
|
||||
elif operation == 'read':
|
||||
await auth._read('w', 'p')
|
||||
else:
|
||||
async with auth._lease('w', 'p'):
|
||||
pass
|
||||
rendered = ''.join(traceback.format_exception(caught.value))
|
||||
assert all(marker not in rendered for marker in markers)
|
||||
assert caught.value.__suppress_context__
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_credential_serialization_failure_is_sanitized(auth):
|
||||
import traceback
|
||||
|
||||
class Secret:
|
||||
def __repr__(self):
|
||||
return 'SERIALIZATION-SECRET'
|
||||
|
||||
with pytest.raises(ValueError, match='credential storage') as caught:
|
||||
async with auth._lease('w', 'p') as owner:
|
||||
await auth._save('w', 'p', owner, {'tokens': {'refresh_token': Secret()}})
|
||||
assert 'SERIALIZATION-SECRET' not in ''.join(traceback.format_exception(caught.value))
|
||||
assert caught.value.__suppress_context__
|
||||
|
||||
|
||||
def test_token_refresh_fallback_and_config_validation():
|
||||
old = _tokens(token_response())
|
||||
refreshed = _tokens({'access_token': 'opaque-access', 'expires_in': 3600}, old)
|
||||
assert refreshed['refresh_token'] == old['refresh_token']
|
||||
assert refreshed['connection_id'] == old['connection_id']
|
||||
for expiry in [float('nan'), float('inf'), -1, 'bad']:
|
||||
with pytest.raises(ValueError):
|
||||
_tokens(token_response(expires_in=expiry))
|
||||
data = {'requester': 'openai-codex'}
|
||||
validate_config(data)
|
||||
assert data['api_keys'] == []
|
||||
for update in [{'base_url': 'https://evil.invalid'}, {'api_keys': ['secret']}]:
|
||||
with pytest.raises(ValueError):
|
||||
validate_config({**data, **update})
|
||||
ordinary = {'requester': 'openai-chat-completions', 'api_keys': ['key'], 'base_url': 'https://custom.invalid'}
|
||||
before = dict(ordinary)
|
||||
validate_config(ordinary)
|
||||
assert ordinary == before
|
||||
@@ -0,0 +1,377 @@
|
||||
"""Upgrade real historical ORM schemas through both published migration heads.
|
||||
|
||||
The fixtures are DDL snapshots, not current metadata stamped as an old version.
|
||||
Every ancestor migration runs before we seed branch-specific data and merge.
|
||||
PostgreSQL cases use a unique task-owned schema; no existing tables are dropped.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.entity import persistence
|
||||
from langbot.pkg.entity.persistence.base import Base
|
||||
from langbot.pkg.persistence.alembic_runner import (
|
||||
get_alembic_current,
|
||||
get_alembic_head,
|
||||
run_alembic_downgrade,
|
||||
run_alembic_upgrade,
|
||||
)
|
||||
from langbot.pkg.utils import importutil
|
||||
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
importutil.import_modules_in_pkg(persistence)
|
||||
|
||||
FIXTURES = Path(__file__).with_name('fixtures')
|
||||
WORKSPACE = '41100000-0000-4000-8000-000000000001'
|
||||
ACCOUNT = '41100000-0000-4000-8000-000000000002'
|
||||
NOW = datetime.datetime(2026, 9, 1)
|
||||
|
||||
|
||||
@pytest.fixture(params=['sqlite', pytest.param('postgresql', marks=pytest.mark.slow)])
|
||||
async def convergence_engine(request, tmp_path):
|
||||
if request.param == 'sqlite':
|
||||
engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "convergence.db"}')
|
||||
try:
|
||||
yield engine
|
||||
finally:
|
||||
await engine.dispose()
|
||||
return
|
||||
|
||||
url = os.environ.get('TEST_POSTGRES_URL')
|
||||
if not url:
|
||||
pytest.skip('TEST_POSTGRES_URL not set')
|
||||
schema = f'lb_convergence_{uuid.uuid4().hex}'
|
||||
admin = create_async_engine(url)
|
||||
engine = create_async_engine(url, connect_args={'server_settings': {'search_path': f'{schema},public'}})
|
||||
try:
|
||||
async with admin.begin() as conn:
|
||||
await conn.exec_driver_sql(f'CREATE SCHEMA {schema}')
|
||||
yield engine
|
||||
finally:
|
||||
await engine.dispose()
|
||||
async with admin.begin() as conn:
|
||||
await conn.exec_driver_sql(f'DROP SCHEMA IF EXISTS {schema} CASCADE')
|
||||
await admin.dispose()
|
||||
|
||||
|
||||
async def _insert(conn, table_name, **values):
|
||||
table = await conn.run_sync(lambda sync: sa.Table(table_name, sa.MetaData(), autoload_with=sync))
|
||||
# Historical baseline tables predate Workspace identity and event bindings.
|
||||
await conn.execute(table.insert().values(**{key: value for key, value in values.items() if key in table.c}))
|
||||
|
||||
|
||||
async def _seed_common(engine):
|
||||
async with engine.begin() as conn:
|
||||
tables = await conn.run_sync(lambda sync: set(sa.inspect(sync).get_table_names()))
|
||||
await _insert(conn, 'metadata', key='instance_uuid', value='migration-convergence')
|
||||
await _insert(
|
||||
conn, 'users', uuid=ACCOUNT, user='owner@example.com', normalized_email='owner@example.com', password='hash'
|
||||
)
|
||||
if 'workspaces' in tables:
|
||||
await _insert(
|
||||
conn,
|
||||
'workspaces',
|
||||
uuid=WORKSPACE,
|
||||
instance_uuid='migration-convergence',
|
||||
name='Fixture',
|
||||
slug='fixture',
|
||||
created_by_account_uuid=ACCOUNT,
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'workspace_memberships',
|
||||
uuid='41100000-0000-4000-8000-000000000003',
|
||||
workspace_uuid=WORKSPACE,
|
||||
account_uuid=ACCOUNT,
|
||||
role='owner',
|
||||
source='local',
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'model_providers',
|
||||
uuid='provider-1',
|
||||
workspace_uuid=WORKSPACE,
|
||||
name='Fixture provider',
|
||||
requester='openai',
|
||||
base_url='https://fixture.invalid',
|
||||
api_keys=['fixture-key'],
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'legacy_pipelines',
|
||||
uuid='pipeline-1',
|
||||
workspace_uuid=WORKSPACE,
|
||||
name='Fixture pipeline',
|
||||
description='preserve pipeline',
|
||||
for_version='4',
|
||||
is_default=True,
|
||||
stages=[],
|
||||
config={},
|
||||
extensions_preferences={},
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'bots',
|
||||
uuid='bot-1',
|
||||
workspace_uuid=WORKSPACE,
|
||||
name='Fixture bot',
|
||||
description='preserve bot',
|
||||
adapter='fixture',
|
||||
adapter_config={},
|
||||
enable=False,
|
||||
use_pipeline_uuid='pipeline-1',
|
||||
event_bindings=[
|
||||
{
|
||||
'id': 'route-1',
|
||||
'event_pattern': 'message.*',
|
||||
'target_type': 'pipeline',
|
||||
'target_uuid': 'pipeline-1',
|
||||
'enabled': True,
|
||||
}
|
||||
],
|
||||
plugin_processors=[{'processor_uuid': 'agent-1', 'enabled': True}],
|
||||
)
|
||||
for index, bot in enumerate(('bot-1', 'bot-2')):
|
||||
await _insert(
|
||||
conn,
|
||||
'monitoring_messages',
|
||||
id=f'message-{index}',
|
||||
workspace_uuid=WORKSPACE,
|
||||
timestamp=NOW,
|
||||
bot_id=bot,
|
||||
bot_name=bot,
|
||||
pipeline_id='pipeline-1',
|
||||
pipeline_name='Fixture pipeline',
|
||||
message_content=f'preserve message {index}',
|
||||
session_id='shared-session',
|
||||
status='success',
|
||||
level='info',
|
||||
role='user',
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'monitoring_sessions',
|
||||
workspace_uuid=WORKSPACE,
|
||||
session_id='shared-session',
|
||||
bot_id='bot-1',
|
||||
bot_name='bot-1',
|
||||
pipeline_id='pipeline-1',
|
||||
pipeline_name='Fixture pipeline',
|
||||
message_count=1,
|
||||
start_time=NOW,
|
||||
last_activity=NOW,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
|
||||
async def _seed_branch_data(engine, source):
|
||||
async with engine.begin() as conn:
|
||||
if source == 'master':
|
||||
await _insert(
|
||||
conn,
|
||||
'passkey_credentials',
|
||||
uuid='41100000-0000-4000-8000-000000000004',
|
||||
account_uuid=ACCOUNT,
|
||||
name='Fixture passkey',
|
||||
credential_id='fixture-credential',
|
||||
public_key='fixture-public-key',
|
||||
sign_count=7,
|
||||
backed_up=True,
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'codex_credentials',
|
||||
provider_uuid='provider-1',
|
||||
workspace_uuid=WORKSPACE,
|
||||
payload={'fixture': 'preserve-codex'},
|
||||
version=3,
|
||||
lease_until=0.0,
|
||||
)
|
||||
# Master already supports colliding bot sessions before this merge.
|
||||
await _insert(
|
||||
conn,
|
||||
'monitoring_sessions',
|
||||
workspace_uuid=WORKSPACE,
|
||||
session_id='shared-session',
|
||||
bot_id='bot-2',
|
||||
bot_name='bot-2',
|
||||
pipeline_id='pipeline-1',
|
||||
pipeline_name='Fixture pipeline',
|
||||
message_count=1,
|
||||
start_time=NOW,
|
||||
last_activity=NOW,
|
||||
is_active=True,
|
||||
)
|
||||
if source == 'beta':
|
||||
await _insert(
|
||||
conn,
|
||||
'agents',
|
||||
uuid='agent-1',
|
||||
workspace_uuid=WORKSPACE,
|
||||
name='Fixture agent',
|
||||
description='preserve agent',
|
||||
kind='event_processor',
|
||||
component_ref='fixture:Runner',
|
||||
config={'fixture': 'preserve-agent'},
|
||||
supported_event_patterns=['message.*'],
|
||||
)
|
||||
await _insert(
|
||||
conn,
|
||||
'runner_state',
|
||||
runner_id='fixture:Runner',
|
||||
binding_identity='agent-1',
|
||||
scope='bot',
|
||||
scope_key='fixture-scope',
|
||||
state_key='fixture-state',
|
||||
value_json='{"sentinel": 411}',
|
||||
bot_id='bot-1',
|
||||
workspace_id=WORKSPACE,
|
||||
created_at=NOW,
|
||||
updated_at=NOW,
|
||||
)
|
||||
|
||||
|
||||
async def _assert_merged_schema_and_data(engine, source):
|
||||
assert await get_alembic_current(engine) == get_alembic_head()
|
||||
async with engine.connect() as conn:
|
||||
inspector_data = await conn.run_sync(
|
||||
lambda sync: {
|
||||
'tables': set(sa.inspect(sync).get_table_names()),
|
||||
'bot_columns': {c['name'] for c in sa.inspect(sync).get_columns('bots')},
|
||||
'agent_columns': {c['name'] for c in sa.inspect(sync).get_columns('agents')},
|
||||
'session_pk': sa.inspect(sync).get_pk_constraint('monitoring_sessions')['constrained_columns'],
|
||||
}
|
||||
)
|
||||
assert {'agents', 'runner_state', 'codex_credentials', 'passkey_credentials'} <= inspector_data['tables']
|
||||
assert {'event_bindings', 'plugin_processors'} <= inspector_data['bot_columns']
|
||||
assert 'enabled' not in inspector_data['agent_columns']
|
||||
assert inspector_data['session_pk'] == ['workspace_uuid', 'bot_id', 'session_id']
|
||||
assert await conn.scalar(sa.text('SELECT password FROM users')) == 'hash'
|
||||
assert await conn.scalar(sa.text('SELECT COUNT(*) FROM workspace_memberships')) == 1
|
||||
assert await conn.scalar(sa.text('SELECT name FROM model_providers')) == 'Fixture provider'
|
||||
assert await conn.scalar(sa.text('SELECT description FROM legacy_pipelines')) == 'preserve pipeline'
|
||||
assert (
|
||||
await conn.execute(sa.text('SELECT message_content FROM monitoring_messages ORDER BY id'))
|
||||
).scalars().all() == [
|
||||
'preserve message 0',
|
||||
'preserve message 1',
|
||||
]
|
||||
assert (
|
||||
await conn.execute(sa.text('SELECT bot_id FROM monitoring_sessions ORDER BY bot_id'))
|
||||
).scalars().all() == [
|
||||
'bot-1',
|
||||
'bot-2',
|
||||
]
|
||||
bots = await conn.run_sync(lambda sync: sa.Table('bots', sa.MetaData(), autoload_with=sync))
|
||||
row = (await conn.execute(sa.select(bots.c.event_bindings, bots.c.plugin_processors))).one()
|
||||
assert row.event_bindings[0]['target_uuid'] == 'pipeline-1'
|
||||
if source == 'beta':
|
||||
assert row.plugin_processors == [{'processor_uuid': 'agent-1', 'enabled': True}]
|
||||
assert await conn.scalar(sa.text('SELECT value_json FROM runner_state')) == '{"sentinel": 411}'
|
||||
assert await conn.scalar(sa.text('SELECT name FROM agents')) == 'Fixture agent'
|
||||
if source == 'master':
|
||||
assert await conn.scalar(sa.text('SELECT sign_count FROM passkey_credentials')) == 7
|
||||
credentials = await conn.run_sync(
|
||||
lambda sync: sa.Table('codex_credentials', sa.MetaData(), autoload_with=sync)
|
||||
)
|
||||
assert await conn.scalar(sa.select(credentials.c.payload)) == {'fixture': 'preserve-codex'}
|
||||
if engine.dialect.name == 'postgresql':
|
||||
rows = (
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
'SELECT c.relname, c.relrowsecurity, c.relforcerowsecurity, '
|
||||
'EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid = c.oid '
|
||||
"AND p.polname = 'langbot_workspace_isolation') AS policy "
|
||||
'FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace '
|
||||
"WHERE n.nspname = current_schema() AND c.relname IN ('agents', 'codex_credentials', 'monitoring_sessions')"
|
||||
)
|
||||
)
|
||||
).all()
|
||||
assert len(rows) == 3
|
||||
assert all(row.relrowsecurity and row.relforcerowsecurity and row.policy for row in rows)
|
||||
else:
|
||||
assert (await conn.exec_driver_sql('PRAGMA foreign_key_check')).all() == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('source', ['baseline', 'master', 'beta'])
|
||||
async def test_historical_schema_and_populated_branch_upgrade(convergence_engine, source):
|
||||
engine = convergence_engine
|
||||
fixture = json.loads((FIXTURES / f'{source}_schema.json').read_text())
|
||||
async with engine.begin() as conn:
|
||||
for statement in fixture['dialects'][engine.dialect.name]:
|
||||
await conn.exec_driver_sql(statement)
|
||||
# Execute the published ancestors; never stamp an empty or current schema.
|
||||
await run_alembic_upgrade(engine, fixture['revision'])
|
||||
assert await get_alembic_current(engine) == fixture['revision']
|
||||
async with engine.connect() as conn:
|
||||
tables = await conn.run_sync(lambda sync: set(sa.inspect(sync).get_table_names()))
|
||||
if source == 'master':
|
||||
assert {'codex_credentials', 'passkey_credentials'} <= tables
|
||||
assert 'agents' not in tables and 'runner_state' not in tables
|
||||
elif source == 'beta':
|
||||
assert {'agents', 'runner_state'} <= tables
|
||||
assert 'codex_credentials' not in tables and 'passkey_credentials' not in tables
|
||||
assert await conn.run_sync(
|
||||
lambda sync: sa.inspect(sync).get_pk_constraint('monitoring_sessions')['constrained_columns']
|
||||
) == ['workspace_uuid', 'session_id']
|
||||
else:
|
||||
assert 'workspaces' not in tables
|
||||
await _seed_common(engine)
|
||||
await _seed_branch_data(engine, source)
|
||||
if source == 'baseline' and engine.dialect.name == 'postgresql':
|
||||
# Match PersistenceManager's staged legacy bootstrap: expand the
|
||||
# account/resource contract before creating deferred tenant tables.
|
||||
# RLS 0011 requires those tables (including monitoring_tool_calls).
|
||||
await run_alembic_upgrade(engine, '0010_scope_resources')
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
await _assert_merged_schema_and_data(engine, source)
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
await _assert_merged_schema_and_data(engine, source)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_database_startup_schema_then_real_migrations(convergence_engine):
|
||||
engine = convergence_engine
|
||||
async with engine.begin() as conn:
|
||||
assert await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names()) == []
|
||||
# This is the documented fresh-install contract: create_all precedes Alembic.
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
assert await get_alembic_current(engine) == get_alembic_head()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_merge_only_downgrade_preserves_both_branch_schemas(convergence_engine):
|
||||
engine = convergence_engine
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
await _seed_common(engine)
|
||||
await _seed_branch_data(engine, 'master')
|
||||
await _seed_branch_data(engine, 'beta')
|
||||
# Targeting a parent removes only the merge, leaving both parent heads.
|
||||
# A relative -1 is ambiguous at a merge point. Neither feature is reverted.
|
||||
await run_alembic_downgrade(engine, '0024_passkey_credentials')
|
||||
async with engine.connect() as conn:
|
||||
revisions = set((await conn.execute(sa.text('SELECT version_num FROM alembic_version'))).scalars())
|
||||
knowledge_columns = await conn.run_sync(
|
||||
lambda sync: {column['name'] for column in sa.inspect(sync).get_columns('knowledge_bases')}
|
||||
)
|
||||
assert revisions == {'0024_passkey_credentials', '0026_knowledge_base_drafts'}
|
||||
assert 'initialized' in knowledge_columns
|
||||
await run_alembic_upgrade(engine, 'head')
|
||||
await _assert_merged_schema_and_data(engine, 'master')
|
||||
await _assert_merged_schema_and_data(engine, 'beta')
|
||||
@@ -64,6 +64,17 @@ async def sqlite_engine(sqlite_db_url):
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
def test_migration_graph_has_one_head_containing_both_released_branches():
|
||||
cfg = Config()
|
||||
cfg.set_main_option('script_location', _ALEMBIC_DIR)
|
||||
scripts = ScriptDirectory.from_config(cfg)
|
||||
heads = scripts.get_heads()
|
||||
assert len(heads) == 1, f'Release migrations must converge, found {heads}'
|
||||
ancestors = {revision.revision for revision in scripts.walk_revisions()}
|
||||
assert {'0024_passkey_credentials', '0025_bot_plugin_processors'} <= ancestors
|
||||
assert all(len(revision) <= 32 for revision in ancestors)
|
||||
|
||||
|
||||
class TestSQLiteMigrationBaseline:
|
||||
"""Tests for baseline stamp workflow."""
|
||||
|
||||
@@ -313,6 +324,15 @@ class TestSQLiteMigrationUpgrade:
|
||||
class TestSQLiteMigrationFreshDatabase:
|
||||
"""Tests for fresh database workflow."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_scoped_sessions_skips_absent_table(self, sqlite_engine):
|
||||
"""A partial schema needs no session key migration in either direction."""
|
||||
await run_alembic_stamp(sqlite_engine, '0022_codex_credentials')
|
||||
await run_alembic_upgrade(sqlite_engine, '0023_bot_scoped_sessions')
|
||||
assert await get_alembic_current(sqlite_engine) == '0023_bot_scoped_sessions'
|
||||
await run_alembic_downgrade(sqlite_engine, '0022_codex_credentials')
|
||||
assert await get_alembic_current(sqlite_engine) == '0022_codex_credentials'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fresh_db_upgrade_from_scratch(self, tmp_path):
|
||||
"""
|
||||
|
||||
@@ -549,10 +549,14 @@ class TestPostgreSQLWorkspaceMigration:
|
||||
await conn.run_sync(lambda sync_conn: sa.inspect(sync_conn).get_table_names())
|
||||
)
|
||||
assert 'workspaces' not in tables_before_migration
|
||||
assert 'codex_credentials' not in tables_before_migration
|
||||
assert 'passkey_credentials' not in tables_before_migration
|
||||
|
||||
await manager._initialize_managed_schema()
|
||||
|
||||
async with postgres_engine.connect() as conn:
|
||||
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
|
||||
assert 'passkey_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
|
||||
account = (await conn.execute(text('SELECT uuid, status, source FROM users'))).mappings().one()
|
||||
workspace = (
|
||||
(await conn.execute(text('SELECT * FROM workspaces WHERE source = :source'), {'source': 'local'}))
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
"""Monitoring regressions through asyncpg, Cloud UoW guards, and migrated RLS.
|
||||
|
||||
TEST_POSTGRES_URL must identify a disposable PostgreSQL/pgvector test server
|
||||
with permission to create databases and roles. Each run owns a fresh database;
|
||||
no existing tables are dropped. Without that URL these tests are skipped.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from langbot.pkg.api.http.context import ExecutionContext
|
||||
from langbot.pkg.api.http.service.monitoring import MonitoringService
|
||||
from langbot.pkg.entity.persistence import monitoring as models
|
||||
from langbot.pkg.entity.persistence.workspace import Workspace
|
||||
from langbot.pkg.persistence.mgr import PersistenceManager, PersistenceMode
|
||||
from langbot.pkg.persistence.tenant_uow import TenantScopeRequiredError
|
||||
from langbot.pkg.pipeline.monitoring_helper import MonitoringHelper
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.slow, pytest.mark.asyncio(loop_scope='module')]
|
||||
|
||||
WORKSPACE_A = '00000000-0000-0000-0000-00000000000a'
|
||||
WORKSPACE_B = '00000000-0000-0000-0000-00000000000b'
|
||||
RESOURCE = dict(bot_id='same-bot', bot_name='Bot', pipeline_id='same-pipeline', pipeline_name='Pipeline')
|
||||
MONITORING_TABLES = tuple(
|
||||
table for table in models.MonitoringMessage.metadata.sorted_tables if table.name.startswith('monitoring_')
|
||||
)
|
||||
|
||||
|
||||
def _context(workspace_uuid):
|
||||
return ExecutionContext(
|
||||
instance_uuid='monitoring-postgres-test',
|
||||
workspace_uuid=workspace_uuid,
|
||||
placement_generation=1,
|
||||
bot_uuid=RESOURCE['bot_id'],
|
||||
pipeline_uuid=RESOURCE['pipeline_id'],
|
||||
)
|
||||
|
||||
|
||||
def _application(url):
|
||||
return SimpleNamespace(
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'database': {
|
||||
'use': 'postgresql',
|
||||
'postgresql': {
|
||||
'host': url.host,
|
||||
'port': url.port,
|
||||
'user': url.username,
|
||||
'password': url.password,
|
||||
'database': url.database,
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
logger=logging.getLogger('monitoring-postgres-test'),
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope='module', loop_scope='module')
|
||||
async def cloud_database():
|
||||
url = os.environ.get('TEST_POSTGRES_URL')
|
||||
if not url:
|
||||
pytest.skip('TEST_POSTGRES_URL not set')
|
||||
admin_url = sa.engine.make_url(url)
|
||||
admin = create_async_engine(admin_url, isolation_level='AUTOCOMMIT')
|
||||
suffix = uuid.uuid4().hex[:12]
|
||||
database_name = f'lb_monitoring_{suffix}'
|
||||
runtime_role = f'lb_monitoring_{suffix}'
|
||||
password = f'Test{uuid.uuid4().hex}'
|
||||
database_created = role_created = False
|
||||
release_manager = runtime_manager = None
|
||||
quote = admin.dialect.identifier_preparer.quote
|
||||
from langbot.pkg.persistence import mgr as mgr_module
|
||||
from langbot.pkg.persistence.databases.postgresql import PostgreSQLDatabaseManager
|
||||
from langbot.pkg.utils import constants
|
||||
|
||||
with pytest.MonkeyPatch.context() as patch:
|
||||
patch.setattr(mgr_module.database, 'preregistered_managers', [PostgreSQLDatabaseManager])
|
||||
patch.setattr(constants, 'instance_id', 'monitoring-postgres-test')
|
||||
try:
|
||||
async with admin.connect() as conn:
|
||||
await conn.execute(sa.text(f'CREATE DATABASE {quote(database_name)}'))
|
||||
database_created = True
|
||||
await conn.execute(
|
||||
sa.text(f"CREATE ROLE {quote(runtime_role)} LOGIN NOSUPERUSER NOBYPASSRLS PASSWORD '{password}'")
|
||||
)
|
||||
role_created = True
|
||||
release_app = _application(admin_url.set(database=database_name))
|
||||
release_manager = PersistenceManager(release_app, mode=PersistenceMode.RELEASE_MIGRATION)
|
||||
release_app.persistence_mgr = release_manager
|
||||
await release_manager.initialize()
|
||||
async with release_manager.get_db_engine().begin() as conn:
|
||||
for workspace in (WORKSPACE_A, WORKSPACE_B):
|
||||
await conn.execute(
|
||||
sa.insert(Workspace).values(
|
||||
uuid=workspace,
|
||||
instance_uuid='monitoring-postgres-test',
|
||||
name=workspace,
|
||||
slug=workspace,
|
||||
source='cloud_projection',
|
||||
)
|
||||
)
|
||||
tables = release_manager._runtime_business_table_names()
|
||||
quoted_tables = ', '.join(f'public.{quote(name)}' for name in tables)
|
||||
await conn.execute(
|
||||
sa.text(f'GRANT CONNECT ON DATABASE {quote(database_name)} TO {quote(runtime_role)}')
|
||||
)
|
||||
await conn.execute(sa.text(f'GRANT USAGE ON SCHEMA public TO {quote(runtime_role)}'))
|
||||
await conn.execute(
|
||||
sa.text(f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE {quoted_tables} TO {quote(runtime_role)}')
|
||||
)
|
||||
await conn.execute(sa.text(f'GRANT SELECT ON public.alembic_version TO {quote(runtime_role)}'))
|
||||
sequences = await release_manager._runtime_business_sequence_names(conn, tables)
|
||||
if sequences:
|
||||
names = ', '.join(f'public.{quote(name)}' for name in sequences)
|
||||
await conn.execute(sa.text(f'GRANT USAGE, SELECT ON SEQUENCE {names} TO {quote(runtime_role)}'))
|
||||
runtime_app = _application(admin_url.set(database=database_name, username=runtime_role, password=password))
|
||||
runtime_manager = PersistenceManager(runtime_app, mode=PersistenceMode.CLOUD_RUNTIME)
|
||||
runtime_app.persistence_mgr = runtime_manager
|
||||
await runtime_manager.initialize()
|
||||
runtime_app.monitoring_service = MonitoringService(runtime_app)
|
||||
yield runtime_app, release_manager.get_db_engine()
|
||||
finally:
|
||||
if runtime_manager is not None:
|
||||
await runtime_manager.shutdown()
|
||||
if release_manager is not None:
|
||||
await release_manager.shutdown()
|
||||
async with admin.connect() as conn:
|
||||
if database_created:
|
||||
await conn.execute(sa.text(f'DROP DATABASE {quote(database_name)} WITH (FORCE)'))
|
||||
if role_created:
|
||||
await conn.execute(sa.text(f'DROP ROLE {quote(runtime_role)}'))
|
||||
await admin.dispose()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(loop_scope='module')
|
||||
async def service(cloud_database):
|
||||
application, admin = cloud_database
|
||||
async with admin.begin() as conn:
|
||||
for table in MONITORING_TABLES:
|
||||
await conn.execute(sa.delete(table))
|
||||
application.instance_config.data.pop('monitoring', None)
|
||||
return application.monitoring_service
|
||||
|
||||
|
||||
async def _read(service, method, context, *args, **kwargs):
|
||||
# HTTP auth binds a tenant scope; exercise that same guard for service reads.
|
||||
async with service.ap.persistence_mgr.tenant_scope(context.workspace_uuid):
|
||||
return await getattr(service, method)(context, *args, **kwargs)
|
||||
|
||||
|
||||
def _query(context, sender_id):
|
||||
return SimpleNamespace(
|
||||
_execution_context=context,
|
||||
launcher_type='person',
|
||||
launcher_id='same-user',
|
||||
sender_id=sender_id,
|
||||
message_chain=SimpleNamespace(model_dump=lambda: [{'type': 'Plain', 'text': 'hello'}]),
|
||||
resp_message_chain=[SimpleNamespace(model_dump=lambda: [{'type': 'Plain', 'text': 'reply'}])],
|
||||
message_event=SimpleNamespace(sender=SimpleNamespace(nickname='Alice')),
|
||||
variables={'public': 'value', '_private': 'hidden'},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user_id', [123456789, -100123456789, 0, None, '', '00123', ' opaque用户 '])
|
||||
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
|
||||
async def test_optional_user_ids_round_trip_through_asyncpg(service, user_id, record_type):
|
||||
context = _context(WORKSPACE_A)
|
||||
expected = str(user_id) if isinstance(user_id, int) else user_id
|
||||
if record_type == 'message':
|
||||
record_id = await service.record_message(
|
||||
context,
|
||||
**RESOURCE,
|
||||
message_content='hello',
|
||||
session_id='same-session',
|
||||
user_id=user_id,
|
||||
)
|
||||
details = await _read(service, 'get_message_details', context, record_id)
|
||||
assert details['message']['user_id'] == expected
|
||||
elif record_type == 'session':
|
||||
await service.record_session_start(context, **RESOURCE, session_id='same-session', user_id=user_id)
|
||||
rows, total = await _read(service, 'get_sessions', context)
|
||||
assert total == 1
|
||||
assert rows[0]['user_id'] == expected
|
||||
else:
|
||||
await service.record_feedback(context, feedback_id='same-feedback', feedback_type=1, user_id=user_id)
|
||||
rows, total = await _read(service, 'get_feedback_list', context)
|
||||
assert total == 1
|
||||
assert rows[0]['user_id'] == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user_id', [123456789, -100123456789])
|
||||
async def test_query_lifecycle_persists_messages_session_and_llm_link(service, user_id, caplog):
|
||||
context = _context(WORKSPACE_A)
|
||||
query = _query(context, user_id)
|
||||
message_id = await MonitoringHelper.record_query_start(service.ap, query, **RESOURCE)
|
||||
assert message_id, caplog.text
|
||||
await MonitoringHelper.record_llm_call(
|
||||
service.ap,
|
||||
query,
|
||||
**RESOURCE,
|
||||
model_name='model',
|
||||
input_tokens=3,
|
||||
output_tokens=5,
|
||||
duration_ms=25,
|
||||
message_id=message_id,
|
||||
)
|
||||
await MonitoringHelper.record_query_success(service.ap, message_id, query)
|
||||
await MonitoringHelper.record_query_response(service.ap, query, **RESOURCE)
|
||||
rows, total = await _read(service, 'get_messages', context)
|
||||
assert total == 2
|
||||
assert {row['role'] for row in rows} == {'user', 'assistant'}
|
||||
assert {row['user_id'] for row in rows} == {str(user_id)}
|
||||
details = await _read(service, 'get_message_details', context, message_id)
|
||||
assert details['message']['status'] == 'success'
|
||||
assert details['message']['variables'] == '{"public": "value"}'
|
||||
assert details['llm_calls'][0]['message_id'] == message_id
|
||||
assert details['llm_stats']['total_tokens'] == 8
|
||||
sessions, total = await _read(service, 'get_sessions', context)
|
||||
assert total == 1
|
||||
assert sessions[0]['session_id'] == 'person_same-user'
|
||||
assert sessions[0]['user_id'] == str(user_id)
|
||||
assert not [record for record in caplog.records if record.levelno >= logging.ERROR]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user_id', [123, -123])
|
||||
async def test_query_error_persists_error_message_and_linked_log(service, user_id, caplog):
|
||||
context = _context(WORKSPACE_A)
|
||||
message_id = await MonitoringHelper.record_query_error(
|
||||
service.ap,
|
||||
_query(context, user_id),
|
||||
**RESOURCE,
|
||||
error=ValueError('failed query'),
|
||||
)
|
||||
assert message_id, caplog.text
|
||||
details = await _read(service, 'get_message_details', context, message_id)
|
||||
assert details['message']['user_id'] == str(user_id)
|
||||
assert details['message']['status'] == 'error'
|
||||
assert details['errors'][0]['message_id'] == message_id
|
||||
assert details['errors'][0]['error_type'] == 'ValueError'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('user_id', [True, 1.5, b'123', ['123']])
|
||||
@pytest.mark.parametrize('record_type', ['message', 'session', 'feedback'])
|
||||
async def test_unsupported_user_ids_fail_at_the_write_boundary(service, user_id, record_type):
|
||||
context = _context(WORKSPACE_A)
|
||||
with pytest.raises(TypeError, match='user_id must be a string, integer, or None'):
|
||||
if record_type == 'message':
|
||||
await service.record_message(
|
||||
context,
|
||||
**RESOURCE,
|
||||
message_content='hello',
|
||||
session_id='session',
|
||||
user_id=user_id,
|
||||
)
|
||||
elif record_type == 'session':
|
||||
await service.record_session_start(context, **RESOURCE, session_id='session', user_id=user_id)
|
||||
else:
|
||||
await service.record_feedback(context, feedback_id='feedback', feedback_type=1, user_id=user_id)
|
||||
async with service.ap.persistence_mgr.tenant_scope(WORKSPACE_A):
|
||||
for model in (models.MonitoringMessage, models.MonitoringSession, models.MonitoringFeedback):
|
||||
count = await service.ap.persistence_mgr.execute_async(sa.select(sa.func.count()).select_from(model))
|
||||
assert count.scalar_one() == 0
|
||||
|
||||
|
||||
async def test_session_analysis_aggregates_under_cloud_sql_guard(service):
|
||||
context = _context(WORKSPACE_A)
|
||||
await service.record_session_start(context, **RESOURCE, session_id='same-session')
|
||||
await service.record_message(context, **RESOURCE, session_id='same-session', message_content='hello')
|
||||
result = await _read(service, 'get_session_analysis', context, 'same-session')
|
||||
assert result['found'] is True
|
||||
assert result['message_stats'] == {'total': 1, 'success': 1, 'error': 0, 'pending': 0}
|
||||
assert result['llm_stats']['total_calls'] == 0
|
||||
assert result['tool_stats']['total_calls'] == 0
|
||||
assert result['session_duration_seconds'] == 0
|
||||
|
||||
|
||||
async def test_rls_is_enforced_without_application_workspace_predicates(service, cloud_database):
|
||||
_, admin = cloud_database
|
||||
for workspace in (WORKSPACE_A, WORKSPACE_B):
|
||||
await service.record_message(
|
||||
_context(workspace), **RESOURCE, session_id='same-session', message_content=workspace
|
||||
)
|
||||
async with admin.connect() as conn:
|
||||
states = (
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
'SELECT relname, relrowsecurity, relforcerowsecurity FROM pg_class '
|
||||
"WHERE relname LIKE 'monitoring_%' AND relkind = 'r'"
|
||||
)
|
||||
)
|
||||
).all()
|
||||
assert len(states) == len(MONITORING_TABLES)
|
||||
assert all(enabled and forced for _, enabled, forced in states)
|
||||
engine = service.ap.persistence_mgr.get_db_engine()
|
||||
async with engine.connect() as conn:
|
||||
role = (
|
||||
await conn.execute(sa.text('SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user'))
|
||||
).one()
|
||||
assert role == (False, False)
|
||||
assert (await conn.execute(sa.select(models.MonitoringMessage.id))).all() == []
|
||||
for workspace in (WORKSPACE_A, WORKSPACE_B):
|
||||
async with service.ap.persistence_mgr.tenant_uow(workspace):
|
||||
rows = (
|
||||
await service.ap.persistence_mgr.execute_async(sa.select(models.MonitoringMessage.workspace_uuid))
|
||||
).all()
|
||||
assert rows == [(workspace,)]
|
||||
with pytest.raises(TenantScopeRequiredError):
|
||||
await service.ap.persistence_mgr.execute_async(sa.select(models.MonitoringMessage.id))
|
||||
|
||||
|
||||
async def test_traffic_series_aggregates_all_rows_under_cloud_rls(service):
|
||||
import datetime
|
||||
from langbot.pkg.api.http.service.monitoring_traffic import get_traffic_series
|
||||
|
||||
context = _context(WORKSPACE_A)
|
||||
for workspace, count in ((WORKSPACE_A, 61), (WORKSPACE_B, 2)):
|
||||
async with service.ap.persistence_mgr.tenant_scope(workspace):
|
||||
await service.ap.persistence_mgr.execute_async(
|
||||
sa.insert(models.MonitoringMessage).values(
|
||||
[
|
||||
dict(
|
||||
workspace_uuid=workspace,
|
||||
id=f'{workspace}-m-{i}',
|
||||
**RESOURCE,
|
||||
session_id='shared',
|
||||
message_content='test',
|
||||
status='success',
|
||||
level='info',
|
||||
timestamp=datetime.datetime(2026, 9, 11, 1, 30),
|
||||
)
|
||||
for i in range(count)
|
||||
]
|
||||
)
|
||||
)
|
||||
async with service.ap.persistence_mgr.tenant_uow(WORKSPACE_A):
|
||||
result = await get_traffic_series(
|
||||
service.ap,
|
||||
context,
|
||||
bot_ids=[RESOURCE['bot_id']],
|
||||
start_time=datetime.datetime(2026, 9, 11),
|
||||
end_time=datetime.datetime(2026, 9, 12),
|
||||
)
|
||||
assert result['truncated'] is False
|
||||
assert sum(point['messages'] for point in result['points']) == 61
|
||||
@@ -0,0 +1,226 @@
|
||||
"""Disposable PostgreSQL only: real row-lock admission, rollback, and FORCE RLS."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import copy
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from tests.unit_tests.api.service import test_pipeline_migration as base
|
||||
from langbot.pkg.agent.runner.interaction_store import InteractionStore, InteractionScopeError
|
||||
from langbot.pkg.persistence.pipeline_admission import lock_pipeline_admission
|
||||
from langbot.pkg.entity.persistence.agent_interaction import AgentInteraction
|
||||
|
||||
URL = os.environ.get('LANGBOT_ADMISSION_TEST_URL')
|
||||
pytestmark = [pytest.mark.asyncio, pytest.mark.skipif(not URL, reason='disposable PostgreSQL URL required')]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def env(tmp_path, monkeypatch):
|
||||
admin = create_async_engine(URL)
|
||||
async with admin.begin() as conn:
|
||||
await conn.execute(sa.text('DROP SCHEMA public CASCADE'))
|
||||
await conn.execute(sa.text('CREATE SCHEMA public'))
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
'DO $$ BEGIN CREATE ROLE admission_runtime LOGIN; EXCEPTION WHEN duplicate_object THEN NULL; END $$'
|
||||
)
|
||||
)
|
||||
monkeypatch.setattr(base, 'create_async_engine', lambda *a, **kw: admin)
|
||||
async for env in base.env.__wrapped__(tmp_path, monkeypatch):
|
||||
async with admin.begin() as conn:
|
||||
for table in ['legacy_pipelines', 'pipeline_migration_snapshots']:
|
||||
await conn.execute(sa.text(f'ALTER TABLE {table} ENABLE ROW LEVEL SECURITY'))
|
||||
await conn.execute(sa.text(f'ALTER TABLE {table} FORCE ROW LEVEL SECURITY'))
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
f"CREATE POLICY isolation ON {table} USING (workspace_uuid = current_setting('langbot.workspace_uuid', true)) WITH CHECK (workspace_uuid = current_setting('langbot.workspace_uuid', true))"
|
||||
)
|
||||
)
|
||||
await conn.execute(sa.text('GRANT USAGE ON SCHEMA public TO admission_runtime'))
|
||||
await conn.execute(
|
||||
sa.text('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO admission_runtime')
|
||||
)
|
||||
await conn.execute(sa.text('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO admission_runtime'))
|
||||
runtime = create_async_engine(URL.replace('postgres@', 'admission_runtime@'))
|
||||
env.pm.db = base.NS(get_engine=lambda: runtime)
|
||||
env.runtime = runtime
|
||||
yield env
|
||||
await runtime.dispose()
|
||||
|
||||
|
||||
async def write(env, workspace=base.WS, pipeline='one', expected=None, check=lambda: True):
|
||||
return await InteractionStore(env.runtime).create_request(
|
||||
interaction_id='form',
|
||||
run_id='run',
|
||||
binding_id='binding',
|
||||
runner_id=base.RID,
|
||||
processor_type='pipeline',
|
||||
processor_id=pipeline,
|
||||
workspace_id=workspace,
|
||||
request={},
|
||||
delivery_target={},
|
||||
expected_config=copy.deepcopy(base.SOURCE if expected is None else expected),
|
||||
authority_check=check,
|
||||
)
|
||||
|
||||
|
||||
async def blocked(env):
|
||||
# Observe PostgreSQL's lock waiter, not an arbitrary scheduling sleep.
|
||||
async with asyncio.timeout(5):
|
||||
while True:
|
||||
async with env.engine.connect() as conn:
|
||||
n = (
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
"SELECT count(*) FROM pg_stat_activity WHERE usename='admission_runtime' AND wait_event_type='Lock'"
|
||||
)
|
||||
)
|
||||
).scalar()
|
||||
if n:
|
||||
return
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('outcome', ['commit', 'rollback', 'cancel'])
|
||||
async def test_real_writer_waits_and_revalidates_after_transaction(env, outcome):
|
||||
writer = None
|
||||
try:
|
||||
async with env.pm.tenant_uow(base.WS) as uow:
|
||||
await lock_pipeline_admission(uow.session, base.WS, 'one')
|
||||
writer = asyncio.create_task(write(env))
|
||||
await blocked(env)
|
||||
assert not writer.done()
|
||||
async with env.engine.connect() as conn:
|
||||
assert not (await conn.execute(sa.select(AgentInteraction))).all()
|
||||
await uow.session.execute(
|
||||
sa.update(base.LegacyPipeline)
|
||||
.where(base.LegacyPipeline.uuid == 'one')
|
||||
.values(config=base.planner(base.SOURCE)['config'])
|
||||
)
|
||||
if outcome == 'rollback':
|
||||
raise RuntimeError('rollback')
|
||||
if outcome == 'cancel':
|
||||
raise asyncio.CancelledError()
|
||||
except (RuntimeError, asyncio.CancelledError):
|
||||
assert outcome != 'commit'
|
||||
if outcome == 'commit':
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await writer
|
||||
else:
|
||||
assert (await writer)[0]['status'] == 'pending'
|
||||
|
||||
|
||||
async def test_real_writer_first_blocks_migration_and_other_tenant_isolated(env):
|
||||
await write(env)
|
||||
async with env.pm.tenant_uow(base.WS) as uow:
|
||||
await lock_pipeline_admission(uow.session, base.WS, 'one')
|
||||
assert (
|
||||
await uow.session.execute(
|
||||
sa.select(AgentInteraction.status).where(AgentInteraction.workspace_id == base.WS)
|
||||
)
|
||||
).scalar_one() == 'pending'
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await write(env, workspace=base.OTHER)
|
||||
async with env.runtime.connect() as conn:
|
||||
assert not (await conn.execute(sa.select(base.LegacyPipeline))).all()
|
||||
async with env.pm.tenant_uow(base.OTHER) as uow:
|
||||
assert (await uow.session.execute(sa.select(base.LegacyPipeline.uuid))).scalars().all() == ['foreign']
|
||||
|
||||
|
||||
async def test_real_migration_commit_activation_and_stale_writer(env):
|
||||
original = env.svc._activate
|
||||
|
||||
async def activate(*args):
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await write(env)
|
||||
return await original(*args)
|
||||
|
||||
env.svc._activate = activate
|
||||
task = await base.execute(env)
|
||||
assert task.task_context.metadata['results'][0]['state'] == 'migrated'
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await write(env)
|
||||
_, snapshots = await base.rows(env)
|
||||
assert snapshots[0]['state'] == 'active'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('boundary', ['cas', 'activation'])
|
||||
async def test_real_no_phantom_between_check_and_publication(env, monkeypatch, boundary):
|
||||
writer = None
|
||||
sql = env.pm.execute_async
|
||||
state = env.svc._interaction_state
|
||||
entered_activation = False
|
||||
activate = env.svc._activate
|
||||
|
||||
async def activation(*args):
|
||||
nonlocal entered_activation
|
||||
entered_activation = True
|
||||
return await activate(*args)
|
||||
|
||||
async def start_waiter():
|
||||
nonlocal writer
|
||||
writer = asyncio.create_task(write(env))
|
||||
await blocked(env)
|
||||
assert not writer.done()
|
||||
|
||||
async def execute_sql(statement, *args, **kwargs):
|
||||
result = await sql(statement, *args, **kwargs)
|
||||
if (
|
||||
boundary == 'cas'
|
||||
and isinstance(statement, sa.sql.dml.Insert)
|
||||
and statement.table.name == 'pipeline_migration_snapshots'
|
||||
):
|
||||
await start_waiter()
|
||||
return result
|
||||
|
||||
async def interaction_state(*args):
|
||||
result = await state(*args)
|
||||
if boundary == 'activation' and entered_activation:
|
||||
await start_waiter()
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(env.pm, 'execute_async', execute_sql)
|
||||
monkeypatch.setattr(env.svc, '_activate', activation)
|
||||
monkeypatch.setattr(env.svc, '_interaction_state', interaction_state)
|
||||
task = await base.execute(env)
|
||||
assert task.task_context.metadata['results'][0]['state'] == 'migrated'
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await writer
|
||||
async with env.engine.connect() as conn:
|
||||
assert not (await conn.execute(sa.select(AgentInteraction))).all()
|
||||
|
||||
|
||||
async def test_real_conversation_revoked_while_waiting(env):
|
||||
live = True
|
||||
async with env.pm.tenant_uow(base.WS) as uow:
|
||||
await lock_pipeline_admission(uow.session, base.WS, 'one')
|
||||
writer = asyncio.create_task(write(env, check=lambda: live))
|
||||
await blocked(env)
|
||||
live = False
|
||||
with pytest.raises(InteractionScopeError):
|
||||
await writer
|
||||
|
||||
|
||||
async def test_real_other_tenant_writer_is_not_serialized(env):
|
||||
async with env.pm.tenant_uow(base.WS) as uow:
|
||||
await lock_pipeline_admission(uow.session, base.WS, 'one')
|
||||
record, _ = await asyncio.wait_for(write(env, workspace=base.OTHER, pipeline='foreign'), 3)
|
||||
assert record['workspace_id'] == base.OTHER
|
||||
|
||||
|
||||
async def test_real_pending_before_commit_prevents_migration(env):
|
||||
body = await base.selection(env)
|
||||
await write(env)
|
||||
with pytest.raises(env.m.MigrationError, match='preview_stale'):
|
||||
await base.execute(env, body)
|
||||
configs, snapshots = await base.rows(env)
|
||||
assert configs['one'] == base.SOURCE and not snapshots
|
||||
env.ap.pipeline_mgr.publish_pipeline.assert_not_called()
|
||||
|
||||
|
||||
# Run the unchanged cancellation/ambiguous-acknowledgement contracts on real PG.
|
||||
test_real_cancel_commit_outcome = base.test_cancel_commit_reports_durable_outcome_and_stops_batch
|
||||
test_real_cancel_activation_retry = base.test_cancel_activation_retry_reconciles_original_snapshot
|
||||
test_real_unavailable_reconciliation = base.test_unavailable_commit_reconciliation_is_conservative
|
||||
test_real_cancel_prepare = base.test_cancel_during_prepare_keeps_original_and_stops_batch
|
||||
@@ -142,7 +142,7 @@ async def test_legacy_sqlite_resources_are_backfilled_and_contracted(tmp_path):
|
||||
assert pk_columns == {
|
||||
'binary_storages': ('workspace_uuid', 'unique_key'),
|
||||
'plugin_settings': ('workspace_uuid', 'plugin_author', 'plugin_name'),
|
||||
'monitoring_sessions': ('workspace_uuid', 'session_id'),
|
||||
'monitoring_sessions': ('workspace_uuid', 'bot_id', 'session_id'),
|
||||
}
|
||||
|
||||
pipeline_run_foreign_keys = await _inspect(
|
||||
@@ -237,8 +237,10 @@ async def test_sqlite_scoped_keys_allow_cross_workspace_but_reject_same_workspac
|
||||
await conn.execute(
|
||||
sa.text(
|
||||
'INSERT INTO monitoring_sessions '
|
||||
'(workspace_uuid, session_id, bot_id, last_activity, is_active) '
|
||||
"VALUES (:workspace_uuid, 'session-1', 'bot-2', CURRENT_TIMESTAMP, 1)"
|
||||
'(workspace_uuid, session_id, bot_id, bot_name, pipeline_id, pipeline_name, '
|
||||
'start_time, last_activity, message_count, is_active) '
|
||||
"VALUES (:workspace_uuid, 'session-1', 'bot-2', 'bot', 'pipeline-2', 'pipeline', "
|
||||
'CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 1, 1)'
|
||||
),
|
||||
{'workspace_uuid': second_workspace_uuid},
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
|
||||
import pytest
|
||||
import sqlalchemy as sa
|
||||
@@ -34,7 +35,7 @@ def _manifest_payloads(backup_directory) -> list[dict]:
|
||||
|
||||
def _assert_verified_backup(payload: dict) -> None:
|
||||
backup_path = pathlib.Path(payload['backup_path'])
|
||||
with sqlite3.connect(f'{backup_path.as_uri()}?mode=ro', uri=True) as connection:
|
||||
with closing(sqlite3.connect(f'{backup_path.as_uri()}?mode=ro', uri=True)) as connection:
|
||||
assert connection.execute('PRAGMA quick_check').fetchall() == [('ok',)]
|
||||
assert connection.execute('SELECT version_num FROM alembic_version').fetchone()[0] == payload['source_revision']
|
||||
|
||||
|
||||
@@ -444,10 +444,12 @@ async def test_persistence_startup_defers_workspace_tables_until_account_upgrade
|
||||
await conn.run_sync(lambda sync_conn: sa.inspect(sync_conn).get_table_names())
|
||||
)
|
||||
assert 'workspaces' not in tables_before_migration
|
||||
assert 'codex_credentials' not in tables_before_migration
|
||||
|
||||
await manager._run_alembic_migrations()
|
||||
|
||||
async with engine.connect() as conn:
|
||||
assert 'codex_credentials' in await conn.run_sync(lambda sync: sa.inspect(sync).get_table_names())
|
||||
workspace = (
|
||||
(await conn.execute(sa.text("SELECT * FROM workspaces WHERE source = 'local'"))).mappings().one()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Real Core/SDK protocol regression tests; no subprocesses or external services.
|
||||
|
||||
Run against the intended local SDK (``uv run --no-sync`` after local install).
|
||||
The in-memory transport carries JSON strings through Handler.run on both sides;
|
||||
send_file, envelope validation, base64 decoding and transfer storage are real.
|
||||
Only Core's database/object-storage services, parser dispatch/provider and host
|
||||
sandbox prerequisite probing are doubles. Worker launch/registration is
|
||||
represented by its already-registered state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.plugin.handler import RuntimeConnectionHandler
|
||||
from langbot_plugin.entities.io.actions.enums import CommonAction, LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginWorkerPolicy, RuntimeIdentity
|
||||
from langbot_plugin.runtime.context import RuntimeContext
|
||||
from langbot_plugin.runtime.io.connection import Connection
|
||||
from langbot_plugin.entities.io.errors import ActionCallError, ConnectionClosedError
|
||||
from langbot_plugin.runtime.io.handler import FILE_CHUNK_LENGTH, Handler
|
||||
from langbot_plugin.runtime.io.handlers.control import ControlConnectionHandler
|
||||
from langbot_plugin.runtime.io.handlers.plugin import PluginConnectionHandler
|
||||
from langbot_plugin.runtime.plugin.mgr import PluginManager
|
||||
from langbot_plugin.runtime.security import PLUGIN_FILE_STORAGE_DIR_ENV
|
||||
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
PAYLOAD = bytes(range(256)) * 161 + b'\x00original RAG file\xff'
|
||||
BINDING = InstallationBinding(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=7,
|
||||
installation_uuid='00000000-0000-4000-8000-000000000001',
|
||||
runtime_revision=3,
|
||||
artifact_digest='a' * 64,
|
||||
)
|
||||
LEGACY = ActionContext(**BINDING.model_dump(exclude={'runtime_revision', 'artifact_digest'}))
|
||||
|
||||
|
||||
class QueueConnection(Connection):
|
||||
"""Only the byte transport is replaced, not the request/response machinery."""
|
||||
|
||||
def __init__(self):
|
||||
self.incoming = asyncio.Queue()
|
||||
self.sent = []
|
||||
self.peer = None
|
||||
|
||||
async def send(self, message: str) -> None:
|
||||
assert isinstance(message, str)
|
||||
self.sent.append(json.loads(message))
|
||||
await self.peer.incoming.put(message)
|
||||
|
||||
async def receive(self) -> str:
|
||||
message = await self.incoming.get()
|
||||
if message is None:
|
||||
raise ConnectionClosedError('test transport closed')
|
||||
return message
|
||||
|
||||
async def close(self) -> None:
|
||||
await self.incoming.put(None)
|
||||
await self.peer.incoming.put(None)
|
||||
|
||||
|
||||
def connection_pair():
|
||||
left, right = QueueConnection(), QueueConnection()
|
||||
left.peer, right.peer = right, left
|
||||
return left, right
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def protocol_stack(tmp_path, monkeypatch, profile='oss_dev', binding=LEGACY):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
stored = tmp_path / 'original.bin'
|
||||
stored.write_bytes(PAYLOAD)
|
||||
storage_calls = []
|
||||
|
||||
async def get_file_stream(execution_context, storage_path):
|
||||
storage_calls.append((execution_context, storage_path))
|
||||
assert execution_context.workspace_uuid == BINDING.workspace_uuid
|
||||
assert storage_path == 'knowledge/original.bin'
|
||||
return stored.read_bytes()
|
||||
|
||||
async def get_execution_binding(workspace_uuid, expected_generation):
|
||||
assert workspace_uuid == BINDING.workspace_uuid
|
||||
assert expected_generation == BINDING.placement_generation
|
||||
return BINDING
|
||||
|
||||
setting = SimpleNamespace(
|
||||
plugin_author='tester',
|
||||
plugin_name='engine',
|
||||
installation_uuid=BINDING.installation_uuid,
|
||||
runtime_revision=BINDING.runtime_revision,
|
||||
artifact_digest=BINDING.artifact_digest,
|
||||
)
|
||||
app = SimpleNamespace(
|
||||
deployment=SimpleNamespace(mode='oss' if profile == 'oss_dev' else 'cloud'),
|
||||
logger=logging.getLogger(__name__),
|
||||
persistence_mgr=SimpleNamespace(execute_async=AsyncMock(return_value=SimpleNamespace(first=lambda: setting))),
|
||||
workspace_service=SimpleNamespace(get_execution_binding=get_execution_binding),
|
||||
rag_runtime_service=SimpleNamespace(get_file_stream=get_file_stream),
|
||||
)
|
||||
core_conn, control_conn = connection_pair()
|
||||
monkeypatch.setenv(PLUGIN_FILE_STORAGE_DIR_ENV, str(tmp_path / 'core-transfer'))
|
||||
core = RuntimeConnectionHandler(core_conn, AsyncMock(return_value=False), app)
|
||||
core.register_installation_binding(BINDING, plugin_author='tester', plugin_name='engine')
|
||||
runtime = RuntimeContext()
|
||||
runtime.plugin_mgr = PluginManager(runtime)
|
||||
# No worker is launched: omit only host nsjail/cgroup prerequisite probing.
|
||||
monkeypatch.setattr(runtime.plugin_mgr.worker_launcher, 'configure', lambda policy, profile: None)
|
||||
monkeypatch.setenv(PLUGIN_FILE_STORAGE_DIR_ENV, str(tmp_path / 'runtime-transfer'))
|
||||
control = ControlConnectionHandler(control_conn, runtime)
|
||||
runtime.activate_control_handler(control)
|
||||
bridge_conn, plugin_conn = connection_pair()
|
||||
bridge = PluginConnectionHandler(bridge_conn, runtime, file_storage_dir=str(tmp_path / 'bridge-transfer'))
|
||||
plugin = Handler(plugin_conn, file_storage_dir=str(tmp_path / 'plugin-transfer'))
|
||||
# Trusted state left by registration, not plugin-supplied action data.
|
||||
bridge.bind_action_context(binding)
|
||||
runtime.plugin_mgr.plugin_handlers.append(bridge)
|
||||
runtime.plugin_mgr.plugins.append(SimpleNamespace(_runtime_plugin_handler=bridge))
|
||||
handlers = [core, control, bridge, plugin]
|
||||
tasks = [asyncio.create_task(handler.run()) for handler in handlers]
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
core.set_runtime_config(
|
||||
runtime_identity=RuntimeIdentity(instance_uuid='instance-a', runtime_id='test-runtime'),
|
||||
worker_policy=PluginWorkerPolicy(
|
||||
max_cpus=1,
|
||||
max_memory_mb=128,
|
||||
max_pids=32,
|
||||
max_open_files=64,
|
||||
max_file_size_mb=8,
|
||||
require_hard_limits=False,
|
||||
),
|
||||
runtime_profile=profile,
|
||||
cloud_service_url=None,
|
||||
),
|
||||
5,
|
||||
)
|
||||
if isinstance(binding, InstallationBinding):
|
||||
runtime.activate_installation_binding(binding)
|
||||
else:
|
||||
runtime.bind_workspace(binding)
|
||||
yield SimpleNamespace(
|
||||
core=core,
|
||||
control=control,
|
||||
runtime=runtime,
|
||||
bridge=bridge,
|
||||
plugin=plugin,
|
||||
core_conn=core_conn,
|
||||
control_conn=control_conn,
|
||||
bridge_conn=bridge_conn,
|
||||
plugin_conn=plugin_conn,
|
||||
app=app,
|
||||
storage_calls=storage_calls,
|
||||
)
|
||||
finally:
|
||||
for handler in handlers:
|
||||
await handler.close()
|
||||
await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=True), 5)
|
||||
|
||||
|
||||
def assert_chunks(connection, binding, payload=PAYLOAD):
|
||||
chunks = [message for message in connection.sent if message.get('action') == CommonAction.FILE_CHUNK.value]
|
||||
expected = (len(payload) + FILE_CHUNK_LENGTH - 1) // FILE_CHUNK_LENGTH
|
||||
assert expected > 1
|
||||
assert len(chunks) == expected
|
||||
assert [chunk['data']['chunk_index'] for chunk in chunks] == list(range(expected))
|
||||
assert {chunk['data']['chunk_amount'] for chunk in chunks} == {expected}
|
||||
assert all(chunk['context'] == binding.model_dump() for chunk in chunks)
|
||||
assert len({chunk['data']['file_key'] for chunk in chunks}) == 1
|
||||
return chunks[0]['data']['file_key']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'profile,binding',
|
||||
[('oss_dev', LEGACY), ('oss_dev', BINDING), ('shared', BINDING)],
|
||||
ids=['legacy-oss', 'managed-oss', 'managed-shared'],
|
||||
)
|
||||
async def test_knowledge_file_roundtrip_reaches_plugin_original_bytes(tmp_path, monkeypatch, profile, binding):
|
||||
async with protocol_stack(tmp_path, monkeypatch, profile, binding) as stack:
|
||||
# Legacy plugin API sends no authority; Runtime supplies its trusted binding.
|
||||
result = await asyncio.wait_for(
|
||||
stack.plugin.call_action(
|
||||
PluginToRuntimeAction.GET_KNOWLEDEGE_FILE_STREAM,
|
||||
{'storage_path': 'knowledge/original.bin'},
|
||||
),
|
||||
5,
|
||||
)
|
||||
assert await stack.plugin.read_local_file(result['file_key']) == PAYLOAD
|
||||
assert len(stack.storage_calls) == 1
|
||||
# Core resolves persisted installation authority even for legacy callers;
|
||||
# the Runtime still preserves the legacy envelope on the plugin hop.
|
||||
core_key = assert_chunks(stack.core_conn, BINDING)
|
||||
plugin_key = assert_chunks(stack.bridge_conn, binding)
|
||||
assert result['file_key'] == plugin_key != core_key
|
||||
assert not (Path(stack.control.file_storage_dir) / core_key).exists()
|
||||
assert not stack.control._owned_transfer_files
|
||||
callbacks = [
|
||||
message
|
||||
for message in stack.control_conn.sent
|
||||
if message.get('action') == PluginToRuntimeAction.GET_KNOWLEDEGE_FILE_STREAM.value
|
||||
]
|
||||
assert len(callbacks) == 1
|
||||
assert callbacks[0]['context'] == binding.model_dump()
|
||||
assert callbacks[0]['data'] == {'storage_path': 'knowledge/original.bin'}
|
||||
|
||||
|
||||
async def test_shared_control_rejects_legacy_chunks_before_storage(tmp_path, monkeypatch):
|
||||
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
|
||||
with stack.core.installation_scope(LEGACY):
|
||||
with pytest.raises(ActionCallError, match='InstallationBinding|Legacy FILE_CHUNK'):
|
||||
await asyncio.wait_for(stack.core.send_file(PAYLOAD, ''), 5)
|
||||
assert not list(Path(stack.control.file_storage_dir).iterdir())
|
||||
assert not stack.control._owned_transfer_files
|
||||
|
||||
|
||||
async def test_candidate_artifact_pretransfer_does_not_require_active_installation(tmp_path, monkeypatch):
|
||||
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
|
||||
candidate = BINDING.model_copy(
|
||||
update={'installation_uuid': 'candidate-installation', 'runtime_revision': 1, 'artifact_digest': 'c' * 64}
|
||||
)
|
||||
assert not stack.runtime.is_current_installation_binding(candidate)
|
||||
with stack.core.installation_scope(candidate):
|
||||
key = await asyncio.wait_for(stack.core.send_file(PAYLOAD, 'lbp'), 5)
|
||||
assert_chunks(stack.core_conn, candidate)
|
||||
assert await stack.control.read_local_file(key) == PAYLOAD
|
||||
assert not stack.runtime.is_current_installation_binding(candidate)
|
||||
|
||||
|
||||
async def test_nested_parser_target_owns_file_and_action_envelopes(tmp_path, monkeypatch):
|
||||
async with protocol_stack(tmp_path, monkeypatch, 'shared', BINDING) as stack:
|
||||
target = BINDING.model_copy(
|
||||
update={
|
||||
'installation_uuid': 'parser-installation',
|
||||
'runtime_revision': 2,
|
||||
'artifact_digest': 'b' * 64,
|
||||
}
|
||||
)
|
||||
stack.runtime.activate_installation_binding(target)
|
||||
parser_calls = []
|
||||
restored = []
|
||||
|
||||
async def parse_document(author, name, context_data, file_bytes):
|
||||
parser_calls.append((stack.control.current_action_context, author, name, context_data, file_bytes))
|
||||
return {'documents': [{'text': 'parsed'}]}
|
||||
|
||||
stack.runtime.plugin_mgr.parse_document = parse_document
|
||||
|
||||
class ParserConnector:
|
||||
async def require_workspace_context(self, context):
|
||||
assert context.workspace_uuid == BINDING.workspace_uuid
|
||||
|
||||
async def call_parser(self, plugin_name, context_data, file_bytes):
|
||||
assert plugin_name == 'tester/parser'
|
||||
assert stack.core.current_action_context == BINDING
|
||||
with stack.core.installation_scope(target):
|
||||
result = await stack.core.parse_document('tester', 'parser', context_data, file_bytes)
|
||||
restored.append(stack.core.resolve_outbound_action_context(None))
|
||||
return result
|
||||
|
||||
stack.app.plugin_connector = ParserConnector()
|
||||
result = await asyncio.wait_for(
|
||||
stack.plugin.call_action(
|
||||
PluginToRuntimeAction.INVOKE_PARSER,
|
||||
{
|
||||
'plugin_author': 'tester',
|
||||
'plugin_name': 'parser',
|
||||
'storage_path': 'knowledge/original.bin',
|
||||
'filename': 'original.bin',
|
||||
},
|
||||
),
|
||||
5,
|
||||
)
|
||||
assert result == {'documents': [{'text': 'parsed'}]}
|
||||
key = assert_chunks(stack.core_conn, target)
|
||||
parse_requests = [
|
||||
message
|
||||
for message in stack.core_conn.sent
|
||||
if message.get('action') == LangBotToRuntimeAction.PARSE_DOCUMENT.value
|
||||
]
|
||||
assert len(parse_requests) == 1
|
||||
assert parse_requests[0]['context'] == target.model_dump()
|
||||
assert parse_requests[0]['data']['context']['file_key'] == key
|
||||
assert parser_calls == [
|
||||
(
|
||||
target,
|
||||
'tester',
|
||||
'parser',
|
||||
{
|
||||
'mime_type': 'application/octet-stream',
|
||||
'filename': 'original.bin',
|
||||
'metadata': {},
|
||||
},
|
||||
PAYLOAD,
|
||||
)
|
||||
]
|
||||
assert restored == [BINDING]
|
||||
assert stack.core.current_action_context is None
|
||||
assert stack.core.resolve_outbound_action_context(None) is None
|
||||
assert not (Path(stack.control.file_storage_dir) / key).exists()
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Real embedded SeekDB regression tests.
|
||||
|
||||
Install the optional dependency before running these slow tests::
|
||||
|
||||
uv sync --dev --extra seekdb
|
||||
uv run pytest tests/integration/vector/test_seekdb.py -m slow -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip('pyseekdb')
|
||||
|
||||
from langbot.pkg.vector.vdbs.seekdb import SeekDBVectorDatabase
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.slow]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def backend(tmp_path):
|
||||
app = SimpleNamespace(
|
||||
instance_config=SimpleNamespace(
|
||||
data={
|
||||
'vdb': {
|
||||
'runtime_cache_limit': 16,
|
||||
'seekdb': {
|
||||
'mode': 'embedded',
|
||||
'path': str(tmp_path),
|
||||
'database': 'langbot_test',
|
||||
},
|
||||
}
|
||||
}
|
||||
),
|
||||
logger=SimpleNamespace(
|
||||
info=lambda *args, **kwargs: None,
|
||||
warning=lambda *args, **kwargs: None,
|
||||
),
|
||||
)
|
||||
database = SeekDBVectorDatabase(app)
|
||||
collection = f'test_{uuid.uuid4().hex}'
|
||||
yield database, collection
|
||||
|
||||
await database.delete_collection(collection)
|
||||
await database.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_and_text_round_trip(backend) -> None:
|
||||
database, collection = backend
|
||||
original = 'He said "hello".\nC:\\notes\\file.txt isn\'t empty. 中文'
|
||||
updated = f'Updated: {original}'
|
||||
|
||||
await database.add_embeddings(
|
||||
collection,
|
||||
['document-a'],
|
||||
[[1.0, 0.0, 0.0]],
|
||||
[{'file_id': 'file-a', 'text': original}],
|
||||
[original],
|
||||
)
|
||||
await database.add_embeddings(
|
||||
collection,
|
||||
['document-a'],
|
||||
[[0.0, 1.0, 0.0]],
|
||||
[{'file_id': 'file-a', 'text': updated}],
|
||||
[updated],
|
||||
)
|
||||
|
||||
items, _ = await database.list_by_filter(collection, {'file_id': 'file-a'})
|
||||
|
||||
assert len(items) == 1
|
||||
assert items[0]['id'] == 'document-a'
|
||||
assert items[0]['document'] == updated
|
||||
assert items[0]['metadata']['text'] == updated
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_text_and_hybrid_results_keep_relevance_order(backend) -> None:
|
||||
database, collection = backend
|
||||
documents = [
|
||||
'orchid orchid orchid flower',
|
||||
'orchid grows in a garden with many other beautiful plants',
|
||||
'a completely unrelated topic',
|
||||
]
|
||||
|
||||
await database.add_embeddings(
|
||||
collection,
|
||||
['best', 'weak', 'noise'],
|
||||
[[1.0, 0.0, 0.0], [0.9, 0.1, 0.0], [0.0, 0.0, 1.0]],
|
||||
[
|
||||
{'file_id': item_id, 'document_id': item_id, 'text': document}
|
||||
for item_id, document in zip(['best', 'weak', 'noise'], documents, strict=True)
|
||||
],
|
||||
documents,
|
||||
)
|
||||
seekdb_collection = await database.get_or_create_collection(collection)
|
||||
await asyncio.to_thread(seekdb_collection.refresh_index)
|
||||
|
||||
full_text = await database.search(
|
||||
collection,
|
||||
[1.0, 0.0, 0.0],
|
||||
k=3,
|
||||
search_type='full_text',
|
||||
query_text='orchid',
|
||||
)
|
||||
hybrid = await database.search(
|
||||
collection,
|
||||
[1.0, 0.0, 0.0],
|
||||
k=3,
|
||||
search_type='hybrid',
|
||||
query_text='orchid',
|
||||
vector_weight=0.65,
|
||||
)
|
||||
|
||||
assert full_text['ids'][0][:2] == ['best', 'weak']
|
||||
assert full_text['distances'][0] == sorted(full_text['distances'][0])
|
||||
assert hybrid['ids'][0] == ['best', 'weak', 'noise']
|
||||
assert hybrid['distances'][0] == sorted(hybrid['distances'][0])
|
||||
Reference in New Issue
Block a user