mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-25 05:46:13 +00:00
feat(agent-runner): route pipeline runs through event-first flow
- run_from_query() now delegates to run(event, binding) instead of maintaining a separate legacy execution path - Pipeline Query is converted to AgentEventEnvelope via PipelineCompatAdapter - Pipeline config is converted to AgentBinding with StatePolicy - bound_plugins authorization preserved from Pipeline - Legacy compatibility fields preserved: - query_id → context.runtime.query_id → session registry - prompt → context.compatibility.extra.prompt (not top-level) - params → context.compatibility.extra.params (with proper filtering) - max-round → bootstrap.messages and compatibility.legacy_messages - Pipeline path gains event-first host capabilities: - EventLog and Transcript writing - ArtifactStore registration - PersistentStateStore for state.updated - Removed legacy handlers: - _handle_artifact_created_query() (replaced by _handle_artifact_created) - _handle_state_updated() (replaced by _handle_state_updated_event) This change unifies the execution path while preserving backward compatibility for Pipeline-based runners. EventGateway is not implemented in this branch; only the event-first entry point is reserved.
This commit is contained in:
@@ -467,208 +467,6 @@ class TestArtifactRefsLifecycle:
|
||||
assert merged[0]['artifact_id'] == 'artifact-1'
|
||||
|
||||
|
||||
class TestArtifactCreatedQueryFlow:
|
||||
"""Test artifact.created handling in legacy Query-based flow."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_app(self):
|
||||
"""Create mock application."""
|
||||
ap = MagicMock(spec=app.Application)
|
||||
ap.logger = MagicMock()
|
||||
ap.plugin_connector = MagicMock()
|
||||
ap.plugin_connector.is_enable_plugin = True
|
||||
ap.persistence_mgr = MagicMock()
|
||||
ap.persistence_mgr.get_db_engine = MagicMock()
|
||||
return ap
|
||||
|
||||
@pytest.fixture
|
||||
def mock_registry(self):
|
||||
"""Create mock registry."""
|
||||
registry = MagicMock()
|
||||
registry.get = AsyncMock()
|
||||
return registry
|
||||
|
||||
@pytest.fixture
|
||||
def mock_query(self):
|
||||
"""Create mock Query."""
|
||||
from langbot_plugin.api.entities.builtin.pipeline import query as pipeline_query
|
||||
query = MagicMock(spec=pipeline_query.Query)
|
||||
query.query_id = str(uuid.uuid4())
|
||||
query.pipeline_config = {'runner': {'id': 'test-runner'}}
|
||||
query.variables = {}
|
||||
return query
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_flow_run_id_mismatch_raises_protocol_error(
|
||||
self, mock_app, mock_registry, mock_query
|
||||
):
|
||||
"""Test run_id mismatch in Query flow raises RunnerProtocolError."""
|
||||
orchestrator = AgentRunOrchestrator(mock_app, mock_registry)
|
||||
run_id = str(uuid.uuid4())
|
||||
wrong_run_id = str(uuid.uuid4())
|
||||
|
||||
result_dict = {
|
||||
'type': 'artifact.created',
|
||||
'run_id': wrong_run_id,
|
||||
'data': {'artifact_type': 'image'},
|
||||
}
|
||||
|
||||
mock_descriptor = MagicMock()
|
||||
mock_descriptor.id = 'test-runner'
|
||||
|
||||
with pytest.raises(RunnerProtocolError) as exc_info:
|
||||
await orchestrator._handle_artifact_created_query(
|
||||
result_dict=result_dict,
|
||||
query=mock_query,
|
||||
descriptor=mock_descriptor,
|
||||
run_id=run_id,
|
||||
conversation_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
assert 'run_id mismatch' in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_flow_invalid_base64_raises_protocol_error(
|
||||
self, mock_app, mock_registry, mock_query
|
||||
):
|
||||
"""Test invalid base64 in Query flow raises RunnerProtocolError."""
|
||||
orchestrator = AgentRunOrchestrator(mock_app, mock_registry)
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
result_dict = {
|
||||
'type': 'artifact.created',
|
||||
'run_id': run_id,
|
||||
'data': {
|
||||
'artifact_type': 'image',
|
||||
'content_base64': '!!!invalid!!!',
|
||||
},
|
||||
}
|
||||
|
||||
mock_descriptor = MagicMock()
|
||||
mock_descriptor.id = 'test-runner'
|
||||
|
||||
with pytest.raises(RunnerProtocolError) as exc_info:
|
||||
await orchestrator._handle_artifact_created_query(
|
||||
result_dict=result_dict,
|
||||
query=mock_query,
|
||||
descriptor=mock_descriptor,
|
||||
run_id=run_id,
|
||||
conversation_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
assert 'invalid base64' in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_flow_missing_artifact_type_raises_protocol_error(
|
||||
self, mock_app, mock_registry, mock_query
|
||||
):
|
||||
"""Test missing artifact_type in Query flow raises RunnerProtocolError."""
|
||||
orchestrator = AgentRunOrchestrator(mock_app, mock_registry)
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
result_dict = {
|
||||
'type': 'artifact.created',
|
||||
'run_id': run_id,
|
||||
'data': {}, # missing artifact_type
|
||||
}
|
||||
|
||||
mock_descriptor = MagicMock()
|
||||
mock_descriptor.id = 'test-runner'
|
||||
|
||||
with pytest.raises(RunnerProtocolError) as exc_info:
|
||||
await orchestrator._handle_artifact_created_query(
|
||||
result_dict=result_dict,
|
||||
query=mock_query,
|
||||
descriptor=mock_descriptor,
|
||||
run_id=run_id,
|
||||
conversation_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
assert 'missing required field' in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_flow_oversized_content_raises_protocol_error(
|
||||
self, mock_app, mock_registry, mock_query
|
||||
):
|
||||
"""Test oversized content in Query flow raises RunnerProtocolError."""
|
||||
orchestrator = AgentRunOrchestrator(mock_app, mock_registry)
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
oversized_content = b'x' * (MAX_ARTIFACT_INLINE_BYTES + 1)
|
||||
content_base64 = base64.b64encode(oversized_content).decode('utf-8')
|
||||
|
||||
result_dict = {
|
||||
'type': 'artifact.created',
|
||||
'run_id': run_id,
|
||||
'data': {
|
||||
'artifact_type': 'image',
|
||||
'content_base64': content_base64,
|
||||
},
|
||||
}
|
||||
|
||||
mock_descriptor = MagicMock()
|
||||
mock_descriptor.id = 'test-runner'
|
||||
|
||||
with pytest.raises(RunnerProtocolError) as exc_info:
|
||||
await orchestrator._handle_artifact_created_query(
|
||||
result_dict=result_dict,
|
||||
query=mock_query,
|
||||
descriptor=mock_descriptor,
|
||||
run_id=run_id,
|
||||
conversation_id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
assert 'exceeds limit' in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_flow_register_success(
|
||||
self, mock_app, mock_registry, mock_query
|
||||
):
|
||||
"""Test successful artifact registration in Query flow."""
|
||||
orchestrator = AgentRunOrchestrator(mock_app, mock_registry)
|
||||
run_id = str(uuid.uuid4())
|
||||
conversation_id = str(uuid.uuid4())
|
||||
artifact_id = str(uuid.uuid4())
|
||||
|
||||
content = b'test content'
|
||||
content_base64 = base64.b64encode(content).decode('utf-8')
|
||||
|
||||
result_dict = {
|
||||
'type': 'artifact.created',
|
||||
'run_id': run_id,
|
||||
'data': {
|
||||
'artifact_id': artifact_id,
|
||||
'artifact_type': 'voice',
|
||||
'mime_type': 'audio/mp3',
|
||||
'content_base64': content_base64,
|
||||
},
|
||||
}
|
||||
|
||||
mock_descriptor = MagicMock()
|
||||
mock_descriptor.id = 'test-runner'
|
||||
|
||||
with patch('langbot.pkg.agent.runner.artifact_store.ArtifactStore') as MockArtifactStore:
|
||||
mock_artifact_store = MagicMock()
|
||||
mock_artifact_store.register_artifact = AsyncMock(return_value=artifact_id)
|
||||
MockArtifactStore.return_value = mock_artifact_store
|
||||
|
||||
await orchestrator._handle_artifact_created_query(
|
||||
result_dict=result_dict,
|
||||
query=mock_query,
|
||||
descriptor=mock_descriptor,
|
||||
run_id=run_id,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
# Verify artifact was registered
|
||||
mock_artifact_store.register_artifact.assert_called_once()
|
||||
call_kwargs = mock_artifact_store.register_artifact.call_args.kwargs
|
||||
assert call_kwargs['artifact_id'] == artifact_id
|
||||
assert call_kwargs['artifact_type'] == 'voice'
|
||||
assert call_kwargs['content'] == content
|
||||
assert call_kwargs['conversation_id'] == conversation_id
|
||||
|
||||
|
||||
class TestResultNormalizerArtifactCreated:
|
||||
"""Test ResultNormalizer handling of artifact.created."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user