mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-08 06:46:02 +00:00
refactor(agent-runner): simplify event-first entry path
This commit is contained in:
@@ -45,7 +45,7 @@ def make_session(
|
||||
Args:
|
||||
run_id: Unique run identifier
|
||||
runner_id: Runner descriptor ID
|
||||
query_id: Pipeline query ID
|
||||
query_id: Host entry query ID
|
||||
plugin_identity: Plugin identifier (author/name)
|
||||
resources: AgentResources dict (uses make_resources() default if None)
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ class MockLauncherType:
|
||||
|
||||
|
||||
class MockConversation:
|
||||
uuid = 'conv-uuid'
|
||||
messages = []
|
||||
def __init__(self):
|
||||
self.uuid = 'conv-uuid'
|
||||
self.messages = []
|
||||
|
||||
|
||||
class MockMessage:
|
||||
@@ -51,7 +52,9 @@ class MockAdapter:
|
||||
class MockSession:
|
||||
launcher_type = MockLauncherType()
|
||||
launcher_id = 'user123'
|
||||
using_conversation = MockConversation()
|
||||
|
||||
def __init__(self):
|
||||
self.using_conversation = MockConversation()
|
||||
|
||||
|
||||
class MockQuery:
|
||||
@@ -155,6 +158,10 @@ class MockApplication:
|
||||
self.model_mgr = MagicMock()
|
||||
self.model_mgr.get_model_by_uuid = AsyncMock(return_value=None)
|
||||
|
||||
# Mock sess_mgr
|
||||
self.sess_mgr = MagicMock()
|
||||
self.sess_mgr.get_conversation = AsyncMock()
|
||||
|
||||
|
||||
class TestStreamingBehavior:
|
||||
"""Tests for streaming mode behavior."""
|
||||
@@ -232,7 +239,7 @@ class TestConfigMigrationInChatHandler:
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolve_runner_id_from_old_format(self):
|
||||
"""ConfigMigration should handle old runner format."""
|
||||
"""ConfigMigration should not resolve removed runner aliases."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -242,7 +249,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:
|
||||
@@ -399,6 +406,50 @@ class TestChatHandlerAsyncBehavior:
|
||||
assert query.resp_messages[0].content == 'Response 1'
|
||||
assert query.resp_messages[1].content == 'Response 2'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_history_update_recreates_conversation_if_tool_resets_it(self):
|
||||
"""History update should tolerate CREATE_NEW_CONVERSATION during runner execution."""
|
||||
from langbot.pkg.pipeline.process.handlers.chat import ChatMessageHandler
|
||||
from langbot.pkg.pipeline import entities
|
||||
|
||||
response = MockMessageChunk('Tool response')
|
||||
new_conversation = MockConversation()
|
||||
|
||||
class ResetConversationOrchestrator(MockAgentRunOrchestrator):
|
||||
async def run_from_query(self, query):
|
||||
query.session.using_conversation = None
|
||||
yield response
|
||||
|
||||
mock_ap = MockApplication(orchestrator=ResetConversationOrchestrator())
|
||||
mock_ap.plugin_connector.emit_event = AsyncMock(return_value=MockEventContext(prevented=False))
|
||||
mock_ap.sess_mgr.get_conversation = AsyncMock(return_value=new_conversation)
|
||||
|
||||
query = MockQuery()
|
||||
query.adapter.is_stream = False
|
||||
|
||||
handler = ChatMessageHandler(mock_ap)
|
||||
|
||||
mock_event = MagicMock()
|
||||
mock_event.return_value = MagicMock()
|
||||
|
||||
def make_result(*args, **kwargs):
|
||||
return MagicMock(result_type=kwargs.get('result_type', entities.ResultType.CONTINUE))
|
||||
|
||||
with patch('langbot.pkg.pipeline.process.handlers.chat.events') as mock_events_module, \
|
||||
patch('langbot.pkg.pipeline.entities.StageProcessResult', side_effect=make_result):
|
||||
mock_events_module.PersonNormalMessageReceived = mock_event
|
||||
mock_events_module.GroupNormalMessageReceived = mock_event
|
||||
|
||||
results = []
|
||||
async for result in handler.handle(query):
|
||||
results.append(result)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.CONTINUE
|
||||
mock_ap.sess_mgr.get_conversation.assert_awaited_once()
|
||||
assert query.session.using_conversation is new_conversation
|
||||
assert new_conversation.messages == [query.user_message, response]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_not_found_error(self):
|
||||
"""Handler should catch RunnerNotFoundError and return INTERRUPT."""
|
||||
@@ -550,4 +601,4 @@ class TestChatHandlerAsyncBehavior:
|
||||
# Should return CONTINUE with reply message
|
||||
assert len(results) == 1
|
||||
assert results[0].result_type == entities.ResultType.CONTINUE
|
||||
assert len(query.resp_messages) == 1
|
||||
assert len(query.resp_messages) == 1
|
||||
|
||||
@@ -1,56 +1,14 @@
|
||||
"""Tests for agent runner config migration."""
|
||||
"""Tests for current AgentRunner config helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from langbot.pkg.agent.runner.config_migration import (
|
||||
ConfigMigration,
|
||||
OLD_RUNNER_TO_PLUGIN_RUNNER_ID,
|
||||
)
|
||||
|
||||
|
||||
class TestOldRunnerMapping:
|
||||
"""Tests for OLD_RUNNER_TO_PLUGIN_RUNNER_ID mapping."""
|
||||
|
||||
def test_local_agent_mapping(self):
|
||||
"""Local-agent should map to official plugin."""
|
||||
assert OLD_RUNNER_TO_PLUGIN_RUNNER_ID['local-agent'] == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_dify_mapping(self):
|
||||
"""Dify should map to official plugin."""
|
||||
assert OLD_RUNNER_TO_PLUGIN_RUNNER_ID['dify-service-api'] == 'plugin:langbot/dify-agent/default'
|
||||
|
||||
def test_n8n_mapping(self):
|
||||
"""n8n should map to official plugin."""
|
||||
assert OLD_RUNNER_TO_PLUGIN_RUNNER_ID['n8n-service-api'] == 'plugin:langbot/n8n-agent/default'
|
||||
|
||||
def test_coze_mapping(self):
|
||||
"""Coze should map to official plugin."""
|
||||
assert OLD_RUNNER_TO_PLUGIN_RUNNER_ID['coze-api'] == 'plugin:langbot/coze-agent/default'
|
||||
|
||||
def test_all_runners_mapped(self):
|
||||
"""All old runners should have mapping."""
|
||||
expected_runners = [
|
||||
'local-agent',
|
||||
'dify-service-api',
|
||||
'n8n-service-api',
|
||||
'coze-api',
|
||||
'dashscope-app-api',
|
||||
'langflow-api',
|
||||
'tbox-app-api',
|
||||
]
|
||||
for runner in expected_runners:
|
||||
assert runner in OLD_RUNNER_TO_PLUGIN_RUNNER_ID
|
||||
mapped = OLD_RUNNER_TO_PLUGIN_RUNNER_ID[runner]
|
||||
assert mapped.startswith('plugin:langbot/')
|
||||
assert mapped.endswith('/default')
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
|
||||
|
||||
class TestResolveRunnerId:
|
||||
"""Tests for ConfigMigration.resolve_runner_id."""
|
||||
|
||||
def test_resolve_new_format_runner_id(self):
|
||||
"""Resolve runner ID from new format."""
|
||||
def test_resolve_current_runner_id(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -62,8 +20,7 @@ class TestResolveRunnerId:
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolve_old_format_runner_name(self):
|
||||
"""Resolve runner ID from old format."""
|
||||
def test_does_not_resolve_old_runner_field(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -72,49 +29,18 @@ class TestResolveRunnerId:
|
||||
},
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolve_old_format_plugin_runner(self):
|
||||
"""Resolve already migrated plugin:* runner."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'plugin:alice/my-agent/custom',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:alice/my-agent/custom'
|
||||
|
||||
def test_resolve_no_runner_config(self):
|
||||
"""Resolve runner ID when not configured."""
|
||||
pipeline_config = {}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id is None
|
||||
|
||||
def test_resolve_priority_new_over_old(self):
|
||||
"""New format takes priority over old format."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
'runner': 'dify-service-api', # This should be ignored
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
def test_resolve_no_runner_config(self):
|
||||
runner_id = ConfigMigration.resolve_runner_id({})
|
||||
assert runner_id is None
|
||||
|
||||
|
||||
class TestResolveRunnerConfig:
|
||||
"""Tests for ConfigMigration.resolve_runner_config."""
|
||||
|
||||
def test_resolve_new_format_config(self):
|
||||
"""Resolve runner config from new format."""
|
||||
def test_resolve_current_config(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
@@ -132,13 +58,11 @@ class TestResolveRunnerConfig:
|
||||
)
|
||||
assert config == {'model': 'uuid-123', 'custom_option': 10}
|
||||
|
||||
def test_resolve_old_format_config(self):
|
||||
"""Runtime config resolver should not read old format."""
|
||||
def test_does_not_read_old_runner_block(self):
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
'model': 'uuid-123',
|
||||
'custom_option': 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -149,62 +73,18 @@ class TestResolveRunnerConfig:
|
||||
)
|
||||
assert config == {}
|
||||
|
||||
def test_resolve_legacy_config_for_migration(self):
|
||||
"""Migration helper should read old format."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
'model': 'uuid-123',
|
||||
'custom_option': 10,
|
||||
'knowledge-base': 'kb-123',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config = ConfigMigration.resolve_legacy_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
)
|
||||
assert config == {'model': 'uuid-123', 'custom_option': 10, 'knowledge-bases': ['kb-123']}
|
||||
assert 'knowledge-base' not in config
|
||||
|
||||
def test_resolve_no_config(self):
|
||||
"""Resolve runner config when not found."""
|
||||
pipeline_config = {}
|
||||
|
||||
config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
{},
|
||||
'plugin:langbot/local-agent/default',
|
||||
)
|
||||
assert config == {}
|
||||
|
||||
def test_resolve_priority_new_over_old(self):
|
||||
"""New format config takes priority."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'model': 'new-uuid',
|
||||
},
|
||||
},
|
||||
'local-agent': {
|
||||
'model': 'old-uuid',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config = ConfigMigration.resolve_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
)
|
||||
assert config == {'model': 'new-uuid'}
|
||||
|
||||
|
||||
class TestGetExpireTime:
|
||||
"""Tests for ConfigMigration.get_expire_time."""
|
||||
|
||||
def test_get_expire_time_zero(self):
|
||||
"""Get expire time when zero."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -217,7 +97,6 @@ class TestGetExpireTime:
|
||||
assert expire_time == 0
|
||||
|
||||
def test_get_expire_time_positive(self):
|
||||
"""Get expire time when positive."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -230,22 +109,44 @@ class TestGetExpireTime:
|
||||
assert expire_time == 3600
|
||||
|
||||
def test_get_expire_time_default(self):
|
||||
"""Get expire time when not configured."""
|
||||
pipeline_config = {}
|
||||
|
||||
expire_time = ConfigMigration.get_expire_time(pipeline_config)
|
||||
expire_time = ConfigMigration.get_expire_time({})
|
||||
assert expire_time == 0
|
||||
|
||||
|
||||
class TestGetOldRunnerName:
|
||||
"""Tests for ConfigMigration.get_old_runner_name."""
|
||||
class TestNormalizePipelineConfig:
|
||||
"""Tests for ConfigMigration.migrate_pipeline_config."""
|
||||
|
||||
def test_get_old_runner_name_mapped(self):
|
||||
"""Get old runner name for mapped runner ID."""
|
||||
old_name = ConfigMigration.get_old_runner_name('plugin:langbot/local-agent/default')
|
||||
assert old_name == 'local-agent'
|
||||
def test_normalizes_current_containers(self):
|
||||
config = {'ai': {}}
|
||||
|
||||
def test_get_old_runner_name_not_mapped(self):
|
||||
"""Get old runner name for unmapped runner ID."""
|
||||
old_name = ConfigMigration.get_old_runner_name('plugin:alice/my-agent/custom')
|
||||
assert old_name is None
|
||||
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_does_not_migrate_old_runner_blocks(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {'model': 'old-model'},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert 'id' not in migrated['ai']['runner']
|
||||
assert migrated['ai']['local-agent'] == {'model': 'old-model'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for pipeline config migration to new runner format."""
|
||||
"""Tests for persisted AgentRunner config shape."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -10,62 +10,8 @@ from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
class TestMigratePipelineConfig:
|
||||
"""Tests for ConfigMigration.migrate_pipeline_config."""
|
||||
|
||||
def test_migrate_old_local_agent_config(self):
|
||||
"""Old local-agent config should migrate to plugin format."""
|
||||
old_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'local-agent',
|
||||
'expire-time': 0,
|
||||
},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'model-uuid', 'fallbacks': []},
|
||||
'knowledge-base': 'kb-uuid',
|
||||
'prompt': [{'role': 'system', 'content': 'Hello'}],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(old_config)
|
||||
|
||||
# Should have new format
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/local-agent/default'
|
||||
assert 'runner' not in migrated['ai']['runner'] or migrated['ai']['runner'].get('runner') != 'local-agent'
|
||||
|
||||
# Config should be in runner_config
|
||||
assert 'plugin:langbot/local-agent/default' in migrated['ai']['runner_config']
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/local-agent/default']['knowledge-bases'] == ['kb-uuid']
|
||||
assert 'knowledge-base' not in migrated['ai']['runner_config']['plugin:langbot/local-agent/default']
|
||||
|
||||
# Expire-time preserved
|
||||
assert migrated['ai']['runner']['expire-time'] == 0
|
||||
|
||||
def test_migrate_old_dify_service_api_config(self):
|
||||
"""Old dify-service-api config should migrate to dify-agent plugin."""
|
||||
old_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'dify-service-api',
|
||||
'expire-time': 300,
|
||||
},
|
||||
'dify-service-api': {
|
||||
'base-url': 'https://api.dify.ai/v1',
|
||||
'api-key': 'test-key',
|
||||
'app-type': 'chat',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(old_config)
|
||||
|
||||
assert migrated['ai']['runner']['id'] == 'plugin:langbot/dify-agent/default'
|
||||
assert 'plugin:langbot/dify-agent/default' in migrated['ai']['runner_config']
|
||||
assert migrated['ai']['runner_config']['plugin:langbot/dify-agent/default']['api-key'] == 'test-key'
|
||||
assert migrated['ai']['runner']['expire-time'] == 300
|
||||
|
||||
def test_new_format_config_stays_unchanged(self):
|
||||
"""New format config should not change."""
|
||||
new_config = {
|
||||
def test_current_format_config_stays_unchanged(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
@@ -80,134 +26,67 @@ class TestMigratePipelineConfig:
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(new_config)
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
# Should remain unchanged
|
||||
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_new_format_local_agent_config_normalizes_legacy_kb_key(self):
|
||||
"""Migration should normalize legacy KB aliases before runtime."""
|
||||
def test_old_runner_field_is_not_mapped(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'knowledge-base': 'kb-legacy',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
runner_config = migrated['ai']['runner_config']['plugin:langbot/local-agent/default']
|
||||
|
||||
assert runner_config == {'knowledge-bases': ['kb-legacy']}
|
||||
|
||||
def test_migrate_all_old_runners(self):
|
||||
"""All old runner names should be migrated."""
|
||||
old_runners = [
|
||||
'local-agent',
|
||||
'dify-service-api',
|
||||
'n8n-service-api',
|
||||
'coze-api',
|
||||
'dashscope-app-api',
|
||||
'langflow-api',
|
||||
'tbox-app-api',
|
||||
]
|
||||
|
||||
expected_ids = [
|
||||
'plugin:langbot/local-agent/default',
|
||||
'plugin:langbot/dify-agent/default',
|
||||
'plugin:langbot/n8n-agent/default',
|
||||
'plugin:langbot/coze-agent/default',
|
||||
'plugin:langbot/dashscope-agent/default',
|
||||
'plugin:langbot/langflow-agent/default',
|
||||
'plugin:langbot/tbox-agent/default',
|
||||
]
|
||||
|
||||
for old_runner, expected_id in zip(old_runners, expected_ids):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': old_runner, 'expire-time': 0},
|
||||
old_runner: {'test-key': 'test-value'},
|
||||
},
|
||||
}
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
assert migrated['ai']['runner']['id'] == expected_id
|
||||
assert expected_id in migrated['ai']['runner_config']
|
||||
|
||||
def test_migrate_empty_config(self):
|
||||
"""Empty config should not break."""
|
||||
config = {}
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
assert migrated == {}
|
||||
|
||||
def test_migrate_config_without_ai_section(self):
|
||||
"""Config without ai section should not break."""
|
||||
config = {'trigger': {}}
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
assert 'trigger' in migrated
|
||||
|
||||
def test_expire_time_preserved(self):
|
||||
"""expire-time should be preserved during migration."""
|
||||
old_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'runner': 'local-agent',
|
||||
'expire-time': 3600,
|
||||
},
|
||||
'local-agent': {},
|
||||
'local-agent': {
|
||||
'model': 'old-model',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(old_config)
|
||||
assert migrated['ai']['runner']['expire-time'] == 3600
|
||||
migrated = ConfigMigration.migrate_pipeline_config(config)
|
||||
|
||||
assert migrated['ai']['runner'] == {
|
||||
'runner': 'local-agent',
|
||||
'expire-time': 3600,
|
||||
}
|
||||
assert migrated['ai']['runner_config'] == {}
|
||||
assert migrated['ai']['local-agent'] == {'model': 'old-model'}
|
||||
|
||||
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."""
|
||||
|
||||
def test_default_config_is_new_format(self):
|
||||
"""Default pipeline template should use the new runner config shape."""
|
||||
def test_default_config_is_current_format(self):
|
||||
from langbot.pkg.utils import paths as path_utils
|
||||
|
||||
template_path = path_utils.get_resource_path('templates/default-pipeline-config.json')
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Should have new format
|
||||
assert 'ai' in config
|
||||
assert 'runner' in config['ai']
|
||||
assert 'id' in config['ai']['runner']
|
||||
assert config['ai']['runner']['id'] == ''
|
||||
|
||||
# Plugin runner selection and config defaults are rendered at creation
|
||||
# time from installed AgentRunner metadata.
|
||||
assert 'runner_config' in config['ai']
|
||||
assert config['ai']['runner_config'] == {}
|
||||
|
||||
# Should NOT have old local-agent key
|
||||
assert 'local-agent' not in config['ai']
|
||||
|
||||
def test_default_config_does_not_hardcode_plugin_schema(self):
|
||||
"""Default template should not duplicate plugin-provided config schema."""
|
||||
from langbot.pkg.utils import paths as path_utils
|
||||
|
||||
template_path = path_utils.get_resource_path('templates/default-pipeline-config.json')
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
class TestResolveRunnerId:
|
||||
"""Tests for current runner id resolution."""
|
||||
|
||||
assert config['ai']['runner_config'] == {}
|
||||
|
||||
|
||||
class TestResolveRunnerIdAliases:
|
||||
"""Tests for runner id alias resolution."""
|
||||
|
||||
def test_resolve_new_format_id(self):
|
||||
"""resolve_runner_id should work with new format."""
|
||||
def test_resolve_current_id(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'id': 'plugin:test/my-runner/default'},
|
||||
@@ -216,45 +95,20 @@ class TestResolveRunnerIdAliases:
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:test/my-runner/default'
|
||||
|
||||
def test_resolve_old_format_runner(self):
|
||||
"""resolve_runner_id should map old format to plugin ID."""
|
||||
def test_old_runner_field_is_ignored(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
},
|
||||
}
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolve_plugin_format_in_runner_field(self):
|
||||
"""resolve_runner_id should handle plugin:* in runner field."""
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'plugin:langbot/local-agent/default'},
|
||||
},
|
||||
}
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:langbot/local-agent/default'
|
||||
|
||||
def test_resolve_new_format_priority(self):
|
||||
"""New format id should take priority over old runner field."""
|
||||
config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:new-runner/default',
|
||||
'runner': 'local-agent', # Old field, should be ignored
|
||||
},
|
||||
},
|
||||
}
|
||||
runner_id = ConfigMigration.resolve_runner_id(config)
|
||||
assert runner_id == 'plugin:new-runner/default'
|
||||
assert runner_id is None
|
||||
|
||||
|
||||
class TestResolveRunnerConfig:
|
||||
"""Tests for runtime runner config resolution."""
|
||||
|
||||
def test_resolve_new_format_config(self):
|
||||
"""resolve_runner_config should read from runner_config."""
|
||||
def test_resolve_current_config(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
@@ -265,8 +119,7 @@ class TestResolveRunnerConfig:
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config['custom-option'] == 20
|
||||
|
||||
def test_resolve_old_format_config(self):
|
||||
"""resolve_runner_config should not read old ai.local-agent at runtime."""
|
||||
def test_old_runner_block_is_ignored(self):
|
||||
config = {
|
||||
'ai': {
|
||||
'local-agent': {'custom-option': 20},
|
||||
@@ -274,26 +127,3 @@ class TestResolveRunnerConfig:
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config == {}
|
||||
|
||||
def test_resolve_legacy_runner_config_for_migration(self):
|
||||
"""resolve_legacy_runner_config should read old ai.local-agent for migration."""
|
||||
config = {
|
||||
'ai': {
|
||||
'local-agent': {'custom-option': 20},
|
||||
},
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_legacy_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config == {'custom-option': 20}
|
||||
|
||||
def test_resolve_new_format_priority(self):
|
||||
"""New format runner_config should take priority."""
|
||||
config = {
|
||||
'ai': {
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {'custom-option': 25},
|
||||
},
|
||||
'local-agent': {'custom-option': 10}, # Old, should be ignored
|
||||
},
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config['custom-option'] == 25
|
||||
|
||||
@@ -1,31 +1,15 @@
|
||||
"""Tests for Pipeline adapter params and prompt packaging."""
|
||||
"""Tests for Query entry adapter params packaging."""
|
||||
from __future__ import annotations
|
||||
|
||||
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
|
||||
|
||||
|
||||
class FakeMessage:
|
||||
"""Fake prompt/history message."""
|
||||
def __init__(self, content='Hello'):
|
||||
self.content = content
|
||||
self.role = 'user'
|
||||
|
||||
def model_dump(self, mode='json'):
|
||||
return {'role': self.role, 'content': self.content}
|
||||
|
||||
|
||||
class FakePrompt:
|
||||
"""Fake prompt container."""
|
||||
def __init__(self, messages=None):
|
||||
self.messages = messages or []
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
|
||||
|
||||
class TestBuildParams:
|
||||
"""Tests for PipelineAdapter.build_params filtering."""
|
||||
"""Tests for QueryEntryAdapter.build_params filtering."""
|
||||
|
||||
def test_params_empty_when_no_variables(self):
|
||||
query = type('Query', (), {'variables': None})()
|
||||
assert PipelineAdapter.build_params(query) == {}
|
||||
assert QueryEntryAdapter.build_params(query) == {}
|
||||
|
||||
def test_params_filters_underscore_prefix(self):
|
||||
query = type('Query', (), {
|
||||
@@ -37,7 +21,7 @@ class TestBuildParams:
|
||||
},
|
||||
})()
|
||||
|
||||
params = PipelineAdapter.build_params(query)
|
||||
params = QueryEntryAdapter.build_params(query)
|
||||
assert '_internal_var' not in params
|
||||
assert '_pipeline_bound_plugins' not in params
|
||||
assert '_monitoring_bot_name' not in params
|
||||
@@ -61,7 +45,7 @@ class TestBuildParams:
|
||||
},
|
||||
})()
|
||||
|
||||
params = PipelineAdapter.build_params(query)
|
||||
params = QueryEntryAdapter.build_params(query)
|
||||
assert 'api_key' not in params
|
||||
assert 'API_KEY' not in params
|
||||
assert 'token' not in params
|
||||
@@ -89,7 +73,7 @@ class TestBuildParams:
|
||||
},
|
||||
})()
|
||||
|
||||
params = PipelineAdapter.build_params(query)
|
||||
params = QueryEntryAdapter.build_params(query)
|
||||
assert params['launcher_type'] == 'telegram'
|
||||
assert params['launcher_id'] == 'group_123'
|
||||
assert params['sender_id'] == 'user_001'
|
||||
@@ -116,7 +100,7 @@ class TestBuildParams:
|
||||
},
|
||||
})()
|
||||
|
||||
params = PipelineAdapter.build_params(query)
|
||||
params = QueryEntryAdapter.build_params(query)
|
||||
assert 'string_value' in params
|
||||
assert 'int_value' in params
|
||||
assert 'float_value' in params
|
||||
@@ -139,41 +123,40 @@ class TestBuildParams:
|
||||
},
|
||||
})()
|
||||
|
||||
params = PipelineAdapter.build_params(query)
|
||||
params = QueryEntryAdapter.build_params(query)
|
||||
assert 'nested_list_with_bad' not in params
|
||||
assert 'nested_dict_with_bad' not in params
|
||||
assert 'good_nested_list' in params
|
||||
assert 'good_nested_dict' in params
|
||||
|
||||
def test_is_json_serializable_primitives_and_collections(self):
|
||||
assert PipelineAdapter.is_json_serializable(None) is True
|
||||
assert PipelineAdapter.is_json_serializable('string') is True
|
||||
assert PipelineAdapter.is_json_serializable(42) is True
|
||||
assert PipelineAdapter.is_json_serializable(['a', 'b']) is True
|
||||
assert PipelineAdapter.is_json_serializable({'key': 'value'}) is True
|
||||
assert PipelineAdapter.is_json_serializable((1, 2, 3)) is True
|
||||
assert QueryEntryAdapter.is_json_serializable(None) is True
|
||||
assert QueryEntryAdapter.is_json_serializable('string') is True
|
||||
assert QueryEntryAdapter.is_json_serializable(42) is True
|
||||
assert QueryEntryAdapter.is_json_serializable(['a', 'b']) is True
|
||||
assert QueryEntryAdapter.is_json_serializable({'key': 'value'}) is True
|
||||
assert QueryEntryAdapter.is_json_serializable((1, 2, 3)) is True
|
||||
|
||||
def test_is_json_serializable_rejects_sets_and_objects(self):
|
||||
class CustomObject:
|
||||
pass
|
||||
|
||||
assert PipelineAdapter.is_json_serializable(CustomObject()) is False
|
||||
assert PipelineAdapter.is_json_serializable({1, 2, 3}) is False
|
||||
assert PipelineAdapter.is_json_serializable([1, {2, 3}]) is False
|
||||
assert PipelineAdapter.is_json_serializable({'key': {1, 2}}) is False
|
||||
assert QueryEntryAdapter.is_json_serializable(CustomObject()) is False
|
||||
assert QueryEntryAdapter.is_json_serializable({1, 2, 3}) is False
|
||||
assert QueryEntryAdapter.is_json_serializable([1, {2, 3}]) is False
|
||||
assert QueryEntryAdapter.is_json_serializable({'key': {1, 2}}) is False
|
||||
|
||||
|
||||
class TestBuildPrompt:
|
||||
"""Tests for PipelineAdapter.build_prompt."""
|
||||
class TestBuildAdapterContext:
|
||||
"""Tests for QueryEntryAdapter.build_adapter_context."""
|
||||
|
||||
def test_prompt_empty_when_missing(self):
|
||||
query = type('Query', (), {})()
|
||||
assert PipelineAdapter.build_prompt(query) == []
|
||||
|
||||
def test_prompt_serializes_messages(self):
|
||||
def test_adapter_context_does_not_push_prompt(self):
|
||||
query = type('Query', (), {
|
||||
'prompt': FakePrompt([FakeMessage('Effective prompt')]),
|
||||
'variables': {},
|
||||
'query_id': 123,
|
||||
'prompt': object(),
|
||||
})()
|
||||
|
||||
prompt = PipelineAdapter.build_prompt(query)
|
||||
assert prompt == [{'role': 'user', 'content': 'Effective prompt'}]
|
||||
context = QueryEntryAdapter.build_adapter_context(query, binding=None)
|
||||
|
||||
assert context == {'params': {}, 'query_id': 123}
|
||||
|
||||
@@ -259,7 +259,7 @@ class TestContextValidation:
|
||||
# Protocol v1 DOES have these
|
||||
assert 'delivery' in context_dict, "delivery is REQUIRED in Protocol v1"
|
||||
assert 'context' in context_dict, "context (ContextAccess) is REQUIRED in Protocol v1"
|
||||
assert 'bootstrap' in context_dict, "bootstrap should exist (can be None)"
|
||||
assert 'bootstrap' not in context_dict, "Host must not inline bootstrap/history windows"
|
||||
assert 'adapter' in context_dict, "adapter should exist"
|
||||
assert 'metadata' in context_dict, "metadata should exist"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Tests for event-first Protocol v1 entities and Pipeline adapter.
|
||||
"""Tests for event-first Protocol v1 entities and Query entry adapter.
|
||||
|
||||
Tests cover:
|
||||
1. Pipeline Query -> AgentEventEnvelope conversion
|
||||
2. Pipeline config -> AgentBinding conversion
|
||||
1. Query -> AgentEventEnvelope conversion
|
||||
2. Current config -> AgentBinding conversion
|
||||
3. AgentRunContext not inlining full history by default
|
||||
4. LangBot Host not defining context-window controls
|
||||
5. Event-first run() entry point
|
||||
@@ -31,32 +31,32 @@ from langbot_plugin.api.entities.builtin.agent_runner.permissions import (
|
||||
)
|
||||
|
||||
# Import LangBot host models
|
||||
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
|
||||
|
||||
class TestPipelineQueryToEventEnvelope:
|
||||
"""Test Pipeline Query -> AgentEventEnvelope conversion."""
|
||||
class TestQueryToEventEnvelope:
|
||||
"""Test Query -> AgentEventEnvelope conversion."""
|
||||
|
||||
def test_query_to_event_basic_fields(self, mock_query):
|
||||
"""Test basic field conversion from Query to Event envelope."""
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.event_type == "message.received"
|
||||
assert event.source == "pipeline_adapter"
|
||||
assert event.source == "host_adapter"
|
||||
assert event.bot_id == mock_query.bot_uuid
|
||||
assert event.actor is not None
|
||||
assert event.actor.actor_type == "user"
|
||||
|
||||
def test_query_to_event_input(self, mock_query):
|
||||
"""Test input conversion from Query."""
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.input is not None
|
||||
assert event.input.text == "Hello world"
|
||||
|
||||
def test_query_to_event_conversation(self, mock_query):
|
||||
"""Test conversation context extraction."""
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.conversation_id == "conv-uuid-123"
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestPipelineQueryToEventEnvelope:
|
||||
mock_query.session.using_conversation.uuid = None
|
||||
mock_query.variables["conversation_id"] = "conv-from-vars"
|
||||
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.conversation_id == "conv-from-vars"
|
||||
|
||||
@@ -73,13 +73,13 @@ class TestPipelineQueryToEventEnvelope:
|
||||
"""Debug Chat and legacy pipeline runs may not have a conversation UUID."""
|
||||
mock_query.session.using_conversation.uuid = None
|
||||
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.conversation_id == "person_launcher-123"
|
||||
|
||||
def test_query_to_event_delivery_context(self, mock_query):
|
||||
"""Test delivery context extraction."""
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.delivery is not None
|
||||
assert event.delivery.surface == "platform"
|
||||
@@ -98,7 +98,7 @@ class TestPipelineQueryToEventEnvelope:
|
||||
})
|
||||
mock_query.message_event = source_event
|
||||
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.source_event_type == "platform.message.created"
|
||||
assert event.event_time == 1700000000
|
||||
@@ -111,28 +111,28 @@ class TestPipelineQueryToEventEnvelope:
|
||||
"""Test delivery context building when Query has no message_chain."""
|
||||
delattr(mock_query, "message_chain")
|
||||
|
||||
event = PipelineAdapter.query_to_event(mock_query)
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert event.delivery.reply_target == {"message_id": None}
|
||||
|
||||
def test_query_to_event_scopes_pipeline_local_event_ids(self, mock_query):
|
||||
"""Pipeline-local message IDs must not become global audit IDs."""
|
||||
first = PipelineAdapter.query_to_event(mock_query)
|
||||
first = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
mock_query.launcher_id = "launcher-456"
|
||||
second = PipelineAdapter.query_to_event(mock_query)
|
||||
second = QueryEntryAdapter.query_to_event(mock_query)
|
||||
|
||||
assert first.event_id.startswith("pipeline:")
|
||||
assert first.event_id.startswith("host:")
|
||||
assert first.event_id != "789"
|
||||
assert second.event_id != first.event_id
|
||||
|
||||
|
||||
class TestPipelineConfigToBinding:
|
||||
"""Test Pipeline config -> AgentBinding conversion."""
|
||||
class TestQueryConfigToBinding:
|
||||
"""Test current config -> AgentBinding conversion."""
|
||||
|
||||
def test_config_to_binding_runner_id(self, mock_query):
|
||||
"""Test binding runner_id extraction."""
|
||||
binding = PipelineAdapter.pipeline_config_to_binding(
|
||||
binding = QueryEntryAdapter.config_to_binding(
|
||||
mock_query, "plugin:author/plugin/runner"
|
||||
)
|
||||
|
||||
@@ -140,7 +140,7 @@ class TestPipelineConfigToBinding:
|
||||
|
||||
def test_config_to_binding_scope(self, mock_query):
|
||||
"""Test binding scope extraction."""
|
||||
binding = PipelineAdapter.pipeline_config_to_binding(
|
||||
binding = QueryEntryAdapter.config_to_binding(
|
||||
mock_query, "plugin:test/plugin/runner"
|
||||
)
|
||||
|
||||
@@ -177,8 +177,8 @@ class TestAgentRunContextProtocolV1:
|
||||
assert ctx.event is not None
|
||||
assert ctx.event.event_type == "message.received"
|
||||
|
||||
def test_sdk_context_messages_default_empty(self):
|
||||
"""Test that messages default to empty (not full history)."""
|
||||
def test_sdk_context_has_no_history_message_fields(self):
|
||||
"""AgentRunContext should not expose inline history message fields."""
|
||||
trigger = AgentTrigger(type="message.received")
|
||||
event = AgentEventContext(
|
||||
event_id="evt_1",
|
||||
@@ -200,34 +200,9 @@ class TestAgentRunContextProtocolV1:
|
||||
runtime=AgentRuntimeContext(),
|
||||
)
|
||||
|
||||
# messages is now in bootstrap, not top-level
|
||||
assert ctx.bootstrap is None or ctx.bootstrap.messages == []
|
||||
|
||||
def test_sdk_context_bootstrap_optional(self):
|
||||
"""Test that bootstrap is optional."""
|
||||
trigger = AgentTrigger(type="message.received")
|
||||
event = AgentEventContext(
|
||||
event_id="evt_1",
|
||||
event_type="message.received",
|
||||
source="platform",
|
||||
)
|
||||
input = AgentInput(text="Hello")
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.resources import AgentResources
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.runtime import AgentRuntimeContext
|
||||
from langbot_plugin.api.entities.builtin.agent_runner.delivery import DeliveryContext
|
||||
|
||||
ctx = AgentRunContext(
|
||||
run_id="run_1",
|
||||
trigger=trigger,
|
||||
event=event,
|
||||
input=input,
|
||||
delivery=DeliveryContext(surface="platform"),
|
||||
resources=AgentResources(),
|
||||
runtime=AgentRuntimeContext(),
|
||||
)
|
||||
|
||||
# bootstrap is optional
|
||||
assert ctx.bootstrap is None or isinstance(ctx.bootstrap.messages, list)
|
||||
assert "messages" not in AgentRunContext.model_fields
|
||||
assert "bootstrap" not in AgentRunContext.model_fields
|
||||
assert not hasattr(ctx, "bootstrap")
|
||||
|
||||
|
||||
class TestHostManagedHistoryNotInProtocol:
|
||||
@@ -306,7 +281,7 @@ class TestSDKResultProtocolV1:
|
||||
# Fixtures
|
||||
@pytest.fixture
|
||||
def mock_query():
|
||||
"""Create a mock Pipeline Query for testing."""
|
||||
"""Create a mock query for testing."""
|
||||
query = Mock()
|
||||
query.query_id = 123
|
||||
query.bot_uuid = "bot-uuid-123"
|
||||
|
||||
@@ -576,11 +576,10 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
|
||||
assert 'kb_custom' in allowed_kbs
|
||||
|
||||
def test_retrieve_kb_fix_old_format(self):
|
||||
"""Fix should work for old format pipeline config."""
|
||||
def test_retrieve_kb_ignores_old_runner_format(self):
|
||||
"""Old runner format is not resolved by current AgentRunner helpers."""
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
|
||||
# Old format: ai.runner.runner = 'local-agent'
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
@@ -590,31 +589,7 @@ class TestRETRIEVEKNOWLEDGEBASEBugFix:
|
||||
}
|
||||
|
||||
runner_id = ConfigMigration.resolve_runner_id(pipeline_config)
|
||||
# Should resolve to plugin:langbot/local-agent/default
|
||||
assert 'local-agent' in runner_id
|
||||
|
||||
def test_retrieve_kb_legacy_single_key_is_migration_only(self):
|
||||
"""Old singular knowledge-base config is normalized before runtime."""
|
||||
from langbot.pkg.agent.runner.config_migration import ConfigMigration
|
||||
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {
|
||||
'id': 'plugin:langbot/local-agent/default',
|
||||
},
|
||||
'runner_config': {
|
||||
'plugin:langbot/local-agent/default': {
|
||||
'knowledge-base': 'kb_single', # Old singular field
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
migrated = ConfigMigration.migrate_pipeline_config(pipeline_config)
|
||||
runner_id = ConfigMigration.resolve_runner_id(migrated)
|
||||
runner_config = ConfigMigration.resolve_runner_config(migrated, runner_id)
|
||||
|
||||
assert runner_config == {'knowledge-bases': ['kb_single']}
|
||||
assert runner_id is None
|
||||
|
||||
|
||||
class TestHandlerActionAuthorization:
|
||||
@@ -850,7 +825,7 @@ class TestSDKAgentRunAPIProxyFieldConsistency:
|
||||
"""CALL_TOOL: SDK includes 'run_id' field."""
|
||||
# SDK agent_run_api.py line 144: "run_id": self.run_id
|
||||
# Host handler.py line 458: run_id = data.get('run_id')
|
||||
sdk_fields = ['run_id', 'tool_name', 'parameters', 'session', 'query_id']
|
||||
sdk_fields = ['run_id', 'tool_name', 'parameters']
|
||||
host_expected_fields = ['tool_name', 'parameters', 'run_id']
|
||||
|
||||
for field in host_expected_fields:
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.errors import RunnerExecutionError
|
||||
from langbot.pkg.agent.runner.orchestrator import AgentRunOrchestrator
|
||||
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
from langbot.pkg.agent.runner.session_registry import get_session_registry
|
||||
from langbot.pkg.agent.runner.persistent_state_store import reset_persistent_state_store
|
||||
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
|
||||
@@ -239,7 +239,7 @@ def test_context_builder_includes_consumable_base64_attachments():
|
||||
[platform_message.Image(base64="data:image/jpeg;base64,aGVsbG8=")]
|
||||
)
|
||||
|
||||
input_data = PipelineAdapter._build_input(query)
|
||||
input_data = QueryEntryAdapter._build_input(query)
|
||||
|
||||
assert input_data.contents[0].text == "see attached"
|
||||
assert input_data.contents[1].image_base64 == "data:image/png;base64,aGVsbG8="
|
||||
@@ -362,8 +362,8 @@ async def test_orchestrator_does_not_package_query_messages_into_context(clean_a
|
||||
assert len(messages) == 1
|
||||
context = plugin_connector.contexts[0]
|
||||
assert context["config"]["custom-option"] == 2
|
||||
assert context["bootstrap"] is None
|
||||
assert set(context["adapter"]) == {"query_id", "extra"}
|
||||
assert "bootstrap" not in context
|
||||
assert set(context["adapter"]) == {"extra"}
|
||||
assert "context_packaging" not in context["runtime"]["metadata"]
|
||||
assert [message.content for message in query.messages] == [
|
||||
"message 1",
|
||||
@@ -473,12 +473,12 @@ async def test_orchestrator_enforces_total_runner_deadline(clean_agent_state):
|
||||
assert await get_session_registry().list_active_runs() == []
|
||||
|
||||
|
||||
class TestPipelineCompatibilityQueryIdInSession:
|
||||
"""Tests for query_id entering session registry."""
|
||||
class TestQueryEntrySessionQueryId:
|
||||
"""Tests for internal query_id entering session registry."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_id_registered_in_session_for_pipeline_flow(self, clean_agent_state):
|
||||
"""query_id from Pipeline flow is registered in session."""
|
||||
async def test_query_id_registered_in_session_for_query_entry_flow(self, clean_agent_state):
|
||||
"""query_id from Query entry flow is registered internally in session."""
|
||||
db_engine = clean_agent_state
|
||||
descriptor = make_descriptor()
|
||||
plugin_connector = FakePluginConnector(
|
||||
@@ -557,12 +557,12 @@ class TestPipelineCompatibilityQueryIdInSession:
|
||||
assert session_during_run["query_id"] is None
|
||||
|
||||
|
||||
class TestPipelineAdapterPromptAndParams:
|
||||
"""Tests for prompt and params handling in Pipeline adapter."""
|
||||
class TestQueryEntryAdapterParams:
|
||||
"""Tests for params handling in Query entry adapter."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prompt_in_adapter_extra(self, clean_agent_state):
|
||||
"""Pipeline prompt is placed in adapter.extra.prompt."""
|
||||
async def test_prompt_not_pushed_into_adapter_extra(self, clean_agent_state):
|
||||
"""Pipeline prompt is not pushed into adapter.extra."""
|
||||
from langbot_plugin.api.entities.builtin.provider import prompt as provider_prompt
|
||||
|
||||
db_engine = clean_agent_state
|
||||
@@ -590,12 +590,8 @@ class TestPipelineAdapterPromptAndParams:
|
||||
_messages = [message async for message in orchestrator.run_from_query(query)]
|
||||
|
||||
context = plugin_connector.contexts[0]
|
||||
# Prompt should be in adapter.extra
|
||||
assert "prompt" in context["adapter"]["extra"]
|
||||
assert len(context["adapter"]["extra"]["prompt"]) == 1
|
||||
assert context["adapter"]["extra"]["prompt"][0]["role"] == "system"
|
||||
# Top-level should NOT have prompt
|
||||
assert "prompt" not in context
|
||||
assert "prompt" not in context["adapter"]["extra"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_params_filtering_keeps_public_param(self, clean_agent_state):
|
||||
@@ -721,8 +717,8 @@ class TestPipelineAdapterPromptAndParams:
|
||||
assert "a_lambda" not in params
|
||||
|
||||
|
||||
class TestPipelineAdapterHostCapabilities:
|
||||
"""Tests for event-first host capabilities via Pipeline adapter path."""
|
||||
class TestQueryEntryAdapterHostCapabilities:
|
||||
"""Tests for event-first host capabilities via Query entry adapter path."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_state_updated_writes_to_persistent_store(self, clean_agent_state):
|
||||
@@ -760,9 +756,9 @@ class TestPipelineAdapterHostCapabilities:
|
||||
persistent_store = get_persistent_state_store(db_engine)
|
||||
# Build snapshot to check if state was written
|
||||
# Note: We need to rebuild the event and binding to query the store
|
||||
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
|
||||
event = PipelineAdapter.query_to_event(query)
|
||||
binding = PipelineAdapter.pipeline_config_to_binding(query, RUNNER_ID)
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
event = QueryEntryAdapter.query_to_event(query)
|
||||
binding = QueryEntryAdapter.config_to_binding(query, RUNNER_ID)
|
||||
|
||||
snapshot = await persistent_store.build_snapshot_from_event(event, binding, descriptor)
|
||||
assert snapshot["conversation"]["external.test_key"] == "test_value"
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
import pytest
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.pipeline_adapter import PipelineAdapter
|
||||
from langbot.pkg.agent.runner.query_entry_adapter import QueryEntryAdapter
|
||||
from langbot.pkg.agent.runner.resource_builder import AgentResourceBuilder
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def make_query(
|
||||
|
||||
|
||||
async def build_resources(app, query, descriptor):
|
||||
binding = PipelineAdapter.pipeline_config_to_binding(query, descriptor.id)
|
||||
binding = QueryEntryAdapter.config_to_binding(query, descriptor.id)
|
||||
return await AgentResourceBuilder(app).build_resources_from_binding(
|
||||
event=Mock(),
|
||||
binding=binding,
|
||||
|
||||
Reference in New Issue
Block a user