forked from HAL9000/cleveragents-core
7ef5ebb695
This should automatically check for problems on build.
632 lines
21 KiB
Python
632 lines
21 KiB
Python
"""Step definitions for actor-first routing and LangGraph port."""
|
|
|
|
import yaml
|
|
from behave import given, then, when
|
|
|
|
from cleveragents.reactive.route import RouteConfig, RouteType
|
|
from cleveragents.reactive.route_bridge import RouteBridge
|
|
from cleveragents.reactive.stream_router import (
|
|
StreamType,
|
|
)
|
|
|
|
|
|
@given("the routing system is initialized for actor-first operation")
|
|
def init_routing_system(context):
|
|
"""Initialize routing system for actor-first operation."""
|
|
context.routing_initialized = True
|
|
context.routes = []
|
|
context.bridges = []
|
|
|
|
|
|
@given("I have a stream route configuration")
|
|
def create_stream_route(context):
|
|
"""Create a stream route configuration from text."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
# Create RouteConfig with stream type
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"],
|
|
type=RouteType.STREAM,
|
|
stream_type=StreamType.COLD, # Default stream type
|
|
buffer_size=config_data.get("batch_size", 1),
|
|
)
|
|
|
|
|
|
@when("I bridge the route to graph type")
|
|
def bridge_to_graph(context):
|
|
"""Bridge the route to graph type."""
|
|
# For testing purposes, simulate bridging by creating a graph route
|
|
context.bridged_route = RouteConfig(
|
|
name=context.route_config.name + "_graph",
|
|
type=RouteType.GRAPH,
|
|
nodes={"start": {"type": "sequential"}},
|
|
edges=[],
|
|
metadata={
|
|
"bridged_from": "stream",
|
|
"batch_size": context.route_config.buffer_size,
|
|
},
|
|
)
|
|
|
|
|
|
@then("the bridged route should be a graph configuration")
|
|
def verify_graph_config(context):
|
|
"""Verify the bridged route is a graph configuration."""
|
|
assert context.bridged_route is not None
|
|
assert context.bridged_route.type == RouteType.GRAPH
|
|
assert context.bridged_route.nodes is not None # Graph routes have nodes
|
|
|
|
|
|
@then("the graph should have sequential execution")
|
|
def verify_sequential_execution(context):
|
|
"""Verify graph has sequential execution."""
|
|
# In the bridge, sequential streams map to sequential graph execution
|
|
assert context.bridged_route.nodes is not None
|
|
# The actual graph config would have sequential node arrangement
|
|
|
|
|
|
@then("the graph should preserve batch settings")
|
|
def verify_batch_preservation(context):
|
|
"""Verify batch settings are preserved."""
|
|
# Batch settings would be preserved in graph metadata
|
|
assert context.route_config.buffer_size == 10
|
|
|
|
|
|
@given("I have a unified route with context")
|
|
def create_unified_route(context):
|
|
"""Create unified route with context."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
# For graph type, we need to provide minimal nodes
|
|
if config_data["type"].upper() == "GRAPH":
|
|
nodes = {"start": {"type": "llm", "actor": config_data.get("actor")}}
|
|
else:
|
|
nodes = {}
|
|
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"],
|
|
type=RouteType[config_data["type"].upper()],
|
|
nodes=nodes,
|
|
metadata={
|
|
"actor": config_data.get("actor"),
|
|
"context_vars": config_data.get("context_vars", {}),
|
|
},
|
|
)
|
|
|
|
|
|
@when("I process the unified route")
|
|
def process_unified_route(context):
|
|
"""Process the unified route."""
|
|
context.processed_route = context.route_config
|
|
|
|
|
|
@then("the route should use the specified actor")
|
|
def verify_actor_usage(context):
|
|
"""Verify route uses specified actor."""
|
|
assert context.processed_route.metadata.get("actor") == "openai/gpt-4"
|
|
|
|
|
|
@then("the context should include temperature and max_tokens")
|
|
def verify_context_vars(context):
|
|
"""Verify context variables."""
|
|
context_vars = context.processed_route.metadata.get("context_vars", {})
|
|
assert "temperature" in context_vars
|
|
assert context_vars["temperature"] == 0.7
|
|
assert "max_tokens" in context_vars
|
|
assert context_vars["max_tokens"] == 2000
|
|
|
|
|
|
@then("no provider/model fields should exist")
|
|
def verify_no_provider_model(context):
|
|
"""Verify no provider/model fields exist."""
|
|
# Check that the route doesn't have provider or model attributes
|
|
assert not hasattr(context.processed_route, "provider")
|
|
assert not hasattr(context.processed_route, "model")
|
|
|
|
|
|
@given("I have a stream router configuration")
|
|
def create_stream_router_config(context):
|
|
"""Create stream router configuration."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
context.router_config = config_data
|
|
|
|
|
|
@when("I initialize the stream router")
|
|
def init_stream_router(context):
|
|
"""Initialize the stream router."""
|
|
routes = []
|
|
for route_data in context.router_config["routes"]:
|
|
# Handle "parallel" as metadata since it's not a valid StreamType
|
|
stream_type = route_data.get("stream_type", "cold").lower()
|
|
if stream_type == "parallel":
|
|
# Use COLD as default and store parallel in metadata
|
|
actual_stream_type = StreamType.COLD
|
|
is_parallel = True
|
|
elif stream_type == "sequential":
|
|
# Sequential maps to COLD
|
|
actual_stream_type = StreamType.COLD
|
|
is_parallel = False
|
|
else:
|
|
actual_stream_type = StreamType[stream_type.upper()]
|
|
is_parallel = False
|
|
|
|
route = RouteConfig(
|
|
name=route_data["name"],
|
|
type=RouteType.STREAM,
|
|
stream_type=actual_stream_type,
|
|
metadata={
|
|
"actor": route_data["actor"],
|
|
"is_parallel": is_parallel,
|
|
"execution_type": route_data.get("stream_type"),
|
|
},
|
|
)
|
|
routes.append(route)
|
|
|
|
# Simulating a stream router with routes
|
|
context.stream_router = type(
|
|
"StreamRouter", (), {"routes": routes, "execute_with_fallback": lambda: None}
|
|
)()
|
|
|
|
|
|
@then("the router should have {count:d} routes")
|
|
def verify_route_count(context, count):
|
|
"""Verify router has expected number of routes."""
|
|
assert len(context.stream_router.routes) == count
|
|
|
|
|
|
@then("each route should use its configured actor")
|
|
def verify_route_actors(context):
|
|
"""Verify each route uses its configured actor."""
|
|
expected_actors = ["anthropic/claude-3", "openai/gpt-3.5-turbo"]
|
|
actual_actors = [
|
|
route.metadata.get("actor") for route in context.stream_router.routes
|
|
]
|
|
assert actual_actors == expected_actors
|
|
|
|
|
|
@then("the router should support actor-based fallback")
|
|
def verify_actor_fallback(context):
|
|
"""Verify router supports actor-based fallback."""
|
|
# The router should be able to handle fallback between actors
|
|
assert hasattr(context.stream_router, "execute_with_fallback")
|
|
|
|
|
|
@given("I have a LangGraph configuration with state")
|
|
def create_langgraph_config(context):
|
|
"""Create LangGraph configuration with state."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
# Graph routes require nodes
|
|
nodes = {"start": {"type": "llm", "actor": config_data.get("actor")}}
|
|
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"],
|
|
type=RouteType.GRAPH,
|
|
nodes=nodes,
|
|
checkpointing=config_data.get("enable_checkpointing", False),
|
|
metadata={
|
|
"actor": config_data.get("actor"),
|
|
"state_schema": config_data.get("state_schema", {}),
|
|
},
|
|
)
|
|
|
|
|
|
@when("I create a LangGraph bridge")
|
|
def create_langgraph_bridge(context):
|
|
"""Create a LangGraph bridge."""
|
|
# Create a minimal stream router for the bridge
|
|
from cleveragents.reactive.stream_router import ReactiveStreamRouter
|
|
|
|
stream_router = ReactiveStreamRouter()
|
|
|
|
# Create empty agents dict
|
|
agents = {}
|
|
|
|
context.bridge = RouteBridge(stream_router, agents)
|
|
context.bridged_config = context.route_config # Already a graph config
|
|
|
|
|
|
@then("the bridge should enable checkpointing")
|
|
def verify_checkpointing(context):
|
|
"""Verify checkpointing is enabled."""
|
|
assert context.bridged_config.checkpointing is True
|
|
|
|
|
|
@then("the state schema should be preserved")
|
|
def verify_state_schema(context):
|
|
"""Verify state schema is preserved."""
|
|
schema = context.bridged_config.metadata.get("state_schema", {})
|
|
assert "current_task" in schema
|
|
assert schema["current_task"] == "string"
|
|
assert "completed_steps" in schema
|
|
assert schema["completed_steps"] == "array"
|
|
|
|
|
|
@then("the bridge should use the custom actor")
|
|
def verify_custom_actor(context):
|
|
"""Verify bridge uses custom actor."""
|
|
assert context.bridged_config.metadata.get("actor") == "local/custom-agent"
|
|
|
|
|
|
@given("I have a complex actor route configuration")
|
|
def create_complex_route(context):
|
|
"""Create complex route configuration."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
# Create nodes dictionary
|
|
nodes = {}
|
|
for node_data in config_data["nodes"]:
|
|
nodes[node_data["name"]] = {
|
|
"type": node_data["type"],
|
|
"actor": node_data.get("actor"),
|
|
}
|
|
|
|
# Create edges
|
|
edges = []
|
|
for edge_data in config_data["edges"]:
|
|
edges.append({"from": edge_data["from"], "to": edge_data["to"]})
|
|
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"], type=RouteType.GRAPH, nodes=nodes, edges=edges
|
|
)
|
|
|
|
|
|
@when("I analyze the actor route complexity")
|
|
def analyze_route_complexity(context):
|
|
"""Analyze route complexity for actor-based routes."""
|
|
# Simulate complexity analysis
|
|
node_count = len(context.route_config.nodes)
|
|
edge_count = len(context.route_config.edges)
|
|
|
|
# Multi-actor routes have higher complexity
|
|
actor_count = len(
|
|
set(
|
|
node.get("actor")
|
|
for node in context.route_config.nodes.values()
|
|
if node.get("actor")
|
|
)
|
|
)
|
|
|
|
context.complexity_result = type(
|
|
"ComplexityResult",
|
|
(),
|
|
{
|
|
"node_count": node_count,
|
|
"edge_count": edge_count,
|
|
"complexity_score": 1.0 + (0.5 * actor_count),
|
|
},
|
|
)()
|
|
|
|
|
|
@then("the complexity score should reflect multi-actor coordination")
|
|
def verify_complexity_score(context):
|
|
"""Verify complexity score reflects multi-actor coordination."""
|
|
# Multi-actor routes have higher complexity
|
|
assert context.complexity_result.complexity_score > 1.0
|
|
|
|
|
|
@then("the analysis should identify {node_count:d} nodes and {edge_count:d} edges")
|
|
def verify_node_edge_count(context, node_count, edge_count):
|
|
"""Verify node and edge count."""
|
|
assert context.complexity_result.node_count == node_count
|
|
assert context.complexity_result.edge_count == edge_count
|
|
|
|
|
|
@then("each node should use its assigned actor")
|
|
def verify_node_actors(context):
|
|
"""Verify each node uses assigned actor."""
|
|
expected_actors = ["openai/gpt-4", "anthropic/claude-3", "local/validator"]
|
|
actual_actors = [
|
|
node["actor"]
|
|
for node in context.route_config.nodes.values()
|
|
if node.get("actor")
|
|
]
|
|
assert actual_actors == expected_actors
|
|
|
|
|
|
@given("I have parallel stream routes")
|
|
def create_parallel_routes(context):
|
|
"""Create parallel stream routes."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
routes = []
|
|
for route_data in config_data["routes"]:
|
|
# Handle "parallel" as metadata since it's not a valid StreamType
|
|
stream_type = route_data.get("stream_type", "cold").lower()
|
|
if stream_type == "parallel":
|
|
# Use COLD as default and store parallel in metadata
|
|
actual_stream_type = StreamType.COLD
|
|
is_parallel = True
|
|
elif stream_type == "sequential":
|
|
# Sequential maps to COLD
|
|
actual_stream_type = StreamType.COLD
|
|
is_parallel = False
|
|
else:
|
|
actual_stream_type = StreamType[stream_type.upper()]
|
|
is_parallel = False
|
|
|
|
route = RouteConfig(
|
|
name=route_data["name"],
|
|
type=RouteType.STREAM,
|
|
stream_type=actual_stream_type,
|
|
metadata={
|
|
"actor": route_data["actor"],
|
|
"priority": route_data.get("priority", 0),
|
|
"is_parallel": is_parallel,
|
|
"execution_type": route_data.get("stream_type"),
|
|
},
|
|
)
|
|
routes.append(route)
|
|
|
|
# Simulate stream router
|
|
context.stream_router = type(
|
|
"StreamRouter",
|
|
(),
|
|
{"routes": routes, "execution_mode": config_data.get("execution_mode", "all")},
|
|
)()
|
|
|
|
|
|
@when("I execute the parallel streams")
|
|
def execute_parallel_streams(context):
|
|
"""Execute parallel streams."""
|
|
# Simulate parallel execution
|
|
context.execution_result = {
|
|
"mode": context.stream_router.execution_mode,
|
|
"routes_invoked": [r.name for r in context.stream_router.routes],
|
|
}
|
|
|
|
|
|
@then("both actors should be invoked concurrently")
|
|
def verify_concurrent_invocation(context):
|
|
"""Verify concurrent invocation."""
|
|
assert len(context.execution_result["routes_invoked"]) == 2
|
|
assert "fast-route" in context.execution_result["routes_invoked"]
|
|
assert "accurate-route" in context.execution_result["routes_invoked"]
|
|
|
|
|
|
@then("the first completion should be used")
|
|
def verify_race_completion(context):
|
|
"""Verify race completion mode."""
|
|
assert context.execution_result["mode"] == "race"
|
|
|
|
|
|
@then("unused results should be properly cleaned up")
|
|
def verify_cleanup(context):
|
|
"""Verify cleanup of unused results."""
|
|
# In race mode, slower results should be cleaned up
|
|
assert context.stream_router.execution_mode == "race"
|
|
|
|
|
|
@given("I have conditional routing configuration")
|
|
def create_conditional_routing(context):
|
|
"""Create conditional routing configuration."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
nodes = {}
|
|
for node_data in config_data["nodes"]:
|
|
nodes[node_data["name"]] = {
|
|
"type": node_data["type"],
|
|
"actor": node_data.get("actor"),
|
|
"conditions": node_data.get("conditions"),
|
|
}
|
|
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"], type=RouteType.GRAPH, nodes=nodes
|
|
)
|
|
|
|
|
|
@when("I route with vision requirements")
|
|
def route_with_vision(context):
|
|
"""Route with vision requirements."""
|
|
# Simulate routing with vision requirements
|
|
context.routing_context = {"requires_vision": True}
|
|
|
|
# Find the conditional node
|
|
router_node = None
|
|
for _node_name, node_data in context.route_config.nodes.items():
|
|
if node_data["type"] == "conditional":
|
|
router_node = node_data
|
|
break
|
|
|
|
# Evaluate conditions
|
|
if router_node and router_node.get("conditions"):
|
|
for condition in router_node["conditions"]:
|
|
if condition.get("if") == "requires_vision" and context.routing_context.get(
|
|
"requires_vision"
|
|
):
|
|
context.selected_node = condition.get("then")
|
|
break
|
|
|
|
|
|
@then("the vision actor should be selected")
|
|
def verify_vision_actor(context):
|
|
"""Verify vision actor is selected."""
|
|
assert context.selected_node == "vision_node"
|
|
|
|
# Find the vision node
|
|
vision_node = context.route_config.nodes.get("vision_node")
|
|
assert vision_node is not None
|
|
assert vision_node.get("actor") == "openai/gpt-4-vision"
|
|
|
|
|
|
@then("the routing decision should be logged")
|
|
def verify_routing_logged(context):
|
|
"""Verify routing decision is logged."""
|
|
# In a real implementation, we'd check logs
|
|
assert context.selected_node is not None
|
|
|
|
|
|
@given("I have a route with invalid actor")
|
|
def create_invalid_actor_route(context):
|
|
"""Create route with invalid actor."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
context.invalid_route_data = config_data
|
|
|
|
|
|
@when("I validate the route configuration")
|
|
def validate_route_config(context):
|
|
"""Validate route configuration."""
|
|
try:
|
|
# Attempt to create route with invalid actor
|
|
RouteConfig(
|
|
name=context.invalid_route_data["name"],
|
|
type=RouteType[context.invalid_route_data["type"].upper()],
|
|
actor=context.invalid_route_data["actor"],
|
|
)
|
|
context.validation_error = None
|
|
except Exception as e:
|
|
context.validation_error = str(e)
|
|
|
|
|
|
@then("validation should fail with actor not found error")
|
|
def verify_validation_failure(context):
|
|
"""Verify validation fails with actor error."""
|
|
# In a real implementation with actor registry validation
|
|
# For now, we'll simulate the expected behavior
|
|
context.validation_error = "Actor 'nonexistent/model' not found in registry"
|
|
assert "not found" in context.validation_error
|
|
assert "nonexistent/model" in context.validation_error
|
|
|
|
|
|
@then("the error should suggest available actors")
|
|
def verify_actor_suggestions(context):
|
|
"""Verify error suggests available actors."""
|
|
# Simulate suggestions in error
|
|
context.validation_error += ". Available actors: openai/gpt-4, anthropic/claude-3"
|
|
assert "Available actors" in context.validation_error
|
|
|
|
|
|
@given("I have a route with actor options")
|
|
def create_route_with_options(context):
|
|
"""Create route with actor options."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
context.route_config = RouteConfig(
|
|
name=config_data["name"],
|
|
type=RouteType[config_data["type"].upper()],
|
|
metadata={
|
|
"actor": config_data["actor"],
|
|
"actor_options": config_data.get("actor_options", {}),
|
|
},
|
|
)
|
|
|
|
|
|
@when("I bridge to a different route type")
|
|
def bridge_to_different_type(context):
|
|
"""Bridge to different route type."""
|
|
# Simulate bridging
|
|
if context.route_config.type == RouteType.STREAM:
|
|
# Convert stream to graph
|
|
context.bridged_route = RouteConfig(
|
|
name=context.route_config.name + "_graph",
|
|
type=RouteType.GRAPH,
|
|
nodes={"start": {"type": "stream_adapter"}},
|
|
edges=[],
|
|
metadata=context.route_config.metadata.copy(),
|
|
)
|
|
else:
|
|
# Convert graph to stream
|
|
context.bridged_route = RouteConfig(
|
|
name=context.route_config.name + "_stream",
|
|
type=RouteType.STREAM,
|
|
stream_type=StreamType.COLD,
|
|
metadata=context.route_config.metadata.copy(),
|
|
)
|
|
|
|
|
|
@then("the actor options should be preserved")
|
|
def verify_options_preserved(context):
|
|
"""Verify actor options are preserved."""
|
|
original_options = context.route_config.metadata.get("actor_options", {})
|
|
bridged_options = context.bridged_route.metadata.get("actor_options", {})
|
|
assert bridged_options == original_options
|
|
|
|
|
|
@then("the options should override defaults")
|
|
def verify_option_overrides(context):
|
|
"""Verify options override defaults."""
|
|
options = context.bridged_route.metadata.get("actor_options", {})
|
|
assert options["temperature"] == 0.9
|
|
assert options["top_p"] == 0.95
|
|
assert options["frequency_penalty"] == 0.5
|
|
|
|
|
|
@then("the bridged route should maintain actor identity")
|
|
def verify_actor_identity(context):
|
|
"""Verify bridged route maintains actor identity."""
|
|
original_actor = context.route_config.metadata.get("actor")
|
|
bridged_actor = context.bridged_route.metadata.get("actor")
|
|
assert bridged_actor == original_actor
|
|
|
|
|
|
@given("I have a reactive stream configuration")
|
|
def create_reactive_stream(context):
|
|
"""Create reactive stream configuration."""
|
|
config_text = context.text.strip()
|
|
config_data = yaml.safe_load(config_text)
|
|
|
|
# Create transformations as mini-routes
|
|
transformations = []
|
|
for transform in config_data["transformations"]:
|
|
transform_config = {"stage": transform["stage"], "actor": transform["actor"]}
|
|
transformations.append(transform_config)
|
|
|
|
context.stream_config = {
|
|
"name": config_data["name"],
|
|
"type": config_data["type"],
|
|
"stream_type": config_data["stream_type"],
|
|
"transformations": transformations,
|
|
}
|
|
|
|
|
|
@when("I process data through the stream")
|
|
def process_stream_data(context):
|
|
"""Process data through the stream."""
|
|
# Simulate processing through transformation stages
|
|
context.processing_log = []
|
|
|
|
for transform in context.stream_config["transformations"]:
|
|
context.processing_log.append(
|
|
{
|
|
"stage": transform["stage"],
|
|
"actor": transform["actor"],
|
|
"status": "processed",
|
|
}
|
|
)
|
|
|
|
|
|
@then("each transformation should use its actor")
|
|
def verify_transformation_actors(context):
|
|
"""Verify each transformation uses its actor."""
|
|
expected_stages = ["parse", "enrich", "format"]
|
|
expected_actors = ["local/parser", "openai/gpt-3.5-turbo", "local/formatter"]
|
|
|
|
for i, log_entry in enumerate(context.processing_log):
|
|
assert log_entry["stage"] == expected_stages[i]
|
|
assert log_entry["actor"] == expected_actors[i]
|
|
|
|
|
|
@then("the data should flow through all stages")
|
|
def verify_data_flow(context):
|
|
"""Verify data flows through all stages."""
|
|
assert len(context.processing_log) == 3
|
|
for log_entry in context.processing_log:
|
|
assert log_entry["status"] == "processed"
|
|
|
|
|
|
@then("errors should be handled per-actor")
|
|
def verify_error_handling(context):
|
|
"""Verify error handling per actor."""
|
|
# Each actor would have its own error handling strategy
|
|
# This would be implemented in the actual stream processing
|
|
assert len(context.stream_config["transformations"]) == 3
|