mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
feat(agent-runner): finalize 4.x processor integration
This commit is contained in:
@@ -27,7 +27,7 @@ from tests.e2e.utils.process_manager import find_project_root
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
||||
LOCAL_AGENT_RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
LOCAL_AGENT_RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
FAKE_PROVIDER_UUID = 'e2e-fake-provider'
|
||||
FAKE_MODEL_UUID = 'e2e-fake-local-agent-model'
|
||||
LOCAL_AGENT_PLUGIN_DIRNAME = 'langbot__local-agent'
|
||||
|
||||
@@ -169,10 +169,12 @@ def _base_query(
|
||||
'bot_uuid': 'test-bot-uuid',
|
||||
'pipeline_config': {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'test-prompt',
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'test-prompt',
|
||||
},
|
||||
},
|
||||
},
|
||||
'output': {'misc': {'at-sender': False, 'quote-origin': False}},
|
||||
@@ -417,10 +419,12 @@ def query_with_config(
|
||||
if pipeline_config is None:
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'test-prompt',
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'test-prompt',
|
||||
},
|
||||
},
|
||||
},
|
||||
'output': {'misc': {'at-sender': False, 'quote-origin': False}},
|
||||
|
||||
@@ -14,7 +14,6 @@ from __future__ import annotations
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
import sys
|
||||
|
||||
from tests.factories import FakeApp, text_query, mock_platform_adapter
|
||||
from tests.factories.provider import FakeProvider
|
||||
@@ -48,10 +47,6 @@ def mock_circular_import_chain():
|
||||
# Mock core.app - Application class is referenced but not instantiated
|
||||
mock_core_app = Mock()
|
||||
|
||||
# Mock provider.runner with preregistered_runners list
|
||||
mock_runner = Mock()
|
||||
mock_runner.preregistered_runners = [] # Will be populated in tests
|
||||
|
||||
# Mock utils.importutil - prevents auto-import of runners
|
||||
mock_importutil = Mock()
|
||||
mock_importutil.import_modules_in_pkg = lambda pkg: None
|
||||
@@ -75,8 +70,7 @@ def mock_circular_import_chain():
|
||||
mocks={
|
||||
'langbot.pkg.core.entities': mock_core_entities,
|
||||
'langbot.pkg.core.app': mock_core_app,
|
||||
'langbot.pkg.provider.runner': mock_runner,
|
||||
'langbot.pkg.utils.importutil': mock_importutil,
|
||||
'langbot.pkg.utils.importutil': mock_importutil,
|
||||
'langbot.pkg.pipeline.controller': Mock(),
|
||||
'langbot.pkg.pipeline.pipelinemgr': Mock(),
|
||||
},
|
||||
@@ -108,8 +102,7 @@ def mock_circular_import_chain():
|
||||
class FakeRunner:
|
||||
"""Minimal fake runner class for pipeline integration tests.
|
||||
|
||||
Note: preregistered_runners expects a CLASS, not an instance.
|
||||
The handler calls runner_cls(self.ap, query.pipeline_config) to instantiate.
|
||||
The test orchestrator instantiates this class for each configured run.
|
||||
"""
|
||||
|
||||
name = 'local-agent'
|
||||
@@ -243,12 +236,23 @@ def fake_platform_adapter():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_fake_runner():
|
||||
"""Factory fixture to set a fake runner CLASS in preregistered_runners."""
|
||||
def set_fake_runner(pipeline_app):
|
||||
"""Attach a minimal AgentRunOrchestrator-compatible test double."""
|
||||
|
||||
def _set_runner(runner_cls):
|
||||
# preregistered_runners expects a list of runner classes
|
||||
sys.modules['langbot.pkg.provider.runner'].preregistered_runners = [runner_cls]
|
||||
runner = runner_cls()
|
||||
orchestrator = Mock()
|
||||
orchestrator.try_claim_steering_from_query = AsyncMock(return_value=False)
|
||||
|
||||
async def run_from_query(query):
|
||||
async for result in runner.run(query):
|
||||
yield result
|
||||
|
||||
orchestrator.run_from_query = run_from_query
|
||||
orchestrator.resolve_runner_id_for_telemetry = Mock(
|
||||
return_value='plugin:langbot-team/LocalAgent/default'
|
||||
)
|
||||
pipeline_app.agent_run_orchestrator = orchestrator
|
||||
|
||||
return _set_runner
|
||||
|
||||
@@ -260,11 +264,13 @@ def create_minimal_pipeline_config():
|
||||
"""Create minimal pipeline configuration for tests."""
|
||||
return {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent', 'expire-time': None},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'default',
|
||||
'knowledge-bases': [],
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default', 'expire-time': 0},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': {'primary': 'test-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'default',
|
||||
'knowledge-bases': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'output': {
|
||||
@@ -366,7 +372,7 @@ class TestPreProcessorStage:
|
||||
|
||||
result = await preproc_stage.process(query, 'PreProcessor')
|
||||
|
||||
assert result.result_type == entities.ResultType.CONTINUE
|
||||
assert result.result_type.value == entities.ResultType.CONTINUE.value
|
||||
assert result.new_query.session is not None
|
||||
assert result.new_query.user_message is not None
|
||||
|
||||
@@ -393,7 +399,7 @@ class TestPreProcessorStage:
|
||||
|
||||
result = await preproc_stage.process(query, 'PreProcessor')
|
||||
|
||||
assert result.result_type == entities.ResultType.CONTINUE
|
||||
assert result.result_type.value == entities.ResultType.CONTINUE.value
|
||||
# Check user_message content
|
||||
assert result.new_query.user_message is not None
|
||||
assert result.new_query.user_message.role == 'user'
|
||||
@@ -466,7 +472,7 @@ class TestProcessorStage:
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.INTERRUPT
|
||||
assert results[0].result_type.value == entities.ResultType.INTERRUPT.value
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_processor_prevent_default_with_reply_continues(self, pipeline_app, fake_platform_adapter):
|
||||
@@ -501,7 +507,7 @@ class TestProcessorStage:
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.CONTINUE
|
||||
assert results[0].result_type.value == entities.ResultType.CONTINUE.value
|
||||
assert len(query.resp_messages) == 1
|
||||
assert query.resp_messages[0] == reply_chain
|
||||
|
||||
@@ -546,7 +552,7 @@ class TestRunnerExceptionFlow:
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.INTERRUPT
|
||||
assert results[0].result_type.value == entities.ResultType.INTERRUPT.value
|
||||
assert results[0].user_notice == 'Request failed.'
|
||||
assert results[0].error_notice is not None
|
||||
|
||||
@@ -585,7 +591,7 @@ class TestRunnerExceptionFlow:
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.INTERRUPT
|
||||
assert results[0].result_type.value == entities.ResultType.INTERRUPT.value
|
||||
assert 'Custom runtime error' in results[0].user_notice
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -623,7 +629,7 @@ class TestRunnerExceptionFlow:
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.INTERRUPT
|
||||
assert results[0].result_type.value == entities.ResultType.INTERRUPT.value
|
||||
assert results[0].user_notice is None
|
||||
|
||||
|
||||
@@ -655,7 +661,7 @@ class TestSendResponseBackStage:
|
||||
|
||||
result = await respback_stage.process(query, 'SendResponseBackStage')
|
||||
|
||||
assert result.result_type == entities.ResultType.CONTINUE
|
||||
assert result.result_type.value == entities.ResultType.CONTINUE.value
|
||||
|
||||
# Check that adapter was called
|
||||
outbound = platform.get_outbound_messages()
|
||||
@@ -815,7 +821,7 @@ class TestStageChainIntegration:
|
||||
|
||||
# Run PreProcessor
|
||||
result1 = await preproc_stage.process(query, 'PreProcessor')
|
||||
assert result1.result_type == entities.ResultType.CONTINUE
|
||||
assert result1.result_type.value == entities.ResultType.CONTINUE.value
|
||||
query = result1.new_query
|
||||
|
||||
# Run Processor
|
||||
@@ -831,7 +837,7 @@ class TestStageChainIntegration:
|
||||
|
||||
# Run SendResponseBackStage
|
||||
result3 = await respback_stage.process(query, 'SendResponseBackStage')
|
||||
assert result3.result_type == entities.ResultType.CONTINUE
|
||||
assert result3.result_type.value == entities.ResultType.CONTINUE.value
|
||||
|
||||
# Verify adapter was called
|
||||
outbound = platform.get_outbound_messages()
|
||||
@@ -879,14 +885,14 @@ class TestStageChainIntegration:
|
||||
|
||||
# Run PreProcessor
|
||||
result1 = await preproc_stage.process(query, 'PreProcessor')
|
||||
assert result1.result_type == entities.ResultType.CONTINUE
|
||||
assert result1.result_type.value == entities.ResultType.CONTINUE.value
|
||||
query = result1.new_query
|
||||
|
||||
# Run Processor - should INTERRUPT
|
||||
results = await collect_processor_results(processor_stage, query, 'MessageProcessor')
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.INTERRUPT
|
||||
assert results[0].result_type.value == entities.ResultType.INTERRUPT.value
|
||||
|
||||
# Chain stops here - no resp_messages
|
||||
assert len(query.resp_messages) == 0
|
||||
|
||||
@@ -69,7 +69,7 @@ class MockQuery:
|
||||
self.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
},
|
||||
'runner_config': {},
|
||||
},
|
||||
@@ -133,7 +133,7 @@ class MockAgentRunOrchestrator:
|
||||
return False
|
||||
|
||||
def resolve_runner_id_for_telemetry(self, query):
|
||||
return 'plugin:langbot/local-agent/default'
|
||||
return 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
class MockApplication:
|
||||
@@ -234,16 +234,16 @@ class TestConfigMigrationInChatHandler:
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
def test_resolve_runner_id_from_old_format(self):
|
||||
"""ConfigMigration resolves old runner aliases for compatibility."""
|
||||
def test_old_runner_field_is_not_resolved(self):
|
||||
"""The 4.x path requires ai.runner.id."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -253,7 +253,7 @@ class TestConfigMigrationInChatHandler:
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_id is None
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
@@ -268,18 +268,18 @@ class TestErrorHandling:
|
||||
def test_runner_execution_error_retryable(self):
|
||||
"""RunnerExecutionError should have retryable property."""
|
||||
error = RunnerExecutionError(
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
'Upstream timeout',
|
||||
retryable=True,
|
||||
)
|
||||
assert error.runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert error.runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert error.retryable is True
|
||||
assert 'timeout' in str(error)
|
||||
|
||||
def test_runner_execution_error_not_retryable(self):
|
||||
"""RunnerExecutionError can be non-retryable."""
|
||||
error = RunnerExecutionError(
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
'Configuration error',
|
||||
retryable=False,
|
||||
)
|
||||
@@ -288,11 +288,11 @@ class TestErrorHandling:
|
||||
def test_runner_not_authorized_error_properties(self):
|
||||
"""RunnerNotAuthorizedError should have bound_plugins property."""
|
||||
error = RunnerNotAuthorizedError(
|
||||
'plugin:langbot/local-agent/default',
|
||||
['langbot/dify-agent'],
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
['langbot-team/DifyAgent'],
|
||||
)
|
||||
assert error.runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert error.bound_plugins == ['langbot/dify-agent']
|
||||
assert error.runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert error.bound_plugins == ['langbot-team/DifyAgent']
|
||||
|
||||
|
||||
class TestChatHandlerImports:
|
||||
@@ -500,7 +500,7 @@ class TestChatHandlerAsyncBehavior:
|
||||
from langbot.pkg.pipeline import entities
|
||||
|
||||
orchestrator = MockAgentRunOrchestrator(
|
||||
error=RunnerNotAuthorizedError('plugin:langbot/local-agent/default', ['other/plugin'])
|
||||
error=RunnerNotAuthorizedError('plugin:langbot-team/LocalAgent/default', ['other/plugin'])
|
||||
)
|
||||
mock_ap = MockApplication(orchestrator=orchestrator)
|
||||
mock_ap.plugin_connector.emit_event = AsyncMock(return_value=MockEventContext(prevented=False))
|
||||
@@ -538,7 +538,7 @@ class TestChatHandlerAsyncBehavior:
|
||||
from langbot.pkg.pipeline import entities
|
||||
|
||||
orchestrator = MockAgentRunOrchestrator(
|
||||
error=RunnerExecutionError('plugin:langbot/local-agent/default', 'timeout', retryable=True)
|
||||
error=RunnerExecutionError('plugin:langbot-team/LocalAgent/default', 'timeout', retryable=True)
|
||||
)
|
||||
mock_ap = MockApplication(orchestrator=orchestrator)
|
||||
mock_ap.plugin_connector.emit_event = AsyncMock(return_value=MockEventContext(prevented=False))
|
||||
|
||||
@@ -12,15 +12,15 @@ class TestResolveRunnerId:
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
def test_resolves_old_runner_field(self):
|
||||
def test_does_not_resolve_legacy_runner_field(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -30,33 +30,7 @@ class TestResolveRunnerId:
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolves_deerflow_and_weknora_legacy_runner_fields(self):
|
||||
assert (
|
||||
ConfigMigration.resolve_runner_id(
|
||||
{
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'deerflow-api',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
== 'plugin:langbot/deerflow-agent/default'
|
||||
)
|
||||
assert (
|
||||
ConfigMigration.resolve_runner_id(
|
||||
{
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'weknora-api',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
== 'plugin:langbot/weknora-agent/default'
|
||||
)
|
||||
assert runner_id is None
|
||||
|
||||
def test_resolve_no_runner_config(self):
|
||||
runner_id = ConfigMigration.resolve_runner_id({})
|
||||
@@ -70,7 +44,7 @@ class TestResolveRunnerConfig:
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': 'uuid-123',
|
||||
'custom_option': 10,
|
||||
},
|
||||
@@ -80,11 +54,11 @@ class TestResolveRunnerConfig:
|
||||
|
||||
config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
)
|
||||
assert config == {'model': 'uuid-123', 'custom_option': 10}
|
||||
|
||||
def test_reads_old_runner_block(self):
|
||||
def test_does_not_read_legacy_runner_block(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
@@ -95,46 +69,14 @@ class TestResolveRunnerConfig:
|
||||
|
||||
config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
)
|
||||
assert config == {'model': {'primary': 'uuid-123', 'fallbacks': []}}
|
||||
|
||||
def test_reads_deerflow_and_weknora_legacy_runner_blocks(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'deerflow-api': {
|
||||
'api-base': 'http://127.0.0.1:2026',
|
||||
'assistant-id': 'lead_agent',
|
||||
},
|
||||
'weknora-api': {
|
||||
'base-url': 'http://localhost:8080/api/v1',
|
||||
'app-type': 'agent',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
deerflow_config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/deerflow-agent/default',
|
||||
)
|
||||
weknora_config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/weknora-agent/default',
|
||||
)
|
||||
|
||||
assert deerflow_config == {
|
||||
'api-base': 'http://127.0.0.1:2026',
|
||||
'assistant-id': 'lead_agent',
|
||||
}
|
||||
assert weknora_config == {
|
||||
'base-url': 'http://localhost:8080/api/v1',
|
||||
'app-type': 'agent',
|
||||
}
|
||||
assert config == {}
|
||||
|
||||
def test_resolve_no_config(self):
|
||||
config = ConfigMigration.resolve_runner_config(
|
||||
{},
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
)
|
||||
assert config == {}
|
||||
|
||||
@@ -169,89 +111,3 @@ class TestGetExpireTime:
|
||||
def test_get_expire_time_default(self):
|
||||
expire_time = ConfigMigration.get_expire_time({})
|
||||
assert expire_time == 0
|
||||
|
||||
|
||||
class TestNormalizePipelineConfig:
|
||||
"""Tests for ConfigMigration.migrate_pipeline_config."""
|
||||
|
||||
def test_normalizes_current_containers(self):
|
||||
config = {'ai': {}}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated == {'ai': {'runner': {}, 'runner_config': {}}}
|
||||
|
||||
def test_preserves_current_config(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:test/my-runner/default'},
|
||||
'runner_config': {
|
||||
'plugin:test/my-runner/default': {'custom-option': 20},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:test/my-runner/default'
|
||||
assert migrated['ai']['runner_config']['plugin:test/my-runner/default']['custom-option'] == 20
|
||||
|
||||
def test_migrates_old_runner_blocks(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {'model': 'old-model', 'knowledge-base': 'kb_1'},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default'
|
||||
assert 'runner' not in migrated['ai']['runner']
|
||||
assert 'local-agent' not in migrated['ai']
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] == {
|
||||
'model': {'primary': 'old-model', 'fallbacks': []},
|
||||
'knowledge-bases': ['kb_1'],
|
||||
}
|
||||
|
||||
def test_migrates_deerflow_legacy_runner_block(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'deerflow-api'},
|
||||
'deerflow-api': {
|
||||
'api-base': 'http://127.0.0.1:2026',
|
||||
'assistant-id': 'lead_agent',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/deerflow-agent/default'
|
||||
assert 'runner' not in migrated['ai']['runner']
|
||||
assert 'deerflow-api' not in migrated['ai']
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/deerflow-agent/default'] == {
|
||||
'api-base': 'http://127.0.0.1:2026',
|
||||
'assistant-id': 'lead_agent',
|
||||
}
|
||||
|
||||
def test_migrates_weknora_legacy_runner_block(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'weknora-api'},
|
||||
'weknora-api': {
|
||||
'base-url': 'http://localhost:8080/api/v1',
|
||||
'app-type': 'agent',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/weknora-agent/default'
|
||||
assert 'runner' not in migrated['ai']['runner']
|
||||
assert 'weknora-api' not in migrated['ai']
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/weknora-agent/default'] == {
|
||||
'base-url': 'http://localhost:8080/api/v1',
|
||||
'app-type': 'agent',
|
||||
}
|
||||
|
||||
@@ -7,65 +7,6 @@ import json
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
|
||||
|
||||
class TestMigratePipelineConfig:
|
||||
"""Tests for ConfigMigration.migrate_pipeline_config."""
|
||||
|
||||
def test_current_format_config_stays_unchanged(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'expire-time': 0,
|
||||
},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'model': {'primary': '', 'fallbacks': []},
|
||||
'custom-option': 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default'
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['custom-option'] == 10
|
||||
|
||||
def test_old_runner_field_is_mapped(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'local-agent',
|
||||
'expire-time': 3600,
|
||||
},
|
||||
'local-agent': {
|
||||
'model': 'old-model',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner'] == {
|
||||
'expire-time': 3600,
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
}
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default'] == {
|
||||
'model': {'primary': 'old-model', 'fallbacks': []},
|
||||
}
|
||||
assert 'local-agent' not in migrated['ai']
|
||||
|
||||
def test_empty_config_is_unchanged(self):
|
||||
config = {}
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
assert migrated == {}
|
||||
|
||||
def test_config_without_ai_section_is_unchanged(self):
|
||||
config = {'trigger': {}}
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
assert migrated == {'trigger': {}}
|
||||
|
||||
|
||||
class TestDefaultPipelineConfig:
|
||||
"""Tests for default-pipeline-config.json format."""
|
||||
|
||||
@@ -97,14 +38,14 @@ class TestResolveRunnerId:
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:test/my-runner/default'
|
||||
|
||||
def test_old_runner_field_is_mapped(self):
|
||||
def test_old_runner_field_is_not_supported(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
},
|
||||
}
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_id is None
|
||||
|
||||
|
||||
class TestResolveRunnerConfig:
|
||||
@@ -114,18 +55,18 @@ class TestResolveRunnerConfig:
|
||||
config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {'custom-option': 20},
|
||||
'plugin:langbot-team/LocalAgent/default': {'custom-option': 20},
|
||||
},
|
||||
},
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot-team/LocalAgent/default')
|
||||
assert runner_config['custom-option'] == 20
|
||||
|
||||
def test_old_runner_block_is_read(self):
|
||||
def test_old_runner_block_is_not_supported(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'local-agent': {'custom-option': 20},
|
||||
},
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config == {'custom-option': 20}
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot-team/LocalAgent/default')
|
||||
assert runner_config == {}
|
||||
|
||||
@@ -158,27 +158,26 @@ class TestQueryConfigToAgentConfig:
|
||||
|
||||
assert agent_config.runner_id == "plugin:author/plugin/runner"
|
||||
|
||||
def test_config_to_agent_config_uses_legacy_runner_config_migration(self, mock_query):
|
||||
"""Temporary query adapter must share the normal runner config resolver."""
|
||||
def test_config_to_agent_config_uses_current_runner_config(self, mock_query):
|
||||
"""Temporary query adapters use the current runner config container."""
|
||||
mock_query.pipeline_config = {
|
||||
"ai": {
|
||||
"runner": {"runner": "local-agent"},
|
||||
"local-agent": {
|
||||
"model": "model-primary",
|
||||
"knowledge-base": "kb-001",
|
||||
"runner": {"id": "plugin:langbot-team/LocalAgent/default"},
|
||||
"runner_config": {
|
||||
"plugin:langbot-team/LocalAgent/default": {
|
||||
"model": {"primary": "model-primary", "fallbacks": []},
|
||||
"knowledge-bases": ["kb-001"],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
agent_config = QueryEntryAdapter.config_to_agent_config(
|
||||
mock_query,
|
||||
"plugin:langbot/local-agent/default",
|
||||
"plugin:langbot-team/LocalAgent/default",
|
||||
)
|
||||
|
||||
assert agent_config.runner_config["model"] == {
|
||||
"primary": "model-primary",
|
||||
"fallbacks": [],
|
||||
}
|
||||
assert agent_config.runner_config["model"] == {"primary": "model-primary", "fallbacks": []}
|
||||
assert agent_config.runner_config["knowledge-bases"] == ["kb-001"]
|
||||
|
||||
def test_resolver_projects_agent_scope(self, mock_query):
|
||||
|
||||
@@ -544,10 +544,10 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'knowledge-bases': ['kb_001'],
|
||||
},
|
||||
},
|
||||
@@ -583,8 +583,8 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
|
||||
assert 'kb_custom' in allowed_kbs
|
||||
|
||||
def test_retrieve_kb_reads_old_runner_format(self):
|
||||
"""Old runner format is resolved for migration compatibility."""
|
||||
def test_retrieve_kb_does_not_read_old_runner_format(self):
|
||||
"""The 4.x authorization path ignores legacy Pipeline runner fields."""
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
|
||||
pipeline_config = {
|
||||
@@ -600,8 +600,8 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
runner_config = ConfigMigration.resolve_runner_config(pipeline_config, runner_id)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_config.get('knowledge-bases') == ['kb_legacy']
|
||||
assert runner_id is None
|
||||
assert runner_config == {}
|
||||
|
||||
|
||||
class TestHandlerActionAuthorization:
|
||||
|
||||
@@ -16,12 +16,12 @@ class TestRunnerIdParsing:
|
||||
|
||||
def test_parse_plugin_runner_id(self):
|
||||
"""Parse valid plugin runner ID."""
|
||||
runner_id = 'plugin:langbot/local-agent/default'
|
||||
runner_id = 'plugin:langbot-team/LocalAgent/default'
|
||||
parts = parse_runner_id(runner_id)
|
||||
|
||||
assert parts.source == 'plugin'
|
||||
assert parts.plugin_author == 'langbot'
|
||||
assert parts.plugin_name == 'local-agent'
|
||||
assert parts.plugin_author == 'langbot-team'
|
||||
assert parts.plugin_name == 'LocalAgent'
|
||||
assert parts.runner_name == 'default'
|
||||
|
||||
def test_parse_plugin_runner_id_complex_names(self):
|
||||
@@ -36,7 +36,7 @@ class TestRunnerIdParsing:
|
||||
|
||||
def test_parse_invalid_plugin_runner_id_missing_parts(self):
|
||||
"""Parse invalid plugin runner ID with missing parts."""
|
||||
runner_id = 'plugin:langbot/local-agent'
|
||||
runner_id = 'plugin:langbot-team/LocalAgent'
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
parse_runner_id(runner_id)
|
||||
@@ -76,20 +76,20 @@ class TestRunnerIdFormatting:
|
||||
"""Format plugin runner ID."""
|
||||
runner_id = format_runner_id(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
def test_format_invalid_source(self):
|
||||
"""Format runner ID with invalid source."""
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
format_runner_id(
|
||||
source='builtin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
@@ -103,19 +103,19 @@ class TestRunnerIdParts:
|
||||
"""Get plugin ID from parts."""
|
||||
parts = RunnerIdParts(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
assert parts.to_plugin_id() == 'langbot/local-agent'
|
||||
assert parts.to_plugin_id() == 'langbot-team/LocalAgent'
|
||||
|
||||
def test_frozen_dataclass(self):
|
||||
"""RunnerIdParts should be immutable."""
|
||||
parts = RunnerIdParts(
|
||||
source='plugin',
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ class TestIsPluginRunnerId:
|
||||
|
||||
def test_is_plugin_runner_id_true(self):
|
||||
"""Check plugin runner ID returns True."""
|
||||
assert is_plugin_runner_id('plugin:langbot/local-agent/default') is True
|
||||
assert is_plugin_runner_id('plugin:langbot-team/LocalAgent/default') is True
|
||||
|
||||
def test_is_plugin_runner_id_false(self):
|
||||
"""Check non-plugin runner ID returns False."""
|
||||
|
||||
@@ -26,7 +26,7 @@ from langbot_plugin.api.entities.builtin.provider import session as provider_ses
|
||||
from langbot_plugin.api.entities.builtin.resource import tool as resource_tool
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
class FakeLogger:
|
||||
@@ -153,8 +153,8 @@ def make_descriptor() -> AgentRunnerDescriptor:
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
capabilities={
|
||||
'streaming': True,
|
||||
@@ -231,7 +231,7 @@ def make_query():
|
||||
],
|
||||
),
|
||||
variables={
|
||||
'_pipeline_bound_plugins': ['langbot/local-agent'],
|
||||
'_pipeline_bound_plugins': ['langbot-team/LocalAgent'],
|
||||
'_fallback_model_uuids': ['model_fallback'],
|
||||
'_pipeline_bound_skills': ['demo'],
|
||||
'public_param': 'visible',
|
||||
@@ -354,8 +354,8 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
|
||||
assert messages[0].content == 'fake response'
|
||||
assert plugin_connector.calls == [
|
||||
{
|
||||
'plugin_author': 'langbot',
|
||||
'plugin_name': 'local-agent',
|
||||
'plugin_author': 'langbot-team',
|
||||
'plugin_name': 'LocalAgent',
|
||||
'runner_name': 'default',
|
||||
}
|
||||
]
|
||||
@@ -400,7 +400,7 @@ async def test_orchestrator_runs_fake_plugin_with_authorized_context(clean_agent
|
||||
|
||||
session_during_run = plugin_connector.sessions_during_run[0]
|
||||
assert session_during_run is not None
|
||||
assert session_during_run['plugin_identity'] == 'langbot/local-agent'
|
||||
assert session_during_run['plugin_identity'] == 'langbot-team/LocalAgent'
|
||||
assert session_during_run['authorization']['authorized_ids']['tool'] == {'langbot/test-tool/search'}
|
||||
assert session_during_run['authorization']['authorized_ids']['skill'] == {'demo'}
|
||||
assert await get_session_registry().get(context['run_id']) is None
|
||||
|
||||
@@ -35,11 +35,11 @@ class FakeApplication:
|
||||
# Return sample runner data
|
||||
return [
|
||||
{
|
||||
'plugin_author': 'langbot',
|
||||
'plugin_name': 'local-agent',
|
||||
'plugin_author': 'langbot-team',
|
||||
'plugin_name': 'LocalAgent',
|
||||
'runner_name': 'default',
|
||||
'manifest': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'id': 'plugin:langbot-team/LocalAgent/default',
|
||||
'name': 'default',
|
||||
'label': {'en_US': 'Local Agent'},
|
||||
'capabilities': {'streaming': True},
|
||||
@@ -98,11 +98,11 @@ class TestRegistryDiscovery:
|
||||
|
||||
runners = await registry.list_runners(use_cache=False)
|
||||
|
||||
# Should find 2 valid runners (langbot/local-agent and alice/my-agent)
|
||||
# Should find 2 valid runners (langbot-team/LocalAgent and alice/my-agent)
|
||||
assert len(runners) == 2
|
||||
|
||||
ids = [r.id for r in runners]
|
||||
assert 'plugin:langbot/local-agent/default' in ids
|
||||
assert 'plugin:langbot-team/LocalAgent/default' in ids
|
||||
assert 'plugin:alice/my-agent/custom' in ids
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -143,15 +143,15 @@ class TestRegistryDiscovery:
|
||||
|
||||
# First: get with bound_plugins filter (should not pollute cache)
|
||||
descriptor = await registry.get(
|
||||
'plugin:langbot/local-agent/default',
|
||||
bound_plugins=['langbot/local-agent'],
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
assert descriptor.id == 'plugin:langbot/local-agent/default'
|
||||
assert descriptor.id == 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
# Cache should contain ALL runners (both langbot and alice)
|
||||
assert registry._cache is not None
|
||||
assert len(registry._cache) == 2 # Both runners in cache
|
||||
assert 'plugin:langbot/local-agent/default' in registry._cache
|
||||
assert 'plugin:langbot-team/LocalAgent/default' in registry._cache
|
||||
assert 'plugin:alice/my-agent/custom' in registry._cache
|
||||
|
||||
# Second: list_runners without filter should return ALL runners
|
||||
@@ -173,11 +173,11 @@ class TestRegistryGet:
|
||||
ap = FakeApplication()
|
||||
registry = AgentRunnerRegistry(ap)
|
||||
|
||||
descriptor = await registry.get('plugin:langbot/local-agent/default')
|
||||
descriptor = await registry.get('plugin:langbot-team/LocalAgent/default')
|
||||
|
||||
assert descriptor.id == 'plugin:langbot/local-agent/default'
|
||||
assert descriptor.plugin_author == 'langbot'
|
||||
assert descriptor.plugin_name == 'local-agent'
|
||||
assert descriptor.id == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert descriptor.plugin_author == 'langbot-team'
|
||||
assert descriptor.plugin_name == 'LocalAgent'
|
||||
assert descriptor.runner_name == 'default'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -199,8 +199,8 @@ class TestRegistryGet:
|
||||
|
||||
# Authorized - langbot plugin in bound list
|
||||
descriptor = await registry.get(
|
||||
'plugin:langbot/local-agent/default',
|
||||
bound_plugins=['langbot/local-agent'],
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
assert descriptor is not None
|
||||
|
||||
@@ -208,7 +208,7 @@ class TestRegistryGet:
|
||||
with pytest.raises(RunnerNotAuthorizedError):
|
||||
await registry.get(
|
||||
'plugin:alice/my-agent/custom',
|
||||
bound_plugins=['langbot/local-agent'],
|
||||
bound_plugins=['langbot-team/LocalAgent'],
|
||||
)
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ class TestRegistryMetadataForPipeline:
|
||||
# Should have options for each runner
|
||||
assert len(options) == 2
|
||||
option_ids = [o['name'] for o in options]
|
||||
assert 'plugin:langbot/local-agent/default' in option_ids
|
||||
assert 'plugin:langbot-team/LocalAgent/default' in option_ids
|
||||
assert 'plugin:alice/my-agent/custom' in option_ids
|
||||
|
||||
# Config comes from the typed manifest.
|
||||
|
||||
@@ -32,11 +32,11 @@ class FakeApplication:
|
||||
def make_descriptor():
|
||||
"""Create a test descriptor."""
|
||||
return AgentRunnerDescriptor(
|
||||
id='plugin:langbot/local-agent/default',
|
||||
id='plugin:langbot-team/LocalAgent/default',
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent', 'zh_Hans': '内置 Agent'},
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
capabilities={'streaming': True},
|
||||
)
|
||||
@@ -190,7 +190,7 @@ class TestNormalizeRunFailed:
|
||||
with pytest.raises(RunnerExecutionError) as exc_info:
|
||||
await normalizer.normalize(result_dict, descriptor)
|
||||
|
||||
assert exc_info.value.runner_id == 'plugin:langbot/local-agent/default'
|
||||
assert exc_info.value.runner_id == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert exc_info.value.retryable is True
|
||||
assert 'timeout' in str(exc_info.value)
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ class TestAgentServiceCreateUpdateDelete:
|
||||
async def test_create_agent_uses_default_runner_config_from_registry(self):
|
||||
app = _make_app()
|
||||
runner = SimpleNamespace(
|
||||
id='plugin:langbot/local-agent/default',
|
||||
id='plugin:langbot-team/LocalAgent/default',
|
||||
config_schema=[
|
||||
{'name': 'model', 'default': 'gpt-4.1'},
|
||||
{'name': 'temperature', 'default': 0.2},
|
||||
|
||||
@@ -353,19 +353,6 @@ class TestPipelineServiceCreatePipeline:
|
||||
}
|
||||
|
||||
|
||||
class _MockResultWithBots:
|
||||
"""Helper class to mock SQLAlchemy result with iterable .all() method."""
|
||||
|
||||
def __init__(self, bots_list):
|
||||
self._bots_list = bots_list
|
||||
|
||||
def all(self):
|
||||
return self._bots_list
|
||||
|
||||
def first(self):
|
||||
return self._bots_list[0] if self._bots_list else None
|
||||
|
||||
|
||||
class TestPipelineServiceUpdatePipeline:
|
||||
"""Tests for update_pipeline method."""
|
||||
|
||||
@@ -379,20 +366,18 @@ class TestPipelineServiceUpdatePipeline:
|
||||
ap.pipeline_mgr.load_pipeline = AsyncMock()
|
||||
ap.sess_mgr = SimpleNamespace()
|
||||
ap.sess_mgr.session_list = []
|
||||
ap.bot_service = None # No bot_service when not updating name
|
||||
|
||||
ap.persistence_mgr.execute_async = AsyncMock()
|
||||
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid', 'name': 'Updated'})
|
||||
|
||||
# Execute with protected fields - no name change, so no bot sync
|
||||
# Execute with protected fields.
|
||||
pipeline_data = {
|
||||
'uuid': 'should-be-removed',
|
||||
'for_version': 'should-be-removed',
|
||||
'stages': ['should-be-removed'],
|
||||
'is_default': True,
|
||||
'description': 'New description', # Not name change, so no bot_service needed
|
||||
'description': 'New description',
|
||||
}
|
||||
await service.update_pipeline('test-uuid', pipeline_data)
|
||||
|
||||
@@ -402,8 +387,8 @@ class TestPipelineServiceUpdatePipeline:
|
||||
assert ['should-be-removed'] not in update_params.values()
|
||||
assert not any(value is True for value in update_params.values())
|
||||
|
||||
async def test_update_pipeline_syncs_bot_names(self):
|
||||
"""Updates bot use_pipeline_name when pipeline name changes."""
|
||||
async def test_update_pipeline_name_does_not_rewrite_bot_routes(self):
|
||||
"""Bot event bindings remain independent from pipeline display names."""
|
||||
# Setup
|
||||
ap = SimpleNamespace()
|
||||
ap.persistence_mgr = SimpleNamespace()
|
||||
@@ -415,45 +400,14 @@ class TestPipelineServiceUpdatePipeline:
|
||||
ap.bot_service = SimpleNamespace()
|
||||
ap.bot_service.update_bot = AsyncMock()
|
||||
|
||||
# Create proper mock Bot entities with uuid attribute
|
||||
mock_bot1 = Mock()
|
||||
mock_bot1.uuid = 'bot-uuid-1'
|
||||
mock_bot2 = Mock()
|
||||
mock_bot2.uuid = 'bot-uuid-2'
|
||||
|
||||
# Create bot list
|
||||
bot_list = [mock_bot1, mock_bot2]
|
||||
|
||||
# Create mock result using helper class
|
||||
bot_result = _MockResultWithBots(bot_list)
|
||||
|
||||
# The order of calls in update_pipeline:
|
||||
# 1. UPDATE (line 125) - returns Mock (no result needed)
|
||||
# 2. SELECT bots (line 136) - returns bot_result with .all()
|
||||
call_count = 0
|
||||
|
||||
async def mock_execute(query):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
# First call is the UPDATE - just return a Mock
|
||||
return Mock()
|
||||
elif call_count == 2:
|
||||
# Second call is the SELECT bots - return proper result
|
||||
return bot_result
|
||||
return Mock() # Any additional calls
|
||||
|
||||
ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
|
||||
ap.persistence_mgr.serialize_model = Mock(return_value={})
|
||||
ap.persistence_mgr.execute_async = AsyncMock(return_value=Mock())
|
||||
|
||||
service = PipelineService(ap)
|
||||
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid', 'name': 'New Name'})
|
||||
|
||||
# Execute with name change
|
||||
await service.update_pipeline('test-uuid', {'name': 'New Name'})
|
||||
|
||||
# Verify - bot_service.update_bot was called for each bot
|
||||
assert ap.bot_service.update_bot.call_count == 2
|
||||
ap.bot_service.update_bot.assert_not_awaited()
|
||||
|
||||
async def test_update_pipeline_clears_conversations(self):
|
||||
"""Clears session conversations using this pipeline."""
|
||||
|
||||
@@ -76,6 +76,8 @@ async def test_mcp_server_exposes_bot_event_route_tools():
|
||||
|
||||
assert 'list_bot_event_route_statuses' in tool_names
|
||||
assert 'test_bot_event_route' in tool_names
|
||||
assert 'list_processors' in tool_names
|
||||
assert 'list_agents' not in tool_names
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -39,7 +39,7 @@ def make_runner(runner_id: str, config_schema: list[dict]):
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_pipeline_config_uses_first_installed_runner_schema():
|
||||
local_agent = make_runner(
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
[
|
||||
{'name': 'model', 'type': 'model-fallback-selector', 'default': {'primary': '', 'fallbacks': []}},
|
||||
{'name': 'prompt', 'type': 'prompt-editor', 'default': [{'role': 'system', 'content': 'Hello'}]},
|
||||
|
||||
@@ -185,9 +185,9 @@ def test_resolve_box_session_id_reads_current_runner_config():
|
||||
query = make_query(101)
|
||||
query.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot/local-agent/default'},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'box-session-id-template': 'bot-{launcher_id}-{sender_id}',
|
||||
},
|
||||
},
|
||||
@@ -450,7 +450,14 @@ def test_box_service_forced_template_ignores_pipeline_config():
|
||||
launcher_id='test_user',
|
||||
sender_id='test_user',
|
||||
pipeline_config={
|
||||
'ai': {'local-agent': {'box-session-id-template': '{launcher_type}_{launcher_id}_{sender_id}'}}
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'box-session-id-template': '{launcher_type}_{launcher_id}_{sender_id}'
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -469,7 +476,16 @@ def test_box_service_empty_forced_template_respects_pipeline_config():
|
||||
query_id=7,
|
||||
launcher_type='group',
|
||||
launcher_id='room-1',
|
||||
pipeline_config={'ai': {'local-agent': {'box-session-id-template': '{launcher_type}_{launcher_id}'}}},
|
||||
pipeline_config={
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'box-session-id-template': '{launcher_type}_{launcher_id}'
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
assert service.resolve_box_session_id(query) == 'group_room-1'
|
||||
|
||||
@@ -27,7 +27,7 @@ import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||
from langbot.pkg.pipeline import entities as pipeline_entities
|
||||
|
||||
|
||||
DEFAULT_RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
DEFAULT_RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
class MockApplication:
|
||||
|
||||
@@ -337,8 +337,13 @@ class TestChatHandlerExceptions:
|
||||
query.pipeline_config = {
|
||||
'output': {'misc': {'exception-handling': 'show-hint', 'failure-hint': 'Request failed.'}},
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {'prompt': 'default', 'model': {'primary': 'test'}},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'prompt': 'default',
|
||||
'model': {'primary': 'test'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -385,8 +390,13 @@ class TestChatHandlerExceptions:
|
||||
query.pipeline_config = {
|
||||
'output': {'misc': {'exception-handling': 'show-error'}},
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {'prompt': 'default', 'model': {'primary': 'test'}},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'prompt': 'default',
|
||||
'model': {'primary': 'test'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -430,8 +440,13 @@ class TestChatHandlerExceptions:
|
||||
query.pipeline_config = {
|
||||
'output': {'misc': {'exception-handling': 'hide'}},
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {'prompt': 'default', 'model': {'primary': 'test'}},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'prompt': 'default',
|
||||
'model': {'primary': 'test'},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -50,8 +50,13 @@ async def _run_preprocessor(mock_app, sample_query, conversation):
|
||||
|
||||
sample_query.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent', 'expire-time': 60},
|
||||
'local-agent': {'model': {'primary': '', 'fallbacks': []}, 'prompt': []},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default', 'expire-time': 60},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': {'primary': '', 'fallbacks': []},
|
||||
'prompt': [],
|
||||
},
|
||||
},
|
||||
},
|
||||
'trigger': {'misc': {'combine-quote-message': False}},
|
||||
'output': {'misc': {'exception-handling': 'show-hint'}},
|
||||
|
||||
@@ -11,10 +11,7 @@ async def test_update_pipeline_filters_protected_fields_without_mutating_input(m
|
||||
loaded_pipeline = Mock()
|
||||
service.get_pipeline = AsyncMock(return_value=loaded_pipeline)
|
||||
|
||||
bot = Mock(uuid='bot-uuid')
|
||||
bot_result = Mock(all=Mock(return_value=[bot]))
|
||||
mock_app.persistence_mgr.execute_async = AsyncMock(side_effect=[None, bot_result])
|
||||
mock_app.bot_service = Mock(update_bot=AsyncMock())
|
||||
mock_app.persistence_mgr.execute_async = AsyncMock(return_value=None)
|
||||
mock_app.pipeline_mgr = Mock(remove_pipeline=AsyncMock(), load_pipeline=AsyncMock())
|
||||
mock_app.sess_mgr.session_list = []
|
||||
|
||||
@@ -35,9 +32,5 @@ async def test_update_pipeline_filters_protected_fields_without_mutating_input(m
|
||||
updated_fields = {getattr(field, 'key', str(field)) for field in update_stmt._values}
|
||||
assert updated_fields == {'name'}
|
||||
|
||||
mock_app.bot_service.update_bot.assert_awaited_once_with(
|
||||
'bot-uuid',
|
||||
{'use_pipeline_name': 'Updated pipeline'},
|
||||
)
|
||||
mock_app.pipeline_mgr.remove_pipeline.assert_awaited_once_with('pipeline-uuid')
|
||||
mock_app.pipeline_mgr.load_pipeline.assert_awaited_once_with(loaded_pipeline)
|
||||
|
||||
@@ -164,17 +164,20 @@ async def test_runtime_pipeline_execute(mock_app, sample_query):
|
||||
mock_stage.process.assert_called_once()
|
||||
|
||||
|
||||
def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||
"""Local Agent resource selection should override legacy extension prefs."""
|
||||
def test_runtime_pipeline_prefers_runner_mcp_resources(mock_app):
|
||||
"""Runner resource selection should override extension preferences."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
'mcp-resources': [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}],
|
||||
'mcp-resource-agent-read-enabled': False,
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'mcp-resources': [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}],
|
||||
'mcp-resource-agent-read-enabled': False,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,12 +193,17 @@ def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||
|
||||
|
||||
def test_runtime_pipeline_falls_back_to_extension_mcp_resources(mock_app):
|
||||
"""Existing extension prefs remain compatible until a Local Agent value exists."""
|
||||
"""Extension preferences apply when the current runner has no override."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {'ai': {'local-agent': {}}}
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {'plugin:langbot-team/LocalAgent/default': {}},
|
||||
}
|
||||
}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
'mcp_resources': [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}],
|
||||
'mcp_resource_agent_read_enabled': False,
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_entities_module():
|
||||
return import_module('langbot.pkg.pipeline.entities')
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
def attach_agent_runner_descriptor(app, *, multimodal_input=True, tool_calling=True):
|
||||
@@ -46,8 +46,8 @@ def attach_agent_runner_descriptor(app, *, multimodal_input=True, tool_calling=T
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
config_schema=[
|
||||
{'name': 'model', 'type': 'model-fallback-selector'},
|
||||
@@ -499,22 +499,19 @@ class TestPreProcessorToolSelection:
|
||||
mock_event_ctx = Mock()
|
||||
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
|
||||
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
|
||||
attach_agent_runner_descriptor(app)
|
||||
|
||||
stage = preproc.PreProcessor(app)
|
||||
query = text_query('hello')
|
||||
query.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'primary-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'default',
|
||||
'enable-all-tools': False,
|
||||
'tools': ['plugin_tool'],
|
||||
},
|
||||
},
|
||||
'output': {'misc': {'at-sender': False}},
|
||||
'trigger': {'misc': {}},
|
||||
}
|
||||
query.pipeline_config = agent_runner_pipeline_config(
|
||||
{'primary': 'primary-model-uuid', 'fallbacks': []},
|
||||
)
|
||||
query.pipeline_config['ai']['runner_config'][RUNNER_ID].update(
|
||||
{
|
||||
'enable-all-tools': False,
|
||||
'tools': ['plugin_tool'],
|
||||
}
|
||||
)
|
||||
|
||||
result = await stage.process(query, 'PreProcessor')
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import pytest
|
||||
from langbot_plugin.api.entities.builtin.platform import message as platform_message
|
||||
|
||||
|
||||
RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
def _attach_agent_runner_descriptor(app):
|
||||
@@ -19,8 +19,8 @@ def _attach_agent_runner_descriptor(app):
|
||||
id=RUNNER_ID,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
config_schema=[
|
||||
{'name': 'model', 'type': 'model-fallback-selector'},
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestEventRouteTrace:
|
||||
"""A route miss is visible as structured route trace metadata."""
|
||||
bot = self._make_bot([])
|
||||
|
||||
await bot._dispatch_eba_event_to_agent(SimpleNamespace(type='platform.member.joined'), Mock())
|
||||
await bot._dispatch_eba_event_to_processor(SimpleNamespace(type='platform.member.joined'), Mock())
|
||||
|
||||
bot.logger.info.assert_awaited_once()
|
||||
_, kwargs = bot.logger.info.await_args
|
||||
|
||||
@@ -21,7 +21,7 @@ from langbot.pkg.provider.modelmgr.modelmgr import ModelManager
|
||||
from langbot.pkg.provider.modelmgr.token import TokenManager
|
||||
|
||||
|
||||
DEFAULT_RUNNER_ID = 'plugin:langbot/local-agent/default'
|
||||
DEFAULT_RUNNER_ID = 'plugin:langbot-team/LocalAgent/default'
|
||||
|
||||
|
||||
class FakeAgentRunnerRegistry:
|
||||
@@ -30,8 +30,8 @@ class FakeAgentRunnerRegistry:
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Local Agent'},
|
||||
plugin_author='langbot',
|
||||
plugin_name='local-agent',
|
||||
plugin_author='langbot-team',
|
||||
plugin_name='LocalAgent',
|
||||
runner_name='default',
|
||||
config_schema=[
|
||||
{'name': 'model', 'type': 'model-fallback-selector'},
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestPersistActivatedSkill:
|
||||
|
||||
query = SimpleNamespace(variables={ACTIVATED_SKILLS_KEY: {'pdf': {'name': 'pdf'}}})
|
||||
query._agent_run_session = {
|
||||
'runner_id': 'plugin:langbot/local-agent/default',
|
||||
'runner_id': 'plugin:langbot-team/LocalAgent/default',
|
||||
'state_context': {
|
||||
'scope_keys': {'conversation': 'conv-scope-key'},
|
||||
'binding_identity': 'binding-1',
|
||||
@@ -214,7 +214,7 @@ class TestPersistActivatedSkill:
|
||||
assert kwargs['state_key'] == ACTIVATED_SKILL_NAMES_STATE_KEY
|
||||
assert kwargs['value'] == ['pdf']
|
||||
assert kwargs['scope'] == 'conversation'
|
||||
assert kwargs['runner_id'] == 'plugin:langbot/local-agent/default'
|
||||
assert kwargs['runner_id'] == 'plugin:langbot-team/LocalAgent/default'
|
||||
assert kwargs['binding_identity'] == 'binding-1'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -64,9 +64,9 @@ def _make_query() -> Query:
|
||||
pipeline_uuid='pipe-1',
|
||||
pipeline_config={
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:langbot/local-agent/default'},
|
||||
'runner': {'id': 'plugin:langbot-team/LocalAgent/default'},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'plugin:langbot-team/LocalAgent/default': {
|
||||
'model': {'primary': 'model-1', 'fallbacks': []},
|
||||
'prompt': [],
|
||||
'knowledge-bases': [],
|
||||
@@ -307,7 +307,7 @@ async def test_preproc_uses_transcript_history_view_when_available():
|
||||
assert result.result_type == entities_module.ResultType.CONTINUE
|
||||
assert query.messages == transcript_messages
|
||||
stage._load_agent_runner_history_messages.assert_awaited_once_with(
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot-team/LocalAgent/default',
|
||||
'conv-1',
|
||||
bot_id='bot-1',
|
||||
workspace_id=None,
|
||||
|
||||
Reference in New Issue
Block a user