feat: add workflow output draft command
This commit is contained in:
@@ -299,6 +299,58 @@ class WorkflowDraftApi:
|
||||
],
|
||||
)
|
||||
|
||||
async def set_workflow_output_map(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
output_map: dict[str, str],
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
output_bindings: list[dict[str, Any]]
|
||||
if merge:
|
||||
workspace = self._draft_store().get_workspace(workspace_id)
|
||||
remaining = dict(output_map)
|
||||
output_bindings = []
|
||||
output_payload = workspace.draft.get("output")
|
||||
if isinstance(output_payload, list):
|
||||
for binding in output_payload:
|
||||
if not isinstance(binding, dict):
|
||||
continue
|
||||
source = binding.get("path")
|
||||
target = binding.get("target")
|
||||
if isinstance(source, str) and isinstance(target, str):
|
||||
output_bindings.append(
|
||||
{
|
||||
"path": source,
|
||||
"target": remaining.pop(source, target),
|
||||
}
|
||||
)
|
||||
elif isinstance(target, str) and "value" in binding:
|
||||
# Literal workflow outputs cannot be represented by the
|
||||
# path-only CLI map, but --merge must not discard them.
|
||||
output_bindings.append(dict(binding))
|
||||
output_bindings.extend(
|
||||
{"path": source, "target": target}
|
||||
for source, target in remaining.items()
|
||||
)
|
||||
else:
|
||||
output_bindings = [
|
||||
{"path": source, "target": target}
|
||||
for source, target in output_map.items()
|
||||
]
|
||||
return await self.patch_draft_workspace(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
patch=[
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/output",
|
||||
"value": output_bindings,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def _step_input_maps(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -371,6 +371,21 @@ class WorkflowApi:
|
||||
merge=merge,
|
||||
)
|
||||
|
||||
async def set_workflow_output_map(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
output_map: dict[str, str],
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return await self.drafts.set_workflow_output_map(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
output_map=output_map,
|
||||
merge=merge,
|
||||
)
|
||||
|
||||
async def bind_draft(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -114,6 +114,15 @@ class WorkflowDraftSurface(Protocol):
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def set_workflow_output_map(
|
||||
self,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
output_map: dict[str, str],
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
async def bind_draft(
|
||||
self,
|
||||
*,
|
||||
|
||||
@@ -330,6 +330,60 @@ def set_step_output_map(
|
||||
)
|
||||
|
||||
|
||||
@app.command("set-workflow-output")
|
||||
def set_workflow_output(
|
||||
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.")
|
||||
],
|
||||
mapping: Annotated[
|
||||
list[str] | None,
|
||||
typer.Option(
|
||||
"--map",
|
||||
help=(
|
||||
"One output binding GRAPH_SOURCE=OUTPUT_FIELD, for example "
|
||||
"state.markdown=markdown. Repeat in one command."
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
merge: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--merge",
|
||||
help="Preserve existing workflow output bindings and add/update the passed --map entries.",
|
||||
),
|
||||
] = False,
|
||||
) -> None:
|
||||
"""Set the top-level workflow output projection without writing JSON Patch manually.
|
||||
|
||||
Default behavior replaces the full workflow output map. Pass all desired
|
||||
--map entries in one command for a complete replacement. Use --merge only
|
||||
when adding or updating entries across a later revision.
|
||||
|
||||
This edits WorkflowDraft.output (top-level workflow output). Use
|
||||
wf draft set-output for step-level output bindings.
|
||||
|
||||
Repeat --map for multiple mappings:
|
||||
--map state.markdown=markdown --map state.title=title
|
||||
|
||||
Run `wf draft validate <workspace_id>` after editing the projection.
|
||||
"""
|
||||
output_map = _parse_map_flags(mapping)
|
||||
context = load_cli_context(ctx)
|
||||
emit_json(
|
||||
run_cli_operation(
|
||||
context,
|
||||
context.handlers.set_workflow_output_map(
|
||||
workspace_id=workspace_id,
|
||||
revision=revision,
|
||||
output_map=output_map,
|
||||
merge=merge,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@app.command("bind")
|
||||
def bind_draft(
|
||||
ctx: typer.Context,
|
||||
|
||||
@@ -50,6 +50,7 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
|
||||
"wf.workflow.set_draft_route",
|
||||
"wf.workflow.set_step_input_map",
|
||||
"wf.workflow.set_step_output_map",
|
||||
"wf.workflow.set_workflow_output_map",
|
||||
"wf.workflow.create_minimal_draft_workspace",
|
||||
"wf.workflow.create_artifact_from_workspace",
|
||||
"wf.workflow.create_wrapper_from_workspace",
|
||||
|
||||
@@ -243,6 +243,21 @@ class SetStepOutputMapRequest(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class SetWorkflowOutputMapRequest(BaseModel):
|
||||
"""Typed MCP request for replacing or merging workflow output projection."""
|
||||
|
||||
workspace_id: WorkspaceId
|
||||
revision: int = Field(ge=1, description="Expected current workspace revision.")
|
||||
output_map: DraftPathMap
|
||||
merge: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"When false, replace the full workflow output map. When true, "
|
||||
"preserve existing bindings and add/update only output_map entries."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class BindDraftRequest(BaseModel):
|
||||
"""Typed MCP request for binding one draft step path with schema projection."""
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from .models import (
|
||||
SetDraftRouteRequest,
|
||||
SetStepInputMapRequest,
|
||||
SetStepOutputMapRequest,
|
||||
SetWorkflowOutputMapRequest,
|
||||
TraceRange,
|
||||
ValidateDeploymentResult,
|
||||
ValidateDraftWorkspaceRequest,
|
||||
@@ -463,6 +464,27 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.set_workflow_output_map",
|
||||
title="Set Workflow Output Map",
|
||||
description=(
|
||||
"Replace or merge the top-level workflow output projection. "
|
||||
"Map graph source paths such as state.markdown to public output "
|
||||
"fields such as markdown."
|
||||
),
|
||||
)
|
||||
async def set_workflow_output_map(
|
||||
request: SetWorkflowOutputMapRequest,
|
||||
) -> DraftWorkspaceResult:
|
||||
return DraftWorkspaceResult.model_validate(
|
||||
await handlers.set_workflow_output_map(
|
||||
workspace_id=request.workspace_id,
|
||||
revision=request.revision,
|
||||
output_map=request.output_map,
|
||||
merge=request.merge,
|
||||
)
|
||||
)
|
||||
|
||||
@server.tool(
|
||||
name="wf.workflow.bind",
|
||||
title="Bind Draft",
|
||||
|
||||
@@ -42,6 +42,7 @@ from .models import (
|
||||
SetDraftRouteParams,
|
||||
SetStepInputMapParams,
|
||||
SetStepOutputMapParams,
|
||||
SetWorkflowOutputMapParams,
|
||||
StartRunParams,
|
||||
TraceRangeParams,
|
||||
ValidateDeploymentParams,
|
||||
@@ -88,6 +89,7 @@ __all__ = [
|
||||
"SetDraftRouteParams",
|
||||
"SetStepInputMapParams",
|
||||
"SetStepOutputMapParams",
|
||||
"SetWorkflowOutputMapParams",
|
||||
"StartRunParams",
|
||||
"TraceRangeParams",
|
||||
"ValidateDeploymentParams",
|
||||
|
||||
@@ -141,6 +141,24 @@ class RpcDraftClientMixin:
|
||||
},
|
||||
)
|
||||
|
||||
async def set_workflow_output_map(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
workspace_id: str,
|
||||
revision: int,
|
||||
output_map: dict[str, str],
|
||||
merge: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return await self._call(
|
||||
"workflow.draft_workspaces.set_workflow_output_map",
|
||||
{
|
||||
"workspace_id": workspace_id,
|
||||
"revision": revision,
|
||||
"output_map": output_map,
|
||||
"merge": merge,
|
||||
},
|
||||
)
|
||||
|
||||
async def bind_draft(
|
||||
self: RpcCaller,
|
||||
*,
|
||||
|
||||
@@ -28,6 +28,7 @@ from ..models import (
|
||||
SetDraftRouteParams,
|
||||
SetStepInputMapParams,
|
||||
SetStepOutputMapParams,
|
||||
SetWorkflowOutputMapParams,
|
||||
ValidateDraftParams,
|
||||
ValidateDraftWorkspaceParams,
|
||||
)
|
||||
@@ -185,6 +186,23 @@ def register_methods(
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.set_workflow_output_map",
|
||||
errors=[WorkflowRpcError],
|
||||
)
|
||||
async def workflow_draft_workspaces_set_workflow_output_map(
|
||||
params: SetWorkflowOutputMapParams = RpcParams(),
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
return await server.api.set_workflow_output_map(
|
||||
workspace_id=params.workspace_id,
|
||||
revision=params.revision,
|
||||
output_map=params.output_map,
|
||||
merge=params.merge,
|
||||
)
|
||||
except (ValueError, KeyError, LookupError, FileNotFoundError) as exc:
|
||||
raise_workflow_rpc_error(exc)
|
||||
|
||||
@entrypoint.method(
|
||||
name="workflow.draft_workspaces.bind",
|
||||
errors=[WorkflowRpcError],
|
||||
|
||||
@@ -141,6 +141,13 @@ class SetStepOutputMapParams(RpcParamsModel):
|
||||
merge: bool = False
|
||||
|
||||
|
||||
class SetWorkflowOutputMapParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
output_map: dict[str, str]
|
||||
merge: bool = False
|
||||
|
||||
|
||||
class BindDraftParams(RpcParamsModel):
|
||||
workspace_id: str = Field(min_length=1)
|
||||
revision: int = Field(ge=1)
|
||||
|
||||
Reference in New Issue
Block a user