first reducer sweep of failure
i will put second reducer sweep of logic error or second of success
This commit is contained in:
@@ -12,6 +12,7 @@ from .dsl import (
|
||||
input,
|
||||
input_path,
|
||||
merge_maps,
|
||||
not_,
|
||||
state,
|
||||
state_path,
|
||||
)
|
||||
@@ -119,6 +120,7 @@ __all__ = [
|
||||
"rename_fields",
|
||||
"runtime_error",
|
||||
"node",
|
||||
"not_",
|
||||
"outcome",
|
||||
"reducer",
|
||||
"state",
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
||||
from typing import Any, Literal, cast
|
||||
import warnings
|
||||
|
||||
from wf_authoring.ops.values import runtime_error
|
||||
from wf_core import (
|
||||
ConditionNode,
|
||||
Edge,
|
||||
@@ -19,10 +20,12 @@ from wf_core import (
|
||||
)
|
||||
from wf_core.errors import WorkflowExecutionError
|
||||
from wf_core.models.conditions import Condition as CoreCondition
|
||||
from wf_core.runtime.ops.merges import ReducerDefinition
|
||||
|
||||
from ..dsl import Expr, PathArg, PathExpr, compile_condition
|
||||
from ..nodes.callables import SyncRegistryHandler
|
||||
from ..nodes.registry import build_registry
|
||||
from ..reducers import ReducerCatalog
|
||||
from ..schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema_from
|
||||
from ..nodes import NodeSpec
|
||||
from .ids import next_step_id, slug_id
|
||||
@@ -43,6 +46,7 @@ class WorkflowBuilder:
|
||||
state_schema: StateSchemaLike
|
||||
output_schema: SchemaLike
|
||||
start: str | None = None
|
||||
reducers: ReducerCatalog | Mapping[str, ReducerDefinition] | None = None
|
||||
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
|
||||
nodes: list[Any] = field(default_factory=list)
|
||||
edges: list[Edge] = field(default_factory=list)
|
||||
@@ -100,6 +104,19 @@ class WorkflowBuilder:
|
||||
"""Export handlers for all node specs used by this builder."""
|
||||
return build_registry(*self.node_specs.values())
|
||||
|
||||
def reducer_registry(self) -> dict[str, ReducerDefinition]:
|
||||
"""Export custom reducers attached to this builder.
|
||||
|
||||
The core runtime always falls back to built-in reducers. This method
|
||||
returns only the authored additions/overrides so callers can pass the
|
||||
same runtime environment that `execute()` uses.
|
||||
"""
|
||||
if self.reducers is None:
|
||||
return {}
|
||||
if isinstance(self.reducers, ReducerCatalog):
|
||||
return dict(self.reducers.definitions)
|
||||
return dict(self.reducers)
|
||||
|
||||
def execute(self, workflow_input: dict[str, Any]) -> RunState:
|
||||
"""Compile and execute this workflow with its used node registry.
|
||||
|
||||
@@ -107,7 +124,12 @@ class WorkflowBuilder:
|
||||
callers that need custom registries, persistence, or resume behavior should
|
||||
call wf_core execution functions directly.
|
||||
"""
|
||||
return execute_workflow(self.compile(), workflow_input, self.registry())
|
||||
return execute_workflow(
|
||||
self.compile(),
|
||||
workflow_input,
|
||||
self.registry(),
|
||||
reducers=self.reducer_registry(),
|
||||
)
|
||||
|
||||
def condition(
|
||||
self, *, id: str | None = None, check: CoreCondition | Expr
|
||||
@@ -211,24 +233,43 @@ class WorkflowBuilder:
|
||||
return resolved_targets
|
||||
|
||||
def route(
|
||||
self,
|
||||
value: PathExpr | Expr,
|
||||
cases: Mapping[object, BranchRef],
|
||||
*,
|
||||
id: str | None = None,
|
||||
default: BranchRef = runtime_error,
|
||||
) -> dict[object, StepRef]:
|
||||
"""Route graph data by equality checks or one boolean condition.
|
||||
|
||||
`branch()` wires outcomes already produced by a node. `route()` is the
|
||||
companion for ordinary graph data: with a path, it compares that path
|
||||
against case values; with a condition expression, it expects boolean
|
||||
`True`/`False` cases and emits one condition node.
|
||||
"""
|
||||
if isinstance(value, Expr):
|
||||
return self._route_condition(value, cases, id=id, default=default)
|
||||
|
||||
return self._route_value(value, cases, id=id, default=default)
|
||||
|
||||
def _route_value(
|
||||
self,
|
||||
value: PathExpr,
|
||||
cases: Mapping[object, BranchRef],
|
||||
*,
|
||||
id: str | None,
|
||||
default: BranchRef,
|
||||
) -> dict[object, StepRef]:
|
||||
"""Route graph data by equality checks compiled to condition nodes.
|
||||
|
||||
`branch()` wires outcomes already produced by a node. `route()` is the
|
||||
companion for ordinary graph data: it compares one path against case
|
||||
values, expands those checks into a condition chain, and wires the first
|
||||
matching target plus a required fallback.
|
||||
"""
|
||||
"""Expand value cases into an ordered chain of equality checks."""
|
||||
resolved_targets: dict[object, StepRef] = {}
|
||||
default_target = self.use(default) if is_node_spec(default) else default
|
||||
previous_condition: ConditionNode | None = None
|
||||
condition_base = id or "condition"
|
||||
for case_value, target in cases.items():
|
||||
condition = self.condition(check=value.eq(case_value))
|
||||
condition = self.condition(
|
||||
id=self._next_step_id(condition_base),
|
||||
check=value.eq(case_value),
|
||||
)
|
||||
resolved = self.use(target) if is_node_spec(target) else target
|
||||
if previous_condition is not None:
|
||||
self.connect(previous_condition, "false", condition)
|
||||
@@ -241,6 +282,30 @@ class WorkflowBuilder:
|
||||
resolved_targets["default"] = cast(StepRef, default_target)
|
||||
return resolved_targets
|
||||
|
||||
def _route_condition(
|
||||
self,
|
||||
condition: Expr,
|
||||
cases: Mapping[object, BranchRef],
|
||||
*,
|
||||
id: str | None,
|
||||
default: BranchRef,
|
||||
) -> dict[object, StepRef]:
|
||||
"""Route a boolean condition expression through true/false outcomes."""
|
||||
invalid_cases = [case for case in cases if not isinstance(case, bool)]
|
||||
if invalid_cases:
|
||||
raise ValueError("condition route cases must be boolean True/False keys")
|
||||
if not cases:
|
||||
raise ValueError("WorkflowBuilder.route requires at least one case")
|
||||
|
||||
condition_node = self.condition(id=id, check=condition)
|
||||
resolved_targets: dict[object, StepRef] = {}
|
||||
for case_value, outcome in ((True, "true"), (False, "false")):
|
||||
target = cases.get(case_value, default)
|
||||
resolved = self.use(target) if is_node_spec(target) else target
|
||||
self.connect(condition_node, outcome, cast(StepRef, resolved))
|
||||
resolved_targets[case_value] = cast(StepRef, resolved)
|
||||
return resolved_targets
|
||||
|
||||
def compile(self) -> Workflow:
|
||||
if self.start is None:
|
||||
raise WorkflowExecutionError(
|
||||
|
||||
@@ -6,6 +6,7 @@ from .conditions import (
|
||||
exists,
|
||||
expr,
|
||||
input,
|
||||
not_,
|
||||
state,
|
||||
)
|
||||
from .mapping import PathArg, bind_fields, bind_state, merge_maps, normalize_path
|
||||
@@ -28,6 +29,7 @@ __all__ = [
|
||||
"input_path",
|
||||
"merge_maps",
|
||||
"normalize_path",
|
||||
"not_",
|
||||
"state",
|
||||
"state_path",
|
||||
]
|
||||
|
||||
@@ -55,7 +55,9 @@ class Expr:
|
||||
class PathExpr:
|
||||
path: str
|
||||
|
||||
def _binary(self, op: Literal["eq", "ne", "gt", "lt"], other: object) -> Expr:
|
||||
def _binary(
|
||||
self, op: Literal["eq", "ne", "gt", "ge", "lt", "le"], other: object
|
||||
) -> Expr:
|
||||
return Expr(
|
||||
BinaryCondition(
|
||||
op=op,
|
||||
@@ -73,9 +75,15 @@ class PathExpr:
|
||||
def gt(self, other: object) -> Expr:
|
||||
return self._binary("gt", other)
|
||||
|
||||
def ge(self, other: object) -> Expr:
|
||||
return self._binary("ge", other)
|
||||
|
||||
def lt(self, other: object) -> Expr:
|
||||
return self._binary("lt", other)
|
||||
|
||||
def le(self, other: object) -> Expr:
|
||||
return self._binary("le", other)
|
||||
|
||||
def __eq__(self, other: object) -> Expr: # pyright: ignore[reportIncompatibleMethodOverride] # type: ignore[override] # ty: ignore[invalid-method-override]
|
||||
return self._binary("eq", other)
|
||||
|
||||
@@ -85,9 +93,15 @@ class PathExpr:
|
||||
def __gt__(self, other: object) -> Expr:
|
||||
return self.gt(other)
|
||||
|
||||
def __ge__(self, other: object) -> Expr:
|
||||
return self.ge(other)
|
||||
|
||||
def __lt__(self, other: object) -> Expr:
|
||||
return self.lt(other)
|
||||
|
||||
def __le__(self, other: object) -> Expr:
|
||||
return self.le(other)
|
||||
|
||||
|
||||
def expr(value: PathExpr | GraphPath) -> PathExpr:
|
||||
if isinstance(value, PathExpr):
|
||||
@@ -111,6 +125,11 @@ def exists(value: PathExpr | GraphPath) -> Expr:
|
||||
return Expr(ExistsCondition(op="exists", path=_path_str(value)))
|
||||
|
||||
|
||||
def not_(value: Condition | Expr) -> Expr:
|
||||
"""Negate a condition without relying on Python's visually subtle `~expr`."""
|
||||
return Expr(NotCondition(op="not", arg=compile_condition(value)))
|
||||
|
||||
|
||||
def compile_condition(value: Condition | Expr) -> Condition:
|
||||
if isinstance(value, Expr):
|
||||
return value.to_condition()
|
||||
|
||||
@@ -18,13 +18,10 @@ class ReducerCatalog:
|
||||
def from_reducers(cls, *reducers: AuthoredReducer) -> "ReducerCatalog":
|
||||
return cls(
|
||||
definitions={
|
||||
reducer.definition.spec.name: reducer.definition
|
||||
for reducer in reducers
|
||||
reducer.definition.spec.name: reducer.definition for reducer in reducers
|
||||
}
|
||||
)
|
||||
|
||||
@property
|
||||
def specs(self) -> dict[str, ReducerSpec]:
|
||||
return {
|
||||
name: definition.spec for name, definition in self.definitions.items()
|
||||
}
|
||||
return {name: definition.spec for name, definition in self.definitions.items()}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Iterator
|
||||
|
||||
from pydantic import BaseModel, TypeAdapter
|
||||
@@ -9,23 +10,26 @@ from wf_core import ReducerRef, SchemaRef, StateField, StateSchema
|
||||
|
||||
SchemaLike = SchemaRef | type[BaseModel] | type[Any] | dict[str, Any]
|
||||
StateSchemaLike = StateSchema | type[BaseModel] | type[Any] | dict[str, Any]
|
||||
ReducerLike = str | ReducerRef | Mapping[str, Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class StateFieldMetadata:
|
||||
"""Authoring metadata attached to BaseModel state fields."""
|
||||
|
||||
reducer: str = "wf.std.replace"
|
||||
reducer: ReducerRef = field(
|
||||
default_factory=lambda: ReducerRef(name="wf.std.replace")
|
||||
)
|
||||
trace: bool = True
|
||||
|
||||
|
||||
def state_field(
|
||||
*,
|
||||
reducer: str = "wf.std.replace",
|
||||
reducer: ReducerLike = "wf.std.replace",
|
||||
trace: bool = True,
|
||||
) -> StateFieldMetadata:
|
||||
"""Declare workflow state behavior for an Annotated BaseModel field."""
|
||||
return StateFieldMetadata(reducer=reducer, trace=trace)
|
||||
return StateFieldMetadata(reducer=_reducer_ref_from(reducer), trace=trace)
|
||||
|
||||
|
||||
def schema_ref_from(value: SchemaLike) -> SchemaRef:
|
||||
@@ -51,9 +55,7 @@ def state_schema_from(value: StateSchemaLike) -> StateSchema:
|
||||
fields = {
|
||||
path: StateField(
|
||||
type=_state_field_type(property_schema),
|
||||
reducer=ReducerRef(
|
||||
name=metadata_by_name.get(path, StateFieldMetadata()).reducer
|
||||
),
|
||||
reducer=metadata_by_name.get(path, StateFieldMetadata()).reducer,
|
||||
trace=metadata_by_name.get(path, StateFieldMetadata()).trace,
|
||||
default=_state_field_default(value, path, property_schema),
|
||||
)
|
||||
@@ -62,6 +64,14 @@ def state_schema_from(value: StateSchemaLike) -> StateSchema:
|
||||
return StateSchema(fields=fields)
|
||||
|
||||
|
||||
def _reducer_ref_from(value: ReducerLike) -> ReducerRef:
|
||||
if isinstance(value, ReducerRef):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return ReducerRef(name=value)
|
||||
return ReducerRef.model_validate(value)
|
||||
|
||||
|
||||
def _state_metadata_by_name(value: object) -> dict[str, StateFieldMetadata]:
|
||||
if not isinstance(value, type) or not issubclass(value, BaseModel):
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user