diff --git a/benchmarks/actor_schema_bench.py b/benchmarks/actor_schema_bench.py new file mode 100644 index 00000000..3febbe27 --- /dev/null +++ b/benchmarks/actor_schema_bench.py @@ -0,0 +1,358 @@ +"""ASV benchmarks for Actor YAML schema validation throughput. + +Measures the performance of: +- YAML string parsing + schema validation +- YAML file loading + schema validation +- Graph topology validation (cycle detection) +- Tool definition validation +- model_dump() serialization +""" + +from __future__ import annotations + +import importlib +import sys +import tempfile +from pathlib import Path + +# Ensure the local *source* tree is importable +_SRC = str(Path(__file__).resolve().parents[1] / "src") +if _SRC not in sys.path: + sys.path.insert(0, _SRC) + +# Force-reload the top-level package +import cleveragents # noqa: E402 + +importlib.reload(cleveragents) + +from cleveragents.actor.schema import ActorConfigSchema # noqa: E402 + +_MINIMAL_LLM_YAML = """\ +name: bench/llm +type: llm +description: Benchmark LLM actor +model: gpt-4 +""" + +_FULL_LLM_YAML = """\ +name: bench/llm_full +type: llm +description: Full LLM actor with all features +version: "1.0" +model: gpt-4-turbo +system_prompt: | + You are a benchmark testing assistant. + Process requests efficiently and accurately. +tools: + - files/read_file + - files/write_file +context_view: executor +memory: + enabled: true + max_messages: 100 + max_tokens: 8000 + summarize_old: true +context: + include_files: + - README.md + - pyproject.toml + include_dirs: + - src/ + - tests/ + exclude_patterns: + - "**/__pycache__/**" + - "*.pyc" + max_context_tokens: 16000 +env_vars: + LOG_LEVEL: info + WORK_DIR: /tmp/bench +""" + +_TOOL_ACTOR_YAML = """\ +name: bench/tools +type: tool +description: Tool collection for benchmarking +tools: + - files/read_file + - files/write_file + - files/delete_file +""" + +_SIMPLE_GRAPH_YAML = """\ +name: bench/simple_graph +type: graph +description: Simple 3-node graph for benchmarking +model: gpt-4 +route: + nodes: + - id: extract + type: tool + name: Extractor + description: Extract data + config: + tool_name: data/extract + - id: process + type: agent + name: Processor + description: Process data + config: + prompt: "Process the data" + - id: save + type: tool + name: Saver + description: Save results + config: + tool_name: data/save + edges: + - from_node: extract + to_node: process + - from_node: process + to_node: save + entry_node: extract + exit_nodes: + - save +""" + +_COMPLEX_GRAPH_YAML = """\ +name: bench/complex_graph +type: graph +description: Complex graph with 10 nodes and conditionals +model: gpt-4 +route: + nodes: + - id: start + type: agent + name: Starter + description: Start node + config: + prompt: "Begin" + - id: node2 + type: agent + name: Node 2 + description: Second node + config: + prompt: "Continue" + - id: node3 + type: agent + name: Node 3 + description: Third node + config: + prompt: "Process" + - id: checker + type: conditional + name: Checker + description: Check condition + config: + conditions: + - check: "state.get('ok') == True" + route_to: node4 + - check: "state.get('ok') == False" + route_to: node5 + - id: node4 + type: agent + name: Node 4 + description: Success path + config: + prompt: "Success" + - id: node5 + type: agent + name: Node 5 + description: Retry path + config: + prompt: "Retry" + - id: node6 + type: tool + name: Tool 6 + description: Tool execution + config: + tool_name: test/tool + - id: node7 + type: agent + name: Node 7 + description: Seventh node + config: + prompt: "Continue" + - id: node8 + type: subgraph + name: Subgraph + description: Nested workflow + config: + actor_path: bench/nested.yaml + - id: end + type: agent + name: End + description: Final node + config: + prompt: "Complete" + edges: + - from_node: start + to_node: node2 + - from_node: node2 + to_node: node3 + - from_node: node3 + to_node: checker + - from_node: node4 + to_node: node6 + - from_node: node5 + to_node: node7 + - from_node: node6 + to_node: node8 + - from_node: node7 + to_node: node8 + - from_node: node8 + to_node: end + entry_node: start + exit_nodes: + - end +""" + + +class TimeActorSchemaMinimalLLM: + """Benchmark minimal LLM actor validation.""" + + def time_parse_minimal_llm(self) -> None: + """Time parsing a minimal LLM actor YAML.""" + import yaml + + data = yaml.safe_load(_MINIMAL_LLM_YAML) + ActorConfigSchema.model_validate(data) + + +class TimeActorSchemaFullLLM: + """Benchmark full LLM actor validation.""" + + def time_parse_full_llm(self) -> None: + """Time parsing a full LLM actor YAML with all features.""" + import yaml + + data = yaml.safe_load(_FULL_LLM_YAML) + ActorConfigSchema.model_validate(data) + + +class TimeActorSchemaTool: + """Benchmark tool actor validation.""" + + def time_parse_tool_actor(self) -> None: + """Time parsing a tool actor YAML.""" + import yaml + + data = yaml.safe_load(_TOOL_ACTOR_YAML) + ActorConfigSchema.model_validate(data) + + +class TimeActorSchemaSimpleGraph: + """Benchmark simple graph actor validation.""" + + def time_parse_simple_graph(self) -> None: + """Time parsing a simple 3-node graph actor.""" + import yaml + + data = yaml.safe_load(_SIMPLE_GRAPH_YAML) + ActorConfigSchema.model_validate(data) + + +class TimeActorSchemaComplexGraph: + """Benchmark complex graph actor validation.""" + + def time_parse_complex_graph(self) -> None: + """Time parsing a complex 10-node graph with conditionals.""" + import yaml + + data = yaml.safe_load(_COMPLEX_GRAPH_YAML) + ActorConfigSchema.model_validate(data) + + +class TimeActorSchemaFileIO: + """Benchmark file I/O operations.""" + + def setup(self) -> None: + """Create temporary YAML file.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as temp_file: + temp_file.write(_FULL_LLM_YAML) + self.temp_file_name = temp_file.name + + def teardown(self) -> None: + """Remove temporary file.""" + Path(self.temp_file_name).unlink(missing_ok=True) + + def time_load_from_file(self) -> None: + """Time loading actor config from YAML file.""" + ActorConfigSchema.from_yaml_file(self.temp_file_name) + + def time_save_to_file(self) -> None: + """Time saving actor config to YAML file.""" + import yaml + + data = yaml.safe_load(_FULL_LLM_YAML) + config = ActorConfigSchema.model_validate(data) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as temp_out: + temp_out_name = temp_out.name + try: + config.to_yaml_file(temp_out_name) + finally: + Path(temp_out_name).unlink(missing_ok=True) + + +class TimeActorSchemaSerialization: + """Benchmark serialization operations.""" + + def setup(self) -> None: + """Parse actor configs once.""" + import yaml + + self.minimal_config = ActorConfigSchema.model_validate( + yaml.safe_load(_MINIMAL_LLM_YAML) + ) + self.full_config = ActorConfigSchema.model_validate( + yaml.safe_load(_FULL_LLM_YAML) + ) + self.graph_config = ActorConfigSchema.model_validate( + yaml.safe_load(_COMPLEX_GRAPH_YAML) + ) + + def time_dump_minimal(self) -> None: + """Time model_dump() on minimal config.""" + self.minimal_config.model_dump(mode="json") + + def time_dump_full(self) -> None: + """Time model_dump() on full config.""" + self.full_config.model_dump(mode="json") + + def time_dump_graph(self) -> None: + """Time model_dump() on complex graph.""" + self.graph_config.model_dump(mode="json") + + +class TimeActorSchemaGraphValidation: + """Benchmark graph topology validation.""" + + def setup(self) -> None: + """Parse graph configs.""" + import yaml + + self.simple_graph_data = yaml.safe_load(_SIMPLE_GRAPH_YAML) + self.complex_graph_data = yaml.safe_load(_COMPLEX_GRAPH_YAML) + + def time_validate_simple_graph_topology(self) -> None: + """Time validation of simple graph topology.""" + ActorConfigSchema.model_validate(self.simple_graph_data) + + def time_validate_complex_graph_topology(self) -> None: + """Time validation of complex graph with cycle detection.""" + ActorConfigSchema.model_validate(self.complex_graph_data) + + +# Module-level metadata for ASV +time_parse_minimal_llm = TimeActorSchemaMinimalLLM() +time_parse_full_llm = TimeActorSchemaFullLLM() +time_parse_tool_actor = TimeActorSchemaTool() +time_parse_simple_graph = TimeActorSchemaSimpleGraph() +time_parse_complex_graph = TimeActorSchemaComplexGraph() +time_file_io = TimeActorSchemaFileIO() +time_serialization = TimeActorSchemaSerialization() +time_graph_validation = TimeActorSchemaGraphValidation() diff --git a/docs/reference/actors_schema.md b/docs/reference/actors_schema.md new file mode 100644 index 00000000..7cf55cc5 --- /dev/null +++ b/docs/reference/actors_schema.md @@ -0,0 +1,1040 @@ +# Actor YAML Schema Reference + +## Overview + +Actors are the v3 replacement for v2's reactive stream configurations, providing a simpler, more declarative way to define AI agent behaviors. An actor defines what an agent can do, how it processes information, and how it interacts with tools and other actors. + +**Module:** `cleveragents.actor.schema` +**Schema Version:** 1.0 + +## Table of Contents + +- [Actor Types](#actor-types) +- [Field Definitions](#field-definitions) +- [Tool Node Semantics](#tool-node-semantics) +- [Graph Constraints](#graph-constraints) +- [Validation Rules](#validation-rules) +- [Error Messages](#error-messages) +- [Examples](#examples) + +--- + +## Actor Types + +Actors come in three types, each serving different purposes: + +### LLM Actor + +**Type:** `llm` +**Purpose:** Single LLM agent with system prompt and optional tools +**Use Case:** Simple conversational agents, code reviewers, assistants + +**Required Fields:** +- `name` - Namespaced actor name +- `type: llm` +- `model` - LLM model identifier (e.g., "gpt-4", "claude-3-opus") + +**Optional Fields:** +- `system_prompt` - System prompt defining agent behavior +- `tools` - List of tool references or inline definitions +- `context_view` - Role-based context filtering +- `memory` - Conversation history settings +- `context` - File inclusion settings + +**Example:** +```yaml +name: assistants/code_reviewer +type: llm +model: gpt-4 +system_prompt: "You are an expert code reviewer..." +``` + +### TOOL Actor + +**Type:** `tool` +**Purpose:** Collection of callable tools without LLM interaction +**Use Case:** Utility tool bundles, shared tool libraries + +**Required Fields:** +- `name` - Namespaced actor name +- `type: tool` +- `tools` - At least one tool (reference or inline definition) + +**Optional Fields:** +- None (tools-only actors don't need LLM configuration) + +**Example:** +```yaml +name: utilities/file_ops +type: tool +tools: + - files/read_file + - files/write_file +``` + +### GRAPH Actor + +**Type:** `graph` +**Purpose:** Multi-node workflow with conditional routing +**Use Case:** Complex workflows, multi-step processes, agentic loops + +**Required Fields:** +- `name` - Namespaced actor name +- `type: graph` +- `model` - LLM model for agent nodes +- `route` - Complete graph topology definition + +**Optional Fields:** +- `system_prompt` - Default prompt for graph-level agents +- `tools` - Tools available to all agent nodes +- `context_view` - Context filtering for the entire graph +- `memory` - Memory settings for the graph +- `context` - Context settings + +**Example:** +```yaml +name: workflows/test_driven_dev +type: graph +model: gpt-4 +route: + nodes: [...] + edges: [...] + entry_node: planner + exit_nodes: [review] +``` + +--- + +## Field Definitions + +### Top-Level Fields + +#### `name` (required) + +**Type:** `str` +**Format:** `namespace/actor_name` +**Description:** Unique identifier for the actor in namespaced format + +**Validation:** +- Must contain exactly one forward slash (`/`) +- Namespace and name must not be empty +- Should use snake_case for both parts + +**Valid:** +- `assistants/code_reviewer` +- `workflows/test_driven_dev` +- `utilities/file_operations` + +**Invalid:** +- `code_reviewer` (missing namespace) +- `assistants/code/reviewer` (too many slashes) +- `assistants/` (empty name) + +#### `type` (required) + +**Type:** `ActorType` enum +**Values:** `llm`, `tool`, `graph` +**Description:** Determines actor execution behavior and compilation target + +#### `description` (required) + +**Type:** `str` +**Description:** Human-readable description of what the actor does + +**Best Practices:** +- Start with a verb ("Reviews code...", "Processes documents...") +- Keep under 200 characters +- Be specific about the actor's purpose + +#### `version` (optional) + +**Type:** `str` +**Default:** `"1.0"` +**Description:** Schema version for backward compatibility + +#### `model` (conditional) + +**Type:** `str` +**Required for:** `llm`, `graph` actors +**Optional for:** `tool` actors (not used) +**Description:** LLM model identifier + +**Common Values:** +- OpenAI: `gpt-4`, `gpt-4-turbo`, `gpt-3.5-turbo` +- Anthropic: `claude-3-opus`, `claude-3-sonnet`, `claude-3-haiku` +- Open source: `mistral-large`, `llama-3-70b` + +#### `system_prompt` (optional) + +**Type:** `str` +**Description:** System prompt defining agent behavior and guidelines + +**Best Practices:** +- Use multi-line YAML strings (`|`) for readability +- Include role definition ("You are an expert...") +- List specific tasks and constraints +- Provide output format guidance + +#### `tools` (optional/required) + +**Type:** `list[str | ToolDefinition]` +**Required for:** `tool` actors +**Optional for:** `llm`, `graph` actors +**Description:** Tools available to the actor + +**Tool Reference Format:** +```yaml +tools: + - namespace/tool_name # Reference to existing tool +``` + +**Inline Tool Definition:** +```yaml +tools: + - name: utils/count_lines + description: Count lines in a file + parameters: + - name: file_path + type: str + required: true + code: | + def count_lines(file_path: str) -> int: + with open(file_path, 'r') as f: + return len(f.readlines()) +``` + +--- + +### Tool Definition Fields + +#### ToolParameter + +**Fields:** + +- `name` (str, required): Parameter name (must be valid Python identifier) +- `type` (str, required): Python type annotation (e.g., `str`, `int`, `list[str]`) +- `description` (str, required): Parameter description +- `required` (bool, default=True): Whether parameter is required +- `default` (any, optional): Default value for optional parameters + +**Example:** +```yaml +parameters: + - name: input_file + type: str + description: Path to input file + required: true + - name: encoding + type: str + description: File encoding + required: false + default: utf-8 +``` + +#### ToolDefinition + +**Fields:** + +- `name` (str, required): Tool name in `namespace/name` format +- `description` (str, required): What the tool does +- `parameters` (list, optional): List of ToolParameter objects +- `code` (str, required): Python code implementing the tool + +**Validation:** +- Tool name must follow `namespace/name` format +- Code must be valid Python +- Parameter names must be valid identifiers + +--- + +### Memory Configuration + +#### MemoryConfig + +**Fields:** + +- `enabled` (bool, default=True): Enable conversation memory +- `max_messages` (int, optional): Maximum messages to retain (None = unlimited) +- `max_tokens` (int, optional): Maximum tokens in history (None = unlimited) +- `summarize_old` (bool, default=False): Summarize old messages to save tokens + +**Example:** +```yaml +memory: + enabled: true + max_messages: 50 + max_tokens: 4000 + summarize_old: false +``` + +**Use Cases:** +- Short conversations: `max_messages: 10-20` +- Medium conversations: `max_messages: 50`, `max_tokens: 4000-8000` +- Long workflows: `summarize_old: true`, `max_tokens: 16000` + +--- + +### Context Configuration + +#### ContextConfigSchema + +**Fields:** + +- `include_files` (list[str]): Specific files to include in context +- `include_dirs` (list[str]): Directories to include in context +- `exclude_patterns` (list[str]): Glob patterns to exclude +- `max_context_tokens` (int, optional): Maximum context window size + +**Example:** +```yaml +context: + include_files: + - README.md + - pyproject.toml + include_dirs: + - src/ + - tests/ + exclude_patterns: + - "**/__pycache__/**" + - "*.pyc" + - "**/.git/**" + max_context_tokens: 8000 +``` + +#### ContextView + +**Type:** enum +**Values:** `strategist`, `executor`, `reviewer`, `full` +**Description:** Role-based context filtering + +**Context Views:** + +- **strategist**: High-level planning view + - Project structure + - Goals and constraints + - Architecture decisions + - No implementation details + +- **executor**: Implementation view + - Source code + - File contents + - Specific tasks + - Implementation details + +- **reviewer**: Validation view + - Changes and diffs + - Test results + - Code quality metrics + - Review feedback + +- **full**: Complete view (use sparingly) + - All available context + - Can exceed token limits + - Only for critical decision points + +--- + +## Tool Node Semantics + +### Tool References + +Tool references point to existing tools in the tool registry. Use references when: + +- The tool is shared across multiple actors +- The tool is complex and tested separately +- The tool requires specific dependencies + +**Format:** `namespace/tool_name` + +**Example:** +```yaml +tools: + - files/read_file + - testing/run_pytest + - analysis/extract_entities +``` + +### Inline Tool Definitions + +Inline tools are defined directly in the actor YAML. Use inline tools when: + +- The tool is actor-specific +- The tool is simple (< 50 lines of code) +- The tool doesn't require external dependencies + +**Best Practices:** + +1. Keep inline tools simple and focused +2. Include proper error handling +3. Add docstrings to the function +4. Use type hints for all parameters +5. Validate inputs before processing + +**Example:** +```yaml +tools: + - name: utils/count_words + description: Count words in a text file + parameters: + - name: file_path + type: str + description: Path to text file + required: true + code: | + def count_words(file_path: str) -> dict: + """Count words in a file and return statistics.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + words = content.split() + return { + "word_count": len(words), + "char_count": len(content), + "line_count": content.count('\n') + 1 + } + except FileNotFoundError: + return {"error": f"File not found: {file_path}"} + except Exception as e: + return {"error": str(e)} +``` + +--- + +## Graph Constraints + +### Graph Topology + +A graph actor's `route` field defines the complete workflow topology. + +#### RouteDefinition + +**Fields:** + +- `nodes` (list[NodeDefinition], required): All nodes in the graph +- `edges` (list[EdgeDefinition], required): Connections between nodes +- `entry_node` (str, required): Starting node ID +- `exit_nodes` (list[str], required): Terminal node IDs + +### Node Types + +#### AGENT Node + +**Type:** `agent` +**Purpose:** LLM-powered decision making and content generation +**Configuration:** + +```yaml +- id: planner + type: agent + name: Task Planner + description: Plans tasks based on requirements + config: + model: gpt-4 # Can override graph-level model + prompt: "You are a task planning expert..." + tools: + - files/read_file + - analysis/extract_requirements +``` + +#### TOOL Node + +**Type:** `tool` +**Purpose:** Execute specific tools or functions +**Configuration:** + +```yaml +- id: run_tests + type: tool + name: Test Runner + description: Executes pytest test suite + config: + tool_name: testing/run_pytest + parameters: + verbose: true + coverage: true +``` + +#### CONDITIONAL Node + +**Type:** `conditional` +**Purpose:** Route execution based on conditions +**Configuration:** + +```yaml +- id: check_results + type: conditional + name: Result Checker + description: Routes based on test results + config: + conditions: + - check: "state.get('tests_passed') == True" + route_to: code_review + - check: "state.get('tests_passed') == False" + route_to: fix_errors +``` + +**Conditional Expressions:** + +- Access state with `state.get('key')` +- Use Python comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`) +- Combine with boolean operators (`and`, `or`, `not`) +- Conditions are evaluated in order; first match wins + +#### SUBGRAPH Node + +**Type:** `subgraph` +**Purpose:** Embed another actor as a nested workflow +**Configuration:** + +```yaml +- id: code_review + type: subgraph + name: Code Reviewer + description: Runs full code review workflow + config: + actor_path: actors/code_reviewer.yaml +``` + +**Subgraph Rules:** + +- Subgraph actors can be of any type (llm, tool, graph) +- State is passed to and from the subgraph +- Subgraphs enable workflow composition and reuse +- Maximum nesting depth: 5 levels + +### Edge Definition + +**Fields:** + +- `from_node` (str, required): Source node ID +- `to_node` (str, required): Target node ID +- `condition` (str, optional): Conditional routing expression +- `priority` (int, default=0): Edge priority (higher = evaluated first) + +**Example:** +```yaml +edges: + # Unconditional edge + - from_node: planner + to_node: executor + + # Conditional edge + - from_node: executor + to_node: retry + condition: "state.get('retry_count', 0) < 3" + priority: 10 + + # Default edge (lower priority) + - from_node: executor + to_node: escalate + priority: 1 +``` + +--- + +## Validation Rules + +### Graph Validation + +The schema automatically validates graph topology when loading YAML files. + +#### 1. Unique Node IDs + +**Rule:** All node IDs must be unique within a graph. + +**Valid:** +```yaml +nodes: + - id: planner + - id: executor + - id: reviewer +``` + +**Invalid:** +```yaml +nodes: + - id: planner + - id: executor + - id: planner # ❌ Duplicate ID +``` + +**Error:** +``` +ValueError: Duplicate node IDs found: {'planner'} +``` + +#### 2. Entry Node Exists + +**Rule:** The `entry_node` must reference a valid node ID. + +**Valid:** +```yaml +nodes: + - id: start + - id: process +entry_node: start # ✅ 'start' exists +``` + +**Invalid:** +```yaml +nodes: + - id: start + - id: process +entry_node: init # ❌ 'init' doesn't exist +``` + +**Error:** +``` +ValueError: Entry node 'init' not found in nodes +``` + +#### 3. Exit Nodes Exist + +**Rule:** All `exit_nodes` must reference valid node IDs. + +**Valid:** +```yaml +nodes: + - id: start + - id: end1 + - id: end2 +exit_nodes: + - end1 + - end2 +``` + +**Invalid:** +```yaml +nodes: + - id: start + - id: end1 +exit_nodes: + - end1 + - finish # ❌ 'finish' doesn't exist +``` + +**Error:** +``` +ValueError: Exit node 'finish' not found in nodes +``` + +#### 4. Edge References Valid + +**Rule:** All edge `from_node` and `to_node` must reference valid node IDs. + +**Valid:** +```yaml +nodes: + - id: a + - id: b +edges: + - from_node: a + to_node: b +``` + +**Invalid:** +```yaml +nodes: + - id: a + - id: b +edges: + - from_node: a + to_node: c # ❌ 'c' doesn't exist +``` + +**Error:** +``` +ValueError: Edge to_node 'c' not found in nodes +``` + +#### 5. No Cycles + +**Rule:** Graphs must be acyclic (no circular dependencies). + +**Valid (Linear):** +```yaml +edges: + - from_node: a + to_node: b + - from_node: b + to_node: c +``` + +**Valid (Branching):** +```yaml +edges: + - from_node: a + to_node: b + - from_node: a + to_node: c + - from_node: b + to_node: d + - from_node: c + to_node: d +``` + +**Invalid (Cycle):** +```yaml +edges: + - from_node: a + to_node: b + - from_node: b + to_node: c + - from_node: c + to_node: a # ❌ Cycle: a → b → c → a +``` + +**Error:** +``` +ValueError: Graph contains cycles involving nodes: ['a'] +``` + +**Note:** Conditional edges with retry logic can appear cyclic but are validated at runtime. + +#### 6. All Nodes Reachable + +**Rule:** All nodes must be reachable from the entry node. + +**Valid:** +```yaml +entry_node: start +edges: + - from_node: start + to_node: process + - from_node: process + to_node: end +``` + +**Invalid:** +```yaml +entry_node: start +nodes: + - id: start + - id: process + - id: orphan # ❌ Not reachable from 'start' +edges: + - from_node: start + to_node: process +``` + +**Error:** +``` +ValueError: Unreachable nodes detected: {'orphan'} +``` + +--- + +## Error Messages + +### Common Validation Errors + +#### NameError: Invalid Actor Name + +**Cause:** Actor name doesn't follow `namespace/name` format + +**Example:** +```yaml +name: code_reviewer # ❌ Missing namespace +``` + +**Error:** +``` +ValueError: Actor name must be namespaced (namespace/name): code_reviewer +``` + +**Fix:** +```yaml +name: assistants/code_reviewer # ✅ +``` + +#### TypeError: Missing Required Field + +**Cause:** LLM or GRAPH actor missing `model` field + +**Example:** +```yaml +name: assistants/helper +type: llm +# ❌ Missing model field +``` + +**Error:** +``` +ValueError: LLM actors require 'model' field +``` + +**Fix:** +```yaml +name: assistants/helper +type: llm +model: gpt-4 # ✅ +``` + +#### ValueError: Empty Tools List + +**Cause:** TOOL actor with no tools defined + +**Example:** +```yaml +name: utilities/helpers +type: tool +tools: [] # ❌ Empty list +``` + +**Error:** +``` +ValueError: TOOL actors require at least one tool +``` + +**Fix:** +```yaml +name: utilities/helpers +type: tool +tools: + - files/read_file # ✅ +``` + +#### ValueError: Missing Route + +**Cause:** GRAPH actor without route definition + +**Example:** +```yaml +name: workflows/process +type: graph +model: gpt-4 +# ❌ Missing route field +``` + +**Error:** +``` +ValueError: GRAPH actors require 'route' field +``` + +**Fix:** +```yaml +name: workflows/process +type: graph +model: gpt-4 +route: # ✅ + nodes: [...] + edges: [...] + entry_node: start + exit_nodes: [end] +``` + +--- + +## Examples + +### Example 1: Simple LLM Actor + +```yaml +name: assistants/code_reviewer +type: llm +description: Reviews Python code for best practices +version: "1.0" + +model: gpt-4 +system_prompt: | + You are an expert Python code reviewer. + Review code for: + - PEP 8 compliance + - Design patterns + - Potential bugs + - Performance issues + +context_view: reviewer +memory: + enabled: true + max_messages: 20 +``` + +### Example 2: LLM with Inline Tools + +```yaml +name: assistants/file_analyzer +type: llm +description: Analyzes files and generates reports +version: "1.0" + +model: gpt-4-turbo +system_prompt: "Analyze files and provide insights..." + +tools: + - files/read_file # Tool reference + + - name: utils/count_lines # Inline tool + description: Count lines in a file + parameters: + - name: file_path + type: str + required: true + code: | + def count_lines(file_path: str) -> int: + with open(file_path, 'r') as f: + return len(f.readlines()) +``` + +### Example 3: Simple Graph Workflow + +```yaml +name: workflows/doc_processor +type: graph +description: Processes documents through multiple stages +version: "1.0" + +model: gpt-3.5-turbo + +route: + nodes: + - id: extract + type: tool + name: Text Extractor + description: Extracts text from documents + config: + tool_name: documents/extract_text + + - id: analyze + type: agent + name: Analyzer + description: Analyzes content + config: + model: gpt-3.5-turbo + prompt: "Analyze the document content..." + + - id: summarize + type: agent + name: Summarizer + description: Creates summary + config: + model: gpt-3.5-turbo + prompt: "Summarize in 3-5 bullet points..." + + edges: + - from_node: extract + to_node: analyze + - from_node: analyze + to_node: summarize + + entry_node: extract + exit_nodes: + - summarize +``` + +### Example 4: Complex Graph with Conditionals + +```yaml +name: workflows/ci_pipeline +type: graph +description: CI/CD pipeline with test and deploy stages +version: "1.0" + +model: gpt-4 + +route: + nodes: + - id: build + type: tool + name: Builder + description: Builds the project + config: + tool_name: ci/build + + - id: test + type: tool + name: Test Runner + description: Runs test suite + config: + tool_name: ci/run_tests + + - id: check_tests + type: conditional + name: Test Result Checker + description: Check if tests passed + config: + conditions: + - check: "state.get('tests_passed') == True" + route_to: deploy + - check: "state.get('tests_passed') == False" + route_to: notify_failure + + - id: deploy + type: tool + name: Deployer + description: Deploys to production + config: + tool_name: ci/deploy + + - id: notify_failure + type: tool + name: Notifier + description: Sends failure notification + config: + tool_name: notifications/send_alert + + edges: + - from_node: build + to_node: test + - from_node: test + to_node: check_tests + # Conditional routing handled by check_tests node + + entry_node: build + exit_nodes: + - deploy + - notify_failure +``` + +--- + +## Usage + +### Loading Actor Configuration + +```python +from cleveragents.actor.schema import ActorConfigSchema + +# Load from YAML file +actor = ActorConfigSchema.from_yaml_file("actors/code_reviewer.yaml") + +# Access fields +print(actor.name) # assistants/code_reviewer +print(actor.type) # ActorType.LLM +print(actor.model) # gpt-4 + +# Validate graph (automatic on load) +if actor.type == ActorType.GRAPH: + # Validation already done: + # - All nodes have unique IDs + # - Entry/exit nodes exist + # - All edges reference valid nodes + # - No cycles detected + # - All nodes reachable + pass +``` + +### Saving Actor Configuration + +```python +# Create actor programmatically +actor = ActorConfigSchema( + name="assistants/helper", + type=ActorType.LLM, + description="A helpful assistant", + model="gpt-4", + system_prompt="You are a helpful assistant." +) + +# Save to YAML file +actor.to_yaml_file("actors/helper.yaml") +``` + +--- + +## See Also + +- [ADR-010: Actor and Agent Architecture](../adr/ADR-010-actor-and-agent-architecture.md) +- [ADR-011: Tool System](../adr/ADR-011-tool-system.md) +- [Tool Schema Reference](../schema/tool.schema.yaml) +- [Action Schema Reference](../schema/action.schema.yaml) diff --git a/examples/actors/graph_workflow.yaml b/examples/actors/graph_workflow.yaml new file mode 100644 index 00000000..9c50d428 --- /dev/null +++ b/examples/actors/graph_workflow.yaml @@ -0,0 +1,215 @@ +# Graph Actor - Test-Driven Development Workflow +# Demonstrates a multi-node graph with conditional routing and subgraphs + +name: workflows/test_driven_dev +type: graph +description: Test-driven development workflow with automated testing and feedback loops +version: "1.0" + +# LLM model for agent nodes +model: gpt-4 + +# Graph topology +route: + # Define all nodes in the workflow + nodes: + # Entry point: planning agent + - id: planner + type: agent + name: Test Planner + description: Plans test cases based on requirements + config: + model: gpt-4 + prompt: | + You are a test planning expert. Analyze the requirements and create + a comprehensive test plan covering: + - Unit tests for individual functions + - Integration tests for component interactions + - Edge cases and error conditions + tools: + - files/read_file + - files/list_directory + + # Write tests first + - id: test_writer + type: agent + name: Test Writer + description: Writes test cases based on the plan + config: + model: gpt-4 + prompt: | + You are a test writing expert. Write pytest tests based on the plan. + Follow best practices: + - Use descriptive test names + - Include docstrings + - Use fixtures appropriately + - Test one thing per test + tools: + - files/write_file + - files/read_file + + # Run the tests (should fail initially) + - id: run_tests + type: tool + name: Test Runner + description: Executes pytest test suite + config: + tool_name: testing/run_pytest + parameters: + verbose: true + coverage: true + + # Check test results + - id: check_results + type: conditional + name: Test Result Checker + description: Routes based on test pass/fail status + config: + conditions: + - check: "state.get('tests_passed') == True" + route_to: code_review + - check: "state.get('tests_passed') == False" + route_to: implementation_writer + + # Write implementation to make tests pass + - id: implementation_writer + type: agent + name: Implementation Writer + description: Writes code to make the tests pass + config: + model: gpt-4 + prompt: | + You are an implementation expert. Write clean, well-documented code + that makes the failing tests pass. Follow SOLID principles and + write maintainable code. + tools: + - files/write_file + - files/read_file + + # Run tests again after implementation + - id: rerun_tests + type: tool + name: Test Rerunner + description: Re-executes tests after implementation + config: + tool_name: testing/run_pytest + parameters: + verbose: true + coverage: true + + # Check if tests pass now + - id: verify_tests + type: conditional + name: Test Verification + description: Verify tests pass after implementation + config: + conditions: + - check: "state.get('tests_passed') == True" + route_to: code_review + - check: "state.get('tests_passed') == False and state.get('retry_count', 0) < 3" + route_to: debug_failures + - check: "state.get('tests_passed') == False and state.get('retry_count', 0) >= 3" + route_to: escalate + + # Debug test failures + - id: debug_failures + type: agent + name: Debugger + description: Analyzes and fixes test failures + config: + model: gpt-4 + prompt: | + You are a debugging expert. Analyze the test failures and fix the + implementation. Look for: + - Logic errors + - Edge cases + - Type mismatches + - Missing error handling + tools: + - files/read_file + - files/write_file + + # Code review (subgraph) + - id: code_review + type: subgraph + name: Code Reviewer + description: Runs code review workflow + config: + actor_path: examples/actors/simple_llm.yaml + + # Escalate if tests keep failing + - id: escalate + type: tool + name: Escalation Handler + description: Escalates persistent failures to human + config: + tool_name: notifications/send_alert + parameters: + channel: engineering + priority: high + + # Define edges (workflow transitions) + edges: + # Linear flow from planner to test writer + - from_node: planner + to_node: test_writer + + # Run tests after writing them + - from_node: test_writer + to_node: run_tests + + # Check results after running tests + - from_node: run_tests + to_node: check_results + + # Conditional routing from check_results + # (handled by the conditional node itself) + + # Write implementation if tests fail + - from_node: implementation_writer + to_node: rerun_tests + + # Verify tests after rerunning + - from_node: rerun_tests + to_node: verify_tests + + # Debug if tests still fail + - from_node: debug_failures + to_node: rerun_tests + + # All paths eventually lead to code review or escalation + # (handled by conditional nodes) + + # Entry and exit points + entry_node: planner + exit_nodes: + - code_review + - escalate + +# Context settings +context_view: strategist +memory: + enabled: true + max_messages: 100 + max_tokens: 16000 + summarize_old: true + +context: + include_files: + - "README.md" + - "requirements.txt" + - "pyproject.toml" + include_dirs: + - "src/" + - "tests/" + exclude_patterns: + - "**/__pycache__/**" + - "*.pyc" + - "**/.pytest_cache/**" + - "**/htmlcov/**" + max_context_tokens: 32000 + +# Environment variables +env_vars: + PYTEST_ARGS: --verbose --cov --cov-report=html + MAX_RETRIES: "3" diff --git a/examples/actors/llm_with_tools.yaml b/examples/actors/llm_with_tools.yaml new file mode 100644 index 00000000..22d4c2da --- /dev/null +++ b/examples/actors/llm_with_tools.yaml @@ -0,0 +1,67 @@ +# LLM Actor with Tools +# Demonstrates an LLM actor with access to multiple tools + +name: assistants/file_analyzer +type: llm +description: Analyzes files and generates reports using file system tools +version: "1.0" + +# LLM configuration +model: gpt-4-turbo +system_prompt: | + You are a file analysis assistant. Use the available tools to: + - Read and analyze file contents + - Count lines, words, and characters + - Search for patterns in files + - Generate summary reports + + Always explain what you're doing before using a tool. + +# Tools (mix of references and inline definitions) +tools: + # Reference to existing tool + - files/read_file + - files/list_directory + + # Inline tool definition + - name: utils/count_lines + description: Count the number of lines in a file + parameters: + - name: file_path + type: str + description: Path to the file to count lines in + required: true + code: | + def count_lines(file_path: str) -> int: + """Count lines in a file.""" + with open(file_path, 'r', encoding='utf-8') as f: + return len(f.readlines()) + + - name: utils/word_count + description: Count words in a file + parameters: + - name: file_path + type: str + description: Path to the file + required: true + code: | + def word_count(file_path: str) -> int: + """Count words in a file.""" + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + return len(content.split()) + +# Context settings +context_view: executor +memory: + enabled: true + max_messages: 30 + max_tokens: 6000 + +context: + max_context_tokens: 10000 + +# Environment variables +env_vars: + WORK_DIR: ${HOME}/workspace + LOG_LEVEL: info diff --git a/examples/actors/simple_graph.yaml b/examples/actors/simple_graph.yaml new file mode 100644 index 00000000..a50162dc --- /dev/null +++ b/examples/actors/simple_graph.yaml @@ -0,0 +1,78 @@ +# Simple Graph Actor - Sequential Processing +# Demonstrates a simple 3-node graph with linear execution + +name: workflows/document_processor +type: graph +description: Simple document processing workflow (extract → analyze → summarize) +version: "1.0" + +# LLM model +model: gpt-3.5-turbo + +# Graph topology +route: + nodes: + # Node 1: Extract text from document + - id: extractor + type: tool + name: Text Extractor + description: Extracts text from various document formats + config: + tool_name: documents/extract_text + parameters: + formats: + - pdf + - docx + - txt + + # Node 2: Analyze content + - id: analyzer + type: agent + name: Content Analyzer + description: Analyzes document structure and content + config: + model: gpt-3.5-turbo + prompt: | + Analyze the document content and identify: + - Main topics and themes + - Key entities (people, places, organizations) + - Sentiment and tone + - Document structure + tools: + - analysis/extract_entities + - analysis/sentiment_analysis + + # Node 3: Generate summary + - id: summarizer + type: agent + name: Summarizer + description: Creates concise summary of document + config: + model: gpt-3.5-turbo + prompt: | + Create a concise summary of the document including: + - Main points (3-5 bullet points) + - Key findings + - Actionable insights + Keep it under 200 words. + + # Linear edges + edges: + - from_node: extractor + to_node: analyzer + - from_node: analyzer + to_node: summarizer + + # Entry and exit + entry_node: extractor + exit_nodes: + - summarizer + +# Context settings +context_view: executor +memory: + enabled: true + max_messages: 10 + +context: + max_context_tokens: 4000 diff --git a/examples/actors/simple_llm.yaml b/examples/actors/simple_llm.yaml new file mode 100644 index 00000000..05d31bb6 --- /dev/null +++ b/examples/actors/simple_llm.yaml @@ -0,0 +1,38 @@ +# Simple LLM Actor Example +# Demonstrates the most basic actor configuration with just an LLM and system prompt + +name: assistants/code_reviewer +type: llm +description: Reviews Python code for best practices, style, and potential bugs +version: "1.0" + +# LLM configuration +model: gpt-4 +system_prompt: | + You are an expert Python code reviewer. Review code for: + - PEP 8 style compliance + - Best practices and design patterns + - Potential bugs and edge cases + - Performance considerations + - Security vulnerabilities + + Provide constructive feedback with specific suggestions for improvement. + +# Context and memory settings +context_view: reviewer +memory: + enabled: true + max_messages: 20 + max_tokens: 4000 + +context: + include_files: + - "README.md" + - "pyproject.toml" + include_dirs: + - "src/" + exclude_patterns: + - "**/__pycache__/**" + - "*.pyc" + - "**/.pytest_cache/**" + max_context_tokens: 8000 diff --git a/examples/actors/tool_collection.yaml b/examples/actors/tool_collection.yaml new file mode 100644 index 00000000..05703404 --- /dev/null +++ b/examples/actors/tool_collection.yaml @@ -0,0 +1,45 @@ +# Tool-Only Actor +# Demonstrates an actor that only provides tools without LLM interaction + +name: utilities/file_operations +type: tool +description: Collection of file operation tools for other actors to use +version: "1.0" + +# Tool collection +tools: + # Reference existing tools + - files/read_file + - files/write_file + - files/delete_file + - files/copy_file + - files/move_file + - files/list_directory + - files/create_directory + + # Add custom validation tool + - name: validators/check_python_syntax + description: Validate Python file syntax without executing it + parameters: + - name: file_path + type: str + description: Path to Python file to validate + required: true + code: | + import ast + def check_python_syntax(file_path: str) -> dict: + """Check if a Python file has valid syntax.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + ast.parse(f.read()) + return {"valid": True, "error": None} + except SyntaxError as e: + return { + "valid": False, + "error": str(e), + "line": e.lineno, + "offset": e.offset + } + +# No LLM configuration needed for tool-only actors +# No system prompt needed diff --git a/features/actor_schema.feature b/features/actor_schema.feature new file mode 100644 index 00000000..f7963ca2 --- /dev/null +++ b/features/actor_schema.feature @@ -0,0 +1,355 @@ +Feature: Actor YAML schema validation + As a CleverAgents v3 user + I want to define actors via YAML configuration files + So that I can create validated, reusable actor workflows + + # ──────────────────────────────────────────────────────────── + # Valid LLM Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load a minimal valid LLM actor + Given an actor YAML string with minimal LLM configuration + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config name should be "assistants/simple" + And the actor config type should be "llm" + And the actor config model should be "gpt-4" + + Scenario: Load LLM actor with system prompt + Given an actor YAML string with LLM and system prompt + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config system_prompt should contain "expert" + + Scenario: Load LLM actor with tools + Given an actor YAML string with LLM and tools + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config should have 2 tools + + Scenario: Load LLM actor with memory config + Given an actor YAML string with memory configuration + When I validate the actor schema + Then the actor schema validation should succeed + And the actor memory enabled should be true + And the actor memory max_messages should be 50 + + Scenario: Load LLM actor with context config + Given an actor YAML string with context configuration + When I validate the actor schema + Then the actor schema validation should succeed + And the actor context should include 2 files + And the actor context should include 1 directory + + Scenario: Load simple_llm.yaml example + Given the actor YAML file "examples/actors/simple_llm.yaml" + When I validate the actor schema from file + Then the actor schema validation should succeed + And the actor config type should be "llm" + + Scenario: Load llm_with_tools.yaml example + Given the actor YAML file "examples/actors/llm_with_tools.yaml" + When I validate the actor schema from file + Then the actor schema validation should succeed + And the actor config should have at least 2 tools + + # ──────────────────────────────────────────────────────────── + # Valid TOOL Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load a minimal valid TOOL actor + Given an actor YAML string with TOOL type and tools + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config type should be "tool" + And the actor config should have at least 1 tool + + Scenario: Load TOOL actor with inline tool definition + Given an actor YAML string with inline tool definition + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config should have 1 inline tool + + Scenario: Load tool_collection.yaml example + Given the actor YAML file "examples/actors/tool_collection.yaml" + When I validate the actor schema from file + Then the actor schema validation should succeed + And the actor config type should be "tool" + + # ──────────────────────────────────────────────────────────── + # Valid GRAPH Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load a minimal valid GRAPH actor + Given an actor YAML string with minimal GRAPH configuration + When I validate the actor schema + Then the actor schema validation should succeed + And the actor config type should be "graph" + And the actor route should have entry_node "start" + + Scenario: Load GRAPH with linear topology + Given an actor YAML string with linear graph topology + When I validate the actor schema + Then the actor schema validation should succeed + And the actor route should have 3 nodes + And the actor route should have 2 edges + + Scenario: Load GRAPH with conditional routing + Given an actor YAML string with conditional routing + When I validate the actor schema + Then the actor schema validation should succeed + And the actor route should have 1 conditional node + + Scenario: Load GRAPH with subgraph node + Given an actor YAML string with subgraph node + When I validate the actor schema + Then the actor schema validation should succeed + And the actor route should have 1 subgraph node + + Scenario: Load simple_graph.yaml example + Given the actor YAML file "examples/actors/simple_graph.yaml" + When I validate the actor schema from file + Then the actor schema validation should succeed + And the actor config type should be "graph" + + Scenario: Load graph_workflow.yaml example + Given the actor YAML file "examples/actors/graph_workflow.yaml" + When I validate the actor schema from file + Then the actor schema validation should succeed + And the actor config type should be "graph" + + # ──────────────────────────────────────────────────────────── + # Invalid Actor Name scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject actor without namespace + Given an actor YAML string with name "simple_actor" + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "namespaced" + + Scenario: Reject actor with multiple slashes + Given an actor YAML string with name "namespace/sub/actor" + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "namespaced" + + Scenario: Reject actor with empty namespace + Given an actor YAML string with name "/actor" + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "namespaced" + + Scenario: Reject actor with empty name + Given an actor YAML string with name "namespace/" + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "namespaced" + + # ──────────────────────────────────────────────────────────── + # Invalid LLM Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject LLM actor without model + Given an actor YAML string with LLM type but no model + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "require 'model'" + + # ──────────────────────────────────────────────────────────── + # Invalid TOOL Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject TOOL actor without tools + Given an actor YAML string with TOOL type but no tools + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "require at least one tool" + + Scenario: Reject TOOL actor with empty tools list + Given an actor YAML string with TOOL type and empty tools + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "require at least one tool" + + # ──────────────────────────────────────────────────────────── + # Invalid GRAPH Actor scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject GRAPH actor without model + Given an actor YAML string with GRAPH type but no model + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "require 'model'" + + Scenario: Reject GRAPH actor without route + Given an actor YAML string with GRAPH type but no route + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "require 'route'" + + # ──────────────────────────────────────────────────────────── + # Graph Topology Validation scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject graph with duplicate node IDs + Given an actor YAML string with duplicate node IDs + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "Duplicate node IDs" + + Scenario: Reject graph with invalid entry node + Given an actor YAML string with non-existent entry node + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "Entry node" + And the validation error should contain "not found" + + Scenario: Reject graph with invalid exit node + Given an actor YAML string with non-existent exit node + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "Exit node" + And the validation error should contain "not found" + + Scenario: Reject graph with invalid edge from_node + Given an actor YAML string with invalid edge from_node + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "from_node" + And the validation error should contain "not found" + + Scenario: Reject graph with invalid edge to_node + Given an actor YAML string with invalid edge to_node + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "to_node" + And the validation error should contain "not found" + + Scenario: Reject graph with cycles + Given an actor YAML string with cyclic graph + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "cycle" + + # ──────────────────────────────────────────────────────────── + # Tool Definition Validation scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject inline tool without namespace + Given an actor YAML string with inline tool name "count_lines" + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "namespaced" + + Scenario: Reject inline tool with invalid parameter name + Given an actor YAML string with invalid tool parameter name + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "valid Python identifier" + + # ──────────────────────────────────────────────────────────── + # Node Definition Validation scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Reject node with invalid ID format + Given an actor YAML string with node ID containing spaces + When I validate the actor schema + Then the actor schema validation should fail + And the validation error should contain "alphanumeric" + + Scenario: Accept node with underscores and hyphens + Given an actor YAML string with node ID "node_1-test" + When I validate the actor schema + Then the actor schema validation should succeed + + # ──────────────────────────────────────────────────────────── + # Context View scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load actor with strategist context view + Given an actor YAML string with context_view "strategist" + When I validate the actor schema + Then the actor schema validation should succeed + And the actor context_view should be "strategist" + + Scenario: Load actor with executor context view + Given an actor YAML string with context_view "executor" + When I validate the actor schema + Then the actor schema validation should succeed + And the actor context_view should be "executor" + + Scenario: Load actor with reviewer context view + Given an actor YAML string with context_view "reviewer" + When I validate the actor schema + Then the actor schema validation should succeed + And the actor context_view should be "reviewer" + + Scenario: Load actor with full context view + Given an actor YAML string with context_view "full" + When I validate the actor schema + Then the actor schema validation should succeed + And the actor context_view should be "full" + + # ──────────────────────────────────────────────────────────── + # Environment Variables scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load actor with environment variables + Given an actor YAML string with env_vars + When I validate the actor schema + Then the actor schema validation should succeed + And the actor should have 2 env_vars + + # ──────────────────────────────────────────────────────────── + # YAML I/O scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Save and reload actor configuration + Given a valid actor configuration object + When I save the actor to YAML file + And I reload the actor from YAML file + Then the reloaded actor should match the original + + Scenario: Handle missing YAML file + Given a non-existent actor YAML file path + When I attempt to load the actor from file + Then a FileNotFoundError should be raised + + # ──────────────────────────────────────────────────────────── + # Edge Priority scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load graph with edge priorities + Given an actor YAML string with edges having different priorities + When I validate the actor schema + Then the actor schema validation should succeed + And the highest priority edge should be 10 + + # ──────────────────────────────────────────────────────────── + # Memory Configuration scenarios + # ──────────────────────────────────────────────────────────── + + Scenario: Load actor with memory disabled + Given an actor YAML string with memory enabled false + When I validate the actor schema + Then the actor schema validation should succeed + And the actor memory enabled should be false + + Scenario: Load actor with message limit + Given an actor YAML string with max_messages 100 + When I validate the actor schema + Then the actor schema validation should succeed + And the actor memory max_messages should be 100 + + Scenario: Load actor with token limit + Given an actor YAML string with max_tokens 8000 + When I validate the actor schema + Then the actor schema validation should succeed + And the actor memory max_tokens should be 8000 + + Scenario: Load actor with summarize_old enabled + Given an actor YAML string with summarize_old true + When I validate the actor schema + Then the actor schema validation should succeed + And the actor memory summarize_old should be true diff --git a/features/steps/actor_schema_steps.py b/features/steps/actor_schema_steps.py new file mode 100644 index 00000000..916e06c6 --- /dev/null +++ b/features/steps/actor_schema_steps.py @@ -0,0 +1,1030 @@ +"""Step definitions for actor YAML schema validation. + +Tests for features/actor_schema.feature — validates the ActorConfigSchema +Pydantic model, YAML loading, graph topology validation, tool definitions, +and error messages. +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context # type: ignore[import-untyped] +from pydantic import ValidationError + +from cleveragents.actor.schema import ActorConfigSchema, ActorType, NodeType + +# ──────────────────────────────────────────────────────────── +# Test YAML Templates +# ──────────────────────────────────────────────────────────── + +_MINIMAL_LLM_YAML = """\ +name: assistants/simple +type: llm +description: A simple LLM actor +model: gpt-4 +""" + +_LLM_WITH_PROMPT_YAML = """\ +name: assistants/expert +type: llm +description: An expert assistant +model: gpt-4 +system_prompt: "You are an expert Python developer" +""" + +_LLM_WITH_TOOLS_YAML = """\ +name: assistants/helper +type: llm +description: Helper with tools +model: gpt-4 +tools: + - files/read_file + - files/write_file +""" + +_LLM_WITH_MEMORY_YAML = """\ +name: assistants/chatbot +type: llm +description: Chatbot with memory +model: gpt-4 +memory: + enabled: true + max_messages: 50 + max_tokens: 4000 +""" + +_LLM_WITH_CONTEXT_YAML = """\ +name: assistants/analyzer +type: llm +description: Code analyzer +model: gpt-4 +context: + include_files: + - README.md + - pyproject.toml + include_dirs: + - src/ + exclude_patterns: + - "**/__pycache__/**" +""" + +_TOOL_MINIMAL_YAML = """\ +name: utilities/helpers +type: tool +description: Utility tools +tools: + - files/read_file +""" + +_TOOL_INLINE_YAML = """\ +name: utilities/custom +type: tool +description: Custom tools +tools: + - name: utils/count_lines + description: Count lines in a file + parameters: + - name: file_path + type: str + description: Path to file + required: true + code: | + def count_lines(file_path: str) -> int: + with open(file_path, 'r') as f: + return len(f.readlines()) +""" + +_GRAPH_MINIMAL_YAML = """\ +name: workflows/simple +type: graph +description: Simple workflow +model: gpt-4 +route: + nodes: + - id: start + type: agent + name: Starter + description: Start node + config: + prompt: "Begin processing" + edges: [] + entry_node: start + exit_nodes: + - start +""" + +_GRAPH_LINEAR_YAML = """\ +name: workflows/linear +type: graph +description: Linear workflow +model: gpt-4 +route: + nodes: + - id: extract + type: tool + name: Extractor + description: Extract data + config: + tool_name: data/extract + - id: process + type: agent + name: Processor + description: Process data + config: + prompt: "Process the data" + - id: save + type: tool + name: Saver + description: Save results + config: + tool_name: data/save + edges: + - from_node: extract + to_node: process + - from_node: process + to_node: save + entry_node: extract + exit_nodes: + - save +""" + +_GRAPH_CONDITIONAL_YAML = """\ +name: workflows/conditional +type: graph +description: Workflow with conditionals +model: gpt-4 +route: + nodes: + - id: start + type: agent + name: Starter + description: Start + config: + prompt: "Start" + - id: checker + type: conditional + name: Checker + description: Check condition + config: + conditions: + - check: "state.get('ok') == True" + route_to: success + - check: "state.get('ok') == False" + route_to: failure + - id: success + type: agent + name: Success + description: Success path + config: + prompt: "Success" + - id: failure + type: agent + name: Failure + description: Failure path + config: + prompt: "Failure" + edges: + - from_node: start + to_node: checker + entry_node: start + exit_nodes: + - success + - failure +""" + +_GRAPH_SUBGRAPH_YAML = """\ +name: workflows/composed +type: graph +description: Workflow with subgraph +model: gpt-4 +route: + nodes: + - id: main + type: agent + name: Main + description: Main processor + config: + prompt: "Process" + - id: review + type: subgraph + name: Reviewer + description: Review subgraph + config: + actor_path: actors/reviewer.yaml + edges: + - from_node: main + to_node: review + entry_node: main + exit_nodes: + - review +""" + +# ──────────────────────────────────────────────────────────── +# Given Steps +# ──────────────────────────────────────────────────────────── + + +@given("an actor YAML string with minimal LLM configuration") +def step_given_minimal_llm(context: Context) -> None: + """Provide minimal LLM actor YAML.""" + context.actor_yaml_string = _MINIMAL_LLM_YAML + + +@given("an actor YAML string with LLM and system prompt") +def step_given_llm_with_prompt(context: Context) -> None: + """Provide LLM actor with system prompt.""" + context.actor_yaml_string = _LLM_WITH_PROMPT_YAML + + +@given("an actor YAML string with LLM and tools") +def step_given_llm_with_tools(context: Context) -> None: + """Provide LLM actor with tools.""" + context.actor_yaml_string = _LLM_WITH_TOOLS_YAML + + +@given("an actor YAML string with memory configuration") +def step_given_llm_with_memory(context: Context) -> None: + """Provide LLM actor with memory config.""" + context.actor_yaml_string = _LLM_WITH_MEMORY_YAML + + +@given("an actor YAML string with context configuration") +def step_given_llm_with_context(context: Context) -> None: + """Provide LLM actor with context config.""" + context.actor_yaml_string = _LLM_WITH_CONTEXT_YAML + + +@given("an actor YAML string with TOOL type and tools") +def step_given_tool_minimal(context: Context) -> None: + """Provide minimal TOOL actor.""" + context.actor_yaml_string = _TOOL_MINIMAL_YAML + + +@given("an actor YAML string with inline tool definition") +def step_given_tool_inline(context: Context) -> None: + """Provide TOOL actor with inline tool.""" + context.actor_yaml_string = _TOOL_INLINE_YAML + + +@given("an actor YAML string with minimal GRAPH configuration") +def step_given_graph_minimal(context: Context) -> None: + """Provide minimal GRAPH actor.""" + context.actor_yaml_string = _GRAPH_MINIMAL_YAML + + +@given("an actor YAML string with linear graph topology") +def step_given_graph_linear(context: Context) -> None: + """Provide linear GRAPH actor.""" + context.actor_yaml_string = _GRAPH_LINEAR_YAML + + +@given("an actor YAML string with conditional routing") +def step_given_graph_conditional(context: Context) -> None: + """Provide GRAPH with conditional node.""" + context.actor_yaml_string = _GRAPH_CONDITIONAL_YAML + + +@given("an actor YAML string with subgraph node") +def step_given_graph_subgraph(context: Context) -> None: + """Provide GRAPH with subgraph node.""" + context.actor_yaml_string = _GRAPH_SUBGRAPH_YAML + + +@given('an actor YAML string with name "{name}"') +def step_given_actor_with_name(context: Context, name: str) -> None: + """Provide actor YAML with specific name.""" + context.actor_yaml_string = f"""\ +name: {name} +type: llm +description: Test actor +model: gpt-4 +""" + + +@given("an actor YAML string with LLM type but no model") +def step_given_llm_no_model(context: Context) -> None: + """Provide LLM actor without model field.""" + context.actor_yaml_string = """\ +name: assistants/broken +type: llm +description: Missing model +""" + + +@given("an actor YAML string with TOOL type but no tools") +def step_given_tool_no_tools(context: Context) -> None: + """Provide TOOL actor without tools field.""" + context.actor_yaml_string = """\ +name: utilities/broken +type: tool +description: Missing tools +""" + + +@given("an actor YAML string with TOOL type and empty tools") +def step_given_tool_empty_tools(context: Context) -> None: + """Provide TOOL actor with empty tools list.""" + context.actor_yaml_string = """\ +name: utilities/broken +type: tool +description: Empty tools +tools: [] +""" + + +@given("an actor YAML string with GRAPH type but no model") +def step_given_graph_no_model(context: Context) -> None: + """Provide GRAPH actor without model field.""" + context.actor_yaml_string = """\ +name: workflows/broken +type: graph +description: Missing model +route: + nodes: [] + edges: [] + entry_node: start + exit_nodes: [] +""" + + +@given("an actor YAML string with GRAPH type but no route") +def step_given_graph_no_route(context: Context) -> None: + """Provide GRAPH actor without route field.""" + context.actor_yaml_string = """\ +name: workflows/broken +type: graph +description: Missing route +model: gpt-4 +""" + + +@given("an actor YAML string with duplicate node IDs") +def step_given_duplicate_nodes(context: Context) -> None: + """Provide GRAPH with duplicate node IDs.""" + context.actor_yaml_string = """\ +name: workflows/duplicate +type: graph +description: Duplicate nodes +model: gpt-4 +route: + nodes: + - id: node1 + type: agent + name: First + description: First node + config: + prompt: "First" + - id: node1 + type: agent + name: Second + description: Duplicate ID + config: + prompt: "Second" + edges: [] + entry_node: node1 + exit_nodes: + - node1 +""" + + +@given("an actor YAML string with non-existent entry node") +def step_given_invalid_entry_node(context: Context) -> None: + """Provide GRAPH with invalid entry node.""" + context.actor_yaml_string = """\ +name: workflows/bad_entry +type: graph +description: Bad entry node +model: gpt-4 +route: + nodes: + - id: actual_node + type: agent + name: Node + description: The only node + config: + prompt: "Process" + edges: [] + entry_node: missing_node + exit_nodes: + - actual_node +""" + + +@given("an actor YAML string with non-existent exit node") +def step_given_invalid_exit_node(context: Context) -> None: + """Provide GRAPH with invalid exit node.""" + context.actor_yaml_string = """\ +name: workflows/bad_exit +type: graph +description: Bad exit node +model: gpt-4 +route: + nodes: + - id: actual_node + type: agent + name: Node + description: The only node + config: + prompt: "Process" + edges: [] + entry_node: actual_node + exit_nodes: + - missing_node +""" + + +@given("an actor YAML string with invalid edge from_node") +def step_given_invalid_edge_from(context: Context) -> None: + """Provide GRAPH with invalid from_node in edge.""" + context.actor_yaml_string = """\ +name: workflows/bad_edge +type: graph +description: Bad edge from_node +model: gpt-4 +route: + nodes: + - id: node_a + type: agent + name: Node A + description: First node + config: + prompt: "A" + - id: node_b + type: agent + name: Node B + description: Second node + config: + prompt: "B" + edges: + - from_node: missing_node + to_node: node_b + entry_node: node_a + exit_nodes: + - node_b +""" + + +@given("an actor YAML string with invalid edge to_node") +def step_given_invalid_edge_to(context: Context) -> None: + """Provide GRAPH with invalid to_node in edge.""" + context.actor_yaml_string = """\ +name: workflows/bad_edge +type: graph +description: Bad edge to_node +model: gpt-4 +route: + nodes: + - id: node_a + type: agent + name: Node A + description: First node + config: + prompt: "A" + - id: node_b + type: agent + name: Node B + description: Second node + config: + prompt: "B" + edges: + - from_node: node_a + to_node: missing_node + entry_node: node_a + exit_nodes: + - node_b +""" + + +@given("an actor YAML string with cyclic graph") +def step_given_cyclic_graph(context: Context) -> None: + """Provide GRAPH with cycle.""" + context.actor_yaml_string = """\ +name: workflows/cyclic +type: graph +description: Graph with cycle +model: gpt-4 +route: + nodes: + - id: node_a + type: agent + name: Node A + description: First + config: + prompt: "A" + - id: node_b + type: agent + name: Node B + description: Second + config: + prompt: "B" + - id: node_c + type: agent + name: Node C + description: Third + config: + prompt: "C" + edges: + - from_node: node_a + to_node: node_b + - from_node: node_b + to_node: node_c + - from_node: node_c + to_node: node_a + entry_node: node_a + exit_nodes: + - node_c +""" + + +@given('an actor YAML string with inline tool name "{name}"') +def step_given_inline_tool_name(context: Context, name: str) -> None: + """Provide actor with inline tool having specific name.""" + context.actor_yaml_string = f"""\ +name: utilities/test +type: tool +description: Test tool +tools: + - name: {name} + description: Test tool + parameters: [] + code: "def test(): pass" +""" + + +@given("an actor YAML string with invalid tool parameter name") +def step_given_invalid_param_name(context: Context) -> None: + """Provide inline tool with invalid parameter name.""" + context.actor_yaml_string = """\ +name: utilities/bad_param +type: tool +description: Bad parameter +tools: + - name: utils/bad_tool + description: Tool with bad param + parameters: + - name: invalid-name! + type: str + required: true + code: "def bad_tool(): pass" +""" + + +@given("an actor YAML string with node ID containing spaces") +def step_given_invalid_node_id(context: Context) -> None: + """Provide GRAPH with invalid node ID.""" + context.actor_yaml_string = """\ +name: workflows/bad_id +type: graph +description: Bad node ID +model: gpt-4 +route: + nodes: + - id: "node with spaces" + type: agent + name: Bad Node + description: Node with invalid ID + config: + prompt: "Process" + edges: [] + entry_node: "node with spaces" + exit_nodes: + - "node with spaces" +""" + + +@given('an actor YAML string with node ID "{node_id}"') +def step_given_specific_node_id(context: Context, node_id: str) -> None: + """Provide GRAPH with specific node ID.""" + context.actor_yaml_string = f"""\ +name: workflows/test +type: graph +description: Test workflow +model: gpt-4 +route: + nodes: + - id: {node_id} + type: agent + name: Test Node + description: Test node + config: + prompt: "Test" + edges: [] + entry_node: {node_id} + exit_nodes: + - {node_id} +""" + + +@given('an actor YAML string with context_view "{view}"') +def step_given_context_view(context: Context, view: str) -> None: + """Provide actor with specific context view.""" + context.actor_yaml_string = f"""\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +context_view: {view} +""" + + +@given("an actor YAML string with env_vars") +def step_given_env_vars(context: Context) -> None: + """Provide actor with environment variables.""" + context.actor_yaml_string = """\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +env_vars: + LOG_LEVEL: info + WORK_DIR: /tmp/work +""" + + +@given('the actor YAML file "{file_path}"') +def step_given_actor_yaml_file(context: Context, file_path: str) -> None: + """Store actor YAML file path for loading.""" + context.actor_yaml_file = file_path + + +@given("a valid actor configuration object") +def step_given_valid_actor_object(context: Context) -> None: + """Create a valid actor configuration object.""" + context.actor_config = ActorConfigSchema( + name="test/actor", + type=ActorType.LLM, + description="Test actor", + model="gpt-4", + ) + + +@given("a non-existent actor YAML file path") +def step_given_nonexistent_file(context: Context) -> None: + """Provide a non-existent file path.""" + context.actor_yaml_file = "/nonexistent/path/actor.yaml" + + +@given("an actor YAML string with edges having different priorities") +def step_given_edge_priorities(context: Context) -> None: + """Provide GRAPH with edge priorities.""" + context.actor_yaml_string = """\ +name: workflows/priorities +type: graph +description: Workflow with priorities +model: gpt-4 +route: + nodes: + - id: start + type: agent + name: Start + description: Start node + config: + prompt: "Start" + - id: end + type: agent + name: End + description: End node + config: + prompt: "End" + edges: + - from_node: start + to_node: end + priority: 10 + entry_node: start + exit_nodes: + - end +""" + + +@given("an actor YAML string with memory enabled false") +def step_given_memory_disabled(context: Context) -> None: + """Provide actor with memory disabled.""" + context.actor_yaml_string = """\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +memory: + enabled: false +""" + + +@given("an actor YAML string with max_messages {count:d}") +def step_given_max_messages(context: Context, count: int) -> None: + """Provide actor with max_messages limit.""" + context.actor_yaml_string = f"""\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +memory: + max_messages: {count} +""" + + +@given("an actor YAML string with max_tokens {count:d}") +def step_given_max_tokens(context: Context, count: int) -> None: + """Provide actor with max_tokens limit.""" + context.actor_yaml_string = f"""\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +memory: + max_tokens: {count} +""" + + +@given("an actor YAML string with summarize_old true") +def step_given_summarize_old(context: Context) -> None: + """Provide actor with summarize_old enabled.""" + context.actor_yaml_string = """\ +name: assistants/test +type: llm +description: Test actor +model: gpt-4 +memory: + summarize_old: true +""" + + +# ──────────────────────────────────────────────────────────── +# When Steps +# ──────────────────────────────────────────────────────────── + + +@when("I validate the actor schema") +def step_when_validate_schema(context: Context) -> None: + """Validate actor YAML string.""" + import yaml + + try: + data = yaml.safe_load(context.actor_yaml_string) + context.actor_config = ActorConfigSchema.model_validate(data) + context.validation_error = None + except (ValidationError, ValueError) as e: + context.actor_config = None + context.validation_error = e + + +@when("I validate the actor schema from file") +def step_when_validate_from_file(context: Context) -> None: + """Validate actor YAML from file.""" + try: + context.actor_config = ActorConfigSchema.from_yaml_file(context.actor_yaml_file) + context.validation_error = None + except (ValidationError, ValueError, FileNotFoundError) as e: + context.actor_config = None + context.validation_error = e + + +@when("I save the actor to YAML file") +def step_when_save_to_file(context: Context) -> None: + """Save actor configuration to temporary YAML file.""" + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as temp_file: + context.temp_file_name = temp_file.name + context.actor_config.to_yaml_file(context.temp_file_name) + + +@when("I reload the actor from YAML file") +def step_when_reload_from_file(context: Context) -> None: + """Reload actor from saved YAML file.""" + context.reloaded_actor = ActorConfigSchema.from_yaml_file(context.temp_file_name) + Path(context.temp_file_name).unlink() # Clean up + + +@when("I attempt to load the actor from file") +def step_when_attempt_load(context: Context) -> None: + """Attempt to load actor from file (may fail).""" + try: + context.actor_config = ActorConfigSchema.from_yaml_file(context.actor_yaml_file) + context.validation_error = None + except FileNotFoundError as e: + context.actor_config = None + context.validation_error = e + + +# ──────────────────────────────────────────────────────────── +# Then Steps +# ──────────────────────────────────────────────────────────── + + +@then("the actor schema validation should succeed") +def step_then_validation_succeeds(context: Context) -> None: + """Assert validation succeeded.""" + assert context.validation_error is None, ( + f"Expected validation to succeed, but got error: {context.validation_error}" + ) + assert context.actor_config is not None + + +@then("the actor schema validation should fail") +def step_then_validation_fails(context: Context) -> None: + """Assert validation failed.""" + assert context.validation_error is not None, ( + "Expected validation to fail, but it succeeded" + ) + assert context.actor_config is None + + +@then('the actor config name should be "{expected_name}"') +def step_then_name_matches(context: Context, expected_name: str) -> None: + """Assert actor name matches.""" + assert context.actor_config is not None + assert context.actor_config.name == expected_name + + +@then('the actor config type should be "{expected_type}"') +def step_then_type_matches(context: Context, expected_type: str) -> None: + """Assert actor type matches.""" + assert context.actor_config is not None + assert context.actor_config.type.value == expected_type + + +@then('the actor config model should be "{expected_model}"') +def step_then_model_matches(context: Context, expected_model: str) -> None: + """Assert actor model matches.""" + assert context.actor_config is not None + assert context.actor_config.model == expected_model + + +@then('the actor config system_prompt should contain "{text}"') +def step_then_prompt_contains(context: Context, text: str) -> None: + """Assert system prompt contains text.""" + assert context.actor_config is not None + assert context.actor_config.system_prompt is not None + assert text in context.actor_config.system_prompt + + +@then("the actor config should have {count:d} tools") +def step_then_tool_count(context: Context, count: int) -> None: + """Assert tool count matches.""" + assert context.actor_config is not None + assert len(context.actor_config.tools) == count + + +@then("the actor config should have at least {count:d} tool") +def step_then_at_least_tools(context: Context, count: int) -> None: + """Assert at least N tools.""" + assert context.actor_config is not None + assert len(context.actor_config.tools) >= count + + +@then("the actor config should have at least {count:d} tools") +def step_then_at_least_tools_plural(context: Context, count: int) -> None: + """Assert at least N tools (plural).""" + assert context.actor_config is not None + assert len(context.actor_config.tools) >= count + + +@then("the actor config should have {count:d} inline tool") +def step_then_inline_tool_count(context: Context, count: int) -> None: + """Assert inline tool count.""" + assert context.actor_config is not None + inline_count = sum( + 1 for tool in context.actor_config.tools if not isinstance(tool, str) + ) + assert inline_count == count + + +@then("the actor memory enabled should be {expected:w}") +def step_then_memory_enabled(context: Context, expected: str) -> None: + """Assert memory enabled state.""" + assert context.actor_config is not None + expected_bool = expected.lower() == "true" + assert context.actor_config.memory.enabled == expected_bool + + +@then("the actor memory max_messages should be {expected:d}") +def step_then_memory_max_messages(context: Context, expected: int) -> None: + """Assert memory max_messages value.""" + assert context.actor_config is not None + assert context.actor_config.memory.max_messages == expected + + +@then("the actor memory max_tokens should be {expected:d}") +def step_then_memory_max_tokens(context: Context, expected: int) -> None: + """Assert memory max_tokens value.""" + assert context.actor_config is not None + assert context.actor_config.memory.max_tokens == expected + + +@then("the actor memory summarize_old should be {expected:w}") +def step_then_memory_summarize(context: Context, expected: str) -> None: + """Assert memory summarize_old state.""" + assert context.actor_config is not None + expected_bool = expected.lower() == "true" + assert context.actor_config.memory.summarize_old == expected_bool + + +@then("the actor context should include {count:d} files") +def step_then_context_files(context: Context, count: int) -> None: + """Assert context includes N files.""" + assert context.actor_config is not None + assert len(context.actor_config.context.include_files) == count + + +@then("the actor context should include {count:d} directory") +def step_then_context_dirs(context: Context, count: int) -> None: + """Assert context includes N directories.""" + assert context.actor_config is not None + assert len(context.actor_config.context.include_dirs) == count + + +@then('the actor route should have entry_node "{node_id}"') +def step_then_entry_node(context: Context, node_id: str) -> None: + """Assert route entry node matches.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + assert context.actor_config.route.entry_node == node_id + + +@then("the actor route should have {count:d} nodes") +def step_then_node_count(context: Context, count: int) -> None: + """Assert route node count.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + assert len(context.actor_config.route.nodes) == count + + +@then("the actor route should have {count:d} edges") +def step_then_edge_count(context: Context, count: int) -> None: + """Assert route edge count.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + assert len(context.actor_config.route.edges) == count + + +@then("the actor route should have {count:d} conditional node") +def step_then_conditional_count(context: Context, count: int) -> None: + """Assert conditional node count.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + conditional_count = sum( + 1 + for node in context.actor_config.route.nodes + if node.type == NodeType.CONDITIONAL + ) + assert conditional_count == count + + +@then("the actor route should have {count:d} subgraph node") +def step_then_subgraph_count(context: Context, count: int) -> None: + """Assert subgraph node count.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + subgraph_count = sum( + 1 for node in context.actor_config.route.nodes if node.type == NodeType.SUBGRAPH + ) + assert subgraph_count == count + + +@then('the validation error should contain "{text}"') +def step_then_error_contains(context: Context, text: str) -> None: + """Assert error message contains text.""" + assert context.validation_error is not None + error_str = str(context.validation_error) + assert text in error_str, f"Expected '{text}' in error: {error_str}" + + +@then('the actor context_view should be "{expected_view}"') +def step_then_context_view_matches(context: Context, expected_view: str) -> None: + """Assert context view matches.""" + assert context.actor_config is not None + assert context.actor_config.context_view is not None + assert context.actor_config.context_view.value == expected_view + + +@then("the actor should have {count:d} env_vars") +def step_then_env_var_count(context: Context, count: int) -> None: + """Assert env_vars count.""" + assert context.actor_config is not None + assert len(context.actor_config.env_vars) == count + + +@then("the reloaded actor should match the original") +def step_then_reloaded_matches(context: Context) -> None: + """Assert reloaded actor matches original.""" + assert context.actor_config.name == context.reloaded_actor.name + assert context.actor_config.type == context.reloaded_actor.type + assert context.actor_config.model == context.reloaded_actor.model + + +@then("a FileNotFoundError should be raised") +def step_then_file_not_found(context: Context) -> None: + """Assert FileNotFoundError was raised.""" + assert isinstance(context.validation_error, FileNotFoundError) + + +@then("the highest priority edge should be {priority:d}") +def step_then_highest_priority(context: Context, priority: int) -> None: + """Assert highest edge priority.""" + assert context.actor_config is not None + assert context.actor_config.route is not None + max_priority = max(edge.priority for edge in context.actor_config.route.edges) + assert max_priority == priority diff --git a/implementation_plan.md b/implementation_plan.md index f6f30450..bafbf874 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -980,6 +980,75 @@ The following work from the previous implementation has been completed and will - benchmark: success (all benchmarks pass) - **Commit**: `122af46` on `feature/q0-min-coverage` — `fix(tests): resolve unit test and benchmark failures` +**2026-02-17**: Task C1.schema Complete (Aditya) - Actor YAML Schema Models + +- **Implementation**: Created complete actor YAML schema system in `src/cleveragents/actor/schema.py` (695 lines) + - Three actor types with clear separation: LLM (single LLM call + tools), TOOL (tool collections), GRAPH (multi-node workflows) + - Strict name validation enforcing `namespace/name` format (ADR-002 compliance) + - Comprehensive graph topology validation with cycle detection using DFS algorithm + - Type-specific field requirements enforced via Pydantic `model_validator` + - Environment variable interpolation support (`${ENV_VAR}` syntax) + - YAML I/O methods: `from_yaml_file()`, `to_yaml_file()`, `from_yaml()` + - Core models: `ActorType`, `NodeType`, `ContextView` enums; `ToolParameter`, `ToolDefinition`, `MemoryConfig`, `ContextConfigSchema`, `EdgeDefinition`, `NodeDefinition`, `RouteDefinition`, `ActorConfigSchema` Pydantic models + +- **Documentation**: Comprehensive reference docs in `docs/reference/actors_schema.md` (420 lines) + - Actor type definitions, field semantics, tool node behavior + - Graph constraints: unique node IDs, entry/exit validation, cycle detection + - Validation rules with examples for valid/invalid configurations + - Error message catalog + +- **Examples**: 5 YAML files in `examples/actors/` + - `simple_llm.yaml` - Minimal LLM actor (basic code review) + - `llm_with_tools.yaml` - LLM with tools, memory, context config + - `tool_collection.yaml` - TOOL actor with multiple tool references + - `simple_graph.yaml` - Basic 3-node linear graph workflow + - `graph_workflow.yaml` - Complex graph with conditionals, subgraphs, retry logic + +- **Tests**: Comprehensive coverage across all test types + - **Behave**: `features/actor_schema.feature` (50+ scenarios, 300+ steps) + - Valid/invalid configurations for all three actor types + - Name validation (namespace requirement, invalid characters) + - Graph topology (unique nodes, entry/exit validation, cycle detection) + - Tool/node validation, YAML I/O round-trip, error messages + - **Robot**: `robot/actor_schema.robot` (11 test cases) + - Smoke tests for all 5 example YAMLs + - Invalid configuration rejection (missing fields, bad topology, duplicate nodes) + - **ASV**: `benchmarks/actor_schema_bench.py` (8 benchmark classes) + - Parsing/validation for minimal/full LLM, tool, graph actors + - Graph topology validation performance (cycle detection) + - File I/O operations, serialization (`model_dump`) + +- **Branch**: `feature/actor-c1-schema` (11 commits across 10 sub-parts) +- **Commit messages follow Conventional Changelog format**: + 1. `feat(actor): add core enums for actor schema` + 2. `feat(actor): add tool and config pydantic models` + 3. `feat(actor): add graph topology models with validation` + 4. `feat(actor): add main actor schema and YAML I/O` + 5. `docs(actor): add comprehensive actor YAML examples` + 6. `docs(actor): add actor schema reference documentation` + 7. `test(actor): add behave scenarios for actor schema` + 8. `test(actor): add behave step definitions for actor schema` + 9. `test(actor): add robot framework integration tests for actor schema` + 10. `perf(actor): add asv benchmarks for actor schema` + 11. `docs(actor): add c1.schema implementation notes and traceability` + +- **Quality metrics**: All nox sessions pass + - lint: 0 findings (pre-existing issues in other files noted) + - typecheck: 0 errors + - security_scan: 0 findings + - unit_tests: All scenarios pass + - integration_tests: All tests pass + - benchmark: All benchmarks execute successfully + +- **Implementation Notes section added** to task C1.schema in Implementation Checklist (line 2968) + - Full traceability with file paths and line numbers + - Design decisions documented with rationale + - Test coverage strategy explained + - Dependencies and open questions captured + +- **Next steps**: C2.loader (Actor Registry and Loader) depends on this schema foundation + + **2026-02-17**: C0.skill.cli Complete (Aditya) - Skill CLI Commands + Output Formatting - Created `src/cleveragents/application/services/skill_service.py` — `SkillService` with dual-mode storage pattern (in-memory dict fallback, ready for `UnitOfWork` persistence when `C0.skill.registry` lands): @@ -1021,7 +1090,6 @@ The following work from the previous implementation has been completed and will - ASV benchmarks: 4/4 suites pass - Key decision: Followed `PlanLifecycleService` dual-mode pattern — `SkillService` works fully in-memory now; when Luis adds `C0.skill.registry` with `UnitOfWork`/`SkillRepository`, DB writes are added alongside the in-memory dict without changing the public API - Key decision: Module-level `_service` singleton (not DI container) matches the pattern in `action.py` CLI; `_reset_skill_service()` enables test isolation - --- ## Roadmap @@ -1496,6 +1564,11 @@ No standalone Q0-Advanced commits planned. Advanced QA enhancements are bundled ### Section 2B: Commit Traceability Fixups [IMMEDIATE] +- [ ] Traceability [Aditya]: Find the actual commit message used for "docs(skill): add skill YAML schema and examples" in `git log --all`, then update the completed COMMIT entry to replace "Done: commit not found" with the correct `Done: Day ,