misc
This commit is contained in:
@@ -134,6 +134,20 @@ Graph source paths in `in` normally start with `input.`, `state.`, or
|
|||||||
`context.`. Node-local paths do not use those prefixes; they are paths inside
|
`context.`. Node-local paths do not use those prefixes; they are paths inside
|
||||||
the target capability's input or output payload.
|
the target capability's input or output payload.
|
||||||
|
|
||||||
|
In canonical structural paths, `parts` is a list of literal path segments. Do
|
||||||
|
not put `"user.name"` in one segment unless the actual JSON property name is
|
||||||
|
literally `user.name`. For normal nested objects, write:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"root": "input", "parts": ["user", "name"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
not:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"root": "input", "parts": ["user.name"]}
|
||||||
|
```
|
||||||
|
|
||||||
For example, this canonical input/output pair:
|
For example, this canonical input/output pair:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
+71
-13
@@ -14,8 +14,19 @@ class InputPathBinding(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
target: LocalPath
|
target: LocalPath = Field(
|
||||||
path: GraphSourcePath
|
description=(
|
||||||
|
"Node-local input path to populate. Use {'root': 'local', "
|
||||||
|
"'parts': ['field']} or {'root': 'local', 'parts': []} for the "
|
||||||
|
"whole node input payload."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
path: GraphSourcePath = Field(
|
||||||
|
description=(
|
||||||
|
"Workflow source path to read from input, state, or context. "
|
||||||
|
"Example: {'root': 'input', 'parts': ['text']}."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InputValueBinding(BaseModel):
|
class InputValueBinding(BaseModel):
|
||||||
@@ -23,13 +34,26 @@ class InputValueBinding(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
target: LocalPath
|
target: LocalPath = Field(
|
||||||
value: object
|
description="Node-local input path that receives this literal JSON value."
|
||||||
|
)
|
||||||
|
value: object = Field(
|
||||||
|
description=(
|
||||||
|
"Literal JSON-compatible value to pass to the node. Use this for "
|
||||||
|
"constants, not for values read from workflow input or state."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
InputBinding = Annotated[
|
InputBinding = Annotated[
|
||||||
InputPathBinding | InputValueBinding,
|
InputPathBinding | InputValueBinding,
|
||||||
Field(union_mode="left_to_right"),
|
Field(
|
||||||
|
union_mode="left_to_right",
|
||||||
|
description=(
|
||||||
|
"Canonical node input binding. Use either a path binding with "
|
||||||
|
"`path`, or a literal binding with `value`; do not provide both."
|
||||||
|
),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
"""Canonical node input binding, distinguished by `path` vs `value` shape."""
|
"""Canonical node input binding, distinguished by `path` vs `value` shape."""
|
||||||
|
|
||||||
@@ -39,8 +63,18 @@ class OutputBinding(BaseModel):
|
|||||||
|
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
source: LocalPath
|
source: LocalPath = Field(
|
||||||
target: StatePath
|
description=(
|
||||||
|
"Node-local output path to read. Use {'root': 'local', 'parts': []} "
|
||||||
|
"to write the whole node output payload."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
target: StatePath = Field(
|
||||||
|
description=(
|
||||||
|
"Writable workflow state path. Bare state is invalid; use a field "
|
||||||
|
"path such as {'root': 'state', 'parts': ['echoed']}."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class NodeUse(BaseModel):
|
class NodeUse(BaseModel):
|
||||||
@@ -50,8 +84,20 @@ class NodeUse(BaseModel):
|
|||||||
type: Literal["node"]
|
type: Literal["node"]
|
||||||
node: str
|
node: str
|
||||||
desc: str | None = None
|
desc: str | None = None
|
||||||
input: list[InputBinding] = Field(default_factory=list)
|
input: list[InputBinding] = Field(
|
||||||
output: list[OutputBinding] = Field(default_factory=list)
|
default_factory=list,
|
||||||
|
description=(
|
||||||
|
"Bindings that build the node-local input payload from workflow "
|
||||||
|
"input/state/context paths or literal values."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
output: list[OutputBinding] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description=(
|
||||||
|
"Bindings that commit node-local output fields into workflow state "
|
||||||
|
"after the node returns successfully."
|
||||||
|
),
|
||||||
|
)
|
||||||
retry: int | None = Field(default=None, ge=0)
|
retry: int | None = Field(default=None, ge=0)
|
||||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||||
|
|
||||||
@@ -124,8 +170,10 @@ class ForeachNode(BaseModel):
|
|||||||
|
|
||||||
id: str
|
id: str
|
||||||
type: Literal["foreach"]
|
type: Literal["foreach"]
|
||||||
over: GraphSourcePath
|
over: GraphSourcePath = Field(
|
||||||
as_: str = Field(alias="as")
|
description="Workflow input/state/context path that must resolve to a list."
|
||||||
|
)
|
||||||
|
as_: str = Field(alias="as", description="Context key for the current item.")
|
||||||
mode: Literal["serial", "parallel"] = "serial"
|
mode: Literal["serial", "parallel"] = "serial"
|
||||||
on_item_error: Literal["fail", "collect", "skip"] = "fail"
|
on_item_error: Literal["fail", "collect", "skip"] = "fail"
|
||||||
|
|
||||||
@@ -143,8 +191,18 @@ class InterruptNode(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
type: Literal["interrupt"]
|
type: Literal["interrupt"]
|
||||||
kind: str
|
kind: str
|
||||||
request: list[InputBinding] = Field(default_factory=list)
|
request: list[InputBinding] = Field(
|
||||||
resume: list[OutputBinding] = Field(default_factory=list)
|
default_factory=list,
|
||||||
|
description=(
|
||||||
|
"Bindings that build the interrupt request payload sent to the client."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
resume: list[OutputBinding] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description=(
|
||||||
|
"Bindings that commit resume payload fields back into workflow state."
|
||||||
|
),
|
||||||
|
)
|
||||||
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
|
outcomes: list[str] = Field(default_factory=lambda: ["submitted"])
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
|
|||||||
@@ -97,13 +97,13 @@ class TransparentAdminHandlers:
|
|||||||
|
|
||||||
def enable_connection(self, connection_id: str) -> dict[str, Any]:
|
def enable_connection(self, connection_id: str) -> dict[str, Any]:
|
||||||
return self.runtime.require_manager().set_connection_enabled(
|
return self.runtime.require_manager().set_connection_enabled(
|
||||||
connection_id,
|
connection_id=connection_id,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def disable_connection(self, connection_id: str) -> dict[str, Any]:
|
def disable_connection(self, connection_id: str) -> dict[str, Any]:
|
||||||
return self.runtime.require_manager().set_connection_enabled(
|
return self.runtime.require_manager().set_connection_enabled(
|
||||||
connection_id,
|
connection_id=connection_id,
|
||||||
enabled=False,
|
enabled=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from typing import Annotated, Any, Mapping
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
from wf_artifacts import ArtifactKind
|
from wf_artifacts import ArtifactKind
|
||||||
from wf_artifacts.models import RequiredCapability
|
from wf_artifacts.models import RequiredCapability
|
||||||
@@ -546,7 +547,16 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
async def run_deployment(
|
async def run_deployment(
|
||||||
deployment_id: str,
|
deployment_id: str,
|
||||||
workflow_input: dict[str, Any],
|
workflow_input: dict[str, Any],
|
||||||
trace_range: TraceRange | None = None,
|
trace_range: Annotated[
|
||||||
|
TraceRange | None,
|
||||||
|
Field(
|
||||||
|
description=(
|
||||||
|
"Debug traces range to return. Omit for normal compact runs; "
|
||||||
|
"trace entries can include resolved inputs, outputs, and "
|
||||||
|
"state changes."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return await handlers.run_deployment(
|
return await handlers.run_deployment(
|
||||||
deployment_id=deployment_id,
|
deployment_id=deployment_id,
|
||||||
|
|||||||
@@ -111,6 +111,28 @@ def test_node_use_serializes_canonical_binding_paths_as_structural_json():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_canonical_binding_json_schema_describes_nested_fields():
|
||||||
|
schema = NodeUse.model_json_schema()
|
||||||
|
defs = schema["$defs"]
|
||||||
|
input_path = defs["InputPathBinding"]
|
||||||
|
input_value = defs["InputValueBinding"]
|
||||||
|
output = defs["OutputBinding"]
|
||||||
|
|
||||||
|
assert "whole node input payload" in input_path["properties"]["target"][
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
assert "input, state, or context" in input_path["properties"]["path"][
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
assert "Literal JSON-compatible value" in input_value["properties"]["value"][
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
assert "whole node output payload" in output["properties"]["source"][
|
||||||
|
"description"
|
||||||
|
]
|
||||||
|
assert "Bare state is invalid" in output["properties"]["target"]["description"]
|
||||||
|
|
||||||
|
|
||||||
def test_node_use_rejects_mixed_old_and_new_binding_styles():
|
def test_node_use_rejects_mixed_old_and_new_binding_styles():
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
NodeUse.model_validate(
|
NodeUse.model_validate(
|
||||||
|
|||||||
@@ -389,6 +389,14 @@ def test_workflow_tools_have_human_metadata() -> None:
|
|||||||
assert "saved workflow artifacts" in (list_artifacts.description or "")
|
assert "saved workflow artifacts" in (list_artifacts.description or "")
|
||||||
assert run_deployment.title == "Run Workflow Deployment"
|
assert run_deployment.title == "Run Workflow Deployment"
|
||||||
assert "deployment_id" in (run_deployment.description or "")
|
assert "deployment_id" in (run_deployment.description or "")
|
||||||
|
assert "trace_range" in run_deployment.inputSchema["properties"]
|
||||||
|
trace_range_schema = run_deployment.inputSchema["properties"][
|
||||||
|
"trace_range"
|
||||||
|
]
|
||||||
|
assert "Debug traces" in trace_range_schema.get("description", "")
|
||||||
|
assert "null" in [
|
||||||
|
option.get("type") for option in trace_range_schema["anyOf"]
|
||||||
|
]
|
||||||
|
|
||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user