mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
* feat(web): add onboarding wizard for guided bot creation
Implement a full-screen 4-step wizard at /wizard that guides users
through selecting a platform, configuring a bot, choosing an AI engine,
and completing setup. The wizard uses DynamicFormComponent for adapter
and pipeline configuration, embeds BotLogListComponent for real-time
debugging, persists state to localStorage, and integrates with Space
OAuth flow. Also fixes a prompt-editor crash in DynamicFormComponent
when value is undefined.
* feat(wizard): redesign step 0/1 flow, add skip dialog, auto-expand log images
- Step 0: Remove bot name/description fields; auto-derive name from adapter
label; create disabled bot on confirm; advance to Step 1 automatically
- Step 1: Replace 'Create Bot' with 'Save & Enable Bot'; update adapter
config and enable bot; disable form fields after saving
- Add skip confirmation AlertDialog with i18n message
- Add LanguageSelector to wizard header
- Move wizard sidebar entry to last position to prevent fallback redirect loop
- Add defaultExpanded prop to BotLogCard; auto-expand entries with images
in wizard via autoExpandImages prop on BotLogListComponent
- Remove automatic default pipeline creation (write_default_pipeline) from
backend persistence manager since the wizard now handles pipeline creation
- Update all 4 locale files (en-US, zh-Hans, zh-Hant, ja-JP)
* fix(wizard): hide detailed logs link in wizard, allow re-editing bot config after save
- Add hideDetailedLogsLink prop to BotLogListComponent; pass it in wizard
- Remove isEditing on DynamicFormComponent so form stays editable after save
- Always show save button; label changes to 'Re-save' after first save
- Add resaveBot i18n key to all 4 locale files
* style(wizard): move save button into config card header
* fix(wizard): initialize userInfo/systemInfo so model selector works
The wizard runs outside /home layout, so userInfo was null. This caused
the model-fallback-selector to filter out all Space models, showing an
empty dropdown. Fix by calling initializeUserInfo() and
initializeSystemInfo() before fetching wizard data.
Also:
- Hide log toolbar in wizard via hideToolbar prop on BotLogListComponent
- Add empty state message for bot logs (noLogs i18n key, all 4 locales)
* feat(wizard): redesign AI Engine step with left-right split layout
Before selecting a runner: centered grid of runner cards.
After selecting: left panel shows compact runner list for switching,
right panel shows runner config form with slide-in animations.
Also fix prompt field default: add default value to prompt-editor field
in ai.yaml metadata so the prompt is pre-populated with
'You are a helpful assistant.' instead of being empty.
* feat(pipeline): add default values to ai.yaml runner configs and show_if for n8n auth fields
- Sync default values from default-pipeline-config.json to all runner
config fields in ai.yaml so wizard forms are pre-populated
- Add show_if conditions to n8n-service-api auth fields so only the
relevant credentials appear based on selected auth-type
- Fix prompt-editor crash in DynamicFormItemComponent when field.value
is undefined (Array.isArray guard + fallback)
- Improve wizard Step 2 split layout with fixed column widths,
independent scroll, ring clipping fix, and mobile responsiveness
- Use key={selected} on DynamicFormComponent to force remount on
runner switch
- Improve pipeline creation flow: create → fetch defaults → merge AI
section → update (preserves trigger/safety/output defaults)
* feat(dynamic-form): add systemContext prop with __system.* namespace for show_if conditions
- Add systemContext prop to DynamicFormComponent for injecting external
variables accessible via __system.* prefix in show_if conditions
- Extract resolveShowIfValue() helper for cleaner field resolution
- Pass { is_wizard: true } from wizard to hide knowledge-bases field
- Remove bot config save toast in wizard (keep inline indicator)
* feat(sidebar): render wizard as standalone item before Home group with fallback redirect fix
* fix(wizard): remove unused setBotDescription to fix lint error
157 lines
5.9 KiB
Python
157 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import datetime
|
|
import typing
|
|
|
|
|
|
import sqlalchemy.ext.asyncio as sqlalchemy_asyncio
|
|
import sqlalchemy
|
|
|
|
from . import database, migration
|
|
from ..entity.persistence import base, metadata, model as persistence_model
|
|
from ..entity import persistence
|
|
from ..core import app
|
|
from ..utils import constants, importutil
|
|
from . import databases, migrations
|
|
|
|
importutil.import_modules_in_pkg(databases)
|
|
importutil.import_modules_in_pkg(migrations)
|
|
importutil.import_modules_in_pkg(persistence)
|
|
|
|
|
|
class PersistenceManager:
|
|
"""Persistence module manager"""
|
|
|
|
ap: app.Application
|
|
|
|
db: database.BaseDatabaseManager
|
|
"""Database manager"""
|
|
|
|
meta: sqlalchemy.MetaData
|
|
|
|
def __init__(self, ap: app.Application):
|
|
self.ap = ap
|
|
self.meta = base.Base.metadata
|
|
|
|
async def initialize(self):
|
|
database_type = self.ap.instance_config.data.get('database', {}).get('use', 'sqlite')
|
|
self.ap.logger.info(f'Initializing database type: {database_type}...')
|
|
for manager in database.preregistered_managers:
|
|
if manager.name == database_type:
|
|
self.db = manager(self.ap)
|
|
await self.db.initialize()
|
|
break
|
|
|
|
await self.create_tables()
|
|
|
|
# run migrations
|
|
database_version = await self.execute_async(
|
|
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == 'database_version')
|
|
)
|
|
|
|
database_version = int(database_version.fetchone()[1])
|
|
required_database_version = constants.required_database_version
|
|
|
|
if database_version < required_database_version:
|
|
migrations = migration.preregistered_db_migrations
|
|
migrations.sort(key=lambda x: x.number)
|
|
|
|
last_migration_number = database_version
|
|
|
|
for migration_cls in migrations:
|
|
migration_instance = migration_cls(self.ap)
|
|
|
|
if (
|
|
migration_instance.number > database_version
|
|
and migration_instance.number <= required_database_version
|
|
):
|
|
await migration_instance.upgrade()
|
|
await self.execute_async(
|
|
sqlalchemy.update(metadata.Metadata)
|
|
.where(metadata.Metadata.key == 'database_version')
|
|
.values({'value': str(migration_instance.number)})
|
|
)
|
|
last_migration_number = migration_instance.number
|
|
self.ap.logger.info(f'Migration {migration_instance.number} completed.')
|
|
|
|
self.ap.logger.info(f'Successfully upgraded database to version {last_migration_number}.')
|
|
|
|
await self.write_space_model_providers()
|
|
|
|
async def create_tables(self):
|
|
# create tables
|
|
async with self.get_db_engine().connect() as conn:
|
|
await conn.run_sync(self.meta.create_all)
|
|
|
|
await conn.commit()
|
|
|
|
# ======= write initial data =======
|
|
|
|
# write initial metadata
|
|
self.ap.logger.info('Creating initial metadata...')
|
|
for item in metadata.initial_metadata:
|
|
# check if the item exists
|
|
result = await self.execute_async(
|
|
sqlalchemy.select(metadata.Metadata).where(metadata.Metadata.key == item['key'])
|
|
)
|
|
row = result.first()
|
|
if row is None:
|
|
await self.execute_async(sqlalchemy.insert(metadata.Metadata).values(item))
|
|
|
|
async def write_space_model_providers(self):
|
|
space_models_gateway_api_url = self.ap.instance_config.data.get('space', {}).get(
|
|
'models_gateway_api_url', 'https://api.langbot.cloud/v1'
|
|
)
|
|
|
|
# write space model providers
|
|
result = await self.execute_async(
|
|
sqlalchemy.select(persistence_model.ModelProvider).where(
|
|
persistence_model.ModelProvider.requester == 'space-chat-completions'
|
|
)
|
|
)
|
|
exists_space_chat_completions_model_provider = result.first()
|
|
|
|
# api keys will be set/updated when the oauth callback
|
|
if exists_space_chat_completions_model_provider is None:
|
|
self.ap.logger.info('Creating space model providers...')
|
|
space_chat_completions_model_provider = {
|
|
'uuid': '00000000-0000-0000-0000-000000000000',
|
|
'name': 'LangBot Models',
|
|
'requester': 'space-chat-completions',
|
|
'base_url': space_models_gateway_api_url,
|
|
'api_keys': [],
|
|
}
|
|
|
|
await self.execute_async(
|
|
sqlalchemy.insert(persistence_model.ModelProvider).values(space_chat_completions_model_provider)
|
|
)
|
|
else:
|
|
if exists_space_chat_completions_model_provider.base_url != space_models_gateway_api_url:
|
|
await self.execute_async(
|
|
sqlalchemy.update(persistence_model.ModelProvider)
|
|
.where(persistence_model.ModelProvider.uuid == exists_space_chat_completions_model_provider.uuid)
|
|
.values({'base_url': space_models_gateway_api_url})
|
|
)
|
|
|
|
# =================================
|
|
|
|
async def execute_async(self, *args, **kwargs) -> sqlalchemy.engine.cursor.CursorResult:
|
|
async with self.get_db_engine().connect() as conn:
|
|
result = await conn.execute(*args, **kwargs)
|
|
await conn.commit()
|
|
return result
|
|
|
|
def get_db_engine(self) -> sqlalchemy_asyncio.AsyncEngine:
|
|
return self.db.get_engine()
|
|
|
|
def serialize_model(
|
|
self, model: typing.Type[sqlalchemy.Base], data: sqlalchemy.Base, masked_columns: list[str] = []
|
|
) -> dict:
|
|
return {
|
|
column.name: getattr(data, column.name)
|
|
if not isinstance(getattr(data, column.name), (datetime.datetime))
|
|
else getattr(data, column.name).isoformat()
|
|
for column in model.__table__.columns
|
|
if column.name not in masked_columns
|
|
}
|