wf_artifacts to end this "with" usage
This commit is contained in:
@@ -3,7 +3,11 @@ from __future__ import annotations
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from wf_artifacts.drafts import patch_workflow_draft, validate_workflow_draft
|
||||
from wf_artifacts.drafts import (
|
||||
WorkflowDraft,
|
||||
patch_workflow_draft,
|
||||
validate_workflow_draft,
|
||||
)
|
||||
|
||||
from .models import WorkflowDraftWorkspace, summarize_draft_workspace
|
||||
from .store import DraftWorkspaceConflictError, DraftWorkspaceStore
|
||||
@@ -22,11 +26,14 @@ def create_draft_workspace(
|
||||
"""Validate and save a new mutable draft workspace."""
|
||||
now = _now_ms()
|
||||
validation = validate_workflow_draft(draft)
|
||||
normalized_draft = _canonical_draft_if_valid(
|
||||
draft, validation_status=validation["status"]
|
||||
)
|
||||
workspace = WorkflowDraftWorkspace(
|
||||
id=workspace_id,
|
||||
revision=1,
|
||||
title=title,
|
||||
draft=draft,
|
||||
draft=normalized_draft,
|
||||
status=validation["status"],
|
||||
diagnostics=validation["diagnostics"],
|
||||
created_at_epoch_ms=now,
|
||||
@@ -66,7 +73,10 @@ def patch_draft_workspace(
|
||||
next_workspace = workspace.model_copy(
|
||||
update={
|
||||
"revision": workspace.revision + 1,
|
||||
"draft": patched["draft"],
|
||||
"draft": _canonical_draft_if_valid(
|
||||
patched["draft"],
|
||||
validation_status=patched["status"],
|
||||
),
|
||||
"status": patched["status"],
|
||||
"diagnostics": patched["diagnostics"],
|
||||
"updated_at_epoch_ms": _now_ms(),
|
||||
@@ -96,6 +106,23 @@ def _now_ms() -> int:
|
||||
return int(time.time() * 1000)
|
||||
|
||||
|
||||
def _canonical_draft_if_valid(
|
||||
draft: JsonObject,
|
||||
*,
|
||||
validation_status: object,
|
||||
) -> JsonObject:
|
||||
"""Persist valid drafts in the canonical model shape.
|
||||
|
||||
Invalid drafts remain as-authored so diagnostics can still point at the
|
||||
payload the client sent. Once validation passes, legacy `in`/`with`/`out`
|
||||
maps become canonical `input`/`output` binding lists on disk and in MCP
|
||||
responses.
|
||||
"""
|
||||
if validation_status != "valid":
|
||||
return draft
|
||||
return WorkflowDraft.model_validate(draft).model_dump(mode="json")
|
||||
|
||||
|
||||
def _revision_conflict_payload(
|
||||
workspace: WorkflowDraftWorkspace,
|
||||
expected_revision: int,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from wf_authoring import WorkflowBuilder
|
||||
from wf_authoring.dsl import PathExpr
|
||||
from wf_core import JoinNode, Workflow
|
||||
@@ -44,8 +42,8 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
||||
return builder.use_ref(
|
||||
step.use,
|
||||
id=step_id,
|
||||
input=_draft_input_bindings(step),
|
||||
output=_draft_output_bindings(step),
|
||||
input=step.input,
|
||||
output=step.output,
|
||||
desc=step.desc,
|
||||
)
|
||||
if isinstance(step, DraftForeachStep):
|
||||
@@ -89,24 +87,3 @@ def _add_step(builder: WorkflowBuilder, step_id: str, step: DraftStep):
|
||||
default=step.match.default,
|
||||
).entry
|
||||
raise TypeError(f"unsupported draft step {type(step)!r}")
|
||||
|
||||
|
||||
def _draft_input_bindings(step: DraftUseStep) -> list[dict[str, Any]]:
|
||||
"""Translate draft input maps into canonical core input binding structs.
|
||||
|
||||
Draft JSON keeps `in` and `with` because they are compact patch targets for
|
||||
LLM clients. The compiled workflow should not re-emit deprecated builder map
|
||||
sugar, so this adapter boundary converts them to `NodeUse.input`.
|
||||
"""
|
||||
literal_bindings = [
|
||||
{"target": target, "value": value} for target, value in step.with_.items()
|
||||
]
|
||||
path_bindings = [
|
||||
{"target": target, "path": source} for source, target in step.in_.items()
|
||||
]
|
||||
return [*literal_bindings, *path_bindings]
|
||||
|
||||
|
||||
def _draft_output_bindings(step: DraftUseStep) -> list[dict[str, str]]:
|
||||
"""Translate draft output maps into canonical core output binding structs."""
|
||||
return [{"source": source, "target": target} for source, target in step.out.items()]
|
||||
|
||||
@@ -73,6 +73,8 @@ def patch_workflow_draft(draft: JsonObject, patch: JsonPatch) -> JsonObject:
|
||||
)
|
||||
)
|
||||
result = validate_workflow_draft(patched)
|
||||
if result["status"] == "valid":
|
||||
patched = WorkflowDraft.model_validate(patched).model_dump(mode="json")
|
||||
return {"draft": patched, **result}
|
||||
|
||||
|
||||
|
||||
@@ -5,17 +5,20 @@ from typing import Any, Literal
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from wf_core.models.conditions import Condition
|
||||
from wf_core.models.steps import InputBinding, OutputBinding
|
||||
|
||||
JsonObject = dict[str, Any]
|
||||
STEP_KIND_KEYS = frozenset({
|
||||
"use",
|
||||
"foreach",
|
||||
"interrupt",
|
||||
"join",
|
||||
"when",
|
||||
"choose",
|
||||
"match",
|
||||
})
|
||||
STEP_KIND_KEYS = frozenset(
|
||||
{
|
||||
"use",
|
||||
"foreach",
|
||||
"interrupt",
|
||||
"join",
|
||||
"when",
|
||||
"choose",
|
||||
"match",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DraftUseStep(BaseModel):
|
||||
@@ -24,34 +27,67 @@ class DraftUseStep(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
use: str
|
||||
in_: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
alias="in",
|
||||
input: list[InputBinding] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Source-to-destination map from graph paths to node-local input "
|
||||
"paths. Example: {'input.text': 'message'}. Values must be strings; "
|
||||
"use 'with' for literals."
|
||||
"Canonical input bindings for this capability. Use path bindings "
|
||||
"for graph-to-local input and value bindings for literals."
|
||||
),
|
||||
)
|
||||
with_: dict[str, Any] = Field(
|
||||
default_factory=dict,
|
||||
alias="with",
|
||||
output: list[OutputBinding] = Field(
|
||||
default_factory=list,
|
||||
description=(
|
||||
"Static node-local input values keyed by destination input field/path. "
|
||||
"Example: {'value': 'CLICKED'}."
|
||||
),
|
||||
)
|
||||
out: dict[str, str] = Field(
|
||||
default_factory=dict,
|
||||
description=(
|
||||
"Source-to-destination map from node-local output paths to workflow "
|
||||
"state destinations. Example: {'echoed': 'state.echoed'}."
|
||||
"Canonical output bindings from node-local output paths to workflow "
|
||||
"state destinations."
|
||||
),
|
||||
)
|
||||
desc: str | None = None
|
||||
retry: int | None = Field(default=None, ge=0)
|
||||
timeout_seconds: int | None = Field(default=None, gt=0)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def _coerce_legacy_maps(cls, value: object) -> object:
|
||||
"""Accept draft `in`/`with`/`out` maps as parse-only compatibility."""
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
data = dict(value)
|
||||
legacy_input = data.pop("in", None)
|
||||
legacy_with = data.pop("with", None)
|
||||
legacy_output = data.pop("out", None)
|
||||
if "input" in data and (legacy_input is not None or legacy_with is not None):
|
||||
raise ValueError("cannot mix canonical input with legacy in/with maps")
|
||||
if "output" in data and legacy_output is not None:
|
||||
raise ValueError("cannot mix canonical output with legacy out map")
|
||||
|
||||
input_bindings = list(data.get("input", []))
|
||||
output_bindings = list(data.get("output", []))
|
||||
if legacy_with is not None:
|
||||
if not isinstance(legacy_with, dict):
|
||||
raise ValueError("draft use 'with' must be a mapping")
|
||||
input_bindings.extend(
|
||||
{"target": target, "value": literal}
|
||||
for target, literal in legacy_with.items()
|
||||
)
|
||||
if legacy_input is not None:
|
||||
if not isinstance(legacy_input, dict):
|
||||
raise ValueError("draft use 'in' must be a mapping")
|
||||
input_bindings.extend(
|
||||
{"target": target, "path": source}
|
||||
for source, target in legacy_input.items()
|
||||
)
|
||||
if legacy_output is not None:
|
||||
if not isinstance(legacy_output, dict):
|
||||
raise ValueError("draft use 'out' must be a mapping")
|
||||
output_bindings.extend(
|
||||
{"source": source, "target": target}
|
||||
for source, target in legacy_output.items()
|
||||
)
|
||||
data["input"] = input_bindings
|
||||
data["output"] = output_bindings
|
||||
return data
|
||||
|
||||
|
||||
class DraftForeachPayload(BaseModel):
|
||||
"""Payload for one draft foreach step."""
|
||||
|
||||
Reference in New Issue
Block a user