mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-26 18:35:38 +08:00
fix: bot routing setup and busy-session response checks (#2728)
Apply group response rules before busy-session steering. Initialize usable private-message and group-mention routes for new bots, including Wizard-created pipelines, and preserve member-configured rules across saves and retries. Fix empty pipeline configuration, refresh the processor sidebar after automatic creation, and show creation feedback. Align routing regressions and use Node 24 for the browser CI job. Correct the task's erroneous agent attribution while preserving every code tree and the original human contribution. Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
f8e32ef14f
commit
d13ae56ea5
@@ -778,7 +778,6 @@ class TestAgentServiceCreateUpdateDelete:
|
||||
'name': 'Pipeline Agent',
|
||||
'description': 'Legacy pipeline',
|
||||
'emoji': 'P',
|
||||
'config': {},
|
||||
},
|
||||
)
|
||||
app.pipeline_service.update_pipeline.assert_awaited_once_with(
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from langbot.pkg.api.http.service.bot import BotService
|
||||
|
||||
|
||||
async def test_concurrent_first_bots_share_one_default_pipeline():
|
||||
pipelines = []
|
||||
|
||||
async def create(context, data, default=False):
|
||||
await asyncio.sleep(0)
|
||||
assert default
|
||||
pipelines.append({'uuid': 'p', 'is_default': True})
|
||||
return 'p'
|
||||
|
||||
ap = SimpleNamespace(
|
||||
pipeline_service=SimpleNamespace(
|
||||
get_pipelines=AsyncMock(side_effect=lambda context: list(pipelines)),
|
||||
create_pipeline=AsyncMock(side_effect=create),
|
||||
)
|
||||
)
|
||||
service = BotService(ap)
|
||||
first, second = await asyncio.gather(
|
||||
service._build_default_event_bindings('workspace'),
|
||||
service._build_default_event_bindings('workspace'),
|
||||
)
|
||||
ap.pipeline_service.create_pipeline.assert_awaited_once()
|
||||
for bindings in (first, second):
|
||||
assert len(bindings) == 2
|
||||
assert all(binding['target_uuid'] == 'p' for binding in bindings)
|
||||
assert bindings[0]['filters'] == [{'field': 'chat_type', 'operator': 'eq', 'value': 'person'}]
|
||||
assert bindings[1]['filters'][-1] == {'field': 'is_at_bot', 'operator': 'eq', 'value': True}
|
||||
|
||||
|
||||
async def test_existing_default_pipeline_preferred_without_creating_another():
|
||||
ap = SimpleNamespace(
|
||||
pipeline_service=SimpleNamespace(
|
||||
get_pipelines=AsyncMock(
|
||||
return_value=[
|
||||
{'uuid': 'recent'},
|
||||
{'uuid': 'default', 'is_default': True},
|
||||
]
|
||||
),
|
||||
create_pipeline=AsyncMock(),
|
||||
)
|
||||
)
|
||||
routes = await BotService(ap)._build_default_event_bindings('workspace')
|
||||
assert all(route['target_uuid'] == 'default' for route in routes)
|
||||
ap.pipeline_service.create_pipeline.assert_not_awaited()
|
||||
|
||||
|
||||
async def test_explicit_manual_route_does_not_create_or_replace_defaults():
|
||||
ap = SimpleNamespace(
|
||||
instance_config=SimpleNamespace(data={}),
|
||||
persistence_mgr=SimpleNamespace(execute_async=AsyncMock()),
|
||||
pipeline_service=SimpleNamespace(get_pipelines=AsyncMock(), create_pipeline=AsyncMock()),
|
||||
)
|
||||
service = BotService(ap)
|
||||
service.get_bot = AsyncMock(return_value={'enable': False})
|
||||
route = {
|
||||
'id': 'manual-route',
|
||||
'event_pattern': 'message.received',
|
||||
'target_type': 'discard',
|
||||
'target_uuid': '',
|
||||
'filters': [{'field': 'chat_id', 'operator': 'eq', 'value': 'manual-group'}],
|
||||
'priority': 10,
|
||||
'enabled': False,
|
||||
'description': 'Keep this manual rule',
|
||||
'order': 0,
|
||||
}
|
||||
await service.create_bot(
|
||||
'workspace',
|
||||
{
|
||||
'name': 'Manual',
|
||||
'adapter': 'onebot-v11',
|
||||
'adapter_config': {},
|
||||
'event_bindings': [route],
|
||||
},
|
||||
)
|
||||
inserted = ap.persistence_mgr.execute_async.await_args_list[0].args[0].compile().params
|
||||
assert inserted['event_bindings'] == [route]
|
||||
ap.pipeline_service.get_pipelines.assert_not_awaited()
|
||||
ap.pipeline_service.create_pipeline.assert_not_awaited()
|
||||
@@ -364,6 +364,7 @@ class TestBotServiceCreateBot:
|
||||
return_value={'uuid': 'uuid-1', 'name': 'Bot 1', 'adapter': 'telegram'}
|
||||
)
|
||||
|
||||
ap.pipeline_service = SimpleNamespace(get_pipelines=AsyncMock(return_value=[{'uuid': 'default-pipeline'}]))
|
||||
service = BotService(ap)
|
||||
|
||||
# Execute & Verify
|
||||
@@ -409,6 +410,7 @@ class TestBotServiceCreateBot:
|
||||
return_value={'uuid': 'new-uuid', 'name': 'New Bot', 'adapter': 'telegram', 'enable': False}
|
||||
)
|
||||
|
||||
ap.pipeline_service = SimpleNamespace(get_pipelines=AsyncMock(return_value=[{'uuid': 'default-pipeline'}]))
|
||||
service = BotService(ap)
|
||||
|
||||
# Execute
|
||||
@@ -429,10 +431,12 @@ class TestBotServiceCreateBot:
|
||||
'name': 'New Bot',
|
||||
'adapter': 'telegram',
|
||||
'adapter_config': {},
|
||||
'event_bindings': [],
|
||||
'plugin_processors': [],
|
||||
}
|
||||
assert {key: values.get(key) for key in expected} == expected
|
||||
assert len(values['event_bindings']) == 2
|
||||
assert all(b['target_uuid'] == 'default-pipeline' for b in values['event_bindings'])
|
||||
assert values['event_bindings'][1]['filters'][-1] == {'field': 'is_at_bot', 'operator': 'eq', 'value': True}
|
||||
ap.platform_mgr.load_bot.assert_not_awaited()
|
||||
|
||||
async def test_create_bot_scopes_insert_without_legacy_pipeline_fields(self):
|
||||
@@ -467,6 +471,7 @@ class TestBotServiceCreateBot:
|
||||
}
|
||||
)
|
||||
|
||||
ap.pipeline_service = SimpleNamespace(get_pipelines=AsyncMock(return_value=[{'uuid': 'default-pipeline'}]))
|
||||
service = BotService(ap)
|
||||
|
||||
# Execute
|
||||
@@ -509,6 +514,7 @@ class TestBotServiceCreateBot:
|
||||
)
|
||||
ap.persistence_mgr = PersistenceManager(ap)
|
||||
ap.persistence_mgr.db = SimpleNamespace(get_engine=lambda: engine)
|
||||
ap.pipeline_service = SimpleNamespace(get_pipelines=AsyncMock(return_value=[{'uuid': 'default-pipeline'}]))
|
||||
service = BotService(ap)
|
||||
try:
|
||||
async with engine.begin() as connection:
|
||||
|
||||
@@ -249,7 +249,8 @@ class TestPipelineServiceCreatePipeline:
|
||||
with pytest.raises(ValueError, match='Maximum number of pipelines'):
|
||||
await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline'})
|
||||
|
||||
async def test_create_pipeline_no_limit(self):
|
||||
@pytest.mark.parametrize('config_fields', [{}, {'config': {}}])
|
||||
async def test_create_pipeline_no_limit(self, config_fields):
|
||||
"""Creates pipeline without limit when max_pipelines=-1."""
|
||||
# Setup
|
||||
ap = SimpleNamespace()
|
||||
@@ -276,7 +277,7 @@ class TestPipelineServiceCreatePipeline:
|
||||
with patch(
|
||||
'langbot.pkg.utils.paths.get_resource_path', return_value='templates/default-pipeline-config.json'
|
||||
):
|
||||
bot_uuid = await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline'})
|
||||
bot_uuid = await service.create_pipeline(WORKSPACE_UUID, {'name': 'New Pipeline', **config_fields})
|
||||
|
||||
# Verify
|
||||
assert bot_uuid is not None
|
||||
|
||||
@@ -81,6 +81,39 @@ async def test_try_claim_steering_sets_pipeline_context_before_claiming():
|
||||
app.agent_run_orchestrator.try_claim_steering_from_query.assert_awaited_once_with(query)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('text,expected', [('ordinary chat', False), ('qa: hello', True)])
|
||||
@pytest.mark.parametrize('claim_result', [True, False])
|
||||
async def test_busy_group_steering_obeys_response_rules(sample_query, text, expected, claim_result):
|
||||
from langbot_plugin.api.entities.builtin.provider.session import LauncherTypes, Session
|
||||
from langbot_plugin.api.entities.builtin.platform.message import MessageChain, Plain
|
||||
|
||||
app = make_app()
|
||||
app.sess_mgr.get_session.return_value = Session(
|
||||
instance_uuid='test-instance',
|
||||
workspace_uuid='test-workspace',
|
||||
placement_generation=1,
|
||||
launcher_type=LauncherTypes.GROUP,
|
||||
launcher_id=sample_query.launcher_id,
|
||||
)
|
||||
pipeline = make_pipeline()
|
||||
pipeline.pipeline_entity.config['trigger'] = {
|
||||
'group-respond-rules': {'at': True, 'prefix': ['qa:'], 'regexp': [], 'random': 0}
|
||||
}
|
||||
app.pipeline_mgr.get_pipeline_by_uuid.return_value = pipeline
|
||||
app.agent_run_orchestrator.try_claim_steering_from_query.return_value = claim_result
|
||||
sample_query.launcher_type = LauncherTypes.GROUP
|
||||
sample_query.message_chain = MessageChain([Plain(text=text)])
|
||||
claimed = await Controller(app)._try_claim_steering_before_session_slot(sample_query)
|
||||
assert claimed is (expected and claim_result), app.logger.warning.call_args
|
||||
assert str(sample_query.message_chain) == text
|
||||
if expected:
|
||||
forwarded = app.agent_run_orchestrator.try_claim_steering_from_query.await_args.args[0]
|
||||
assert str(forwarded.message_chain) == ' hello'
|
||||
else:
|
||||
app.agent_run_orchestrator.try_claim_steering_from_query.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_consumer_transfers_query_from_queue_to_running_task():
|
||||
app = make_app()
|
||||
|
||||
@@ -65,6 +65,20 @@ async def test_group_message_no_match(mock_app, sample_query):
|
||||
mock_rule.match.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_random_response_draw_is_reused_across_queue_rechecks(sample_query, mock_app, monkeypatch):
|
||||
from langbot.pkg.pipeline.resprule.rules.random import RandomRespRule
|
||||
from unittest.mock import Mock
|
||||
|
||||
draw = Mock(side_effect=[0.9, 0.1])
|
||||
monkeypatch.setattr('langbot.pkg.pipeline.resprule.rules.random.random.random', draw)
|
||||
matcher = RandomRespRule(mock_app)
|
||||
for _ in range(3):
|
||||
result = await matcher.match('hello', sample_query.message_chain, {'random': 0.5}, sample_query)
|
||||
assert result.matching is False
|
||||
draw.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_message_match(mock_app, sample_query):
|
||||
"""Test group message with matching rule"""
|
||||
|
||||
Reference in New Issue
Block a user