From 2ce29092f5252bee60c5ce6a99aaef52fc84b348 Mon Sep 17 00:00:00 2001 From: Brent Edwards Date: Wed, 20 May 2026 17:56:54 +0000 Subject: [PATCH] docs(my-actor.yaml): add an example actor (#3) This code adds documentation of a sample actor. ISSUES CLOSED: #1 Reviewed-on: https://git.cleverthis.com/cleveragents/cleveractors-core/pulls/3 Reviewed-by: Luis Mendes --- docs/my-actor.yaml | 64 ++++++++++++++++++++++++++ features/environment.py | 2 +- features/steps/smoke_steps.py | 8 ++-- src/cleveractors/actor/compiler.py | 16 +++++-- src/cleveractors/actor/loader.py | 13 ++++-- src/cleveractors/core/exceptions.py | 3 ++ src/cleveractors/langgraph/__init__.py | 23 +-------- src/cleveractors/langgraph/state.py | 4 +- 8 files changed, 96 insertions(+), 37 deletions(-) create mode 100644 docs/my-actor.yaml diff --git a/docs/my-actor.yaml b/docs/my-actor.yaml new file mode 100644 index 0000000..ab53622 --- /dev/null +++ b/docs/my-actor.yaml @@ -0,0 +1,64 @@ +# Simple Graph Actor - Sequential Processing +# Demonstrates a simple 3-node graph with linear execution + +name: local/proofreader +type: graph +description: Simple proofreader +version: "1.0.0" + +# LLM model +provider: openai +model: gpt-3.5-turbo + +# Graph topology +route: + nodes: + # Node 1: Extract text from document + - id: extractor + type: tool + name: extractor + description: Extracts text from various document formats + config: + tool_name: documents/extract_text + parameters: + formats: + - pdf + - docx + - txt + + # Node 2: Analyze content + - id: proofreader + type: agent + name: Proofreader + description: Proofreads a document. + config: + model: gpt-3.5-turbo + prompt: | + You are an expert proofreader. Read the given text and correct: + - misspellings + - incorrect grammar + - incorrect punctuation + - incorrect capitalization + - duplicate words + - misplaced modifiers + + Provide constructive feedback with specific suppestions for improvement. + + # Linear edges + edges: + - from_node: extractor + to_node: proofreader + + # Entry and exit + entry_node: extractor + exit_nodes: + - proofreader + +# Context settings +context_view: executor +memory: + enabled: true + max_messages: 10 + +context: + max_context_tokens: 4000 diff --git a/features/environment.py b/features/environment.py index abd89fc..0e339de 100644 --- a/features/environment.py +++ b/features/environment.py @@ -13,5 +13,5 @@ def before_all(context: Any) -> None: """Hook invoked once before any features run.""" -def before_scenario(context: Any, scenario: Any) -> None: # noqa: ARG001 +def before_scenario(context: Any, scenario: Any) -> None: """Per-scenario reset of any context state.""" diff --git a/features/steps/smoke_steps.py b/features/steps/smoke_steps.py index 54e23e1..34833f9 100644 --- a/features/steps/smoke_steps.py +++ b/features/steps/smoke_steps.py @@ -71,11 +71,9 @@ def when_compiled(context: Any) -> None: try: # NB: ``context.config`` is a reserved attribute on Behave's # Context; using ``actor_config`` keeps us out of its way. - context.actor_config = ActorConfigSchema.model_validate( - context.actor_raw - ) + context.actor_config = ActorConfigSchema.model_validate(context.actor_raw) context.compiled = compile_actor(context.actor_config) - except Exception as exc: # noqa: BLE001 (test capture) + except Exception as exc: context.compile_exc = exc @@ -84,7 +82,7 @@ def when_validated(context: Any) -> None: context.validation_exc = None try: ActorConfigSchema.model_validate(context.actor_raw) - except Exception as exc: # noqa: BLE001 + except Exception as exc: context.validation_exc = exc diff --git a/src/cleveractors/actor/compiler.py b/src/cleveractors/actor/compiler.py index dd17534..f51abce 100644 --- a/src/cleveractors/actor/compiler.py +++ b/src/cleveractors/actor/compiler.py @@ -44,6 +44,18 @@ from cleveractors.lsp.models import LspBinding class ActorCompilationError(ValidationError): """Raised when actor compilation fails.""" + def __init__( + self, message: str | None = None, details: dict[str, str] | None = None + ) -> None: + if message is not None and details is not None: + super().__init__(details={"details": message, **details}) + elif message is not None: + super().__init__(details={"details": message}) + elif details is not None: + super().__init__(details=details) + else: + super().__init__(details={}) + class SubgraphCycleError(ActorCompilationError): """Raised when subgraph references form a cycle.""" @@ -179,7 +191,6 @@ def _extract_lsp_bindings(node: NodeDefinition) -> list[LspBinding]: def _detect_subgraph_cycles( - actor_name: str, route_nodes: list[NodeDefinition], resolver: ActorResolver | None, visited: frozenset[str], @@ -212,7 +223,6 @@ def _detect_subgraph_cycles( continue deeper = _detect_subgraph_cycles( - ref_name, referenced.route.nodes, resolver, visited | {ref_name}, @@ -271,7 +281,7 @@ def compile_actor( # Detect cross-actor subgraph cycles cross_cycles = _detect_subgraph_cycles( - config.name, route.nodes, actor_resolver, frozenset({config.name}) + route.nodes, actor_resolver, frozenset({config.name}) ) if cross_cycles: raise SubgraphCycleError( diff --git a/src/cleveractors/actor/loader.py b/src/cleveractors/actor/loader.py index 0061939..77b4068 100644 --- a/src/cleveractors/actor/loader.py +++ b/src/cleveractors/actor/loader.py @@ -184,14 +184,19 @@ class ActorLoader: if duplicate_msgs: all_errors = errors + duplicate_msgs raise ValidationError( - "Actor discovery failed:\n" + "\n".join(all_errors), - details={"duplicates": duplicate_msgs, "errors": errors}, + details={ + "message": "Actor discovery failed:\n" + "\n".join(all_errors), + "duplicates": ", ".join(duplicate_msgs), + "errors": ", ".join(errors), + } ) if errors: raise ValidationError( - "Actor discovery failed:\n" + "\n".join(errors), - details={"errors": errors}, + details={ + "message": "Actor discovery failed:\n" + "\n".join(errors), + "errors": ", ".join(errors), + }, ) new_actors: dict[str, _CacheEntry] = {} diff --git a/src/cleveractors/core/exceptions.py b/src/cleveractors/core/exceptions.py index 84b1adf..d2808d3 100644 --- a/src/cleveractors/core/exceptions.py +++ b/src/cleveractors/core/exceptions.py @@ -31,6 +31,9 @@ class ValidationError(Exception): - Invalid entry / exit points in the graph route. """ + def __init__(self, details: dict[str, str], *args: object) -> None: + super().__init__(details, *args) + class NotFoundError(Exception): """Raised when a named entity is referenced but cannot be located. diff --git a/src/cleveractors/langgraph/__init__.py b/src/cleveractors/langgraph/__init__.py index 89a431f..e92d3a3 100644 --- a/src/cleveractors/langgraph/__init__.py +++ b/src/cleveractors/langgraph/__init__.py @@ -1,32 +1,11 @@ -"""LangGraph primitives adapted for actor-first routing. - -``graph`` sub-module is lazy-loaded because it pulls in -``langchain_core``, ``langsmith``, and ``rx.operators`` which add -multiple seconds of import overhead. Lightweight symbols from -``nodes`` and ``state`` are available eagerly. -""" +"""LangGraph primitives adapted for actor-first routing.""" from .nodes import Edge, NodeConfig, NodeType from .state import GraphState - -def __getattr__(name: str): - _GRAPH_ATTRS = {"GraphConfig", "LangGraph"} - if name in _GRAPH_ATTRS: - from .graph import GraphConfig as _GraphConfig - from .graph import LangGraph as _LangGraph - - globals()["GraphConfig"] = _GraphConfig - globals()["LangGraph"] = _LangGraph - return globals()[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - __all__ = [ "Edge", - "GraphConfig", "GraphState", - "LangGraph", "NodeConfig", "NodeType", ] diff --git a/src/cleveractors/langgraph/state.py b/src/cleveractors/langgraph/state.py index bb6e6fe..a338629 100644 --- a/src/cleveractors/langgraph/state.py +++ b/src/cleveractors/langgraph/state.py @@ -26,6 +26,8 @@ from pydantic import BaseModel, ConfigDict, Field T = TypeVar("T", bound="GraphState") logger = logging.getLogger(__name__) + + class StateUpdateMode(Enum): REPLACE = "replace" MERGE = "merge" @@ -91,5 +93,3 @@ class GraphState(BaseModel): @classmethod def from_dict(cls: type[T], data: dict[str, Any]) -> T: return cls(**data) - -