forked from HAL9000/cleveragents-core
1202 lines
45 KiB
Python
1202 lines
45 KiB
Python
"""Step definitions for comprehensive composite agent coverage tests."""
|
|
|
|
import asyncio
|
|
import tempfile
|
|
import threading
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.agents.base import Agent
|
|
from cleveragents.agents.composite import CompositeAgent
|
|
from cleveragents.core.exceptions import ConfigurationError, ExecutionError
|
|
from cleveragents.langgraph.bridge import RxPyLangGraphBridge
|
|
from cleveragents.reactive.stream_router import ReactiveStreamRouter
|
|
from cleveragents.templates.renderer import TemplateRenderer
|
|
|
|
|
|
@given("I have a composite agent test environment")
|
|
def step_composite_agent_test_environment(context):
|
|
"""Set up test environment for composite agent testing."""
|
|
# Initialize test data on context
|
|
context.test_data = {
|
|
"temp_dir": tempfile.mkdtemp(),
|
|
"mock_template_renderer": MagicMock(spec=TemplateRenderer),
|
|
"mock_stream_router": MagicMock(spec=ReactiveStreamRouter),
|
|
"mock_langgraph_bridge": MagicMock(spec=RxPyLangGraphBridge),
|
|
"mock_agents": {},
|
|
"mock_graphs": {},
|
|
"mock_streams": {},
|
|
"test_results": {},
|
|
"raised_exception": None,
|
|
"config": {},
|
|
"composite_agent": None,
|
|
}
|
|
|
|
# Initialize direct context attributes for convenience
|
|
context.mock_agents = {}
|
|
context.mock_graphs = {}
|
|
context.mock_streams = {}
|
|
|
|
# Set up async event loop for testing
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
except RuntimeError:
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
context.test_data["loop"] = loop
|
|
|
|
|
|
@given("I have a basic composite agent configuration")
|
|
def step_basic_composite_agent_config(context):
|
|
"""Create basic composite agent configuration."""
|
|
context.test_data["config"] = {"components": {}, "routing": {}, "expose_params": {}}
|
|
|
|
|
|
@given("I have a legacy strategy-based configuration")
|
|
def step_legacy_strategy_config(context):
|
|
"""Create legacy strategy-based configuration."""
|
|
context.test_data["config"] = {
|
|
"strategy": "parallel",
|
|
"agents": ["agent1", "agent2"],
|
|
}
|
|
|
|
|
|
@given("I have a composite agent")
|
|
def step_have_composite_agent(context):
|
|
"""Create a basic composite agent."""
|
|
config = {"components": {}, "routing": {}, "expose_params": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
|
|
@given("I have mock child agents")
|
|
def step_mock_child_agents(context):
|
|
"""Create mock child agents."""
|
|
for i in range(3):
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.name = f"child_agent_{i}"
|
|
mock_agent.process = AsyncMock(return_value=f"Response from agent {i}")
|
|
mock_agent.get_capabilities = MagicMock(return_value=[f"capability_{i}"])
|
|
context.mock_agents[f"child_agent_{i}"] = mock_agent
|
|
|
|
|
|
@given("I have mock LangGraph instances")
|
|
def step_mock_langgraph_instances(context):
|
|
"""Create mock LangGraph instances."""
|
|
for i in range(2):
|
|
mock_graph = MagicMock()
|
|
mock_graph.execute = AsyncMock(return_value=f"Graph result {i}")
|
|
context.mock_graphs[f"graph_{i}"] = mock_graph
|
|
|
|
|
|
@given("I have mock stream configurations")
|
|
def step_mock_stream_configurations(context):
|
|
"""Create mock stream configurations."""
|
|
for i in range(2):
|
|
mock_stream = {
|
|
"type": "cold",
|
|
"operators": [{"type": "map", "params": {"function": f"stream_func_{i}"}}],
|
|
}
|
|
context.mock_streams[f"stream_{i}"] = mock_stream
|
|
|
|
|
|
@when("I create a composite agent")
|
|
def step_create_composite_agent(context):
|
|
"""Create a composite agent with the configured settings."""
|
|
try:
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=context.test_data["config"],
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
context.creation_successful = True
|
|
except Exception as e:
|
|
context.raised_exception = e
|
|
context.creation_successful = False
|
|
|
|
|
|
@when("I attempt to create a composite agent with legacy config")
|
|
def step_attempt_create_with_legacy_config(context):
|
|
"""Attempt to create composite agent with legacy configuration."""
|
|
try:
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=context.test_data["config"],
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
context.creation_successful = True
|
|
except Exception as e:
|
|
context.raised_exception = e
|
|
context.creation_successful = False
|
|
|
|
|
|
@when("I add agents to the composite agent")
|
|
def step_add_agents_to_composite(context):
|
|
"""Add mock agents to the composite agent."""
|
|
for name, agent in context.mock_agents.items():
|
|
context.composite_agent.add_agent(name, agent)
|
|
|
|
|
|
@when("I add graphs to the composite agent")
|
|
def step_add_graphs_to_composite(context):
|
|
"""Add mock graphs to the composite agent."""
|
|
for name, graph in context.mock_graphs.items():
|
|
context.composite_agent.add_graph(name, graph)
|
|
|
|
|
|
@when("I add streams to the composite agent")
|
|
def step_add_streams_to_composite(context):
|
|
"""Add mock streams to the composite agent."""
|
|
for name, stream in context.mock_streams.items():
|
|
context.composite_agent.add_stream(name, stream)
|
|
|
|
|
|
@when("I set various parameters on the agent")
|
|
def step_set_parameters_on_agent(context):
|
|
"""Set various parameters on the composite agent."""
|
|
context.test_params = {
|
|
"temperature": 0.7,
|
|
"max_tokens": 1000,
|
|
"custom_param": "test_value",
|
|
}
|
|
for param, value in context.test_params.items():
|
|
context.composite_agent.set_param(param, value)
|
|
|
|
|
|
@when("I call process_message on the composite agent")
|
|
def step_call_process_message(context):
|
|
"""Call process_message method on composite agent."""
|
|
# Call process_message using the child agent that was already added
|
|
context.process_message_result = context.test_data["loop"].run_until_complete(
|
|
context.composite_agent.process_message("test message", {"context": "test"})
|
|
)
|
|
|
|
|
|
@when("I process a message through the composite agent")
|
|
def step_process_message_through_composite(context):
|
|
"""Process a message through the composite agent."""
|
|
try:
|
|
context.process_result = context.test_data["loop"].run_until_complete(
|
|
context.composite_agent.process("test message", {"context": "test"})
|
|
)
|
|
context.process_successful = True
|
|
except Exception as e:
|
|
context.raised_exception = e
|
|
context.process_successful = False
|
|
|
|
|
|
@when("I get the capabilities of the composite agent")
|
|
def step_get_capabilities(context):
|
|
"""Get capabilities from the composite agent."""
|
|
context.capabilities = context.composite_agent.get_capabilities()
|
|
|
|
|
|
@then("the composite agent should be initialized correctly")
|
|
def step_composite_agent_initialized_correctly(context):
|
|
"""Verify composite agent is initialized correctly."""
|
|
assert context.creation_successful
|
|
assert context.composite_agent.name == "test_composite"
|
|
assert hasattr(context.composite_agent, "components")
|
|
assert hasattr(context.composite_agent, "routing")
|
|
assert hasattr(context.composite_agent, "expose_params")
|
|
|
|
|
|
@then("the agent should have empty component collections")
|
|
def step_agent_has_empty_collections(context):
|
|
"""Verify agent has empty component collections."""
|
|
assert len(context.composite_agent.agents) == 0
|
|
assert len(context.composite_agent.graphs) == 0
|
|
assert len(context.composite_agent.streams) == 0
|
|
|
|
|
|
@then("the agent should have default routing configuration")
|
|
def step_agent_has_default_routing(context):
|
|
"""Verify agent has default routing configuration."""
|
|
assert context.composite_agent.routing == {}
|
|
assert context.composite_agent.expose_params == {}
|
|
|
|
|
|
@then("a ConfigurationError should be raised for composite agent")
|
|
def step_configuration_error_raised_composite(context):
|
|
"""Verify a ConfigurationError was raised."""
|
|
assert (
|
|
not context.creation_successful if hasattr(context, "creation_successful") else not context.process_successful
|
|
)
|
|
assert isinstance(context.raised_exception, ConfigurationError)
|
|
|
|
|
|
@then("an ExecutionError should be raised for composite agent")
|
|
def step_execution_error_raised_composite(context):
|
|
"""Verify an ExecutionError was raised."""
|
|
assert not context.process_successful
|
|
assert isinstance(context.raised_exception, ExecutionError)
|
|
|
|
|
|
@then("the error should mention deprecated strategy-based configuration")
|
|
def step_error_mentions_deprecated_strategy(context):
|
|
"""Verify error mentions deprecated strategy configuration."""
|
|
assert "deprecated strategy-based configuration" in str(context.raised_exception)
|
|
|
|
|
|
@then("the agents should be stored in the agents collection")
|
|
def step_agents_stored_in_collection(context):
|
|
"""Verify agents are stored in the agents collection."""
|
|
for name in context.mock_agents.keys():
|
|
assert name in context.composite_agent.agents
|
|
assert context.composite_agent.agents[name] == context.mock_agents[name]
|
|
|
|
|
|
@then("the agents should be added to components configuration")
|
|
def step_agents_added_to_components(context):
|
|
"""Verify agents are added to components configuration."""
|
|
assert "agents" in context.composite_agent.components
|
|
for name in context.mock_agents.keys():
|
|
assert name in context.composite_agent.components["agents"]
|
|
|
|
|
|
@then("the agents collection should contain the correct agents")
|
|
def step_agents_collection_correct(context):
|
|
"""Verify agents collection contains correct agents."""
|
|
assert len(context.composite_agent.agents) == len(context.mock_agents)
|
|
for name, agent in context.mock_agents.items():
|
|
assert context.composite_agent.agents[name] == agent
|
|
|
|
|
|
@then("the graphs should be stored in the graphs collection")
|
|
def step_graphs_stored_in_collection(context):
|
|
"""Verify graphs are stored in the graphs collection."""
|
|
for name in context.mock_graphs.keys():
|
|
assert name in context.composite_agent.graphs
|
|
assert context.composite_agent.graphs[name] == context.mock_graphs[name]
|
|
|
|
|
|
@then("the graphs should be added to components configuration")
|
|
def step_graphs_added_to_components(context):
|
|
"""Verify graphs are added to components configuration."""
|
|
assert "graphs" in context.composite_agent.components
|
|
for name in context.mock_graphs.keys():
|
|
assert name in context.composite_agent.components["graphs"]
|
|
|
|
|
|
@then("the graphs collection should contain the correct graphs")
|
|
def step_graphs_collection_correct(context):
|
|
"""Verify graphs collection contains correct graphs."""
|
|
assert len(context.composite_agent.graphs) == len(context.mock_graphs)
|
|
for name, graph in context.mock_graphs.items():
|
|
assert context.composite_agent.graphs[name] == graph
|
|
|
|
|
|
@then("the streams should be stored in the streams collection")
|
|
def step_streams_stored_in_collection(context):
|
|
"""Verify streams are stored in the streams collection."""
|
|
for name in context.mock_streams.keys():
|
|
assert name in context.composite_agent.streams
|
|
assert context.composite_agent.streams[name] == context.mock_streams[name]
|
|
|
|
|
|
@then("the streams should be added to components configuration")
|
|
def step_streams_added_to_components(context):
|
|
"""Verify streams are added to components configuration."""
|
|
assert "streams" in context.composite_agent.components
|
|
for name in context.mock_streams.keys():
|
|
assert name in context.composite_agent.components["streams"]
|
|
|
|
|
|
@then("the streams collection should contain the correct streams")
|
|
def step_streams_collection_correct(context):
|
|
"""Verify streams collection contains correct streams."""
|
|
assert len(context.composite_agent.streams) == len(context.mock_streams)
|
|
for name, stream in context.mock_streams.items():
|
|
assert context.composite_agent.streams[name] == stream
|
|
|
|
|
|
@then("the parameters should be stored in expose_params")
|
|
def step_parameters_stored_in_expose_params(context):
|
|
"""Verify parameters are stored in expose_params."""
|
|
for param, value in context.test_params.items():
|
|
assert context.composite_agent.expose_params[param] == value
|
|
|
|
|
|
@then("the parameters should be available for propagation")
|
|
def step_parameters_available_for_propagation(context):
|
|
"""Verify parameters are available for propagation."""
|
|
# Parameters should be accessible through expose_params
|
|
assert len(context.composite_agent.expose_params) == len(context.test_params)
|
|
|
|
|
|
@then("it should delegate to the process method")
|
|
def step_should_delegate_to_process(context):
|
|
"""Verify process_message delegates to process method."""
|
|
assert context.process_message_result == "Child agent response"
|
|
|
|
|
|
@then("return the same result as process")
|
|
def step_return_same_result_as_process(context):
|
|
"""Verify same result is returned."""
|
|
# The result should match what the mock agent returns
|
|
assert "response" in context.process_message_result.lower()
|
|
|
|
|
|
# Additional step definitions for specific routing scenarios
|
|
|
|
|
|
@given("I have a composite agent with agents but no routing")
|
|
def step_composite_with_agents_no_routing(context):
|
|
"""Create composite agent with agents but no routing."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
# Add a mock agent
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.process = AsyncMock(return_value="Agent response")
|
|
context.composite_agent.add_agent("first_agent", mock_agent)
|
|
|
|
|
|
@given("I have a composite agent with graphs but no routing")
|
|
def step_composite_with_graphs_no_routing(context):
|
|
"""Create composite agent with graphs but no routing."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Add a mock graph
|
|
mock_graph = MagicMock()
|
|
mock_graph.execute = AsyncMock(return_value="Graph response")
|
|
context.composite_agent.add_graph("first_graph", mock_graph)
|
|
|
|
|
|
@given("I have a composite agent with streams but no routing")
|
|
def step_composite_with_streams_no_routing(context):
|
|
"""Create composite agent with streams but no routing."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Add a mock stream with simplified behavior
|
|
mock_stream = {"type": "cold"}
|
|
context.composite_agent.add_stream("first_stream", mock_stream)
|
|
|
|
# Mock the stream router with proper observable behavior
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
context.test_data["mock_stream_router"].observables = {"__output__": mock_observable}
|
|
|
|
# Mock send_message to trigger the observer callback
|
|
def mock_send_message(stream_name, message, context_data):
|
|
# Simulate message processing and callback
|
|
def delayed_callback():
|
|
# Find the observer callback from the subscribe call
|
|
if mock_observable.subscribe.called:
|
|
observer = mock_observable.subscribe.call_args[0][0]
|
|
if hasattr(observer, "on_next"):
|
|
observer.on_next("Stream processed immediately")
|
|
|
|
# Call callback with slight delay to simulate async processing
|
|
timer = threading.Timer(0.01, delayed_callback)
|
|
timer.start()
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
|
|
@given("I have a composite agent with no components")
|
|
def step_composite_with_no_components(context):
|
|
"""Create composite agent with no components."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
|
|
@given("I have a composite agent with explicit agent routing")
|
|
def step_composite_with_agent_routing(context):
|
|
"""Create composite agent with explicit agent routing."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {
|
|
"input": {"type": "agent", "name": "target_agent"},
|
|
"output": {"name": "output_stream"},
|
|
},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
# Add the target agent
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.process = AsyncMock(return_value="Routed agent response")
|
|
context.composite_agent.add_agent("target_agent", mock_agent)
|
|
|
|
|
|
@given("I have a composite agent with explicit graph routing")
|
|
def step_composite_with_graph_routing(context):
|
|
"""Create composite agent with explicit graph routing."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "target_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Add the target graph
|
|
mock_graph = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.messages = [{"content": "Graph routed response"}]
|
|
mock_graph.execute = AsyncMock(return_value=mock_result)
|
|
context.composite_agent.add_graph("target_graph", mock_graph)
|
|
|
|
|
|
@given("I have a composite agent with explicit stream routing")
|
|
def step_composite_with_stream_routing(context):
|
|
"""Create composite agent with explicit stream routing."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {
|
|
"input": {"type": "stream", "name": "target_stream"},
|
|
"output": {"name": "custom_output"},
|
|
},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Mock the stream router behavior
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
context.test_data["mock_stream_router"].observables = {
|
|
"custom_output": mock_observable,
|
|
"__output__": mock_observable,
|
|
}
|
|
|
|
# Mock send_message to trigger the observer callback
|
|
def mock_send_message(stream_name, message, context_data):
|
|
# Simulate message processing and callback
|
|
def delayed_callback():
|
|
# Find the observer callback from the subscribe call
|
|
if mock_observable.subscribe.called:
|
|
observer = mock_observable.subscribe.call_args[0][0]
|
|
if hasattr(observer, "on_next"):
|
|
observer.on_next("Stream routing response")
|
|
|
|
# Call callback with slight delay to simulate async processing
|
|
timer = threading.Timer(0.01, delayed_callback)
|
|
timer.start()
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
# Add the target stream
|
|
context.composite_agent.add_stream("target_stream", {"type": "cold"})
|
|
|
|
|
|
@given("I have a composite agent with unknown routing type")
|
|
def step_composite_with_unknown_routing(context):
|
|
"""Create composite agent with unknown routing type."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "unknown_type", "name": "target"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
|
|
@given("I have a composite agent with agent routing to non-existent agent")
|
|
def step_composite_with_missing_agent_routing(context):
|
|
"""Create composite agent with routing to non-existent agent."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "agent", "name": "missing_agent"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
|
|
@given("I have a composite agent with graph routing but no bridge")
|
|
def step_composite_with_graph_no_bridge(context):
|
|
"""Create composite agent with graph routing but no bridge."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "some_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=None,
|
|
)
|
|
|
|
|
|
@given("I have a composite agent with graph routing to non-existent graph")
|
|
def step_composite_with_missing_graph_routing(context):
|
|
"""Create composite agent with routing to non-existent graph."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "missing_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Mock bridge to return None for missing graph
|
|
context.test_data["mock_langgraph_bridge"].get_graph.return_value = None
|
|
|
|
|
|
@given("I have a composite agent with LangGraph bridge")
|
|
def step_composite_with_langgraph_bridge(context):
|
|
"""Create composite agent with LangGraph bridge."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "bridge_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
|
|
@given("the bridge has a graph not in local collection")
|
|
def step_bridge_has_external_graph(context):
|
|
"""Set up bridge to have graph not in local collection."""
|
|
mock_graph = MagicMock()
|
|
mock_graph.execute = AsyncMock(return_value="Bridge graph response")
|
|
context.test_data["mock_langgraph_bridge"].get_graph.return_value = mock_graph
|
|
|
|
|
|
@given("I have a composite agent with LangGraph that returns messages")
|
|
def step_composite_with_message_returning_graph(context):
|
|
"""Create composite agent with graph that returns messages."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "message_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Create mock graph with messages result
|
|
mock_graph = MagicMock()
|
|
mock_result = MagicMock()
|
|
mock_result.messages = [
|
|
{"content": "First message"},
|
|
{"content": "Last message content"},
|
|
]
|
|
mock_graph.execute = AsyncMock(return_value=mock_result)
|
|
context.composite_agent.add_graph("message_graph", mock_graph)
|
|
|
|
|
|
@given("I have a composite agent with LangGraph that returns non-message result")
|
|
def step_composite_with_non_message_graph(context):
|
|
"""Create composite agent with graph that returns non-message result."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "graph", "name": "non_message_graph"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Create mock graph with non-message result
|
|
mock_graph = MagicMock()
|
|
mock_graph.execute = AsyncMock(return_value={"result": "graph_output"})
|
|
context.composite_agent.add_graph("non_message_graph", mock_graph)
|
|
|
|
|
|
@given("I have a composite agent with stream routing but no router")
|
|
def step_composite_with_stream_no_router(context):
|
|
"""Create composite agent with stream routing but no router."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "stream", "name": "some_stream"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=None,
|
|
)
|
|
|
|
|
|
@given("I have a composite agent with stream routing and custom output")
|
|
def step_composite_with_custom_stream_output(context):
|
|
"""Create composite agent with custom stream output."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {
|
|
"input": {"type": "stream", "name": "input_stream"},
|
|
"output": {"name": "custom_output_stream"},
|
|
},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Set up mock stream router with custom output
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
|
|
# Mock the custom output stream
|
|
context.test_data["mock_stream_router"].observables = {
|
|
"custom_output_stream": mock_observable,
|
|
"__output__": mock_observable,
|
|
}
|
|
|
|
# Mock send_message to trigger the observer callback
|
|
def mock_send_message(stream_name, message, context_data):
|
|
# Simulate message processing and callback
|
|
def delayed_callback():
|
|
# Find the observer callback from the subscribe call
|
|
if mock_observable.subscribe.called:
|
|
observer = mock_observable.subscribe.call_args[0][0]
|
|
if hasattr(observer, "on_next"):
|
|
observer.on_next("Custom stream response")
|
|
|
|
# Call callback with slight delay to simulate async processing
|
|
timer = threading.Timer(0.01, delayed_callback)
|
|
timer.start()
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
|
|
@given("I have a composite agent with stream routing and default output")
|
|
def step_composite_with_default_stream_output(context):
|
|
"""Create composite agent with default stream output."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "stream", "name": "input_stream"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Set up mock stream router with default output
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
|
|
context.test_data["mock_stream_router"].observables = {"__output__": mock_observable}
|
|
|
|
# Mock send_message to trigger the observer callback
|
|
def mock_send_message(stream_name, message, context_data):
|
|
def delayed_callback():
|
|
if mock_observable.subscribe.called:
|
|
observer = mock_observable.subscribe.call_args[0][0]
|
|
if hasattr(observer, "on_next"):
|
|
observer.on_next("Default stream response")
|
|
|
|
timer = threading.Timer(0.01, delayed_callback)
|
|
timer.start()
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
|
|
@given("I have a composite agent with stream that returns message objects")
|
|
def step_composite_with_message_object_stream(context):
|
|
"""Create composite agent with stream that returns message objects."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "stream", "name": "message_stream"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Set up mock stream router
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
|
|
context.test_data["mock_stream_router"].observables = {"__output__": mock_observable}
|
|
|
|
# Mock send_message to return message object
|
|
def mock_send_message(stream_name, message, context_data):
|
|
def delayed_callback():
|
|
if mock_observable.subscribe.called:
|
|
observer = mock_observable.subscribe.call_args[0][0]
|
|
if hasattr(observer, "on_next"):
|
|
# Create mock message object with content attribute
|
|
mock_message = MagicMock()
|
|
mock_message.content = "Message object content"
|
|
observer.on_next(mock_message)
|
|
|
|
timer = threading.Timer(0.01, delayed_callback)
|
|
timer.start()
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
|
|
@given("I have a composite agent with slow stream processing")
|
|
def step_composite_with_slow_stream(context):
|
|
"""Create composite agent with slow stream processing."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {"input": {"type": "stream", "name": "slow_stream"}},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
)
|
|
|
|
# Set up mock stream router that processes slowly
|
|
mock_observable = MagicMock()
|
|
mock_subscription = MagicMock()
|
|
mock_observable.subscribe.return_value = mock_subscription
|
|
|
|
context.test_data["mock_stream_router"].observables = {"__output__": mock_observable}
|
|
|
|
# Mock send_message that takes longer than timeout
|
|
def mock_send_message(stream_name, message, context_data):
|
|
# Don't call the callback, simulating timeout
|
|
pass
|
|
|
|
context.test_data["mock_stream_router"].send_message = mock_send_message
|
|
|
|
|
|
@given("I have a composite agent with agents, graphs, and streams")
|
|
def step_composite_with_all_components(context):
|
|
"""Create composite agent with all component types."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
stream_router=context.test_data["mock_stream_router"],
|
|
langgraph_bridge=context.test_data["mock_langgraph_bridge"],
|
|
)
|
|
|
|
# Add agents with capabilities
|
|
for i in range(2):
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.get_capabilities.return_value = [
|
|
f"agent_capability_{i}",
|
|
"shared_capability",
|
|
]
|
|
context.composite_agent.add_agent(f"agent_{i}", mock_agent)
|
|
|
|
# Add graphs
|
|
context.composite_agent.add_graph("graph_1", MagicMock())
|
|
|
|
# Add streams
|
|
context.composite_agent.add_stream("stream_1", {"type": "cold"})
|
|
|
|
|
|
@given("I have a composite agent with legacy strategy attribute")
|
|
def step_composite_with_legacy_strategy(context):
|
|
"""Create composite agent with legacy strategy attribute."""
|
|
config = {"components": {}, "routing": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
# Manually add legacy strategy attribute
|
|
context.composite_agent.strategy = "parallel"
|
|
|
|
|
|
@given("I have a composite agent with a mock child agent")
|
|
def step_composite_with_mock_child_agent(context):
|
|
"""Create composite agent with a single mock child agent."""
|
|
config = {"components": {}, "routing": {}, "expose_params": {}}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
# Add a single mock child agent
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.process = AsyncMock(return_value="Child agent response")
|
|
mock_agent.get_capabilities = MagicMock(return_value=["child_capability"])
|
|
context.composite_agent.add_agent("child_agent", mock_agent)
|
|
|
|
|
|
@given("I have a composite agent with exposed parameters")
|
|
def step_composite_with_exposed_parameters(context):
|
|
"""Create composite agent with exposed parameters."""
|
|
config = {
|
|
"components": {},
|
|
"routing": {},
|
|
"expose_params": {"temperature": 0.5, "model": "gpt-4"},
|
|
}
|
|
context.composite_agent = CompositeAgent(
|
|
name="test_composite",
|
|
config=config,
|
|
template_renderer=context.test_data["mock_template_renderer"],
|
|
)
|
|
|
|
# Add a mock agent for processing
|
|
mock_agent = MagicMock(spec=Agent)
|
|
mock_agent.process = AsyncMock(return_value="Processed with context")
|
|
context.composite_agent.add_agent("test_agent", mock_agent)
|
|
|
|
|
|
@when("I process a message via graph routing")
|
|
def step_process_via_graph_routing(context):
|
|
"""Process message via graph routing."""
|
|
try:
|
|
context.process_result = asyncio.run(context.composite_agent.process("test message"))
|
|
context.process_successful = True
|
|
except Exception as e:
|
|
context.raised_exception = e
|
|
context.process_successful = False
|
|
|
|
|
|
@when("I process a message with additional context")
|
|
def step_process_with_additional_context(context):
|
|
"""Process message with additional context."""
|
|
additional_context = {
|
|
"user_id": "123",
|
|
"temperature": 0.9, # This should be overridden by exposed params
|
|
"new_param": "additional_value",
|
|
}
|
|
|
|
# Capture the context that gets passed to the agent
|
|
def capture_context(message, ctx):
|
|
context.captured_context = ctx
|
|
future = asyncio.Future()
|
|
future.set_result("Processed with captured context")
|
|
return future
|
|
|
|
context.composite_agent.agents["test_agent"].process.side_effect = capture_context
|
|
|
|
try:
|
|
context.test_data["loop"].run_until_complete(
|
|
context.composite_agent.process("test message", additional_context)
|
|
)
|
|
except Exception:
|
|
pass # We're just capturing the context
|
|
|
|
|
|
# Then step definitions for validation
|
|
|
|
|
|
@then("it should use the first available agent")
|
|
def step_should_use_first_agent(context):
|
|
"""Verify it uses the first available agent."""
|
|
assert context.process_successful
|
|
assert "response" in context.process_result.lower()
|
|
|
|
|
|
@then("return the processed message")
|
|
def step_return_processed_message(context):
|
|
"""Verify processed message is returned."""
|
|
assert context.process_successful
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("it should use the first available graph")
|
|
def step_should_use_first_graph(context):
|
|
"""Verify it uses the first available graph."""
|
|
assert context.process_successful
|
|
assert "response" in context.process_result.lower()
|
|
|
|
|
|
@then("return the processed message from graph")
|
|
def step_return_processed_from_graph(context):
|
|
"""Verify processed message from graph is returned."""
|
|
assert context.process_successful
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("it should use the first available stream")
|
|
def step_should_use_first_stream(context):
|
|
"""Verify it uses the first available stream."""
|
|
assert context.process_successful
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("return the processed message from stream")
|
|
def step_return_processed_from_stream(context):
|
|
"""Verify processed message from stream is returned."""
|
|
assert context.process_successful
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("the error should mention no components to process with")
|
|
def step_error_mentions_no_components(context):
|
|
"""Verify error mentions no components."""
|
|
assert "no components to process with" in str(context.raised_exception)
|
|
|
|
|
|
@then("it should route to the specified agent")
|
|
def step_should_route_to_specified_agent(context):
|
|
"""Verify routing to specified agent."""
|
|
assert context.process_successful
|
|
assert "routed agent response" in context.process_result.lower()
|
|
|
|
|
|
@then("return the agent's processed response")
|
|
def step_return_agent_processed_response(context):
|
|
"""Verify agent's processed response is returned."""
|
|
assert "routed" in context.process_result.lower()
|
|
|
|
|
|
@then("it should route to the specified graph")
|
|
def step_should_route_to_specified_graph(context):
|
|
"""Verify routing to specified graph."""
|
|
assert context.process_successful
|
|
|
|
|
|
@then("return the graph's processed response")
|
|
def step_return_graph_processed_response(context):
|
|
"""Verify graph's processed response is returned."""
|
|
assert "graph routed response" in context.process_result.lower()
|
|
|
|
|
|
@then("it should route to the specified stream")
|
|
def step_should_route_to_specified_stream(context):
|
|
"""Verify routing to specified stream."""
|
|
assert context.process_successful
|
|
|
|
|
|
@then("return the stream's processed response")
|
|
def step_return_stream_processed_response(context):
|
|
"""Verify stream's processed response is returned."""
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("the error should mention unknown input type")
|
|
def step_error_mentions_unknown_input_type(context):
|
|
"""Verify error mentions unknown input type."""
|
|
assert "unknown input type" in str(context.raised_exception).lower()
|
|
|
|
|
|
@then("the error should mention agent not found")
|
|
def step_error_mentions_agent_not_found(context):
|
|
"""Verify error mentions agent not found."""
|
|
assert "agent" in str(context.raised_exception).lower()
|
|
assert "not found" in str(context.raised_exception).lower()
|
|
|
|
|
|
@then("the error should mention LangGraph bridge not available")
|
|
def step_error_mentions_bridge_not_available(context):
|
|
"""Verify error mentions LangGraph bridge not available."""
|
|
assert "langgraph bridge not available" in str(context.raised_exception).lower()
|
|
|
|
|
|
@then("the error should mention graph not found")
|
|
def step_error_mentions_graph_not_found(context):
|
|
"""Verify error mentions graph not found."""
|
|
assert "graph" in str(context.raised_exception).lower()
|
|
assert "not found" in str(context.raised_exception).lower()
|
|
|
|
|
|
@then("it should retrieve the graph from the bridge")
|
|
def step_should_retrieve_from_bridge(context):
|
|
"""Verify graph is retrieved from bridge."""
|
|
assert context.process_successful
|
|
context.test_data["mock_langgraph_bridge"].get_graph.assert_called_once()
|
|
|
|
|
|
@then("execute the graph successfully")
|
|
def step_execute_graph_successfully(context):
|
|
"""Verify graph executes successfully."""
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("it should extract content from the last message")
|
|
def step_should_extract_last_message_content(context):
|
|
"""Verify content is extracted from last message."""
|
|
assert context.process_successful
|
|
assert "last message content" in context.process_result.lower()
|
|
|
|
|
|
@then("return the message content")
|
|
def step_return_message_content(context):
|
|
"""Verify message content is returned."""
|
|
assert "content" in context.process_result.lower()
|
|
|
|
|
|
@then("it should convert the result to string")
|
|
def step_should_convert_to_string(context):
|
|
"""Verify result is converted to string."""
|
|
assert context.process_successful
|
|
assert isinstance(context.process_result, str)
|
|
|
|
|
|
@then("return the string representation")
|
|
def step_return_string_representation(context):
|
|
"""Verify string representation is returned."""
|
|
assert context.process_result is not None
|
|
|
|
|
|
@then("the error should mention stream router not available")
|
|
def step_error_mentions_stream_router_not_available(context):
|
|
"""Verify error mentions stream router not available."""
|
|
assert "stream router not available" in str(context.raised_exception).lower()
|
|
|
|
|
|
@then("it should subscribe to the custom output stream")
|
|
def step_should_subscribe_to_custom_output(context):
|
|
"""Verify subscription to custom output stream."""
|
|
assert context.process_successful
|
|
# Verify the mock observable was used
|
|
context.test_data["mock_stream_router"].observables["custom_output_stream"].subscribe.assert_called_once()
|
|
|
|
|
|
@then("return the processed message from custom stream")
|
|
def step_return_from_custom_stream(context):
|
|
"""Verify message from custom stream is returned."""
|
|
assert "custom stream response" in context.process_result.lower()
|
|
|
|
|
|
@then("it should subscribe to the default __output__ stream")
|
|
def step_should_subscribe_to_default_output(context):
|
|
"""Verify subscription to default output stream."""
|
|
assert context.process_successful
|
|
context.test_data["mock_stream_router"].observables["__output__"].subscribe.assert_called_once()
|
|
|
|
|
|
@then("it should extract content from message object")
|
|
def step_should_extract_from_message_object(context):
|
|
"""Verify content is extracted from message object."""
|
|
assert context.process_successful
|
|
assert "message object content" in context.process_result.lower()
|
|
|
|
|
|
@then("it should handle timeout appropriately")
|
|
def step_should_handle_timeout(context):
|
|
"""Verify timeout is handled appropriately."""
|
|
# The process should fail due to timeout
|
|
assert not context.process_successful
|
|
assert "timeout" in str(context.raised_exception).lower() or isinstance(
|
|
context.raised_exception, asyncio.TimeoutError
|
|
)
|
|
|
|
|
|
@then("dispose of the subscription properly")
|
|
def step_should_dispose_subscription(context):
|
|
"""Verify subscription is disposed properly."""
|
|
# Even on timeout, subscription should be disposed
|
|
# This is handled in the finally block
|
|
assert True # If we get here, the finally block executed
|
|
|
|
|
|
@then("it should include composite capability")
|
|
def step_should_include_composite_capability(context):
|
|
"""Verify composite capability is included."""
|
|
assert "composite" in context.capabilities
|
|
|
|
|
|
@then("it should include capabilities from all child agents")
|
|
def step_should_include_child_capabilities(context):
|
|
"""Verify capabilities from child agents are included."""
|
|
# Should include capabilities from both agents
|
|
assert "agent_capability_0" in context.capabilities
|
|
assert "agent_capability_1" in context.capabilities
|
|
|
|
|
|
@then("it should include stateful-workflow for graphs")
|
|
def step_should_include_stateful_workflow(context):
|
|
"""Verify stateful-workflow capability for graphs."""
|
|
assert "stateful-workflow" in context.capabilities
|
|
|
|
|
|
@then("it should include reactive-processing for streams")
|
|
def step_should_include_reactive_processing(context):
|
|
"""Verify reactive-processing capability for streams."""
|
|
assert "reactive-processing" in context.capabilities
|
|
|
|
|
|
@then("it should remove duplicate capabilities")
|
|
def step_should_remove_duplicate_capabilities(context):
|
|
"""Verify duplicate capabilities are removed."""
|
|
# shared_capability appears in both agents, should only appear once
|
|
capability_count = context.capabilities.count("shared_capability")
|
|
assert capability_count == 1
|
|
|
|
|
|
@then("it should include the legacy strategy capability")
|
|
def step_should_include_legacy_strategy(context):
|
|
"""Verify legacy strategy capability is included."""
|
|
assert "parallel" in context.capabilities
|
|
|
|
|
|
@then("the context should be merged with exposed parameters")
|
|
def step_context_merged_with_exposed_params(context):
|
|
"""Verify context is merged with exposed parameters."""
|
|
# Both original context and exposed params should be present
|
|
assert hasattr(context, "captured_context")
|
|
captured = context.captured_context
|
|
assert "user_id" in captured # From additional context
|
|
assert "model" in captured # From exposed params
|
|
|
|
|
|
@then("exposed parameters should take precedence")
|
|
def step_exposed_params_take_precedence(context):
|
|
"""Verify exposed parameters take precedence."""
|
|
captured = context.captured_context
|
|
# temperature from exposed_params (0.5) should override additional context (0.9)
|
|
assert captured["temperature"] == 0.5
|
|
|
|
|
|
@then("the merged context should be used for processing")
|
|
def step_merged_context_used_for_processing(context):
|
|
"""Verify merged context is used for processing."""
|
|
captured = context.captured_context
|
|
# Should have all parameters
|
|
assert "user_id" in captured # From additional context
|
|
assert "temperature" in captured # From exposed params (overridden)
|
|
assert "model" in captured # From exposed params
|
|
assert "new_param" in captured # From additional context
|