diff --git a/src/langbot/pkg/api/http/service/model.py b/src/langbot/pkg/api/http/service/model.py index 03f42e3d..e12498cf 100644 --- a/src/langbot/pkg/api/http/service/model.py +++ b/src/langbot/pkg/api/http/service/model.py @@ -64,9 +64,10 @@ class LLMModelsService: models = result.all() return [self.ap.persistence_mgr.serialize_model(persistence_model.LLMModel, m) for m in models] - async def create_llm_model(self, model_data: dict) -> str: + async def create_llm_model(self, model_data: dict, preserve_uuid: bool = False) -> str: """Create a new LLM model""" - model_data['uuid'] = str(uuid.uuid4()) + if not preserve_uuid: + model_data['uuid'] = str(uuid.uuid4()) # Handle provider creation if needed if 'provider' in model_data: @@ -222,9 +223,10 @@ class EmbeddingModelsService: models = result.all() return [self.ap.persistence_mgr.serialize_model(persistence_model.EmbeddingModel, m) for m in models] - async def create_embedding_model(self, model_data: dict) -> str: + async def create_embedding_model(self, model_data: dict, preserve_uuid: bool = False) -> str: """Create a new embedding model""" - model_data['uuid'] = str(uuid.uuid4()) + if not preserve_uuid: + model_data['uuid'] = str(uuid.uuid4()) if 'provider' in model_data: provider_data = model_data.pop('provider') diff --git a/src/langbot/pkg/api/http/service/space.py b/src/langbot/pkg/api/http/service/space.py index 1961dd4c..cd694883 100644 --- a/src/langbot/pkg/api/http/service/space.py +++ b/src/langbot/pkg/api/http/service/space.py @@ -8,6 +8,7 @@ import sqlalchemy from ....core import app from ....entity.persistence import user +from ....entity.dto.space_model import SpaceModel class SpaceService: @@ -170,3 +171,19 @@ class SpaceService: return credits except Exception: return self._credits_cache.get(user_email, (None, 0))[0] + + async def get_models(self) -> typing.List[SpaceModel]: + """Get models from Space""" + + space_config = self._get_space_config() + space_url = space_config['url'] + + async with aiohttp.ClientSession() as session: + async with session.get(f'{space_url}/api/v1/models') as response: + if response.status != 200: + raise ValueError(f'Failed to get models: {await response.text()}') + data = await response.json() + if data.get('code') != 0: + raise ValueError(f'Failed to get models: {data.get("msg")}') + models_data = data.get('data', {}).get('models', []) + return [SpaceModel.model_validate(model_dict) for model_dict in models_data] diff --git a/src/langbot/pkg/core/stages/build_app.py b/src/langbot/pkg/core/stages/build_app.py index b1695fd5..1ce4ddbd 100644 --- a/src/langbot/pkg/core/stages/build_app.py +++ b/src/langbot/pkg/core/stages/build_app.py @@ -45,68 +45,6 @@ class BuildAppStage(stage.BootingStage): discover.discover_blueprint('templates/components.yaml') ap.discover = discover - proxy_mgr = proxy.ProxyManager(ap) - await proxy_mgr.initialize() - ap.proxy_mgr = proxy_mgr - - ver_mgr = version.VersionManager(ap) - await ver_mgr.initialize() - ap.ver_mgr = ver_mgr - - ap.query_pool = pool.QueryPool() - - log_cache = logcache.LogCache() - ap.log_cache = log_cache - - storage_mgr_inst = storagemgr.StorageMgr(ap) - await storage_mgr_inst.initialize() - ap.storage_mgr = storage_mgr_inst - - persistence_mgr_inst = persistencemgr.PersistenceManager(ap) - ap.persistence_mgr = persistence_mgr_inst - await persistence_mgr_inst.initialize() - - cmd_mgr_inst = cmdmgr.CommandManager(ap) - await cmd_mgr_inst.initialize() - ap.cmd_mgr = cmd_mgr_inst - - llm_model_mgr_inst = llm_model_mgr.ModelManager(ap) - await llm_model_mgr_inst.initialize() - ap.model_mgr = llm_model_mgr_inst - - llm_session_mgr_inst = llm_session_mgr.SessionManager(ap) - await llm_session_mgr_inst.initialize() - ap.sess_mgr = llm_session_mgr_inst - - llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap) - await llm_tool_mgr_inst.initialize() - ap.tool_mgr = llm_tool_mgr_inst - - im_mgr_inst = im_mgr.PlatformManager(ap=ap) - await im_mgr_inst.initialize() - ap.platform_mgr = im_mgr_inst - - # Initialize webhook pusher - webhook_pusher_inst = WebhookPusher(ap) - ap.webhook_pusher = webhook_pusher_inst - - pipeline_mgr = pipelinemgr.PipelineManager(ap) - await pipeline_mgr.initialize() - ap.pipeline_mgr = pipeline_mgr - - rag_mgr_inst = rag_mgr.RAGManager(ap) - await rag_mgr_inst.initialize() - ap.rag_mgr = rag_mgr_inst - - # 初始化向量数据库管理器 - vectordb_mgr_inst = vectordb_mgr.VectorDBManager(ap) - await vectordb_mgr_inst.initialize() - ap.vector_db_mgr = vectordb_mgr_inst - - http_ctrl = http_controller.HTTPController(ap) - await http_ctrl.initialize() - ap.http_ctrl = http_ctrl - user_service_inst = user_service.UserService(ap) ap.user_service = user_service_inst @@ -143,6 +81,68 @@ class BuildAppStage(stage.BootingStage): webhook_service_inst = webhook_service.WebhookService(ap) ap.webhook_service = webhook_service_inst + proxy_mgr = proxy.ProxyManager(ap) + await proxy_mgr.initialize() + ap.proxy_mgr = proxy_mgr + + ver_mgr = version.VersionManager(ap) + await ver_mgr.initialize() + ap.ver_mgr = ver_mgr + + ap.query_pool = pool.QueryPool() + + log_cache = logcache.LogCache() + ap.log_cache = log_cache + + storage_mgr_inst = storagemgr.StorageMgr(ap) + await storage_mgr_inst.initialize() + ap.storage_mgr = storage_mgr_inst + + persistence_mgr_inst = persistencemgr.PersistenceManager(ap) + ap.persistence_mgr = persistence_mgr_inst + await persistence_mgr_inst.initialize() + + cmd_mgr_inst = cmdmgr.CommandManager(ap) + await cmd_mgr_inst.initialize() + ap.cmd_mgr = cmd_mgr_inst + + llm_model_mgr_inst = llm_model_mgr.ModelManager(ap) + ap.model_mgr = llm_model_mgr_inst + await llm_model_mgr_inst.initialize() + + llm_session_mgr_inst = llm_session_mgr.SessionManager(ap) + await llm_session_mgr_inst.initialize() + ap.sess_mgr = llm_session_mgr_inst + + llm_tool_mgr_inst = llm_tool_mgr.ToolManager(ap) + await llm_tool_mgr_inst.initialize() + ap.tool_mgr = llm_tool_mgr_inst + + im_mgr_inst = im_mgr.PlatformManager(ap=ap) + await im_mgr_inst.initialize() + ap.platform_mgr = im_mgr_inst + + # Initialize webhook pusher + webhook_pusher_inst = WebhookPusher(ap) + ap.webhook_pusher = webhook_pusher_inst + + pipeline_mgr = pipelinemgr.PipelineManager(ap) + await pipeline_mgr.initialize() + ap.pipeline_mgr = pipeline_mgr + + rag_mgr_inst = rag_mgr.RAGManager(ap) + await rag_mgr_inst.initialize() + ap.rag_mgr = rag_mgr_inst + + # 初始化向量数据库管理器 + vectordb_mgr_inst = vectordb_mgr.VectorDBManager(ap) + await vectordb_mgr_inst.initialize() + ap.vector_db_mgr = vectordb_mgr_inst + + http_ctrl = http_controller.HTTPController(ap) + await http_ctrl.initialize() + ap.http_ctrl = http_ctrl + async def runtime_disconnect_callback(connector: plugin_connector.PluginRuntimeConnector) -> None: await asyncio.sleep(3) await plugin_connector_inst.initialize() diff --git a/src/langbot/pkg/entity/dto/__init__.py b/src/langbot/pkg/entity/dto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/langbot/pkg/entity/dto/space_model.py b/src/langbot/pkg/entity/dto/space_model.py new file mode 100644 index 00000000..62b9f2b0 --- /dev/null +++ b/src/langbot/pkg/entity/dto/space_model.py @@ -0,0 +1,49 @@ +# [ +# { +# "uuid": "7652ebdb-54dc-412c-a830-e9268ac88471", +# "model_id": "claude-opus-4-5-20251101", +# "display_name": { +# "en_US": "claude-opus-4-5-20251101", +# "zh_Hans": "claude-opus-4-5-20251101" +# }, +# "description": {}, +# "provider": "anthropic", +# "category": "chat", +# "icon_url": "Claude.Color", +# "tags": {}, +# "is_featured": true, +# "featured_order": 999, +# "model_ratio": 2.5, +# "completion_ratio": 5, +# "quota_type": 0, +# "model_price": 0, +# "input_credits": 500, +# "output_credits": 2500, +# "vendor_id": 1, +# "vendor_name": "Anthropic", +# "vendor_icon": "Claude.Color", +# "supported_endpoints": [ +# "anthropic", +# "openai" +# ], +# "status": "active", +# "metadata": null, +# "created_at": "2025-12-30T22:23:38.337207+08:00", +# "updated_at": "2025-12-30T22:23:38.337207+08:00" +# } +# ] + +import pydantic + + +class SpaceModel(pydantic.BaseModel): + uuid: str + model_id: str + provider: str + category: str # chat / embedding + llm_abilities: list[str] | None = None + is_featured: bool = False + featured_order: int = 0 + status: str + created_at: str | None = None + updated_at: str | None = None diff --git a/src/langbot/pkg/entity/errors/provider.py b/src/langbot/pkg/entity/errors/provider.py index b495a6b0..c7831cef 100644 --- a/src/langbot/pkg/entity/errors/provider.py +++ b/src/langbot/pkg/entity/errors/provider.py @@ -7,3 +7,11 @@ class RequesterNotFoundError(Exception): def __str__(self): return f'Requester {self.requester_name} not found' + + +class ProviderNotFoundError(Exception): + def __init__(self, provider_name: str): + self.provider_name = provider_name + + def __str__(self): + return f'Provider {self.provider_name} not found' diff --git a/src/langbot/pkg/entity/persistence/model.py b/src/langbot/pkg/entity/persistence/model.py index e4459585..8ac3bd18 100644 --- a/src/langbot/pkg/entity/persistence/model.py +++ b/src/langbot/pkg/entity/persistence/model.py @@ -32,6 +32,7 @@ class LLMModel(Base): provider_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) abilities = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default=[]) extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) + prefered_ranking = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) updated_at = sqlalchemy.Column( sqlalchemy.DateTime, @@ -50,6 +51,7 @@ class EmbeddingModel(Base): name = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) provider_uuid = sqlalchemy.Column(sqlalchemy.String(255), nullable=False) extra_args = sqlalchemy.Column(sqlalchemy.JSON, nullable=False, default={}) + prefered_ranking = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, default=0) created_at = sqlalchemy.Column(sqlalchemy.DateTime, nullable=False, server_default=sqlalchemy.func.now()) updated_at = sqlalchemy.Column( sqlalchemy.DateTime, diff --git a/src/langbot/pkg/persistence/mgr.py b/src/langbot/pkg/persistence/mgr.py index 92be6a77..1311ff4f 100644 --- a/src/langbot/pkg/persistence/mgr.py +++ b/src/langbot/pkg/persistence/mgr.py @@ -134,7 +134,7 @@ class PersistenceManager: if result.first() is None: self.ap.logger.info('Creating space model providers...') space_chat_completions_model_provider = { - 'uuid': str(uuid.uuid4()), + 'uuid': '00000000-0000-0000-0000-000000000000', 'name': 'LangBot Models', 'requester': 'space-chat-completions', 'base_url': 'https://api.langbot.cloud/v1', diff --git a/src/langbot/pkg/persistence/migrations/dbm015_model_source_tracking.py b/src/langbot/pkg/persistence/migrations/dbm015_model_source_tracking.py index 05182b36..363ff666 100644 --- a/src/langbot/pkg/persistence/migrations/dbm015_model_source_tracking.py +++ b/src/langbot/pkg/persistence/migrations/dbm015_model_source_tracking.py @@ -1,77 +1,14 @@ -import sqlalchemy from .. import migration +# this is a deprecated migration @migration.migration_class(15) class DBMigrateModelSourceTracking(migration.DBMigration): """Add source tracking fields to models tables for Space integration""" async def upgrade(self): """Upgrade""" - # Add source column to llm_models table - llm_columns = await self._get_columns('llm_models') - - if 'source' not in llm_columns: - if self.ap.persistence_mgr.db.name == 'postgresql': - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text("ALTER TABLE llm_models ADD COLUMN source VARCHAR(32) DEFAULT 'local' NOT NULL") - ) - else: - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text("ALTER TABLE llm_models ADD COLUMN source VARCHAR(32) DEFAULT 'local' NOT NULL") - ) - - if 'space_model_id' not in llm_columns: - if self.ap.persistence_mgr.db.name == 'postgresql': - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN space_model_id VARCHAR(255)') - ) - else: - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN space_model_id VARCHAR(255)') - ) - - # Add source column to embedding_models table - embedding_columns = await self._get_columns('embedding_models') - - if 'source' not in embedding_columns: - if self.ap.persistence_mgr.db.name == 'postgresql': - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text( - "ALTER TABLE embedding_models ADD COLUMN source VARCHAR(32) DEFAULT 'local' NOT NULL" - ) - ) - else: - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text( - "ALTER TABLE embedding_models ADD COLUMN source VARCHAR(32) DEFAULT 'local' NOT NULL" - ) - ) - - if 'space_model_id' not in embedding_columns: - if self.ap.persistence_mgr.db.name == 'postgresql': - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN space_model_id VARCHAR(255)') - ) - else: - await self.ap.persistence_mgr.execute_async( - sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN space_model_id VARCHAR(255)') - ) - - async def _get_columns(self, table_name: str) -> list: - """Get column names for a table""" - if self.ap.persistence_mgr.db.name == 'postgresql': - result = await self.ap.persistence_mgr.execute_async( - sqlalchemy.text( - f"SELECT column_name FROM information_schema.columns WHERE table_name = '{table_name}';" - ) - ) - all_result = result.fetchall() - return [row[0] for row in all_result] - else: - result = await self.ap.persistence_mgr.execute_async(sqlalchemy.text(f'PRAGMA table_info({table_name});')) - all_result = result.fetchall() - return [row[1] for row in all_result] + pass async def downgrade(self): """Downgrade""" diff --git a/src/langbot/pkg/persistence/migrations/dbm016_model_provider_refactor.py b/src/langbot/pkg/persistence/migrations/dbm016_model_provider_refactor.py index 286967c3..223f2883 100644 --- a/src/langbot/pkg/persistence/migrations/dbm016_model_provider_refactor.py +++ b/src/langbot/pkg/persistence/migrations/dbm016_model_provider_refactor.py @@ -61,6 +61,12 @@ class DBMigrateModelProviderRefactor(migration.DBMigration): sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN provider_uuid VARCHAR(255)') ) + # Add prefered_ranking column if not exists + if 'prefered_ranking' not in llm_columns: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text('ALTER TABLE llm_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0') + ) + # Only migrate if old columns exist if 'requester' not in llm_columns: return @@ -152,6 +158,12 @@ class DBMigrateModelProviderRefactor(migration.DBMigration): sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN provider_uuid VARCHAR(255)') ) + # Add prefered_ranking column if not exists + if 'prefered_ranking' not in embedding_columns: + await self.ap.persistence_mgr.execute_async( + sqlalchemy.text('ALTER TABLE embedding_models ADD COLUMN prefered_ranking INTEGER NOT NULL DEFAULT 0') + ) + # Only migrate if old columns exist if 'requester' not in embedding_columns: return diff --git a/src/langbot/pkg/provider/modelmgr/modelmgr.py b/src/langbot/pkg/provider/modelmgr/modelmgr.py index 3c369638..0c84eaad 100644 --- a/src/langbot/pkg/provider/modelmgr/modelmgr.py +++ b/src/langbot/pkg/provider/modelmgr/modelmgr.py @@ -41,6 +41,7 @@ class ModelManager: self.requester_dict = requester_dict await self.load_models_from_db() + await self.sync_new_models_from_space() async def load_models_from_db(self): """Load models from database""" @@ -89,6 +90,65 @@ class ModelManager: except Exception as e: self.ap.logger.error(f'Failed to load model {embedding_model.uuid}: {e}\n{traceback.format_exc()}') + async def sync_new_models_from_space(self): + """Sync models from Space""" + space_model_provider = await self.ap.persistence_mgr.execute_async( + sqlalchemy.select(persistence_model.ModelProvider).where( + persistence_model.ModelProvider.requester == 'space-chat-completions' + ) + ) + result = space_model_provider.first() + if result is None: + raise provider_errors.ProviderNotFoundError('LangBot Models') + + space_model_provider = result + + # get the latest models from space + space_models = await self.ap.space_service.get_models() + + exists_llm_models_uuids = [m['uuid'] for m in await self.ap.llm_model_service.get_llm_models()] + exists_embedding_models_uuids = [ + m['uuid'] for m in await self.ap.embedding_models_service.get_embedding_models() + ] + + for space_model in space_models: + if space_model.category == 'chat': + uuid = space_model.uuid + + if uuid in exists_llm_models_uuids: + continue + + # model will be automatically loaded + await self.ap.llm_model_service.create_llm_model( + { + 'uuid': space_model.uuid, + 'name': space_model.model_id, + 'provider_uuid': space_model_provider.uuid, + 'abilities': space_model.llm_abilities or [], + 'extra_args': {}, + 'prefered_ranking': space_model.featured_order, + }, + preserve_uuid=True, + ) + + elif space_model.category == 'embedding': + uuid = space_model.uuid + + if uuid in exists_embedding_models_uuids: + continue + + # model will be automatically loaded + await self.ap.embedding_models_service.create_embedding_model( + { + 'uuid': space_model.uuid, + 'name': space_model.model_id, + 'provider_uuid': space_model_provider.uuid, + 'extra_args': {}, + 'prefered_ranking': space_model.featured_order, + }, + preserve_uuid=True, + ) + async def init_runtime_llm_model( self, model_info: dict,