feat: replace draft output bind with general bind
This commit is contained in:
@@ -180,15 +180,27 @@ class WorkflowDraftAuthoringApi:
|
||||
step = draft_step(workspace.draft, step_id)
|
||||
capability_name = step.get("use")
|
||||
if not isinstance(capability_name, str) or not capability_name:
|
||||
raise ValueError(f"draft step {step_id!r} does not declare a capability use")
|
||||
raise ValueError(
|
||||
f"draft step {step_id!r} does not declare a capability use"
|
||||
)
|
||||
spec = self.context.specs.get_qualified_spec(capability_name)
|
||||
|
||||
source_root, source_parts = _graph_parts(source_path) if not source_path.startswith("local.") else ("local", LocalPath.parse(source_path).parts)
|
||||
target_root, target_parts = _graph_parts(target_path) if not target_path.startswith("local.") else ("local", LocalPath.parse(target_path).parts)
|
||||
source_root, source_parts = (
|
||||
_graph_parts(source_path)
|
||||
if not source_path.startswith("local.")
|
||||
else ("local", LocalPath.parse(source_path).parts)
|
||||
)
|
||||
target_root, target_parts = (
|
||||
_graph_parts(target_path)
|
||||
if not target_path.startswith("local.")
|
||||
else ("local", LocalPath.parse(target_path).parts)
|
||||
)
|
||||
|
||||
if target_root == "local" and source_root in {"input", "state"}:
|
||||
local_field = _local_field(target_path)
|
||||
input_schema = spec.input_schema_contract or spec.input_model.model_json_schema()
|
||||
input_schema = (
|
||||
spec.input_schema_contract or spec.input_model.model_json_schema()
|
||||
)
|
||||
schema_key = "input_schema" if source_root == "input" else "state_schema"
|
||||
target_schema = workspace.draft.get(schema_key, {})
|
||||
if not isinstance(target_schema, dict):
|
||||
@@ -218,7 +230,9 @@ class WorkflowDraftAuthoringApi:
|
||||
|
||||
if source_root == "local" and target_root in {"state", "output"}:
|
||||
local_field = _local_field(source_path)
|
||||
output_schema = spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
output_schema = (
|
||||
spec.output_schema_contract or spec.output_model.model_json_schema()
|
||||
)
|
||||
schema_key = "state_schema" if target_root == "state" else "output_schema"
|
||||
target_schema = workspace.draft.get(schema_key, {})
|
||||
if not isinstance(target_schema, dict):
|
||||
@@ -230,7 +244,9 @@ class WorkflowDraftAuthoringApi:
|
||||
target_parts=target_parts,
|
||||
)
|
||||
output_map = {
|
||||
**self.drafts._step_output_map(workspace_id=workspace_id, step_id=step_id),
|
||||
**self.drafts._step_output_map(
|
||||
workspace_id=workspace_id, step_id=step_id
|
||||
),
|
||||
local_field: target_path,
|
||||
}
|
||||
return await self.drafts.patch_draft_workspace(
|
||||
@@ -246,7 +262,9 @@ class WorkflowDraftAuthoringApi:
|
||||
],
|
||||
)
|
||||
|
||||
raise ValueError(f"unsupported bind direction: {source_path!r} -> {target_path!r}")
|
||||
raise ValueError(
|
||||
f"unsupported bind direction: {source_path!r} -> {target_path!r}"
|
||||
)
|
||||
|
||||
async def add_step_from_capability(
|
||||
self,
|
||||
|
||||
@@ -39,7 +39,10 @@ def input_bindings_payload(
|
||||
def output_bindings_payload(output_map: dict[str, str]) -> list[dict[str, Any]]:
|
||||
"""Serialize draft output maps into canonical string-path binding payloads."""
|
||||
return [
|
||||
{"source": _local_path_payload(source), "target": _graph_source_path_payload(target)}
|
||||
{
|
||||
"source": _local_path_payload(source),
|
||||
"target": _graph_source_path_payload(target),
|
||||
}
|
||||
for source, target in output_map.items()
|
||||
]
|
||||
|
||||
|
||||
@@ -473,6 +473,6 @@ def _draft_repair_hint(
|
||||
if not isinstance(output_field, str) or not isinstance(state_path, str):
|
||||
return None
|
||||
return (
|
||||
f"wf draft bind-output-to-state {workspace_id} --revision {revision} "
|
||||
f"--step {step_id} --output {output_field} --state {state_path}"
|
||||
f"wf draft bind {workspace_id} --revision {revision} "
|
||||
f"--step {step_id} --from local.{output_field} --to {state_path}"
|
||||
)
|
||||
|
||||
@@ -31,17 +31,23 @@ def project_property_to_schema_path(
|
||||
_ensure_object_schema(projected, "target_schema")
|
||||
parent = projected
|
||||
for index, part in enumerate(target_parts[:-1]):
|
||||
properties = _properties_for_object(parent, ".".join(target_parts[:index]) or "target_schema")
|
||||
properties = _properties_for_object(
|
||||
parent, ".".join(target_parts[:index]) or "target_schema"
|
||||
)
|
||||
child = properties.get(part)
|
||||
if child is None:
|
||||
child = {"type": "object", "properties": {}}
|
||||
properties[part] = child
|
||||
if not isinstance(child, dict):
|
||||
raise ValueError(f"schema path {'.'.join(target_parts[: index + 1])!r} is not an object")
|
||||
raise ValueError(
|
||||
f"schema path {'.'.join(target_parts[: index + 1])!r} is not an object"
|
||||
)
|
||||
_ensure_object_schema(child, ".".join(target_parts[: index + 1]))
|
||||
parent = child
|
||||
|
||||
properties = _properties_for_object(parent, ".".join(target_parts[:-1]) or "target_schema")
|
||||
properties = _properties_for_object(
|
||||
parent, ".".join(target_parts[:-1]) or "target_schema"
|
||||
)
|
||||
leaf = target_parts[-1]
|
||||
if leaf in properties:
|
||||
raise ValueError(f"schema path {'.'.join(target_parts)!r} already exists")
|
||||
@@ -76,15 +82,21 @@ def project_output_property_to_state_schema(
|
||||
if msg.startswith("source field ") and "is not declared" in msg:
|
||||
raise ValueError(f"output field {output_field!r} is not declared") from exc
|
||||
if msg.startswith("source field ") and "not a JSON Schema" in msg:
|
||||
raise ValueError(f"output field {output_field!r} is not a JSON Schema object") from exc
|
||||
raise ValueError(
|
||||
f"output field {output_field!r} is not a JSON Schema object"
|
||||
) from exc
|
||||
if "schema path 'target_schema'" in msg and "is not an object" in msg:
|
||||
raise ValueError("state_schema must be an object schema") from exc
|
||||
if msg.startswith("schema path ") and "already exists" in msg:
|
||||
raise ValueError(f"state field {state_field!r} already exists") from exc
|
||||
if "target_schema is not valid JSON Schema" in msg:
|
||||
raise ValueError(f"state_schema is not valid JSON Schema: {msg.split(': ', 1)[1]}") from exc
|
||||
raise ValueError(
|
||||
f"state_schema is not valid JSON Schema: {msg.split(': ', 1)[1]}"
|
||||
) from exc
|
||||
if "source_schema is not valid JSON Schema" in msg:
|
||||
raise ValueError(f"output_schema is not valid JSON Schema: {msg.split(': ', 1)[1]}") from exc
|
||||
raise ValueError(
|
||||
f"output_schema is not valid JSON Schema: {msg.split(': ', 1)[1]}"
|
||||
) from exc
|
||||
raise
|
||||
|
||||
|
||||
|
||||
@@ -299,42 +299,39 @@ def set_step_output_map(
|
||||
)
|
||||
|
||||
|
||||
@app.command("bind-output-to-state")
|
||||
def bind_output_to_state(
|
||||
@app.command("bind")
|
||||
def bind_draft(
|
||||
ctx: typer.Context,
|
||||
workspace_id: Annotated[str, typer.Argument(help="Draft workspace id.")],
|
||||
revision: Annotated[
|
||||
int, typer.Option("--revision", min=1, help="Expected workspace revision.")
|
||||
],
|
||||
step_id: Annotated[str, typer.Option("--step", help="Draft step id.")],
|
||||
output_field: Annotated[
|
||||
source_path: Annotated[
|
||||
str,
|
||||
typer.Option("--output", help="Top-level capability output field."),
|
||||
typer.Option("--from", help="Source path, for example input.x or local.y."),
|
||||
],
|
||||
state_path: Annotated[
|
||||
target_path: Annotated[
|
||||
str,
|
||||
typer.Option("--state", help="Root state path, for example state.after."),
|
||||
typer.Option("--to", help="Target path, for example local.x or state.y."),
|
||||
],
|
||||
) -> None:
|
||||
"""Declare state schema and bind one step output to that state field.
|
||||
"""Bind a capability step path and project the matching schema.
|
||||
|
||||
This is the common command to run before validation when a step output
|
||||
should write to a new state field. It copies the selected capability output
|
||||
field schema into state_schema and merges the output binding
|
||||
local.<output> -> state.<field>.
|
||||
|
||||
Run `wf draft validate <workspace_id>` after this command.
|
||||
Direction matters. Use input/state -> local for step inputs and local ->
|
||||
state/output for step outputs. Run `wf draft validate <workspace_id>` after
|
||||
this command.
|
||||
"""
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.bind_output_to_state(
|
||||
context.handlers.bind_draft(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
step_id=step_id,
|
||||
output_field=output_field,
|
||||
state_path=state_path,
|
||||
source_path=source_path,
|
||||
target_path=target_path,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -243,18 +243,14 @@ class SetStepOutputMapRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class BindOutputToStateRequest(BaseModel):
|
||||
"""Typed MCP request for binding one step output to one root state field."""
|
||||
class BindDraftRequest(BaseModel):
|
||||
"""Typed MCP request for binding one draft step path with schema projection."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
step_id: str = Field(description="Draft step id whose capability output is used.")
|
||||
output_field: str = Field(
|
||||
description="Top-level output field to bind, for example after."
|
||||
)
|
||||
state_path: str = Field(
|
||||
description="Root state path to declare and bind, for example state.after."
|
||||
)
|
||||
step_id: str = Field(description="Capability-backed draft step id.")
|
||||
source_path: str = Field(description="Source path, for example input.x or local.y.")
|
||||
target_path: str = Field(description="Target path, for example local.x or state.y.")
|
||||
|
||||
|
||||
class AddStepFromCapabilityRequest(BaseModel):
|
||||
|
||||
@@ -13,7 +13,7 @@ from wf_mcp.broker.service.workflow_operation_context import context_from_servic
|
||||
|
||||
from .models import (
|
||||
AddStepFromCapabilityRequest,
|
||||
BindOutputToStateRequest,
|
||||
BindDraftRequest,
|
||||
BranchDraftRequest,
|
||||
CallCapabilityResult,
|
||||
CompileDraftWorkspaceRequest,
|
||||
@@ -461,23 +461,24 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.bind_output_to_state",
|
||||
title="Bind Output To State",
|
||||
name="wf.workflow.bind",
|
||||
title="Bind Draft",
|
||||
description=(
|
||||
"Declare one root state field from a draft step capability output "
|
||||
"schema and bind local.<output> to that state path."
|
||||
"Bind a capability step path and project the matching schema. "
|
||||
"Use input/state -> local for step inputs and local -> state/output "
|
||||
"for step outputs."
|
||||
),
|
||||
)
|
||||
async def bind_output_to_state(
|
||||
request: BindOutputToStateRequest,
|
||||
async def bind_draft(
|
||||
request: BindDraftRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.bind_output_to_state(
|
||||
await handlers.bind_draft(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
step_id=request.step_id,
|
||||
output_field=request.output_field,
|
||||
state_path=request.state_path,
|
||||
source_path=request.source_path,
|
||||
target_path=request.target_path,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from .errors import WorkflowRpcError
|
||||
from .models import (
|
||||
AddStepFromCapabilityParams,
|
||||
AdminEmptyParams,
|
||||
BindDraftParams,
|
||||
BranchDraftParams,
|
||||
CallCapabilityParams,
|
||||
CompileDraftWorkspaceParams,
|
||||
@@ -48,6 +49,7 @@ from .models import (
|
||||
__all__ = [
|
||||
"AddStepFromCapabilityParams",
|
||||
"AdminEmptyParams",
|
||||
"BindDraftParams",
|
||||
"BranchDraftParams",
|
||||
"CallCapabilityParams",
|
||||
"CompileDraftWorkspaceParams",
|
||||
|
||||
@@ -141,23 +141,23 @@ class RpcDraftClientMixin:
|
||||
},
|
||||
)
|
||||
|
||||
async def bind_output_to_state(
|
||||
async def bind_draft(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
step_id: str,
|
||||
output_field: str,
|
||||
state_path: str,
|
||||
source_path: str,
|
||||
target_path: str,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.bind_output_to_state",
|
||||
"workflow.draft_workspaces.bind",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"step_id": step_id,
|
||||
"output_field": output_field,
|
||||
"state_path": state_path,
|
||||
"source_path": source_path,
|
||||
"target_path": target_path,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from wf_server import WorkflowServer
|
||||
from ..errors import WorkflowRpcError, raise_workflow_rpc_error
|
||||
from ..models import (
|
||||
AddStepFromCapabilityParams,
|
||||
BindOutputToStateParams,
|
||||
BindDraftParams,
|
||||
BranchDraftParams,
|
||||
CompileDraftWorkspaceParams,
|
||||
CreateArtifactFromWorkspaceParams,
|
||||
@@ -183,19 +183,19 @@ def register_methods(
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.bind_output_to_state",
|
||||
name="workflow.draft_workspaces.bind",
|
||||
errors=[WorkflowRpcError],
|
||||
)
|
||||
async def workflow_draft_workspaces_bind_output_to_state(
|
||||
params: BindOutputToStateParams = RpcParams(),
|
||||
async def workflow_draft_workspaces_bind(
|
||||
params: BindDraftParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.bind_output_to_state(
|
||||
return await server.api.bind_draft(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
step_id=params.step_id,
|
||||
output_field=params.output_field,
|
||||
state_path=params.state_path,
|
||||
source_path=params.source_path,
|
||||
target_path=params.target_path,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@@ -141,12 +141,12 @@ class SetStepOutputMapParams(RpcParamsModel):
|
||||
merge: bool = False
|
||||
|
||||
|
||||
class BindOutputToStateParams(RpcParamsModel):
|
||||
class BindDraftParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
step_id: str = Field(min_length=1)
|
||||
output_field: str = Field(min_length=1)
|
||||
state_path: str = Field(min_length=1)
|
||||
source_path: str = Field(min_length=1)
|
||||
target_path: str = Field(min_length=1)
|
||||
|
||||
|
||||
class AddStepFromCapabilityParams(RpcParamsModel):
|
||||
|
||||
Reference in New Issue
Block a user