feat: Ported the bulk of v2 w/tests
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
"""Behave steps to cover reactive config parser paths."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.core.exceptions import ConfigurationError
|
||||
from cleveragents.reactive.config_parser import ReactiveConfigParser
|
||||
from cleveragents.reactive.route import RouteType
|
||||
from cleveragents.reactive.stream_router import StreamType
|
||||
|
||||
|
||||
@given("a temporary directory for reactive config parsing")
|
||||
def step_temp_directory(context: Any) -> None:
|
||||
context.temp_dir = Path(tempfile.mkdtemp())
|
||||
context.parser = ReactiveConfigParser()
|
||||
context.config_files: list[Path] = []
|
||||
context.parsed_config = None
|
||||
context.parse_error: Exception | None = None
|
||||
|
||||
|
||||
@given("reactive config files covering merges, routes, and pipelines")
|
||||
def step_reactive_config_files(context: Any) -> None:
|
||||
stream_route = {
|
||||
"type": "stream",
|
||||
"stream_type": "hot",
|
||||
"operators": [{"type": "map"}],
|
||||
"subscriptions": ["input"],
|
||||
"publications": ["output"],
|
||||
"agents": ["alpha"],
|
||||
"initial_value": 5,
|
||||
"buffer_size": 2,
|
||||
"template_config": {"tmpl": True},
|
||||
"bridge": {
|
||||
"upgrade_conditions": {"ready": True},
|
||||
"downgrade_conditions": {},
|
||||
"state_extractor": "extract",
|
||||
"state_flattener": "flatten",
|
||||
"preserve_subscriptions": False,
|
||||
"preserve_checkpointing": False,
|
||||
},
|
||||
"metadata": {"kind": "stream"},
|
||||
}
|
||||
|
||||
graph_route = {
|
||||
"type": "graph",
|
||||
"nodes": {"n1": {"agent": "alpha"}},
|
||||
"edges": [{"from": "n1", "to": "n1"}],
|
||||
"entry_point": "n1",
|
||||
"checkpointing": True,
|
||||
"checkpoint_dir": str(context.temp_dir / "ckpts"),
|
||||
"enable_time_travel": True,
|
||||
"parallel_execution": False,
|
||||
"state_class": "State",
|
||||
"metadata": {"kind": "graph"},
|
||||
}
|
||||
|
||||
primary_config = {
|
||||
"agents": {"alpha": {"type": "llm", "config": {"a": 1}}},
|
||||
"routes": [stream_route, graph_route],
|
||||
"merges": [{"sources": ["input"], "target": "route_0"}],
|
||||
"splits": [{"source": "route_0", "targets": ["route_1"]}],
|
||||
}
|
||||
|
||||
secondary_config = {
|
||||
"agents": {"alpha": {"config": {"b": 2}}},
|
||||
"merges": [{"sources": ["input2"], "target": "route_0"}],
|
||||
"templates": {"t1": {"value": 1}},
|
||||
"instances": {"inst": {"config": "val"}},
|
||||
"global_context": {"mode": "test"},
|
||||
"template_engine": "CUSTOM",
|
||||
"prompts": {"p1": "hi"},
|
||||
"pipelines": {
|
||||
"pipe1": {
|
||||
"stages": [
|
||||
{"type": "stream", "name": "route_0"},
|
||||
{"type": "graph", "config": {"name": "route_1"}},
|
||||
],
|
||||
"metadata": {"note": "x"},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
primary_file = context.temp_dir / "primary.yaml"
|
||||
secondary_file = context.temp_dir / "secondary.yaml"
|
||||
primary_file.write_text(yaml.safe_dump(primary_config))
|
||||
secondary_file.write_text(yaml.safe_dump(secondary_config))
|
||||
context.config_files = [primary_file, secondary_file]
|
||||
|
||||
|
||||
@when("I parse the reactive config files")
|
||||
def step_parse_reactive_config_files(context: Any) -> None:
|
||||
try:
|
||||
context.parsed_config = context.parser.parse_files(context.config_files)
|
||||
context.parse_error = None
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
context.parsed_config = None
|
||||
context.parse_error = exc
|
||||
|
||||
|
||||
@then("the merged reactive config should preserve agents and replacements")
|
||||
def step_verify_merges_and_agents(context: Any) -> None:
|
||||
assert context.parse_error is None, f"Unexpected error: {context.parse_error}"
|
||||
rc = context.parsed_config
|
||||
assert rc is not None
|
||||
assert rc.agents["alpha"].type == "llm"
|
||||
assert rc.agents["alpha"].config["a"] == 1
|
||||
assert rc.agents["alpha"].config["b"] == 2
|
||||
assert len(rc.merges) == 2
|
||||
|
||||
|
||||
@then("stream and graph routes should be built with defaults and bridges")
|
||||
def step_verify_routes(context: Any) -> None:
|
||||
rc = context.parsed_config
|
||||
assert rc is not None
|
||||
|
||||
stream_route = rc.routes["route_0"]
|
||||
assert stream_route.type == RouteType.STREAM
|
||||
assert stream_route.stream_type == StreamType.HOT
|
||||
assert stream_route.buffer_size == 2
|
||||
assert stream_route.template_config == {"tmpl": True}
|
||||
assert stream_route.bridge is not None
|
||||
assert stream_route.bridge.preserve_subscriptions is False
|
||||
assert stream_route.bridge.preserve_checkpointing is False
|
||||
|
||||
graph_route = rc.routes["route_1"]
|
||||
assert graph_route.type == RouteType.GRAPH
|
||||
assert graph_route.checkpointing is True
|
||||
assert graph_route.enable_time_travel is True
|
||||
assert graph_route.parallel_execution is False
|
||||
assert graph_route.state_class == "State"
|
||||
|
||||
|
||||
@then("reactive config should include merges splits pipelines and templates")
|
||||
def step_verify_other_sections(context: Any) -> None:
|
||||
rc = context.parsed_config
|
||||
assert rc is not None
|
||||
|
||||
assert len(rc.splits) == 1
|
||||
assert "pipe1" in rc.pipelines
|
||||
assert rc.pipelines["pipe1"].metadata["note"] == "x"
|
||||
assert "t1" in rc.templates
|
||||
assert "inst" in rc.instances
|
||||
assert rc.global_context["mode"] == "test"
|
||||
assert rc.template_engine == "CUSTOM"
|
||||
assert rc.prompts["p1"] == "hi"
|
||||
|
||||
|
||||
@given("a reactive config file with environment placeholders")
|
||||
def step_env_config_file(context: Any) -> None:
|
||||
config = {
|
||||
"agents": {
|
||||
"env_agent": {
|
||||
"type": "llm",
|
||||
"config": {
|
||||
"api_key": "${ENV_API_KEY}",
|
||||
"with_default": "${MISSING_KEY:defaulted}",
|
||||
},
|
||||
}
|
||||
},
|
||||
"routes": {"main": {"type": "stream", "agents": ["env_agent"]}},
|
||||
}
|
||||
config_file = context.temp_dir / "env.yaml"
|
||||
config_file.write_text(yaml.safe_dump(config))
|
||||
context.config_files = [config_file]
|
||||
|
||||
|
||||
@given("environment variables are set for interpolation")
|
||||
def step_set_env_vars(_: Any) -> None:
|
||||
os.environ["ENV_API_KEY"] = "secret-key"
|
||||
os.environ["OTHER_VAR"] = "unused"
|
||||
|
||||
|
||||
@then("environment placeholders should resolve using values and defaults")
|
||||
def step_verify_env_interpolation(context: Any) -> None:
|
||||
assert context.parse_error is None, f"Unexpected error: {context.parse_error}"
|
||||
rc = context.parsed_config
|
||||
assert rc is not None
|
||||
|
||||
agent_cfg = rc.agents["env_agent"].config
|
||||
assert agent_cfg["api_key"] == "secret-key"
|
||||
assert agent_cfg["with_default"] == "defaulted"
|
||||
|
||||
|
||||
@given("a reactive config file with a required environment placeholder")
|
||||
def step_env_required_config(context: Any) -> None:
|
||||
config = {
|
||||
"agents": {"needs_env": {"type": "llm", "config": {"token": "${MUST_HAVE}"}}},
|
||||
"routes": {"main": {"type": "stream", "agents": ["needs_env"]}},
|
||||
}
|
||||
file_path = context.temp_dir / "missing_env.yaml"
|
||||
file_path.write_text(yaml.safe_dump(config))
|
||||
context.config_files = [file_path]
|
||||
if "MUST_HAVE" in os.environ:
|
||||
del os.environ["MUST_HAVE"]
|
||||
|
||||
|
||||
@when("I parse the reactive config files expecting failure")
|
||||
def step_parse_expect_failure(context: Any) -> None:
|
||||
try:
|
||||
context.parsed_config = context.parser.parse_files(context.config_files)
|
||||
context.parse_error = None
|
||||
except Exception as exc:
|
||||
context.parsed_config = None
|
||||
context.parse_error = exc
|
||||
|
||||
|
||||
@then("a configuration error should be raised for the missing variable")
|
||||
def step_verify_missing_env_error(context: Any) -> None:
|
||||
assert context.parse_error is not None, "Expected a configuration error"
|
||||
assert isinstance(context.parse_error, ConfigurationError)
|
||||
Reference in New Issue
Block a user