mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-11 08:16:03 +00:00
feat: make agent runner config schema driven
This commit is contained in:
@@ -132,7 +132,7 @@ class TestResolveRunnerConfig:
|
||||
assert config == {'model': 'uuid-123', 'max_round': 10}
|
||||
|
||||
def test_resolve_old_format_config(self):
|
||||
"""Resolve runner config from old format."""
|
||||
"""Runtime config resolver should not read old format."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
@@ -146,6 +146,23 @@ class TestResolveRunnerConfig:
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
)
|
||||
assert config == {}
|
||||
|
||||
def test_resolve_legacy_config_for_migration(self):
|
||||
"""Migration helper should read old format."""
|
||||
pipeline_config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
'model': 'uuid-123',
|
||||
'max_round': 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
config = ConfigMigration.resolve_legacy_runner_config(
|
||||
pipeline_config,
|
||||
'plugin:langbot/local-agent/default',
|
||||
)
|
||||
assert config == {'model': 'uuid-123', 'max_round': 10}
|
||||
|
||||
def test_resolve_no_config(self):
|
||||
@@ -228,4 +245,4 @@ class TestGetOldRunnerName:
|
||||
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
|
||||
assert old_name is None
|
||||
|
||||
@@ -229,8 +229,8 @@ class TestResolveRunnerIdBackwardCompat:
|
||||
assert runner_id == 'plugin:new-runner/default'
|
||||
|
||||
|
||||
class TestResolveRunnerConfigBackwardCompat:
|
||||
"""Tests for backward compatibility in resolve_runner_config."""
|
||||
class TestResolveRunnerConfig:
|
||||
"""Tests for runtime runner config resolution."""
|
||||
|
||||
def test_resolve_new_format_config(self):
|
||||
"""resolve_runner_config should read from runner_config."""
|
||||
@@ -245,13 +245,23 @@ class TestResolveRunnerConfigBackwardCompat:
|
||||
assert runner_config['max-round'] == 20
|
||||
|
||||
def test_resolve_old_format_config(self):
|
||||
"""resolve_runner_config should read from old ai.local-agent."""
|
||||
"""resolve_runner_config should not read old ai.local-agent at runtime."""
|
||||
config = {
|
||||
'ai': {
|
||||
'local-agent': {'max-round': 15},
|
||||
},
|
||||
}
|
||||
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': {'max-round': 15},
|
||||
},
|
||||
}
|
||||
runner_config = ConfigMigration.resolve_legacy_runner_config(config, 'plugin:langbot/local-agent/default')
|
||||
assert runner_config['max-round'] == 15
|
||||
|
||||
def test_resolve_new_format_priority(self):
|
||||
|
||||
@@ -16,8 +16,9 @@ import pytest
|
||||
import types
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from langbot.pkg.agent.runner.descriptor import AgentRunnerDescriptor
|
||||
from langbot.pkg.agent.runner.session_registry import AgentRunSessionRegistry
|
||||
from langbot.pkg.plugin.handler import _build_tool_detail
|
||||
from langbot.pkg.plugin.handler import _build_tool_detail, _get_pipeline_knowledge_base_uuids
|
||||
|
||||
# Import shared test fixtures from conftest.py
|
||||
from .conftest import make_resources
|
||||
@@ -105,11 +106,53 @@ class MockApplication:
|
||||
self.persistence_mgr.execute_async = AsyncMock(return_value=MagicMock(first=lambda: None))
|
||||
|
||||
|
||||
class FakeAgentRunnerRegistry:
|
||||
async def get(self, runner_id, bound_plugins=None):
|
||||
return AgentRunnerDescriptor(
|
||||
id=runner_id,
|
||||
source='plugin',
|
||||
label={'en_US': 'Test Runner'},
|
||||
plugin_author='test',
|
||||
plugin_name='runner',
|
||||
runner_name='default',
|
||||
config_schema=[
|
||||
{'name': 'knowledge-bases', 'type': 'knowledge-base-multi-selector', 'default': []},
|
||||
],
|
||||
capabilities={'knowledge_retrieval': True},
|
||||
permissions={'knowledge_bases': ['list', 'retrieve']},
|
||||
)
|
||||
|
||||
|
||||
class MockConnection:
|
||||
"""Mock connection for testing."""
|
||||
pass
|
||||
|
||||
|
||||
class TestPipelineKnowledgeBaseScope:
|
||||
"""Tests for schema-driven pipeline KB scope resolution."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_preprocessed_query_scope(self):
|
||||
app = MockApplication()
|
||||
query = MockQuery()
|
||||
query.variables = {'_knowledge_base_uuids': ['kb_var', '__none__', 'kb_var']}
|
||||
|
||||
kb_uuids = await _get_pipeline_knowledge_base_uuids(app, query)
|
||||
|
||||
assert kb_uuids == ['kb_var']
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uses_runner_schema_when_query_scope_not_preprocessed(self):
|
||||
app = MockApplication()
|
||||
app.agent_runner_registry = FakeAgentRunnerRegistry()
|
||||
query = MockQuery()
|
||||
query.variables = {}
|
||||
|
||||
kb_uuids = await _get_pipeline_knowledge_base_uuids(app, query)
|
||||
|
||||
assert kb_uuids == ['kb_001', 'kb_002']
|
||||
|
||||
|
||||
class MockDisconnectCallback:
|
||||
"""Mock disconnect callback for testing."""
|
||||
async def __call__(self):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Integration-style tests for AgentRunOrchestrator with a fake plugin runner."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import types
|
||||
from unittest.mock import AsyncMock
|
||||
@@ -61,9 +62,10 @@ class FakeKnowledgeBase:
|
||||
class FakePluginConnector:
|
||||
is_enable_plugin = True
|
||||
|
||||
def __init__(self, results=None, error: Exception | None = None):
|
||||
def __init__(self, results=None, error: Exception | None = None, delay: float = 0):
|
||||
self.results = results or []
|
||||
self.error = error
|
||||
self.delay = delay
|
||||
self.calls: list[dict] = []
|
||||
self.contexts: list[dict] = []
|
||||
self.sessions_during_run: list[dict | None] = []
|
||||
@@ -83,6 +85,8 @@ class FakePluginConnector:
|
||||
raise self.error
|
||||
|
||||
for result in self.results:
|
||||
if self.delay:
|
||||
await asyncio.sleep(self.delay)
|
||||
yield result
|
||||
|
||||
|
||||
@@ -125,7 +129,11 @@ def make_descriptor() -> AgentRunnerDescriptor:
|
||||
plugin_name="local-agent",
|
||||
runner_name="default",
|
||||
protocol_version="1",
|
||||
capabilities={"streaming": True, "tool_calling": True},
|
||||
capabilities={"streaming": True, "tool_calling": True, "knowledge_retrieval": True},
|
||||
config_schema=[
|
||||
{"name": "model", "type": "model-fallback-selector"},
|
||||
{"name": "knowledge-bases", "type": "knowledge-base-multi-selector", "default": []},
|
||||
],
|
||||
permissions={
|
||||
"models": ["invoke", "stream"],
|
||||
"tools": ["list", "detail", "call"],
|
||||
@@ -367,3 +375,27 @@ async def test_orchestrator_unregisters_session_after_runner_failure():
|
||||
context = plugin_connector.contexts[0]
|
||||
assert plugin_connector.sessions_during_run[0] is not None
|
||||
assert await get_session_registry().get(context["run_id"]) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_orchestrator_enforces_total_runner_deadline():
|
||||
descriptor = make_descriptor()
|
||||
plugin_connector = FakePluginConnector(
|
||||
results=[
|
||||
{
|
||||
"type": "message.completed",
|
||||
"data": {"message": {"role": "assistant", "content": "too late"}},
|
||||
}
|
||||
],
|
||||
delay=0.05,
|
||||
)
|
||||
orchestrator = AgentRunOrchestrator(FakeApplication(plugin_connector), FakeRegistry(descriptor))
|
||||
query = make_query()
|
||||
query.pipeline_config["ai"]["runner_config"][RUNNER_ID]["timeout"] = 0.01
|
||||
|
||||
with pytest.raises(RunnerExecutionError) as exc_info:
|
||||
[message async for message in orchestrator.run_from_query(query)]
|
||||
|
||||
assert exc_info.value.retryable is True
|
||||
assert "runner.timeout" in str(exc_info.value)
|
||||
assert await get_session_registry().get(plugin_connector.contexts[0]["run_id"]) is None
|
||||
|
||||
Reference in New Issue
Block a user