mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-15 02:06:03 +00:00
Compare commits
1 Commits
codex/spac
...
codex/plug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4622f2e86a |
@@ -197,10 +197,11 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
self,
|
||||
file_bytes: bytes,
|
||||
task_context: taskmgr.TaskContext | None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
) -> tuple[str | None, str | None, str | None]:
|
||||
"""Extract plugin identity and dependency metadata from a plugin package."""
|
||||
plugin_author = None
|
||||
plugin_name = None
|
||||
plugin_version = None
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
|
||||
@@ -209,6 +210,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
metadata = manifest.get('metadata', {})
|
||||
plugin_author = metadata.get('author')
|
||||
plugin_name = metadata.get('name')
|
||||
plugin_version = metadata.get('version')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -227,7 +229,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return plugin_author, plugin_name
|
||||
return plugin_author, plugin_name, plugin_version
|
||||
|
||||
async def _install_mcp_from_marketplace(
|
||||
self,
|
||||
@@ -369,6 +371,7 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
):
|
||||
plugin_author = install_info.get('plugin_author')
|
||||
plugin_name = install_info.get('plugin_name')
|
||||
plugin_file_transferred = False
|
||||
|
||||
if install_source == PluginInstallSource.MARKETPLACE:
|
||||
# Handle marketplace plugin/mcp/skill installation
|
||||
@@ -463,9 +466,18 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
)
|
||||
|
||||
file_bytes = download_resp.content
|
||||
self._inspect_plugin_package(file_bytes, task_context)
|
||||
plugin_author, plugin_name, plugin_version = self._inspect_plugin_package(
|
||||
file_bytes,
|
||||
task_context,
|
||||
)
|
||||
if task_context is not None and plugin_author and plugin_name:
|
||||
task_context.metadata['plugin_name'] = f'{plugin_author}/{plugin_name}'
|
||||
if task_context is not None and plugin_version:
|
||||
task_context.metadata['plugin_version'] = plugin_version
|
||||
file_key = await self.handler.send_file(file_bytes, 'lbpkg')
|
||||
install_info['plugin_file_key'] = file_key
|
||||
install_source = PluginInstallSource.LOCAL
|
||||
plugin_file_transferred = True
|
||||
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
|
||||
# Continue to install via runtime
|
||||
else:
|
||||
@@ -481,12 +493,14 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
mcp_resp.raise_for_status()
|
||||
raise Exception(f'Failed to get MCP {plugin_author}/{plugin_name}')
|
||||
|
||||
if install_source == PluginInstallSource.LOCAL:
|
||||
if install_source == PluginInstallSource.LOCAL and not plugin_file_transferred:
|
||||
# transfer file before install
|
||||
file_bytes = install_info['plugin_file']
|
||||
plugin_author, plugin_name = self._inspect_plugin_package(file_bytes, task_context)
|
||||
plugin_author, plugin_name, plugin_version = self._inspect_plugin_package(file_bytes, task_context)
|
||||
if task_context is not None and plugin_author and plugin_name:
|
||||
task_context.metadata['plugin_name'] = f'{plugin_author}/{plugin_name}'
|
||||
if task_context is not None and plugin_version:
|
||||
task_context.metadata['plugin_version'] = plugin_version
|
||||
file_key = await self.handler.send_file(file_bytes, 'lbpkg')
|
||||
install_info['plugin_file_key'] = file_key
|
||||
del install_info['plugin_file']
|
||||
@@ -523,9 +537,11 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
task_context.metadata['download_speed'] = downloaded / elapsed if elapsed > 0 else 0
|
||||
|
||||
file_bytes = b''.join(chunks)
|
||||
plugin_author, plugin_name = self._inspect_plugin_package(file_bytes, task_context)
|
||||
plugin_author, plugin_name, plugin_version = self._inspect_plugin_package(file_bytes, task_context)
|
||||
if task_context is not None and plugin_author and plugin_name:
|
||||
task_context.metadata['plugin_name'] = f'{plugin_author}/{plugin_name}'
|
||||
if task_context is not None and plugin_version:
|
||||
task_context.metadata['plugin_version'] = plugin_version
|
||||
file_key = await self.handler.send_file(file_bytes, 'lbpkg')
|
||||
install_info['plugin_file_key'] = file_key
|
||||
self.ap.logger.info(f'Transfered file {file_key} to plugin runtime')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import sqlalchemy
|
||||
import traceback
|
||||
|
||||
@@ -85,17 +84,8 @@ class ModelManager:
|
||||
self.ap.logger.info('LangBot Space Models service is disabled, skipping sync.')
|
||||
return
|
||||
|
||||
sync_timeout = space_config.get('models_sync_timeout')
|
||||
try:
|
||||
if sync_timeout:
|
||||
await asyncio.wait_for(
|
||||
self.sync_new_models_from_space(),
|
||||
timeout=float(sync_timeout),
|
||||
)
|
||||
else:
|
||||
await self.sync_new_models_from_space()
|
||||
except asyncio.TimeoutError:
|
||||
self.ap.logger.warning(f'LangBot Space model sync timed out after {sync_timeout}s, skipping startup sync.')
|
||||
except Exception as e:
|
||||
self.ap.logger.warning('Failed to sync new models from LangBot Space, model list may not be updated.')
|
||||
self.ap.logger.warning(f' - Error: {e}')
|
||||
|
||||
@@ -49,6 +49,30 @@ class TestExtractDepsMetadata:
|
||||
assert 'flask' in task_context.metadata['deps_list']
|
||||
assert 'numpy' in task_context.metadata['deps_list']
|
||||
|
||||
def test_extract_plugin_identity_includes_version(self):
|
||||
"""Extract plugin identity and version from manifest.yaml."""
|
||||
connector = self._create_connector()
|
||||
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w') as zf:
|
||||
zf.writestr(
|
||||
'manifest.yaml',
|
||||
'\n'.join(
|
||||
[
|
||||
'metadata:',
|
||||
' author: langbot-team',
|
||||
' name: LangRAG',
|
||||
' version: 0.1.8',
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
assert connector._inspect_plugin_package(zip_buffer.getvalue(), None) == (
|
||||
'langbot-team',
|
||||
'LangRAG',
|
||||
'0.1.8',
|
||||
)
|
||||
|
||||
def test_extract_deps_empty_requirements(self):
|
||||
"""Handle empty requirements.txt."""
|
||||
connector = self._create_connector()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
@@ -89,28 +88,6 @@ def test_token_manager_next_token_ignores_empty_token_list():
|
||||
assert token_mgr.using_token_index == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_model_manager_initialize_skips_space_sync_after_timeout():
|
||||
ap = SimpleNamespace()
|
||||
ap.discover = SimpleNamespace(get_components_by_kind=Mock(return_value=[]))
|
||||
ap.instance_config = SimpleNamespace(data={'space': {'models_sync_timeout': 0.01}})
|
||||
ap.logger = Mock()
|
||||
|
||||
mgr = ModelManager(ap)
|
||||
mgr.load_models_from_db = AsyncMock()
|
||||
|
||||
async def slow_sync():
|
||||
await asyncio.sleep(1)
|
||||
|
||||
mgr.sync_new_models_from_space = AsyncMock(side_effect=slow_sync)
|
||||
|
||||
await mgr.initialize()
|
||||
|
||||
mgr.load_models_from_db.assert_awaited_once()
|
||||
mgr.sync_new_models_from_space.assert_awaited_once()
|
||||
ap.logger.warning.assert_any_call('LangBot Space model sync timed out after 0.01s, skipping startup sync.')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_updated_llm_model_is_immediately_usable_by_local_agent_pipeline():
|
||||
from langbot.pkg.api.http.service.model import LLMModelsService
|
||||
|
||||
Reference in New Issue
Block a user