docs(my-actor.yaml): add an example actor (#3)
CI / security (push) Successful in 33s
CI / unit_tests (push) Successful in 42s
CI / coverage (push) Successful in 46s
CI / typecheck (push) Successful in 1m1s
CI / lint (push) Successful in 1m1s
CI / dead_code (push) Successful in 1m5s
CI / build (push) Successful in 27s

This code adds documentation of a sample actor.

ISSUES CLOSED: #1

Reviewed-on: #3
Reviewed-by: Luis Mendes <luis.mendes@cleverthis.com>
This commit was merged in pull request #3.
This commit is contained in:
2026-05-20 17:56:54 +00:00
committed by Forgejo
parent d2d26f1780
commit 2ce29092f5
8 changed files with 96 additions and 37 deletions
+64
View File
@@ -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
+1 -1
View File
@@ -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."""
+3 -5
View File
@@ -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
+13 -3
View File
@@ -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(
+9 -4
View File
@@ -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] = {}
+3
View File
@@ -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.
+1 -22
View File
@@ -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",
]
+2 -2
View File
@@ -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)