workflow as node looking alright

This commit is contained in:
lda
2026-04-28 18:32:52 +07:00 Verified
parent 3d428c71db
commit c78ccbc8dd
4 changed files with 101 additions and 8 deletions
+36 -2
View File
@@ -19,12 +19,14 @@ from wf_authoring import (
bind_fields, bind_fields,
bind_state, bind_state,
build_registry, build_registry,
expr,
exists, exists,
state, state,
state_path,
context_path, context_path,
input_path, input_path,
node, node,
state_path, subgraph_node,
) )
@@ -487,7 +489,9 @@ def test_node_decorator_detects_async_automatically() -> None:
def test_condition_dsl_compiles_to_core_condition() -> None: def test_condition_dsl_compiles_to_core_condition() -> None:
condition = state("should_email").eq(True) & exists(state("summary")) condition = expr(state_path("should_email")).eq(True) & exists(
state_path("summary")
)
assert condition.to_condition().model_dump() == { assert condition.to_condition().model_dump() == {
"op": "and", "op": "and",
@@ -503,3 +507,33 @@ def test_condition_dsl_compiles_to_core_condition() -> None:
}, },
], ],
} }
def test_subgraph_node_wraps_compiled_workflow() -> None:
class ChildInput(BaseModel):
folder_id: str
should_email: bool
class ChildOutput(BaseModel):
summary: str
email_status: str
child_workflow = build_demo_workflow()
child_registry = build_demo_registry()
wrapped = subgraph_node(
name="wrapped_demo",
workflow=child_workflow,
registry=child_registry,
input_model=ChildInput,
output_model=ChildOutput,
)
registry = build_registry(wrapped)
result = registry["wrapped_demo"](
{"folder_id": "demo-folder", "should_email": False},
RuntimeContext(current_node_id="parent"),
)
assert result["outcome"] == "ok"
assert result["output"]["email_status"] == "skipped"
assert "summary" in result["output"]
+4 -1
View File
@@ -1,9 +1,10 @@
from .builder import WorkflowBuilder from .builder import WorkflowBuilder
from .catalog import NodeCatalog, NodeCatalogEntry from .catalog import NodeCatalog, NodeCatalogEntry
from .conditions import context, exists, input, state from .conditions import context, exists, expr, input, state
from .mapping import bind_fields, bind_state, merge_maps from .mapping import bind_fields, bind_state, merge_maps
from .paths import GraphPath, context_path, graph_path, input_path, state_path from .paths import GraphPath, context_path, graph_path, input_path, state_path
from .spec import NodeReturn, NodeSpec, build_registry, node from .spec import NodeReturn, NodeSpec, build_registry, node
from .subgraph import subgraph_node
__all__ = [ __all__ = [
"NodeCatalog", "NodeCatalog",
@@ -18,6 +19,7 @@ __all__ = [
"merge_maps", "merge_maps",
"context", "context",
"context_path", "context_path",
"expr",
"exists", "exists",
"graph_path", "graph_path",
"input", "input",
@@ -25,4 +27,5 @@ __all__ = [
"node", "node",
"state", "state",
"state_path", "state_path",
"subgraph_node",
] ]
+20 -5
View File
@@ -12,14 +12,23 @@ from wf_core.model import (
PathOperand, PathOperand,
VariadicCondition, VariadicCondition,
) )
from .paths import GraphPath, context_path, input_path, state_path
def _operand(value: object) -> PathOperand | LiteralOperand: def _operand(value: object) -> PathOperand | LiteralOperand:
if isinstance(value, PathExpr): if isinstance(value, PathExpr):
return PathOperand(path=value.path) return PathOperand(path=value.path)
if isinstance(value, GraphPath):
return PathOperand(path=value.value)
return LiteralOperand(value=value) return LiteralOperand(value=value)
def _path_str(value: PathExpr | GraphPath) -> str:
if isinstance(value, PathExpr):
return value.path
return value.value
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
class Expr: class Expr:
condition: Condition condition: Condition
@@ -79,20 +88,26 @@ class PathExpr:
return self.lt(other) return self.lt(other)
def expr(value: PathExpr | GraphPath) -> PathExpr:
if isinstance(value, PathExpr):
return value
return PathExpr(path=value.value)
def state(field: str) -> PathExpr: def state(field: str) -> PathExpr:
return PathExpr(path=f"state.{field}") return expr(state_path(field))
def input(field: str) -> PathExpr: def input(field: str) -> PathExpr:
return PathExpr(path=f"input.{field}") return expr(input_path(field))
def context(field: str) -> PathExpr: def context(field: str) -> PathExpr:
return PathExpr(path=f"context.{field}") return expr(context_path(field))
def exists(value: PathExpr) -> Expr: def exists(value: PathExpr | GraphPath) -> Expr:
return Expr(ExistsCondition(op="exists", path=value.path)) return Expr(ExistsCondition(op="exists", path=_path_str(value)))
def compile_condition(value: Condition | Expr) -> Condition: def compile_condition(value: Condition | Expr) -> Condition:
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, TypeVar
from pydantic import BaseModel
from wf_core import RuntimeContext, Workflow, execute_workflow
from .spec import NodeSpec
InputT = TypeVar("InputT", bound=BaseModel)
OutputT = TypeVar("OutputT", bound=BaseModel)
def subgraph_node(
*,
name: str,
workflow: Workflow,
registry: Mapping[str, Any],
input_model: type[InputT],
output_model: type[OutputT],
description: str | None = None,
) -> NodeSpec[InputT, OutputT]:
def run_subgraph(payload: InputT, ctx: RuntimeContext) -> OutputT:
child_run = execute_workflow(
workflow,
payload.model_dump(),
registry,
)
return output_model.model_validate(child_run.output)
return NodeSpec(
name=name,
input_model=input_model,
output_model=output_model,
outcomes=("ok",),
fn=run_subgraph,
description=description or f"Subgraph wrapper for {workflow.name}",
is_async=False,
)