feat(agent-runner): add plugin runner host integration

This commit is contained in:
huanghuoguoguo
2026-06-20 10:18:52 +08:00
parent d22fa82d7c
commit cede35b31b
129 changed files with 26980 additions and 6209 deletions
@@ -13,10 +13,13 @@ Source: src/langbot/pkg/api/http/service/model.py
from __future__ import annotations
import pytest
from unittest.mock import AsyncMock, Mock
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from langbot.pkg.agent.runner.default_config import AgentRunnerDefaultConfigService
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
from langbot.pkg.api.http.service.model import (
LLMModelsService,
EmbeddingModelsService,
@@ -29,6 +32,7 @@ from langbot.pkg.entity.persistence.model import LLMModel, EmbeddingModel, Reran
pytestmark = pytest.mark.asyncio
RUNNER_ID = 'plugin:test/runner/default'
def _create_mock_llm_model(
@@ -101,6 +105,22 @@ def _create_mock_result(items: list = None, first_item=None):
return result
class FakeAgentRunnerRegistry:
async def get(self, runner_id, bound_plugins=None):
return AgentRunnerDescriptor(
id=runner_id,
source='plugin',
label={'en_US': 'Test Runner'},
plugin_author='test',
plugin_name='runner',
runner_name='default',
config_schema=[
{'name': 'model', 'type': 'model-fallback-selector', 'default': {'primary': '', 'fallbacks': []}},
],
permissions={'models': ['invoke']},
)
class TestParseProviderApiKeys:
"""Tests for _parse_provider_api_keys helper function."""
@@ -451,6 +471,52 @@ class TestLLMModelsServiceCreateLLMModel:
assert runtime_entity.extra_args == {'temperature': 0.2}
assert 'context_length' not in runtime_entity.extra_args
async def test_create_llm_model_auto_sets_schema_defined_default_pipeline_model(self):
"""Auto-default model selection should use runner schema, not legacy field names."""
ap = SimpleNamespace()
ap.logger = Mock()
ap.persistence_mgr = SimpleNamespace()
ap.model_mgr = SimpleNamespace()
ap.model_mgr.provider_dict = {'provider-uuid': Mock()}
ap.model_mgr.llm_models = []
ap.model_mgr.load_llm_model_with_provider = AsyncMock(return_value=Mock())
ap.pipeline_service = SimpleNamespace(update_pipeline=AsyncMock())
ap.agent_runner_registry = FakeAgentRunnerRegistry()
ap.agent_runner_default_config_service = AgentRunnerDefaultConfigService(ap)
pipeline = SimpleNamespace(
uuid='pipeline-uuid',
config={
'ai': {
'runner': {'id': RUNNER_ID},
'runner_config': {
RUNNER_ID: {
'model': {'primary': '', 'fallbacks': []},
},
},
},
},
)
ap.persistence_mgr.execute_async = AsyncMock(return_value=_create_mock_result(first_item=pipeline))
service = LLMModelsService(ap)
model_uuid = await service.create_llm_model({
'uuid': 'new-model-uuid',
'name': 'New LLM',
'provider_uuid': 'provider-uuid',
'abilities': [],
'extra_args': {},
}, preserve_uuid=True)
assert model_uuid == 'new-model-uuid'
ap.pipeline_service.update_pipeline.assert_awaited_once()
updated_config = ap.pipeline_service.update_pipeline.await_args.args[1]['config']
assert updated_config['ai']['runner_config'][RUNNER_ID]['model'] == {
'primary': 'new-model-uuid',
'fallbacks': [],
}
async def test_create_llm_model_provider_not_found_raises_error(self):
"""Raises Exception when provider not found in runtime."""
# Setup
@@ -0,0 +1,77 @@
"""Tests for dynamic default pipeline config rendering."""
from __future__ import annotations
from types import SimpleNamespace
import pytest
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
from langbot.pkg.api.http.service.pipeline import PipelineService
class FakeLogger:
def warning(self, msg):
pass
class FakeRegistry:
def __init__(self, runners):
self.runners = runners
async def list_runners(self, bound_plugins=None):
return self.runners
def make_runner(runner_id: str, config_schema: list[dict]):
parts = runner_id.removeprefix('plugin:').split('/')
return AgentRunnerDescriptor(
id=runner_id,
source='plugin',
label={'en_US': runner_id},
plugin_author=parts[0],
plugin_name=parts[1],
runner_name=parts[2],
config_schema=config_schema,
)
@pytest.mark.asyncio
async def test_default_pipeline_config_uses_first_installed_runner_schema():
local_agent = make_runner(
'plugin:langbot/local-agent/default',
[
{'name': 'model', 'type': 'model-fallback-selector', 'default': {'primary': '', 'fallbacks': []}},
{'name': 'prompt', 'type': 'prompt-editor', 'default': [{'role': 'system', 'content': 'Hello'}]},
],
)
custom_agent = make_runner(
'plugin:alice/custom-agent/default',
[{'name': 'api-key', 'type': 'string', 'default': ''}],
)
ap = SimpleNamespace(
logger=FakeLogger(),
agent_runner_registry=FakeRegistry([custom_agent, local_agent]),
)
config = await PipelineService(ap).get_default_pipeline_config()
assert config['ai']['runner']['id'] == 'plugin:alice/custom-agent/default'
assert config['ai']['runner_config'] == {
'plugin:alice/custom-agent/default': {
'api-key': '',
},
}
@pytest.mark.asyncio
async def test_default_pipeline_config_stays_neutral_without_installed_runners():
ap = SimpleNamespace(
logger=FakeLogger(),
agent_runner_registry=FakeRegistry([]),
)
config = await PipelineService(ap).get_default_pipeline_config()
assert config['ai']['runner']['id'] == ''
assert config['ai']['runner_config'] == {}