migrate all everything to the new stuff

This commit is contained in:
lda
2026-05-21 06:16:58 +07:00 Verified
parent 0a079ec5f1
commit 274bf34edf
16 changed files with 532 additions and 341 deletions
+39 -28
View File
@@ -4,7 +4,18 @@ from typing import Any
from pydantic import BaseModel
from wf_authoring import NodeReturn, NodeSpec, WorkflowBuilder, node, outcome, state
from wf_authoring import (
NodeReturn,
NodeSpec,
WorkflowBuilder,
input_from,
input_path,
node,
outcome,
output_to,
state,
state_path,
)
from wf_core import END
@@ -169,8 +180,8 @@ def _message_use(
return graph.use(
spec,
id=id,
in_map={"state.message": "message"},
out_map={"message": "state.message"},
input=[input_from(state_path("message"), "message")],
output=[output_to("message", state_path("message"))],
)
@@ -184,8 +195,8 @@ def _status_use(
return graph.use(
spec,
id=id,
in_map={"state.status": "status"},
out_map={"message": "state.message"},
input=[input_from(state_path("status"), "status")],
output=[output_to("message", state_path("message"))],
)
@@ -199,11 +210,11 @@ def _metrics_use(
return graph.use(
spec,
id=id,
in_map={
"state.message": "message",
"state.length": "length",
},
out_map={"message": "state.message"},
input=[
input_from(state_path("message"), "message"),
input_from(state_path("length"), "length"),
],
output=[output_to("message", state_path("message"))],
)
@@ -213,8 +224,8 @@ def build_branch_workflow() -> WorkflowBuilder:
router = graph.use(
classify_message,
id="classify",
in_map={"input.text": "text"},
out_map={"message": "state.message"},
input=[input_from(input_path("text"), "text")],
output=[output_to("message", state_path("message"))],
)
graph.branch(
router,
@@ -237,8 +248,8 @@ def build_handle_workflow() -> WorkflowBuilder:
lookup = graph.use(
lookup_message,
id="lookup",
in_map={"input.text": "text"},
out_map={"message": "state.message"},
input=[input_from(input_path("text"), "text")],
output=[output_to("message", state_path("message"))],
)
deliver = _message_use(graph, deliver_message, id="deliver")
failed = _message_use(graph, fail_safely, id="failed")
@@ -256,8 +267,8 @@ def build_match_workflow() -> WorkflowBuilder:
classifier = graph.use(
classify_status,
id="classify_status",
in_map={"input.text": "text"},
out_map={"status": "state.status"},
input=[input_from(input_path("text"), "text")],
output=[output_to("status", state_path("status"))],
)
decision = graph.match(
state("status"),
@@ -282,11 +293,11 @@ def build_when_workflow() -> WorkflowBuilder:
measure = graph.use(
measure_text,
id="measure",
in_map={"input.text": "text"},
out_map={
"message": "state.message",
"length": "state.length",
},
input=[input_from(input_path("text"), "text")],
output=[
output_to("message", state_path("message")),
output_to("length", state_path("length")),
],
)
decision = graph.when(
state("length").ge(6),
@@ -307,11 +318,11 @@ def build_choose_workflow() -> WorkflowBuilder:
measure = graph.use(
measure_text,
id="measure",
in_map={"input.text": "text"},
out_map={
"message": "state.message",
"length": "state.length",
},
input=[input_from(input_path("text"), "text")],
output=[
output_to("message", state_path("message")),
output_to("length", state_path("length")),
],
)
decision = graph.choose(
(state("length").ge(20), _metrics_use(graph, long_message, id="long")),
@@ -333,8 +344,8 @@ def build_use_ref_workflow() -> WorkflowBuilder:
echo = graph.use_ref(
"demo.echo",
id="echo",
in_map={"input.text": "message"},
out_map={"echoed": "state.message"},
input=[input_from(input_path("text"), "message")],
output=[output_to("echoed", state_path("message"))],
)
graph.connect(echo, "ok", END)
graph.set_entry_point(echo)
+58 -11
View File
@@ -9,7 +9,8 @@ DemoHandler = Callable[[dict[str, object], RuntimeContext], dict[str, object]]
def build_demo_workflow() -> Workflow:
return Workflow.model_validate({
return Workflow.model_validate(
{
"name": "drive_summary_demo",
"input_schema": {
"type": "object",
@@ -120,8 +121,18 @@ def build_demo_workflow() -> Workflow:
"type": "node",
"node": "drive_list_files",
"desc": "List files from a Google Drive folder",
"in_map": {"input.folder_id": "folder_id"},
"out_map": {"documents": "state.documents"},
"input": [
{
"target": {"root": "local", "parts": ["folder_id"]},
"path": {"root": "input", "parts": ["folder_id"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["documents"]},
"target": {"root": "state", "parts": ["documents"]},
}
],
},
{
"id": "summarize_each",
@@ -136,16 +147,36 @@ def build_demo_workflow() -> Workflow:
"type": "node",
"node": "summarize_document",
"desc": "Summarize one document",
"in_map": {"context.document": "document"},
"out_map": {"item_summary": "state.item_summaries"},
"input": [
{
"target": {"root": "local", "parts": ["document"]},
"path": {"root": "context", "parts": ["document"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["item_summary"]},
"target": {"root": "state", "parts": ["item_summaries"]},
}
],
},
{
"id": "combine_summaries",
"type": "node",
"node": "combine_summaries",
"desc": "Combine item summaries into one final summary",
"in_map": {"state.item_summaries": "item_summaries"},
"out_map": {"summary": "state.summary"},
"input": [
{
"target": {"root": "local", "parts": ["item_summaries"]},
"path": {"root": "state", "parts": ["item_summaries"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["summary"]},
"target": {"root": "state", "parts": ["summary"]},
}
],
},
{
"id": "should_email",
@@ -161,8 +192,18 @@ def build_demo_workflow() -> Workflow:
"type": "node",
"node": "send_email",
"desc": "Send the summary by email",
"in_map": {"state.summary": "summary"},
"out_map": {"email_status": "state.email_status"},
"input": [
{
"target": {"root": "local", "parts": ["summary"]},
"path": {"root": "state", "parts": ["summary"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["email_status"]},
"target": {"root": "state", "parts": ["email_status"]},
}
],
},
{
"id": "approve_email",
@@ -183,7 +224,12 @@ def build_demo_workflow() -> Workflow:
"type": "node",
"node": "mark_email_skipped",
"desc": "Record that email delivery was skipped",
"out_map": {"email_status": "state.email_status"},
"output": [
{
"source": {"root": "local", "parts": ["email_status"]},
"target": {"root": "state", "parts": ["email_status"]},
}
],
},
],
"edges": [
@@ -203,7 +249,8 @@ def build_demo_workflow() -> Workflow:
{"from": "send_email", "outcome": "sent", "to": END},
{"from": "skip_email", "outcome": "ok", "to": END},
],
})
}
)
def drive_list_files(
+23 -6
View File
@@ -34,7 +34,8 @@ async def run_example() -> dict[str, object]:
await service.refresh_connection_catalog("fixture.personal")
plan = RawWorkflowPlan.model_validate({
plan = RawWorkflowPlan.model_validate(
{
"name": "mcp_echo_workflow",
"input_schema": {
"type": "object",
@@ -56,15 +57,30 @@ async def run_example() -> dict[str, object]:
"id": "echo",
"type": "node",
"node": "fixture.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [
{
"target": {"root": "local", "parts": ["text"]},
"path": {"root": "input", "parts": ["text"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
],
},
{
"id": "raise_mcp_error",
"type": "node",
"node": "wf.std.runtime_error",
"in_map": {"input.text": "message"},
"out_map": {},
"input": [
{
"target": {"root": "local", "parts": ["message"]},
"path": {"root": "input", "parts": ["text"]},
}
],
"output": [],
},
],
"edges": [
@@ -72,7 +88,8 @@ async def run_example() -> dict[str, object]:
{"from": "echo", "outcome": "error", "to": "raise_mcp_error"},
{"from": "raise_mcp_error", "outcome": "ok", "to": END},
],
})
}
)
run = await service.run_workflow_from_plan(plan, {"text": "hello from MCP"})
return {
+20 -11
View File
@@ -4,7 +4,16 @@ from typing import Literal
from pydantic import BaseModel
from wf_authoring import NodeReturn, WorkflowBuilder, node, outcome
from wf_authoring import (
NodeReturn,
WorkflowBuilder,
input_from,
input_path,
node,
outcome,
output_to,
state_path,
)
from wf_core import END
@@ -65,20 +74,20 @@ def build_normalized_wrapper() -> WorkflowBuilder:
raw = graph.use(
raw_status_tool,
id="raw_tool",
in_map={"input.text": "text"},
out_map={
"status": "state.status",
"message": "state.message",
},
input=[input_from(input_path("text"), "text")],
output=[
output_to("status", state_path("status")),
output_to("message", state_path("message")),
],
)
normalizer = graph.use(
normalize_status,
id="normalize",
in_map={
"state.status": "status",
"state.message": "message",
},
out_map={"message": "state.message"},
input=[
input_from(state_path("status"), "status"),
input_from(state_path("message"), "message"),
],
output=[output_to("message", state_path("message"))],
)
graph.connect(raw, "ok", normalizer)
graph.connect(normalizer, "done", END)
+6
View File
@@ -10,9 +10,12 @@ from .dsl import (
expr,
graph_path,
input,
input_from,
input_path,
input_value,
merge_maps,
not_,
output_to,
state,
state_path,
)
@@ -111,7 +114,9 @@ __all__ = [
"first_item_or_none",
"graph_path",
"input",
"input_from",
"input_path",
"input_value",
"is_empty",
"last_item",
"last_item_or_none",
@@ -124,6 +129,7 @@ __all__ = [
"node",
"not_",
"outcome",
"output_to",
"reducer",
"state",
"state_field",
+13 -1
View File
@@ -9,7 +9,16 @@ from .conditions import (
not_,
state,
)
from .mapping import PathArg, bind_fields, bind_state, merge_maps, normalize_path
from .mapping import (
PathArg,
bind_fields,
bind_state,
input_from,
input_value,
merge_maps,
normalize_path,
output_to,
)
from .paths import GraphPath, context_path, graph_path, input_path, state_path
__all__ = [
@@ -26,10 +35,13 @@ __all__ = [
"expr",
"graph_path",
"input",
"input_from",
"input_path",
"input_value",
"merge_maps",
"normalize_path",
"not_",
"output_to",
"state",
"state_path",
]
+33 -2
View File
@@ -1,10 +1,17 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import TypeAlias
from typing import Any, TypeAlias
from wf_core.models.steps import InputPathBinding, InputValueBinding, OutputBinding
from .path_inputs import (
PathInput,
coerce_graph_path,
coerce_local_path,
coerce_state_path,
)
from .paths import GraphPath
from .path_inputs import PathInput, coerce_graph_path, coerce_state_path
PathArg: TypeAlias = PathInput | GraphPath
@@ -41,6 +48,30 @@ def bind_state(**mapping: PathArg) -> dict[str, str]:
}
def input_from(path: PathArg, target: PathInput) -> InputPathBinding:
"""Bind a workflow graph path into a node-local input path."""
return InputPathBinding(
target=coerce_local_path(target),
path=coerce_graph_path(path.path if isinstance(path, GraphPath) else path),
)
def input_value(target: PathInput, value: Any) -> InputValueBinding:
"""Bind a literal value into a node-local input path."""
return InputValueBinding(target=coerce_local_path(target), value=value)
def output_to(source: PathInput, target: PathArg) -> OutputBinding:
"""Bind a node-local output path back into workflow state."""
return OutputBinding(
source=coerce_local_path(source),
target=coerce_state_path(
target.path if isinstance(target, GraphPath) else target,
allow_legacy_root=True,
),
)
def merge_maps(*maps: Mapping[str, str]) -> dict[str, str]:
merged: dict[str, str] = {}
for mapping in maps:
+20 -2
View File
@@ -3,6 +3,8 @@ from __future__ import annotations
from typing import Any, cast
from wf_artifacts import RequiredCapability, create_workflow_artifact_from_plan
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from wf_platform import NodeSpecInventory
@@ -265,8 +267,8 @@ def _plan() -> dict[str, object]:
"id": "echo",
"type": "node",
"node": "demo.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [_input_binding("input.text", "text")],
"output": [_output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
@@ -277,3 +279,19 @@ def _set_first_node_ref(plan: dict[str, object], node_ref: str) -> None:
"""Set the first node ref in a loosely typed raw plan test fixture."""
nodes = cast("list[dict[str, Any]]", plan["nodes"])
nodes[0]["node"] = node_ref
def _input_binding(path: str, target: str) -> dict[str, object]:
"""Return canonical JSON for raw workflow plan fixtures."""
return InputPathBinding(
path=GraphSourcePath.parse(path),
target=LocalPath.parse(target),
).model_dump(mode="json")
def _output_binding(source: str, target: str) -> dict[str, object]:
"""Return canonical JSON for raw workflow plan fixtures."""
return OutputBinding(
source=LocalPath.parse(source),
target=StatePath.parse(target),
).model_dump(mode="json")
+18 -11
View File
@@ -5,7 +5,14 @@ from collections.abc import Iterator, Mapping
import pytest
from wf_authoring import WorkflowBuilder, input_path, state, state_path
from wf_authoring import (
WorkflowBuilder,
input_from,
input_path,
output_to,
state,
state_path,
)
from wf_core import END, RunStatus, WorkflowExecutionError
from wf_core.models.steps import InputPathBinding, InputValueBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
@@ -59,8 +66,8 @@ def test_builder_preserves_explicit_nested_node_local_maps() -> None:
step = builder.use(
auto_bind_node,
in_map={"state.text": "payload.text"},
out_map={"payload.text": "state.text"},
input=[input_from(state_path("text"), "payload.text")],
output=[output_to("payload.text", state_path("text"))],
)
assert isinstance(step.input[0], InputPathBinding)
@@ -80,8 +87,8 @@ def test_builder_use_accepts_typed_paths_and_literal_iterable_paths() -> None:
step = builder.use(
auto_bind_node,
in_map={input_path('"text.with.dot"'): ("payload.text",)},
out_map={("payload.text",): state_path(("state field",))},
input=[input_from(input_path('"text.with.dot"'), ("payload.text",))],
output=[output_to(("payload.text",), state_path(("state field",)))],
)
assert isinstance(step.input[0], InputPathBinding)
@@ -139,8 +146,8 @@ def test_builder_preserves_explicit_root_node_local_maps() -> None:
step = builder.use(
auto_bind_node,
in_map={"state.text": "."},
out_map={".": "state.text"},
input=[input_from(state_path("text"), ".")],
output=[output_to(".", state_path("text"))],
)
assert isinstance(step.input[0], InputPathBinding)
@@ -161,8 +168,8 @@ def test_builder_emits_canonical_node_bindings() -> None:
step = builder.use(
auto_bind_node,
id="update",
in_map={"input.text": "text"},
out_map={"text": "state.text"},
input=[input_from(input_path("text"), "text")],
output=[output_to("text", state_path("text"))],
)
builder.connect(step, "ok", END)
@@ -300,8 +307,8 @@ def test_builder_use_ref_creates_external_node_use_without_node_def() -> None:
step = builder.use_ref(
"demo.echo",
id="echo",
in_map={"input.text": "text"},
out_map={"echoed": "state.echoed"},
input=[input_from(input_path("text"), "text")],
output=[output_to("echoed", state_path("echoed"))],
)
builder.set_entry_point(step)
builder.connect(step, "ok", END)
+15 -11
View File
@@ -21,7 +21,9 @@ from wf_authoring import (
bind_fields,
bind_state,
build_registry,
input_from,
state,
output_to,
state_path,
context_path,
input_path,
@@ -151,8 +153,8 @@ def build_authoring_demo_workflow():
list_files = builder.use(
drive_list_files_spec,
id="list_files",
in_map=bind_fields(folder_id=input_path("folder_id")),
out_map=bind_state(documents=state_path("documents")),
input=[input_from(input_path("folder_id"), "folder_id")],
output=[output_to("documents", state_path("documents"))],
desc="List files from a Google Drive folder",
)
summarize_each = builder.foreach(
@@ -165,15 +167,15 @@ def build_authoring_demo_workflow():
summarize_one = builder.use(
summarize_document_spec,
id="summarize_one",
in_map=bind_fields(document=context_path("document")),
out_map=bind_state(item_summary=state_path("item_summaries")),
input=[input_from(context_path("document"), "document")],
output=[output_to("item_summary", state_path("item_summaries"))],
desc="Summarize one document",
)
combine_summaries = builder.use(
combine_summaries_spec,
id="combine_summaries",
in_map=bind_fields(item_summaries=state_path("item_summaries")),
out_map=bind_state(summary=state_path("summary")),
input=[input_from(state_path("item_summaries"), "item_summaries")],
output=[output_to("summary", state_path("summary"))],
desc="Combine item summaries into one final summary",
)
should_email = builder.condition(
@@ -183,8 +185,8 @@ def build_authoring_demo_workflow():
send_email = builder.use(
send_email_spec,
id="send_email",
in_map=bind_fields(summary=state_path("summary")),
out_map=bind_state(email_status=state_path("email_status")),
input=[input_from(state_path("summary"), "summary")],
output=[output_to("email_status", state_path("email_status"))],
desc="Send the summary by email",
)
approve_email = builder.interrupt(
@@ -203,7 +205,7 @@ def build_authoring_demo_workflow():
skip_email = builder.use(
mark_email_skipped_spec,
id="skip_email",
out_map=bind_state(email_status=state_path("email_status")),
output=[output_to("email_status", state_path("email_status"))],
desc="Record that email delivery was skipped",
)
@@ -373,12 +375,14 @@ def test_foreach_stress_with_many_documents() -> None:
assert len(run.state["documents"]) == document_count
assert len(run.state["item_summaries"]) == document_count
assert (
len([
len(
[
frame
for frame in run.frames.values()
if frame.kind == "foreach_iteration"
and frame.status == FrameStatus.COMPLETED
])
]
)
== document_count
)
assert len([entry for entry in run.trace if entry.step_type == "foreach"]) == (
+16 -12
View File
@@ -4,8 +4,6 @@ import pytest
from wf_authoring import (
WorkflowBuilder,
bind_fields,
bind_state,
build_registry,
coalesce,
constant,
@@ -14,9 +12,11 @@ from wf_authoring import (
first_item_maybe,
first_item_or_none,
is_empty,
input_from,
last_item,
last_item_or_none,
length,
output_to,
pick_path,
pick_key,
project_fields,
@@ -39,21 +39,23 @@ def _build_first_workflow(use_safe_first: bool = False):
builder = WorkflowBuilder(
name="first_demo",
input_schema=SchemaRef(type="object"),
state_schema=StateSchema.model_validate({
state_schema=StateSchema.model_validate(
{
"type": "object",
"properties": {
"items": {"type": "array"},
"item": {"type": ["string", "null"]},
},
}),
}
),
output_schema=SchemaRef(type="object"),
start="pick_first",
)
node = builder.use(
spec,
id="pick_first",
in_map=bind_fields(items=state_path("items")),
out_map=bind_state(item=state_path("item")),
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
builder.connect(node, "ok", "__end__")
return builder.compile(), build_registry(spec)
@@ -63,28 +65,30 @@ def _build_first_maybe_workflow():
builder = WorkflowBuilder(
name="first_maybe_demo",
input_schema=SchemaRef(type="object"),
state_schema=StateSchema.model_validate({
state_schema=StateSchema.model_validate(
{
"type": "object",
"properties": {
"items": {"type": "array"},
"item": {"type": ["string", "null"]},
"missing": {"type": "boolean"},
},
}),
}
),
output_schema=SchemaRef(type="object"),
start="pick_first",
)
pick_first = builder.use(
first_item_maybe,
id="pick_first",
in_map=bind_fields(items=state_path("items")),
out_map=bind_state(item=state_path("item")),
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
mark_missing = builder.use(
first_item_or_none,
id="mark_missing",
in_map=bind_fields(items=state_path("items")),
out_map=bind_state(item=state_path("item")),
input=[input_from(state_path("items"), "items")],
output=[output_to("item", state_path("item"))],
)
builder.connect(pick_first, "found", "__end__")
builder.connect(pick_first, "missing", mark_missing)
+3 -2
View File
@@ -9,8 +9,10 @@ from wf_authoring import (
ReducerCatalog,
WorkflowBuilder,
node,
output_to,
reducer,
state_field,
state_path,
)
from wf_core import ReducerRef
@@ -102,8 +104,7 @@ def test_builder_executes_with_custom_reducer_catalog() -> None:
)
step = builder.use(
emit,
in_map={},
out_map={"total": "state.total"},
output=[output_to("total", state_path("total"))],
)
builder.set_entry_point(step)
builder.connect(step, "ok", "__end__")
+8 -4
View File
@@ -23,7 +23,9 @@ from .test_support import (
FailingDiscoveryAdapter,
FakeAdapter,
echo_tool,
input_binding,
local_temp_root,
output_binding,
)
@@ -32,7 +34,8 @@ def test_load_broker_config_resolves_relative_store_root() -> None:
tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps({
json.dumps(
{
"store_root": ".broker-store",
"connections": [
{
@@ -41,7 +44,8 @@ def test_load_broker_config_resolves_relative_store_root() -> None:
"account": "personal",
}
],
}),
}
),
encoding="utf-8",
)
@@ -518,8 +522,8 @@ def _echo_artifact() -> WorkflowArtifact:
"id": "echo",
"type": "node",
"node": "demo.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
+14 -12
View File
@@ -26,7 +26,9 @@ from .test_support import (
FakeAdapter,
echo_tool,
finalize_tool,
input_binding,
local_temp_root,
output_binding,
)
@@ -55,8 +57,8 @@ def _single_echo_plan(plan_name: str, node_name: str) -> RawWorkflowPlan:
"id": "echo",
"type": "node",
"node": node_name,
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
edges=[
@@ -360,15 +362,15 @@ def test_service_compiles_and_runs_raw_plan() -> None:
"id": "echo",
"type": "node",
"node": "demo.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
},
{
"id": "finalize",
"type": "node",
"node": "demo.personal.finalize_tool",
"in_map": {"state.echoed": "echoed"},
"out_map": {"result": "state.result"},
"input": [input_binding("state.echoed", "echoed")],
"output": [output_binding("result", "state.result")],
},
],
edges=[
@@ -424,8 +426,8 @@ def test_service_resolves_registered_spec_with_dotted_local_name() -> None:
"id": "echo",
"type": "node",
"node": "demo.personal.foo.bar",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
edges=[
@@ -571,8 +573,8 @@ def test_service_does_not_resolve_specs_hidden_from_planner() -> None:
"id": "echo",
"type": "node",
"node": "hidden.source.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
edges=[
@@ -865,8 +867,8 @@ def test_service_records_tool_call_events() -> None:
"id": "echo",
"type": "node",
"node": "demo.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
edges=[
+18
View File
@@ -9,6 +9,8 @@ from pydantic import BaseModel, Field
from wf_authoring import NodeReturn, node
from wf_core import RuntimeContext
from wf_core.models.steps import InputPathBinding, OutputBinding
from wf_core.paths import GraphSourcePath, LocalPath, StatePath
from wf_mcp.capabilities import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
from wf_mcp.models import AuthRecord, ConnectionConfig
from wf_mcp.sdk import ToolCallResult
@@ -55,6 +57,22 @@ def fixture_server_path() -> str:
return str(Path(__file__).resolve().parents[1] / "fixtures" / "mcp_echo_server.py")
def input_binding(path: str, target: str) -> dict[str, object]:
"""Return canonical JSON for a node input path binding in raw plans."""
return InputPathBinding(
path=GraphSourcePath.parse(path),
target=LocalPath.parse(target),
).model_dump(mode="json")
def output_binding(source: str, target: str) -> dict[str, object]:
"""Return canonical JSON for a node output binding in raw plans."""
return OutputBinding(
source=LocalPath.parse(source),
target=StatePath.parse(target),
).model_dump(mode="json")
def everything_server_connection() -> ConnectionConfig | None:
transport = os.environ.get("MCP_EVERYTHING_TRANSPORT", "stdio")
if transport == "stdio":
+5 -5
View File
@@ -25,7 +25,7 @@ from wf_platform import (
SourceVisibility,
)
from .test_support import echo_tool, local_temp_root
from .test_support import echo_tool, input_binding, local_temp_root, output_binding
class AmountInput(BaseModel):
@@ -1263,8 +1263,8 @@ def _echo_artifact() -> WorkflowArtifact:
"id": "echo",
"type": "node",
"node": "demo.personal.echo_tool",
"in_map": {"input.text": "text"},
"out_map": {"echoed": "state.echoed"},
"input": [input_binding("input.text", "text")],
"output": [output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
@@ -1367,8 +1367,8 @@ def _custom_reducer_artifact() -> WorkflowArtifact:
"id": "amount",
"type": "node",
"node": "demo.personal.amount_tool",
"in_map": {"input.amount": "amount"},
"out_map": {"amount": "state.total"},
"input": [input_binding("input.amount", "amount")],
"output": [output_binding("amount", "state.total")],
}
],
"edges": [{"from": "amount", "outcome": "ok", "to": "__end__"}],