use new input/output field

This commit is contained in:
lda
2026-05-21 03:09:26 +07:00 Verified
parent c37be4f13c
commit 6e768435d8
8 changed files with 400 additions and 82 deletions
+136 -43
View File
@@ -1,6 +1,6 @@
from __future__ import annotations
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from typing import Any, Literal, cast
import warnings
@@ -40,13 +40,17 @@ from ..schemas import SchemaLike, StateSchemaLike, schema_ref_from, state_schema
from ..nodes import NodeSpec
from .ids import next_step_id, slug_id
from .mapping import (
InputBindingArg,
MapArg,
OutputBindingArg,
auto_input_map,
auto_output_map,
coerce_path,
normalize_input_bindings,
normalize_input_mapping,
normalize_input_values,
normalize_mapping,
normalize_output_bindings,
normalize_output_mapping,
)
from .refs import (
@@ -99,6 +103,49 @@ def _canonical_output_bindings(
]
def _reject_mixed_binding_styles(
*,
input: object | None,
output: object | None,
in_map: object | None,
input_values: object | None,
out_map: object | None,
) -> None:
"""Keep canonical binding lists and deprecated map sugar from mixing."""
if input is not None and (in_map is not None or input_values is not None):
raise TypeError(
"cannot mix canonical input with deprecated in_map/input_values"
)
if output is not None and out_map is not None:
raise TypeError("cannot mix canonical output with deprecated out_map")
def _warn_deprecated_binding_sugar(
*,
in_map: object | None,
input_values: object | None,
out_map: object | None,
) -> None:
"""Warn when callers explicitly use map sugar instead of canonical bindings."""
used = [
name
for name, value in (
("in_map", in_map),
("input_values", input_values),
("out_map", out_map),
)
if value is not None
]
if not used:
return
warnings.warn(
f"{', '.join(used)} are deprecated WorkflowBuilder sugar; use canonical "
"input/output binding lists instead",
DeprecationWarning,
stacklevel=3,
)
@dataclass(slots=True)
class WorkflowBuilder:
name: str
@@ -122,41 +169,62 @@ class WorkflowBuilder:
spec: NodeSpec[Any, Any],
*,
id: str | None = None,
input: Sequence[InputBindingArg] | None = None,
output: Sequence[OutputBindingArg] | None = None,
in_map: MapArg | None = None,
input_values: Mapping[Any, Any] | None = None,
out_map: MapArg | None = None,
desc: str | None = None,
) -> NodeUse:
_reject_mixed_binding_styles(
input=input,
output=output,
in_map=in_map,
input_values=input_values,
out_map=out_map,
)
_warn_deprecated_binding_sugar(
in_map=in_map,
input_values=input_values,
out_map=out_map,
)
self.node_specs[spec.name] = spec
normalized_input_schema = cast(SchemaRef, self.input_schema)
normalized_state_schema = cast(StateSchema, self.state_schema)
raw_in_map = (
auto_input_map(
spec,
input_schema=normalized_input_schema,
state_schema=normalized_state_schema,
if input is not None:
node_input = normalize_input_bindings(input)
else:
raw_in_map = (
auto_input_map(
spec,
input_schema=normalized_input_schema,
state_schema=normalized_state_schema,
)
if in_map is None
else in_map
)
node_input = _canonical_input_bindings(
normalize_input_mapping(raw_in_map),
normalize_input_values(input_values),
)
if output is not None:
node_output = normalize_output_bindings(output)
else:
raw_out_map = (
auto_output_map(spec, state_schema=normalized_state_schema)
if out_map is None
else out_map
)
node_output = _canonical_output_bindings(
normalize_output_mapping(raw_out_map)
)
if in_map is None
else in_map
)
normalized_in_map = normalize_input_mapping(raw_in_map)
normalized_input_values = normalize_input_values(input_values)
raw_out_map = (
auto_output_map(spec, state_schema=normalized_state_schema)
if out_map is None
else out_map
)
normalized_out_map = normalize_output_mapping(raw_out_map)
node = NodeUse(
id=id or self._next_step_id(slug_id(spec.name)),
type="node",
node=spec.name,
desc=desc or spec.description,
input=_canonical_input_bindings(
normalized_in_map,
normalized_input_values,
),
output=_canonical_output_bindings(normalized_out_map),
input=node_input,
output=node_output,
)
self.nodes.append(node)
return node
@@ -166,6 +234,8 @@ class WorkflowBuilder:
name: str,
*,
id: str | None = None,
input: Sequence[InputBindingArg] | None = None,
output: Sequence[OutputBindingArg] | None = None,
in_map: MapArg | None = None,
input_values: Mapping[Any, Any] | None = None,
out_map: MapArg | None = None,
@@ -178,19 +248,38 @@ class WorkflowBuilder:
hatch for MCP/saved-workflow capability refs that are resolved later by
the environment runner into node definitions and registry handlers.
"""
normalized_in_map = normalize_input_mapping(in_map)
normalized_input_values = normalize_input_values(input_values)
normalized_out_map = normalize_output_mapping(out_map)
_reject_mixed_binding_styles(
input=input,
output=output,
in_map=in_map,
input_values=input_values,
out_map=out_map,
)
_warn_deprecated_binding_sugar(
in_map=in_map,
input_values=input_values,
out_map=out_map,
)
node_input = (
normalize_input_bindings(input)
if input is not None
else _canonical_input_bindings(
normalize_input_mapping(in_map),
normalize_input_values(input_values),
)
)
node_output = (
normalize_output_bindings(output)
if output is not None
else _canonical_output_bindings(normalize_output_mapping(out_map))
)
node = NodeUse(
id=id or self._next_step_id(slug_id(name)),
type="node",
node=name,
desc=desc,
input=_canonical_input_bindings(
normalized_in_map,
normalized_input_values,
),
output=_canonical_output_bindings(normalized_out_map),
input=node_input,
output=node_output,
)
self.nodes.append(node)
return node
@@ -263,14 +352,16 @@ class WorkflowBuilder:
) -> ForeachNode:
# Core foreach still stores `over` as a string. Keep this compatibility
# path isolated until ForeachNode grows a typed GraphSourcePath field.
node = ForeachNode.model_validate({
"id": id or self._next_step_id(f"foreach_{slug_id(as_)}"),
"type": "foreach",
"over": coerce_path(over),
"as": as_,
"mode": mode,
"on_item_error": on_item_error,
})
node = ForeachNode.model_validate(
{
"id": id or self._next_step_id(f"foreach_{slug_id(as_)}"),
"type": "foreach",
"over": coerce_path(over),
"as": as_,
"mode": mode,
"on_item_error": on_item_error,
}
)
self.nodes.append(node)
return node
@@ -304,11 +395,13 @@ class WorkflowBuilder:
source = self._resolve_branch_ref(from_)
target = self._resolve_branch_ref(to)
self.edges.append(
Edge.model_validate({
"from": step_id(source),
"outcome": outcome,
"to": step_id(target),
})
Edge.model_validate(
{
"from": step_id(source),
"outcome": outcome,
"to": step_id(target),
}
)
)
return source, target
+69 -11
View File
@@ -1,9 +1,15 @@
from __future__ import annotations
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from typing import Any, TypeAlias
from wf_core import SchemaRef, StateSchema
from wf_core.models.steps import (
InputBinding,
InputPathBinding,
InputValueBinding,
OutputBinding,
)
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from ..dsl import GraphPath
@@ -18,6 +24,8 @@ MapArg: TypeAlias = Mapping[Any, Any]
InputMap: TypeAlias = dict[GraphSourcePath, LocalPath]
OutputMap: TypeAlias = dict[LocalPath, StatePath]
InputValues: TypeAlias = dict[LocalPath, Any]
InputBindingArg: TypeAlias = InputBinding | Mapping[str, object]
OutputBindingArg: TypeAlias = OutputBinding | Mapping[str, object]
def coerce_path(value: object) -> str:
@@ -41,16 +49,26 @@ def normalize_mapping(mapping: MapArg | None) -> dict[str, str]:
}
def _reject_mapping_path_key(value: object, *, field_name: str) -> None:
"""Reject structural dict keys; use canonical binding lists for JSON shapes."""
if isinstance(value, Mapping):
raise TypeError(
f"structural path dicts cannot be map keys in {field_name}; "
"use canonical input/output binding lists instead"
)
def normalize_input_mapping(mapping: MapArg | None) -> InputMap:
"""Normalize `in_map`: graph source path -> node-local input path."""
if mapping is None:
return {}
return {
coerce_graph_path(source.path if isinstance(source, GraphPath) else source): (
coerce_local_path(destination)
)
for source, destination in mapping.items()
}
normalized: InputMap = {}
for source, destination in mapping.items():
_reject_mapping_path_key(source, field_name="in_map")
normalized[
coerce_graph_path(source.path if isinstance(source, GraphPath) else source)
] = coerce_local_path(destination)
return normalized
def normalize_input_values(mapping: Mapping[Any, Any] | None) -> InputValues:
@@ -60,17 +78,57 @@ def normalize_input_values(mapping: Mapping[Any, Any] | None) -> InputValues:
return {coerce_local_path(target): value for target, value in mapping.items()}
def normalize_input_bindings(
bindings: Sequence[InputBindingArg] | None,
) -> list[InputBinding]:
"""Validate canonical input binding structs for WorkflowBuilder.use()."""
if bindings is None:
return []
normalized: list[InputBinding] = []
for binding in bindings:
if isinstance(binding, InputPathBinding | InputValueBinding):
normalized.append(binding)
continue
if not isinstance(binding, Mapping):
raise TypeError(f"unsupported input binding {binding!r}")
if "path" in binding:
normalized.append(InputPathBinding.model_validate(binding))
elif "value" in binding:
normalized.append(InputValueBinding.model_validate(binding))
else:
raise TypeError("input binding must contain either 'path' or 'value'")
return normalized
def normalize_output_mapping(mapping: MapArg | None) -> OutputMap:
"""Normalize `out_map`: node-local output path -> workflow state path."""
if mapping is None:
return {}
return {
coerce_local_path(source): coerce_state_path(
normalized: OutputMap = {}
for source, target in mapping.items():
_reject_mapping_path_key(source, field_name="out_map")
normalized[coerce_local_path(source)] = coerce_state_path(
target.path if isinstance(target, GraphPath) else target,
allow_legacy_root=True,
)
for source, target in mapping.items()
}
return normalized
def normalize_output_bindings(
bindings: Sequence[OutputBindingArg] | None,
) -> list[OutputBinding]:
"""Validate canonical output binding structs for WorkflowBuilder.use()."""
if bindings is None:
return []
normalized: list[OutputBinding] = []
for binding in bindings:
if isinstance(binding, OutputBinding):
normalized.append(binding)
continue
if not isinstance(binding, Mapping):
raise TypeError(f"unsupported output binding {binding!r}")
normalized.append(OutputBinding.model_validate(binding))
return normalized
def auto_input_map(