Files
cleveragents-core/robot/helper_actor_hierarchy.py
hurui200320 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
fix(actor): validate v3 YAML via ActorConfigSchema in agents actor add CLI
- 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
2026-04-17 16:28:45 +08:00

198 lines
5.9 KiB
Python

"""Robot Framework helper for hierarchical actor YAML smoke tests.
Usage:
python robot/helper_actor_hierarchy.py discover-hierarchical
python robot/helper_actor_hierarchy.py parse-lsp-binding
python robot/helper_actor_hierarchy.py parse-tool-sources
python robot/helper_actor_hierarchy.py parse-subgraph-ref
python robot/helper_actor_hierarchy.py reject-bad-lsp
"""
from __future__ import annotations
import sys
from pathlib import Path
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.actor.loader import ActorLoader # noqa: E402
from cleveragents.actor.schema import ActorConfigSchema # noqa: E402
def main() -> int:
if len(sys.argv) < 2:
print("Usage: helper_actor_hierarchy.py <command>")
return 1
cmd = sys.argv[1]
dispatch = {
"discover-hierarchical": _discover_hierarchical,
"parse-lsp-binding": _parse_lsp_binding,
"parse-tool-sources": _parse_tool_sources,
"parse-subgraph-ref": _parse_subgraph_ref,
"reject-bad-lsp": _reject_bad_lsp,
}
handler = dispatch.get(cmd)
if handler is None:
print(f"Unknown command: {cmd}")
return 1
return handler()
def _discover_hierarchical() -> int:
project_root = Path(__file__).resolve().parents[1]
examples_dir = project_root / "examples" / "actors"
loader = ActorLoader(search_roots=[examples_dir])
actors = loader.discover()
for actor in actors:
if actor.name == "workflows/dev-workflow":
print(f"actor-loaded: {actor.name}")
print(f"skills-count: {len(actor.skills)}")
print(f"lsp-configured: {actor.lsp is not None}")
return 0
print("actor-not-found: workflows/dev-workflow")
return 1
def _parse_lsp_binding() -> int:
data = {
"name": "local/lsp-test",
"type": "graph",
"description": "LSP binding test",
"provider": "openai",
"model": "gpt-4",
"route": {
"nodes": [
{
"id": "coder",
"type": "agent",
"name": "Coder",
"description": "Codes with LSP",
"config": {"model": "gpt-4"},
"lsp_binding": {
"server": "local/pyright",
"languages": ["python"],
},
},
],
"edges": [],
"entry_node": "coder",
"exit_nodes": ["coder"],
},
}
config = ActorConfigSchema.model_validate(data)
node = config.route.nodes[0] # type: ignore[union-attr]
if node.lsp_binding:
print(f"lsp-binding-server: {node.lsp_binding.server}")
print(f"lsp-binding-langs: {', '.join(node.lsp_binding.languages)}")
return 0
def _parse_tool_sources() -> int:
data = {
"name": "local/ts-test",
"type": "graph",
"description": "Tool sources test",
"provider": "openai",
"model": "gpt-4",
"route": {
"nodes": [
{
"id": "exec",
"type": "agent",
"name": "Executor",
"description": "Executor with tool sources",
"config": {},
"tool_sources": [
{"type": "skill", "name": "local/file-ops"},
{"type": "mcp", "name": "local/filesystem"},
],
},
],
"edges": [],
"entry_node": "exec",
"exit_nodes": ["exec"],
},
}
config = ActorConfigSchema.model_validate(data)
node = config.route.nodes[0] # type: ignore[union-attr]
print(f"tool-sources-count: {len(node.tool_sources)}")
return 0
def _parse_subgraph_ref() -> int:
data = {
"name": "local/sg-test",
"type": "graph",
"description": "Subgraph ref test",
"provider": "openai",
"model": "gpt-4",
"route": {
"nodes": [
{
"id": "main",
"type": "agent",
"name": "Main",
"description": "Main agent",
"config": {},
},
{
"id": "review",
"type": "subgraph",
"name": "Review",
"description": "Subgraph review",
"actor_ref": "local/code-reviewer",
},
],
"edges": [{"from_node": "main", "to_node": "review"}],
"entry_node": "main",
"exit_nodes": ["review"],
},
}
config = ActorConfigSchema.model_validate(data)
node = config.route.nodes[1] # type: ignore[union-attr]
print(f"actor-ref: {node.actor_ref}")
return 0
def _reject_bad_lsp() -> int:
data = {
"name": "local/bad-lsp-test",
"type": "graph",
"description": "Bad LSP test",
"provider": "openai",
"model": "gpt-4",
"route": {
"nodes": [
{
"id": "n",
"type": "agent",
"name": "N",
"description": "Bad LSP",
"config": {},
"lsp_binding": {"server": "no-slash"},
},
],
"edges": [],
"entry_node": "n",
"exit_nodes": ["n"],
},
}
try:
ActorConfigSchema.model_validate(data)
print("validation-unexpected-success")
return 1
except Exception as exc:
err = str(exc).lower()
if "lsp_binding" in err:
print("validation-error: lsp_binding")
else:
print(f"validation-error-other: {exc}")
return 0
if __name__ == "__main__":
sys.exit(main())