8953449dc2
CI / lint (pull_request) Successful in 30s
CI / typecheck (pull_request) Successful in 1m15s
CI / security (pull_request) Successful in 1m18s
CI / quality (pull_request) Successful in 30s
CI / build (pull_request) Successful in 37s
CI / helm (pull_request) Successful in 29s
CI / push-validation (pull_request) Successful in 26s
CI / e2e_tests (pull_request) Successful in 3m58s
CI / integration_tests (pull_request) Successful in 6m49s
CI / unit_tests (pull_request) Successful in 8m39s
CI / docker (pull_request) Successful in 1m23s
CI / coverage (pull_request) Successful in 13m32s
CI / benchmark-regression (push) Failing after 0s
CI / benchmark-publish (push) Failing after 0s
CI / status-check (pull_request) Successful in 1s
CI / lint (push) Successful in 25s
CI / typecheck (push) Successful in 1m1s
CI / quality (push) Successful in 55s
CI / security (push) Successful in 1m9s
CI / build (push) Successful in 24s
CI / helm (push) Successful in 30s
CI / push-validation (push) Successful in 20s
CI / e2e_tests (push) Successful in 5m13s
CI / integration_tests (push) Successful in 7m17s
CI / unit_tests (push) Successful in 8m50s
CI / docker (push) Successful in 1m30s
CI / coverage (push) Successful in 11m57s
CI / status-check (push) Successful in 2s
- schema.py: provider field changed to Optional[str] with model validator
validate_provider_required_for_llm_graph() that requires it only for LLM
and GRAPH actor types; TOOL actors do not require provider
- schema.py: is_v3_yaml() uses version_str == "3" or version_str.startswith("3.")
to avoid false positives from "30" or "300" version strings
- schema.py: tool namespace validation uses strict 2-part split to reject
empty namespace or empty name (e.g. "/tool", "ns/", "a/b/c")
- cli/commands/actor.py: schema_version extraction uses raw_version pattern
(no # type: ignore[assignment]) for clean static typing
- actor/__init__.py: is_v3_yaml removed from __all__ and _LAZY_IMPORTS
since it is a module-private helper, not a public API
- robot/actor_add_v3_schema_validation.robot: YAML fixtures for 'Reject v3
LLM Actor Without Model Field' and 'Reject v3 TOOL Actor Without Tools
Field' now include required provider field (and model for TOOL fixture)
- robot/helper_actor_add_v3_schema_validation.py: except clauses unified to
catch (subprocess.TimeoutExpired, FileNotFoundError) in both add_actor()
and update_actor() functions
- features/actor_add_v3_schema_validation.feature: 'Update a v3 actor with
valid YAML succeeds' scenario now includes 'And the actor should be
validated via ActorConfigSchema'; error assertions tightened to exact
messages (e.g. "Input should be 'llm', 'tool' or 'graph'", "Node ID must
be alphanumeric", "must be namespaced")
- features/steps/actor_add_v3_schema_validation_steps.py: step_run_actor_update
now spies on ActorConfigSchema.model_validate; failure paths assert
isinstance(result.exception, SystemExit)
ISSUES CLOSED: #5869
639 lines
22 KiB
Python
639 lines
22 KiB
Python
"""Step definitions for features/actor_hierarchy.feature."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
from behave import given, then, when
|
|
from behave.runner import Context
|
|
|
|
from cleveragents.actor.schema import ActorConfigSchema, NodeType
|
|
from cleveragents.core.exceptions import ValidationError
|
|
|
|
|
|
def _base_graph_yaml(
|
|
nodes: list[dict[str, Any]],
|
|
edges: list[dict[str, Any]] | None = None,
|
|
entry_node: str = "agent_node",
|
|
exit_nodes: list[str] | None = None,
|
|
extra: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
if edges is None:
|
|
ids = [n["id"] for n in nodes]
|
|
edges = []
|
|
for i in range(len(ids) - 1):
|
|
edges.append({"from_node": ids[i], "to_node": ids[i + 1]})
|
|
data: dict[str, Any] = {
|
|
"name": "local/hierarchy-test",
|
|
"type": "graph",
|
|
"description": "Test hierarchical actor",
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"route": {
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
"entry_node": entry_node,
|
|
"exit_nodes": exit_nodes or [nodes[-1]["id"]],
|
|
},
|
|
}
|
|
if extra:
|
|
data.update(extra)
|
|
return data
|
|
|
|
|
|
def _simple_agent_node(node_id: str = "agent_node", **overrides: Any) -> dict[str, Any]:
|
|
base: dict[str, Any] = {
|
|
"id": node_id,
|
|
"type": "agent",
|
|
"name": f"Agent {node_id}",
|
|
"description": f"Agent node {node_id}",
|
|
"config": {"model": "gpt-4"},
|
|
}
|
|
base.update(overrides)
|
|
return base
|
|
|
|
|
|
def _get_node(config: ActorConfigSchema, node_id: str) -> Any:
|
|
if config.route is None:
|
|
return None
|
|
for node in config.route.nodes:
|
|
if node.id == node_id:
|
|
return node
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# LSP Binding on NodeDefinition
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given("a hierarchy actor YAML with node lsp_binding")
|
|
def step_hierarchy_yaml_node_lsp(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"code_writer",
|
|
lsp_binding={
|
|
"server": "local/pyright",
|
|
"languages": ["python"],
|
|
},
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="code_writer")
|
|
|
|
|
|
@given("a hierarchy actor YAML with auto lsp_binding")
|
|
def step_hierarchy_yaml_auto_lsp(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"implementer",
|
|
lsp_binding={"auto": True},
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="implementer")
|
|
|
|
|
|
@given("a hierarchy actor YAML with no lsp_binding")
|
|
def step_hierarchy_yaml_no_lsp(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml([node])
|
|
|
|
|
|
@given('a hierarchy actor YAML with lsp_binding server "{server}"')
|
|
def step_hierarchy_yaml_lsp_bad_server(context: Context, server: str) -> None:
|
|
node = _simple_agent_node(
|
|
"code_writer",
|
|
lsp_binding={"server": server, "languages": ["python"]},
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="code_writer")
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Tool-Source References on NodeDefinition
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given("a hierarchy actor YAML with node tool_sources skills")
|
|
def step_hierarchy_yaml_tool_sources_skills(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"executor",
|
|
tool_sources=[{"type": "skill", "name": "local/file-ops"}],
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")
|
|
|
|
|
|
@given("a hierarchy actor YAML with node tool_sources mcp")
|
|
def step_hierarchy_yaml_tool_sources_mcp(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"executor",
|
|
tool_sources=[{"type": "mcp", "name": "local/filesystem"}],
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")
|
|
|
|
|
|
@given("a hierarchy actor YAML with node tool_sources builtin")
|
|
def step_hierarchy_yaml_tool_sources_builtin(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"executor",
|
|
tool_sources=[
|
|
{"type": "builtin", "group": "file_operations"},
|
|
],
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")
|
|
|
|
|
|
@given("a hierarchy actor YAML with mixed node tool_sources")
|
|
def step_hierarchy_yaml_tool_sources_mixed(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"executor",
|
|
tool_sources=[
|
|
{"type": "skill", "name": "local/file-ops"},
|
|
{"type": "mcp", "name": "local/filesystem"},
|
|
{"type": "builtin", "group": "git_operations"},
|
|
],
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")
|
|
|
|
|
|
@given("a hierarchy actor YAML with invalid tool_sources type")
|
|
def step_hierarchy_yaml_tool_sources_invalid(context: Context) -> None:
|
|
node = _simple_agent_node(
|
|
"executor",
|
|
tool_sources=[{"type": "invalid_type", "name": "x/y"}],
|
|
)
|
|
context.hierarchy_yaml = _base_graph_yaml([node], entry_node="executor")
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Subgraph Node Enhancements
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given("a hierarchy actor YAML with subgraph actor_ref")
|
|
def step_hierarchy_yaml_subgraph_ref(context: Context) -> None:
|
|
planner = _simple_agent_node("planner")
|
|
review: dict[str, Any] = {
|
|
"id": "review",
|
|
"type": "subgraph",
|
|
"name": "Review",
|
|
"description": "Code review subgraph",
|
|
"actor_ref": "local/code-reviewer",
|
|
}
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[planner, review],
|
|
edges=[{"from_node": "planner", "to_node": "review"}],
|
|
entry_node="planner",
|
|
exit_nodes=["review"],
|
|
)
|
|
|
|
|
|
@given('a hierarchy actor YAML with subgraph actor_ref "{bad_ref}"')
|
|
def step_hierarchy_yaml_subgraph_bad_ref(context: Context, bad_ref: str) -> None:
|
|
planner = _simple_agent_node("planner")
|
|
review: dict[str, Any] = {
|
|
"id": "review",
|
|
"type": "subgraph",
|
|
"name": "Review",
|
|
"description": "Code review subgraph",
|
|
"actor_ref": bad_ref,
|
|
}
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[planner, review],
|
|
edges=[{"from_node": "planner", "to_node": "review"}],
|
|
entry_node="planner",
|
|
exit_nodes=["review"],
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Skills Field on ActorConfigSchema
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given("a hierarchy actor YAML with skills list")
|
|
def step_hierarchy_yaml_skills(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[node],
|
|
extra={"skills": ["local/file-ops", "local/git-ops"]},
|
|
)
|
|
|
|
|
|
@given("a hierarchy actor YAML with empty skills")
|
|
def step_hierarchy_yaml_empty_skills(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml([node], extra={"skills": []})
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# LSP Fields on ActorConfigSchema (top-level)
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given("a hierarchy actor YAML with lsp list")
|
|
def step_hierarchy_yaml_lsp_list(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[node],
|
|
extra={"lsp": ["local/pyright", "local/clangd"]},
|
|
)
|
|
|
|
|
|
@given("a hierarchy actor YAML with lsp auto")
|
|
def step_hierarchy_yaml_lsp_auto(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml([node], extra={"lsp": {"auto": True}})
|
|
|
|
|
|
@given("a hierarchy actor YAML with lsp_capabilities")
|
|
def step_hierarchy_yaml_lsp_caps(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[node],
|
|
extra={
|
|
"lsp": ["local/pyright"],
|
|
"lsp_capabilities": ["diagnostics", "hover"],
|
|
},
|
|
)
|
|
|
|
|
|
@given("a hierarchy actor YAML with lsp_capabilities all")
|
|
def step_hierarchy_yaml_lsp_caps_all(context: Context) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[node],
|
|
extra={
|
|
"lsp": ["local/pyright"],
|
|
"lsp_capabilities": "all",
|
|
},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Precise Validation Error Messages
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@given('a hierarchy actor YAML with bad entry_node "{bad_node}"')
|
|
def step_hierarchy_yaml_bad_entry(context: Context, bad_node: str) -> None:
|
|
node = _simple_agent_node("agent_node")
|
|
data = _base_graph_yaml([node])
|
|
data["route"]["entry_node"] = bad_node
|
|
context.hierarchy_yaml = data
|
|
|
|
|
|
@given("a hierarchy actor YAML with duplicate node ids")
|
|
def step_hierarchy_yaml_dup_ids(context: Context) -> None:
|
|
n1 = _simple_agent_node("dup_node")
|
|
n2 = _simple_agent_node("dup_node")
|
|
n2["name"] = "Second Dup"
|
|
context.hierarchy_yaml = _base_graph_yaml([n1, n2], entry_node="dup_node")
|
|
|
|
|
|
@given("a hierarchy actor YAML with cycle")
|
|
def step_hierarchy_yaml_cycle(context: Context) -> None:
|
|
n1 = _simple_agent_node("a")
|
|
n2 = _simple_agent_node("b")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[n1, n2],
|
|
edges=[
|
|
{"from_node": "a", "to_node": "b"},
|
|
{"from_node": "b", "to_node": "a"},
|
|
],
|
|
entry_node="a",
|
|
exit_nodes=["b"],
|
|
)
|
|
|
|
|
|
@given('a hierarchy actor YAML with bad edge from_node "{bad_node}"')
|
|
def step_hierarchy_yaml_bad_edge(context: Context, bad_node: str) -> None:
|
|
n1 = _simple_agent_node("real_node")
|
|
context.hierarchy_yaml = _base_graph_yaml(
|
|
[n1],
|
|
edges=[{"from_node": bad_node, "to_node": "real_node"}],
|
|
entry_node="real_node",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# When Steps
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@when("I parse the hierarchy actor YAML")
|
|
def step_parse_hierarchy_yaml(context: Context) -> None:
|
|
context.hierarchy_error = None
|
|
try:
|
|
context.hierarchy_config = ActorConfigSchema.model_validate(
|
|
context.hierarchy_yaml
|
|
)
|
|
except Exception as exc:
|
|
context.hierarchy_error = str(exc)
|
|
context.hierarchy_config = None
|
|
|
|
|
|
@when("I parse the hierarchy actor YAML expecting failure")
|
|
def step_parse_hierarchy_yaml_fail(context: Context) -> None:
|
|
context.hierarchy_error = None
|
|
try:
|
|
context.hierarchy_config = ActorConfigSchema.model_validate(
|
|
context.hierarchy_yaml
|
|
)
|
|
context.hierarchy_error = None
|
|
except Exception as exc:
|
|
context.hierarchy_error = str(exc)
|
|
context.hierarchy_config = None
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Then Steps - Validation
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
@then("the hierarchy actor should be valid")
|
|
def step_hierarchy_valid(context: Context) -> None:
|
|
assert context.hierarchy_config is not None, (
|
|
f"Expected valid config, got error: {context.hierarchy_error}"
|
|
)
|
|
|
|
|
|
@then('the hierarchy graph node "{node_id}" should have lsp_binding')
|
|
def step_hierarchy_node_has_lsp(context: Context, node_id: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None, f"Node '{node_id}' not found"
|
|
assert node.lsp_binding is not None, f"Node '{node_id}' has no lsp_binding"
|
|
|
|
|
|
@then('the hierarchy node "{node_id}" lsp_binding server should be "{server}"')
|
|
def step_hierarchy_lsp_server(context: Context, node_id: str, server: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.lsp_binding is not None
|
|
assert node.lsp_binding.server == server
|
|
|
|
|
|
@then('the hierarchy node "{node_id}" lsp_binding languages should contain "{lang}"')
|
|
def step_hierarchy_lsp_lang(context: Context, node_id: str, lang: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.lsp_binding is not None
|
|
assert lang in node.lsp_binding.languages
|
|
|
|
|
|
@then('the hierarchy node "{node_id}" lsp_binding auto should be true')
|
|
def step_hierarchy_lsp_auto(context: Context, node_id: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.lsp_binding is not None
|
|
assert node.lsp_binding.auto is True
|
|
|
|
|
|
@then('the hierarchy graph node "{node_id}" should not have lsp_binding')
|
|
def step_hierarchy_node_no_lsp(context: Context, node_id: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None, f"Node '{node_id}' not found"
|
|
assert node.lsp_binding is None
|
|
|
|
|
|
@then('the hierarchy graph node "{node_id}" should have tool_sources')
|
|
def step_hierarchy_node_has_tool_sources(context: Context, node_id: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None, f"Node '{node_id}' not found"
|
|
assert node.tool_sources, f"Node '{node_id}' has no tool_sources"
|
|
|
|
|
|
@then('the hierarchy node "{node_id}" tool_sources should have {count:d} items')
|
|
def step_hierarchy_tool_sources_count(
|
|
context: Context, node_id: str, count: int
|
|
) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert len(node.tool_sources) == count
|
|
|
|
|
|
@then(
|
|
'the hierarchy node "{node_id}" tool_sources item {idx:d} '
|
|
'type should be "{ts_type}"'
|
|
)
|
|
def step_hierarchy_tool_source_type(
|
|
context: Context, node_id: str, idx: int, ts_type: str
|
|
) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.tool_sources[idx].type == ts_type
|
|
|
|
|
|
@then(
|
|
'the hierarchy node "{node_id}" tool_sources item {idx:d} '
|
|
'name should be "{ts_name}"'
|
|
)
|
|
def step_hierarchy_tool_source_name(
|
|
context: Context, node_id: str, idx: int, ts_name: str
|
|
) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.tool_sources[idx].name == ts_name
|
|
|
|
|
|
@then(
|
|
'the hierarchy node "{node_id}" tool_sources item {idx:d} '
|
|
'group should be "{ts_group}"'
|
|
)
|
|
def step_hierarchy_tool_source_group(
|
|
context: Context, node_id: str, idx: int, ts_group: str
|
|
) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.tool_sources[idx].group == ts_group
|
|
|
|
|
|
@then('the hierarchy graph node "{node_id}" should not have tool_sources')
|
|
def step_hierarchy_node_no_tool_sources(context: Context, node_id: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert not node.tool_sources
|
|
|
|
|
|
@then('the hierarchy graph node "{node_id}" should have type "{ntype}"')
|
|
def step_hierarchy_node_type(context: Context, node_id: str, ntype: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.type == NodeType(ntype)
|
|
|
|
|
|
@then('the hierarchy node "{node_id}" actor_ref should be "{ref}"')
|
|
def step_hierarchy_node_actor_ref(context: Context, node_id: str, ref: str) -> None:
|
|
node = _get_node(context.hierarchy_config, node_id)
|
|
assert node is not None
|
|
assert node.actor_ref == ref
|
|
|
|
|
|
@then("the hierarchy actor should have {count:d} skills")
|
|
def step_hierarchy_actor_skills_count(context: Context, count: int) -> None:
|
|
assert len(context.hierarchy_config.skills) == count
|
|
|
|
|
|
@then("the hierarchy actor lsp should have {count:d} items")
|
|
def step_hierarchy_actor_lsp_count(context: Context, count: int) -> None:
|
|
assert isinstance(context.hierarchy_config.lsp, list)
|
|
assert len(context.hierarchy_config.lsp) == count
|
|
|
|
|
|
@then("the hierarchy actor lsp_auto should be true")
|
|
def step_hierarchy_actor_lsp_auto(context: Context) -> None:
|
|
assert isinstance(context.hierarchy_config.lsp, dict)
|
|
assert context.hierarchy_config.lsp.get("auto") is True
|
|
|
|
|
|
@then('the hierarchy actor lsp_capabilities should contain "{cap}"')
|
|
def step_hierarchy_actor_lsp_caps_contain(context: Context, cap: str) -> None:
|
|
assert isinstance(context.hierarchy_config.lsp_capabilities, list)
|
|
assert cap in context.hierarchy_config.lsp_capabilities
|
|
|
|
|
|
@then('the hierarchy actor lsp_capabilities should be "{value}"')
|
|
def step_hierarchy_actor_lsp_caps_str(context: Context, value: str) -> None:
|
|
assert context.hierarchy_config.lsp_capabilities == value
|
|
|
|
|
|
@then('the hierarchy parse error should mention "{text}"')
|
|
def step_hierarchy_error_mention(context: Context, text: str) -> None:
|
|
assert context.hierarchy_error is not None, "Expected error but none occurred"
|
|
assert text.lower() in context.hierarchy_error.lower(), (
|
|
f"Error '{context.hierarchy_error}' does not mention '{text}'"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
# Loader Integration
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
def _hierarchy_tmpdir() -> Path:
|
|
return Path(tempfile.mkdtemp(prefix="hierarchy_actor_"))
|
|
|
|
|
|
@given("a hierarchy temp dir with a hierarchical actor YAML file")
|
|
def step_hierarchy_loader_setup(context: Context) -> None:
|
|
from cleveragents.actor.loader import ActorLoader
|
|
|
|
tmpdir = _hierarchy_tmpdir()
|
|
actor_data: dict[str, Any] = {
|
|
"name": "local/hierarchy-demo",
|
|
"type": "graph",
|
|
"description": "Hierarchical demo",
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"skills": ["local/file-ops"],
|
|
"lsp": ["local/pyright"],
|
|
"lsp_capabilities": ["diagnostics"],
|
|
"route": {
|
|
"nodes": [
|
|
{
|
|
"id": "planner",
|
|
"type": "agent",
|
|
"name": "Planner",
|
|
"description": "Plans tasks",
|
|
"config": {"model": "gpt-4"},
|
|
"lsp_binding": {
|
|
"server": "local/pyright",
|
|
"languages": ["python"],
|
|
},
|
|
"tool_sources": [
|
|
{"type": "skill", "name": "local/file-ops"},
|
|
],
|
|
},
|
|
],
|
|
"edges": [],
|
|
"entry_node": "planner",
|
|
"exit_nodes": ["planner"],
|
|
},
|
|
}
|
|
yaml_path = tmpdir / "hierarchy_demo.yaml"
|
|
with yaml_path.open("w") as f:
|
|
yaml.safe_dump(actor_data, f)
|
|
context.hierarchy_tmpdir = tmpdir
|
|
context.hierarchy_loader_cls = ActorLoader
|
|
|
|
|
|
@given("a hierarchy temp dir with invalid lsp_binding actor YAML")
|
|
def step_hierarchy_loader_invalid(context: Context) -> None:
|
|
from cleveragents.actor.loader import ActorLoader
|
|
|
|
tmpdir = _hierarchy_tmpdir()
|
|
actor_data: dict[str, Any] = {
|
|
"name": "local/bad-lsp",
|
|
"type": "graph",
|
|
"description": "Bad LSP binding",
|
|
"provider": "openai",
|
|
"model": "gpt-4",
|
|
"route": {
|
|
"nodes": [
|
|
{
|
|
"id": "code_node",
|
|
"type": "agent",
|
|
"name": "Coder",
|
|
"description": "Codes stuff",
|
|
"config": {},
|
|
"lsp_binding": {
|
|
"server": "no-namespace",
|
|
"languages": ["python"],
|
|
},
|
|
},
|
|
],
|
|
"edges": [],
|
|
"entry_node": "code_node",
|
|
"exit_nodes": ["code_node"],
|
|
},
|
|
}
|
|
yaml_path = tmpdir / "bad_lsp.yaml"
|
|
with yaml_path.open("w") as f:
|
|
yaml.safe_dump(actor_data, f)
|
|
context.hierarchy_tmpdir = tmpdir
|
|
context.hierarchy_loader_cls = ActorLoader
|
|
|
|
|
|
@when("I create a hierarchy loader and run discovery")
|
|
def step_hierarchy_loader_discover(context: Context) -> None:
|
|
loader = context.hierarchy_loader_cls(search_roots=[context.hierarchy_tmpdir])
|
|
context.hierarchy_loaded_actors = loader.discover()
|
|
context.hierarchy_loader_error = None
|
|
|
|
|
|
@when("I create a hierarchy loader and run discovery expecting error")
|
|
def step_hierarchy_loader_discover_error(context: Context) -> None:
|
|
loader = context.hierarchy_loader_cls(search_roots=[context.hierarchy_tmpdir])
|
|
try:
|
|
context.hierarchy_loaded_actors = loader.discover()
|
|
context.hierarchy_loader_error = None
|
|
except (ValidationError, Exception) as exc:
|
|
context.hierarchy_loaded_actors = []
|
|
context.hierarchy_loader_error = str(exc)
|
|
|
|
|
|
@then("the hierarchy loader should find {count:d} actors")
|
|
def step_hierarchy_loader_count(context: Context, count: int) -> None:
|
|
assert len(context.hierarchy_loaded_actors) == count, (
|
|
f"Expected {count} actors, got {len(context.hierarchy_loaded_actors)}"
|
|
)
|
|
|
|
|
|
@then("the hierarchy loaded actor should have skills")
|
|
def step_hierarchy_loaded_has_skills(context: Context) -> None:
|
|
actor = context.hierarchy_loaded_actors[0]
|
|
assert actor.skills, "Loaded actor should have skills"
|
|
|
|
|
|
@then("the hierarchy loaded actor should have lsp")
|
|
def step_hierarchy_loaded_has_lsp(context: Context) -> None:
|
|
actor = context.hierarchy_loaded_actors[0]
|
|
assert actor.lsp is not None, "Loaded actor should have lsp"
|
|
|
|
|
|
@then('the hierarchy loader error should mention "{text}"')
|
|
def step_hierarchy_loader_error_mention(context: Context, text: str) -> None:
|
|
assert context.hierarchy_loader_error is not None, (
|
|
"Expected loader error but none occurred"
|
|
)
|
|
assert text.lower() in context.hierarchy_loader_error.lower(), (
|
|
f"Error '{context.hierarchy_loader_error}' does not mention '{text}'"
|
|
)
|