feat(agent-runner): finalize 4.x processor integration

This commit is contained in:
huanghuoguoguo
2026-07-12 15:44:05 +08:00
parent 29689962f3
commit e6384aae5d
109 changed files with 2200 additions and 1204 deletions
@@ -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'
+12 -8
View File
@@ -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}},
+36 -30
View File
@@ -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
+16 -16
View File
@@ -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))
+10 -154
View File
@@ -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):
+6 -6
View File
@@ -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:
+15 -15
View File
@@ -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
+17 -17
View File
@@ -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."""
+2
View File
@@ -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'}]},
+20 -4
View File
@@ -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'
+1 -1
View File
@@ -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:
+21 -6
View File
@@ -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)
+15 -7
View File
@@ -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,
+13 -16
View File
@@ -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
+3 -3
View File
@@ -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,