draft: ergonomic workflow building for LLM
This commit is contained in:
@@ -7,6 +7,7 @@ authors = [{ name = "lda", email = "[email protected]" }]
|
|||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastmcp>=3.2.4",
|
"fastmcp>=3.2.4",
|
||||||
|
"jsonpatch>=1.33",
|
||||||
"jsonschema>=4.26",
|
"jsonschema>=4.26",
|
||||||
"mcp[cli,rich]>=1",
|
"mcp[cli,rich]>=1",
|
||||||
"pydantic>=2",
|
"pydantic>=2",
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ from .catalog import (
|
|||||||
artifact_node_name,
|
artifact_node_name,
|
||||||
)
|
)
|
||||||
from .factory import create_workflow_artifact_from_plan
|
from .factory import create_workflow_artifact_from_plan
|
||||||
|
from .drafts import (
|
||||||
|
compile_workflow_draft,
|
||||||
|
patch_workflow_draft,
|
||||||
|
validate_workflow_draft,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
ArtifactKind,
|
ArtifactKind,
|
||||||
AvailableCapability,
|
AvailableCapability,
|
||||||
@@ -37,7 +42,10 @@ __all__ = [
|
|||||||
"artifact_catalog_entry",
|
"artifact_catalog_entry",
|
||||||
"artifact_node_name",
|
"artifact_node_name",
|
||||||
"create_workflow_artifact_from_plan",
|
"create_workflow_artifact_from_plan",
|
||||||
|
"compile_workflow_draft",
|
||||||
"logical_ref_for_concrete_ref",
|
"logical_ref_for_concrete_ref",
|
||||||
"normalize_plan_node_refs",
|
"normalize_plan_node_refs",
|
||||||
|
"patch_workflow_draft",
|
||||||
"validate_deployment_dependencies",
|
"validate_deployment_dependencies",
|
||||||
|
"validate_workflow_draft",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,269 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import jsonpatch
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from wf_core import Workflow
|
||||||
|
|
||||||
|
JsonObject = dict[str, Any]
|
||||||
|
JsonPatch = list[dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
def compile_workflow_draft(draft: JsonObject) -> JsonObject:
|
||||||
|
"""Compile the LLM-friendly draft shape into the normalized workflow plan."""
|
||||||
|
plan = _compile_unvalidated_draft(draft)
|
||||||
|
_validate_plan_model(plan)
|
||||||
|
_validate_graph_references(plan)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def validate_workflow_draft(draft: JsonObject) -> JsonObject:
|
||||||
|
"""Return structured draft diagnostics instead of raising on bad input."""
|
||||||
|
try:
|
||||||
|
compiled_plan = compile_workflow_draft(draft)
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"status": "invalid",
|
||||||
|
"diagnostics": [_diagnostic_from_exception(exc)],
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"status": "valid",
|
||||||
|
"diagnostics": [],
|
||||||
|
"compiled_plan": compiled_plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def patch_workflow_draft(draft: JsonObject, patch: JsonPatch) -> JsonObject:
|
||||||
|
"""Apply RFC 6902 JSON Patch to a draft, then validate the patched draft.
|
||||||
|
|
||||||
|
Patch authoring is intentionally draft-first. Compiled raw plans are compiler
|
||||||
|
output, so callers should patch the readable source document and recompile it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
patched = jsonpatch.JsonPatch(patch).apply(deepcopy(draft), in_place=False)
|
||||||
|
except Exception as exc:
|
||||||
|
return {
|
||||||
|
"status": "invalid",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"code": "patch_invalid",
|
||||||
|
"path": "patch",
|
||||||
|
"message": str(exc),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if not isinstance(patched, dict):
|
||||||
|
return {
|
||||||
|
"status": "invalid",
|
||||||
|
"diagnostics": [
|
||||||
|
{
|
||||||
|
"code": "draft_not_object",
|
||||||
|
"path": "",
|
||||||
|
"message": "patched draft must be a JSON object",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
result = validate_workflow_draft(patched)
|
||||||
|
return {"draft": patched, **result}
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_unvalidated_draft(draft: JsonObject) -> JsonObject:
|
||||||
|
if not isinstance(draft, dict):
|
||||||
|
raise ValueError("draft must be a JSON object")
|
||||||
|
|
||||||
|
plan: JsonObject = {
|
||||||
|
"name": _required_str(draft, "name"),
|
||||||
|
"input_schema": _required_object(draft, "input_schema"),
|
||||||
|
"state_schema": _required_object(draft, "state_schema"),
|
||||||
|
"output_schema": _required_object(draft, "output_schema"),
|
||||||
|
"start": _required_str(draft, "start"),
|
||||||
|
"nodes": [
|
||||||
|
_compile_step(step, index) for index, step in enumerate(_steps(draft))
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
_compile_edge(edge, index) for index, edge in enumerate(_edges(draft))
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_step(step: object, index: int) -> JsonObject:
|
||||||
|
path = f"steps[{index}]"
|
||||||
|
if not isinstance(step, dict):
|
||||||
|
raise ValueError(f"{path} must be a JSON object")
|
||||||
|
|
||||||
|
step_id = _required_str(step, "id", path=path)
|
||||||
|
kind = _required_str(step, "kind", path=path)
|
||||||
|
if kind == "use":
|
||||||
|
node: JsonObject = {
|
||||||
|
"id": step_id,
|
||||||
|
"type": "node",
|
||||||
|
"node": _required_str(step, "capability", path=path),
|
||||||
|
"in_map": _optional_object(step, "in", default={}),
|
||||||
|
"out_map": _optional_object(step, "out", default={}),
|
||||||
|
}
|
||||||
|
_copy_optional(step, node, "desc")
|
||||||
|
_copy_optional(step, node, "retry")
|
||||||
|
_copy_optional(step, node, "timeout_seconds")
|
||||||
|
return node
|
||||||
|
if kind == "condition":
|
||||||
|
return {
|
||||||
|
"id": step_id,
|
||||||
|
"type": "condition",
|
||||||
|
"check": _required_object(step, "check", path=path),
|
||||||
|
}
|
||||||
|
if kind == "foreach":
|
||||||
|
node = {
|
||||||
|
"id": step_id,
|
||||||
|
"type": "foreach",
|
||||||
|
"over": _required_str(step, "over", path=path),
|
||||||
|
"as": _required_str(step, "as", path=path),
|
||||||
|
}
|
||||||
|
_copy_optional(step, node, "mode")
|
||||||
|
_copy_optional(step, node, "on_item_error")
|
||||||
|
return node
|
||||||
|
if kind == "interrupt":
|
||||||
|
node = {
|
||||||
|
"id": step_id,
|
||||||
|
"type": "interrupt",
|
||||||
|
"kind": _required_str(step, "interrupt_kind", path=path),
|
||||||
|
"request_map": _optional_object(step, "request", default={}),
|
||||||
|
"out_map": _optional_object(step, "resume", default={}),
|
||||||
|
}
|
||||||
|
_copy_optional(step, node, "outcomes")
|
||||||
|
return node
|
||||||
|
if kind == "join":
|
||||||
|
return {"id": step_id, "type": "join"}
|
||||||
|
raise ValueError(f"{path}.kind has unsupported value {kind!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_edge(edge: object, index: int) -> JsonObject:
|
||||||
|
path = f"edges[{index}]"
|
||||||
|
if not isinstance(edge, dict):
|
||||||
|
raise ValueError(f"{path} must be a JSON object")
|
||||||
|
return {
|
||||||
|
"from": _required_str(edge, "from", path=path),
|
||||||
|
"outcome": _required_str(edge, "outcome", path=path),
|
||||||
|
"to": _required_str(edge, "to", path=path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_plan_model(plan: JsonObject) -> None:
|
||||||
|
try:
|
||||||
|
Workflow.model_validate(plan)
|
||||||
|
except ValidationError as exc:
|
||||||
|
raise ValueError(_validation_error_message(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_graph_references(plan: JsonObject) -> None:
|
||||||
|
node_ids = [node["id"] for node in plan["nodes"] if isinstance(node, dict)]
|
||||||
|
node_id_set = set(node_ids)
|
||||||
|
if len(node_ids) != len(node_id_set):
|
||||||
|
raise ValueError("steps contain duplicate ids")
|
||||||
|
if plan["start"] not in node_id_set:
|
||||||
|
raise ValueError(f"start references unknown step id {plan['start']!r}")
|
||||||
|
for index, edge in enumerate(plan["edges"]):
|
||||||
|
if edge["from"] not in node_id_set:
|
||||||
|
raise ValueError(
|
||||||
|
f"edges[{index}].from references unknown step id {edge['from']!r}"
|
||||||
|
)
|
||||||
|
if edge["to"] != "__end__" and edge["to"] not in node_id_set:
|
||||||
|
raise ValueError(
|
||||||
|
f"edges[{index}].to references unknown step id {edge['to']!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _steps(draft: JsonObject) -> list[object]:
|
||||||
|
steps = draft.get("steps")
|
||||||
|
if not isinstance(steps, list):
|
||||||
|
raise ValueError("steps must be an array")
|
||||||
|
return steps
|
||||||
|
|
||||||
|
|
||||||
|
def _edges(draft: JsonObject) -> list[object]:
|
||||||
|
edges = draft.get("edges")
|
||||||
|
if not isinstance(edges, list):
|
||||||
|
raise ValueError("edges must be an array")
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def _required_object(payload: JsonObject, key: str, *, path: str = "") -> JsonObject:
|
||||||
|
value = payload.get(key)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"{_join_path(path, key)} must be an object")
|
||||||
|
return deepcopy(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _optional_object(
|
||||||
|
payload: JsonObject,
|
||||||
|
key: str,
|
||||||
|
*,
|
||||||
|
default: JsonObject,
|
||||||
|
) -> JsonObject:
|
||||||
|
value = payload.get(key, default)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"{key} must be an object")
|
||||||
|
return deepcopy(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _required_str(payload: JsonObject, key: str, *, path: str = "") -> str:
|
||||||
|
value = payload.get(key)
|
||||||
|
if not isinstance(value, str) or not value:
|
||||||
|
raise ValueError(f"{_join_path(path, key)} must be a non-empty string")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_optional(source: JsonObject, target: JsonObject, key: str) -> None:
|
||||||
|
if key in source:
|
||||||
|
target[key] = deepcopy(source[key])
|
||||||
|
|
||||||
|
|
||||||
|
def _join_path(path: str, key: str) -> str:
|
||||||
|
return f"{path}.{key}" if path else key
|
||||||
|
|
||||||
|
|
||||||
|
def _validation_error_message(exc: ValidationError) -> str:
|
||||||
|
first_error = exc.errors()[0]
|
||||||
|
location = ".".join(str(part) for part in first_error["loc"])
|
||||||
|
return f"{_draft_path(location)}: {first_error['msg']}"
|
||||||
|
|
||||||
|
|
||||||
|
def _diagnostic_from_exception(exc: Exception) -> JsonObject:
|
||||||
|
message = str(exc)
|
||||||
|
path = _path_from_message(message)
|
||||||
|
return {
|
||||||
|
"code": "draft_invalid",
|
||||||
|
"path": path,
|
||||||
|
"step_id": _step_id_from_path(path),
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _path_from_message(message: str) -> str:
|
||||||
|
if ":" in message and message.split(":", 1)[0]:
|
||||||
|
return _draft_path(message.split(":", 1)[0])
|
||||||
|
token = message.split(" ", 1)[0]
|
||||||
|
if token.startswith(("steps[", "edges[")) or token in {
|
||||||
|
"name",
|
||||||
|
"input_schema",
|
||||||
|
"state_schema",
|
||||||
|
"output_schema",
|
||||||
|
"start",
|
||||||
|
"steps",
|
||||||
|
"edges",
|
||||||
|
}:
|
||||||
|
return _draft_path(token)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_path(path: str) -> str:
|
||||||
|
return path.replace("nodes[", "steps[").replace(".type", ".kind")
|
||||||
|
|
||||||
|
|
||||||
|
def _step_id_from_path(path: str) -> str | None:
|
||||||
|
if not path.startswith("steps["):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
@@ -18,6 +18,7 @@ from .events import McpEvent
|
|||||||
|
|
||||||
_CapabilityT = TypeVar("_CapabilityT")
|
_CapabilityT = TypeVar("_CapabilityT")
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class DiscoveredConnectionCapabilities:
|
class DiscoveredConnectionCapabilities:
|
||||||
tools: list[DiscoveredTool] = field(default_factory=list)
|
tools: list[DiscoveredTool] = field(default_factory=list)
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
|
|||||||
# Stable workflow control surface. Keep future workflow-capability test
|
# Stable workflow control surface. Keep future workflow-capability test
|
||||||
# tools pinned here too; they are distinct from raw MCP tool execution.
|
# tools pinned here too; they are distinct from raw MCP tool execution.
|
||||||
"wf.workflow.list_artifacts",
|
"wf.workflow.list_artifacts",
|
||||||
|
"wf.workflow.validate_draft",
|
||||||
|
"wf.workflow.compile_draft",
|
||||||
"wf.workflow.create_artifact_from_plan",
|
"wf.workflow.create_artifact_from_plan",
|
||||||
|
"wf.workflow.create_artifact_from_draft",
|
||||||
|
"wf.workflow.patch_draft",
|
||||||
"wf.workflow.call_capability",
|
"wf.workflow.call_capability",
|
||||||
"wf.workflow.inspect_artifact",
|
"wf.workflow.inspect_artifact",
|
||||||
"wf.workflow.list_deployments",
|
"wf.workflow.list_deployments",
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ from wf_artifacts import (
|
|||||||
WorkflowArtifact,
|
WorkflowArtifact,
|
||||||
WorkflowCapabilityRef,
|
WorkflowCapabilityRef,
|
||||||
WorkflowDeployment,
|
WorkflowDeployment,
|
||||||
|
compile_workflow_draft,
|
||||||
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
||||||
|
patch_workflow_draft,
|
||||||
|
validate_workflow_draft,
|
||||||
validate_deployment_dependencies,
|
validate_deployment_dependencies,
|
||||||
)
|
)
|
||||||
from wf_platform import CapabilityRef, NodeSpecInventory, hash_json_schema, page_items
|
from wf_platform import CapabilityRef, NodeSpecInventory, hash_json_schema, page_items
|
||||||
@@ -250,6 +253,89 @@ class WorkflowSurfaceHandlers:
|
|||||||
"saved": True,
|
"saved": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def validate_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return validate_workflow_draft(draft)
|
||||||
|
|
||||||
|
async def compile_draft(self, *, draft: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
plan = compile_workflow_draft(draft)
|
||||||
|
return {
|
||||||
|
"compiled_plan": plan,
|
||||||
|
"required_capabilities": _required_capability_payloads(
|
||||||
|
_required_capabilities_for_plan(
|
||||||
|
plan,
|
||||||
|
source_bindings=None,
|
||||||
|
service=self.service,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def create_artifact_from_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
title: str,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
outcomes: Sequence[str],
|
||||||
|
kind: ArtifactKind = "workflow",
|
||||||
|
description: str | None = None,
|
||||||
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
|
source_bindings: dict[str, str] | None = None,
|
||||||
|
created_from_catalog_version: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if self.service.artifact_store is None:
|
||||||
|
raise KeyError("workflow artifact store is not configured")
|
||||||
|
plan = compile_workflow_draft(draft)
|
||||||
|
workflow_artifact = build_workflow_artifact_from_plan(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
title=title,
|
||||||
|
kind=kind,
|
||||||
|
description=description,
|
||||||
|
plan=plan,
|
||||||
|
outcomes=tuple(outcomes),
|
||||||
|
required_capabilities={
|
||||||
|
name: RequiredCapability.model_validate(capability)
|
||||||
|
for name, capability in (required_capabilities or {}).items()
|
||||||
|
},
|
||||||
|
source_bindings=source_bindings,
|
||||||
|
observed_node_specs=_observed_node_specs(self.service),
|
||||||
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
|
)
|
||||||
|
self.service.artifact_store.save_artifact(workflow_artifact)
|
||||||
|
self.service._record_event(
|
||||||
|
make_event(
|
||||||
|
"workflow_artifact_saved",
|
||||||
|
capability_id=_artifact_capability_id(workflow_artifact),
|
||||||
|
payload={
|
||||||
|
"artifact_id": workflow_artifact.id,
|
||||||
|
"version": workflow_artifact.version,
|
||||||
|
"created_from_draft": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
required_sources = sorted(
|
||||||
|
{
|
||||||
|
capability.logical_source
|
||||||
|
for capability in workflow_artifact.required_capabilities.values()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"artifact_id": workflow_artifact.id,
|
||||||
|
"version": workflow_artifact.version,
|
||||||
|
"saved": True,
|
||||||
|
"required_logical_sources": required_sources,
|
||||||
|
"suggested_bindings": _suggested_self_bindings(required_sources),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def patch_draft(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
patch: list[dict[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return patch_workflow_draft(draft, patch)
|
||||||
|
|
||||||
async def inspect_artifact(
|
async def inspect_artifact(
|
||||||
self, *, artifact_id: str, version: int
|
self, *, artifact_id: str, version: int
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -400,16 +486,62 @@ def _available_sources(service: WfMcpService) -> list[AvailableSource]:
|
|||||||
return sources
|
return sources
|
||||||
|
|
||||||
|
|
||||||
|
def _required_capabilities_for_plan(
|
||||||
|
plan: dict[str, Any],
|
||||||
|
*,
|
||||||
|
source_bindings: dict[str, str] | None,
|
||||||
|
service: WfMcpService,
|
||||||
|
) -> dict[str, RequiredCapability]:
|
||||||
|
"""Infer a draft dependency summary without persisting an artifact."""
|
||||||
|
artifact = build_workflow_artifact_from_plan(
|
||||||
|
artifact_id="draft_preview",
|
||||||
|
version=1,
|
||||||
|
title="Draft Preview",
|
||||||
|
plan=plan,
|
||||||
|
outcomes=("completed",),
|
||||||
|
source_bindings=source_bindings,
|
||||||
|
observed_node_specs=_observed_node_specs(service),
|
||||||
|
)
|
||||||
|
requirements = dict(artifact.required_capabilities)
|
||||||
|
for node in _plan_nodes(artifact):
|
||||||
|
raw_ref = node.get("node")
|
||||||
|
if not isinstance(raw_ref, str) or raw_ref in requirements:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
parsed = CapabilityRef.parse(raw_ref)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
requirements[raw_ref] = RequiredCapability(
|
||||||
|
logical_source=str(parsed.source),
|
||||||
|
capability_name=parsed.name,
|
||||||
|
kind="node_spec",
|
||||||
|
)
|
||||||
|
return requirements
|
||||||
|
|
||||||
|
|
||||||
|
def _required_capability_payloads(
|
||||||
|
requirements: dict[str, RequiredCapability],
|
||||||
|
) -> dict[str, dict[str, Any]]:
|
||||||
|
return {
|
||||||
|
name: capability.model_dump(mode="json")
|
||||||
|
for name, capability in sorted(requirements.items())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _suggested_self_bindings(required_sources: Sequence[str]) -> dict[str, str]:
|
||||||
|
"""Suggest local bindings for built-in sources that deploy to themselves."""
|
||||||
|
return {
|
||||||
|
source: source for source in required_sources if source in {"wf.std", "wf.mcp"}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _observed_node_specs(service: WfMcpService) -> dict[str, NodeSpecInventory]:
|
def _observed_node_specs(service: WfMcpService) -> dict[str, NodeSpecInventory]:
|
||||||
"""Project current executable specs into serializable observed contracts."""
|
"""Project current executable specs into serializable observed contracts."""
|
||||||
observed: dict[str, NodeSpecInventory] = {}
|
observed: dict[str, NodeSpecInventory] = {}
|
||||||
for source in service.capability_sources.values():
|
for source in service.capability_sources.values():
|
||||||
inventory = source.as_inventory()
|
inventory = source.as_inventory()
|
||||||
observed.update(
|
observed.update(
|
||||||
{
|
{detail.name: detail for detail in inventory.capabilities.node_spec_details}
|
||||||
detail.name: detail
|
|
||||||
for detail in inventory.capabilities.node_spec_details
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
return observed
|
return observed
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,22 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
async def save_artifact(artifact: dict[str, Any]) -> dict[str, Any]:
|
async def save_artifact(artifact: dict[str, Any]) -> dict[str, Any]:
|
||||||
return await handlers.save_artifact(artifact)
|
return await handlers.save_artifact(artifact)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.validate_draft",
|
||||||
|
title="Validate Workflow Draft",
|
||||||
|
description="Validate an LLM-friendly workflow draft without saving it.",
|
||||||
|
)
|
||||||
|
async def validate_draft(draft: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return await handlers.validate_draft(draft=draft)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.compile_draft",
|
||||||
|
title="Compile Workflow Draft",
|
||||||
|
description="Compile an LLM-friendly workflow draft into a raw workflow plan.",
|
||||||
|
)
|
||||||
|
async def compile_draft(draft: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
return await handlers.compile_draft(draft=draft)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.create_artifact_from_plan",
|
name="wf.workflow.create_artifact_from_plan",
|
||||||
title="Create Workflow Artifact From Plan",
|
title="Create Workflow Artifact From Plan",
|
||||||
@@ -117,6 +133,60 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
created_from_catalog_version=created_from_catalog_version,
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.create_artifact_from_draft",
|
||||||
|
title="Create Workflow Artifact From Draft",
|
||||||
|
description=(
|
||||||
|
"Compile an LLM-friendly workflow draft and save it as a versioned "
|
||||||
|
"artifact."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def create_artifact_from_draft(
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
title: str,
|
||||||
|
draft: dict[str, Any],
|
||||||
|
outcomes: list[str],
|
||||||
|
kind: ArtifactKind = "workflow",
|
||||||
|
description: str | None = None,
|
||||||
|
required_capabilities: (
|
||||||
|
Mapping[str, RequiredCapability | dict[str, Any]] | None
|
||||||
|
) = None,
|
||||||
|
source_bindings: Mapping[str, str] | None = None,
|
||||||
|
created_from_catalog_version: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.create_artifact_from_draft(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
title=title,
|
||||||
|
kind=kind,
|
||||||
|
description=description,
|
||||||
|
draft=draft,
|
||||||
|
outcomes=outcomes,
|
||||||
|
required_capabilities={
|
||||||
|
name: (
|
||||||
|
capability.model_dump()
|
||||||
|
if isinstance(capability, RequiredCapability)
|
||||||
|
else capability
|
||||||
|
)
|
||||||
|
for name, capability in (required_capabilities or {}).items()
|
||||||
|
}
|
||||||
|
or None,
|
||||||
|
source_bindings=dict(source_bindings or {}),
|
||||||
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.patch_draft",
|
||||||
|
title="Patch Workflow Draft",
|
||||||
|
description="Apply an RFC 6902 JSON Patch to a workflow draft and validate it.",
|
||||||
|
)
|
||||||
|
async def patch_draft(
|
||||||
|
draft: dict[str, Any],
|
||||||
|
patch: list[dict[str, Any]],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
return await handlers.patch_draft(draft=draft, patch=patch)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.inspect_artifact",
|
name="wf.workflow.inspect_artifact",
|
||||||
title="Inspect Workflow Artifact",
|
title="Inspect Workflow Artifact",
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from wf_artifacts.drafts import compile_workflow_draft, patch_workflow_draft
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_draft_maps_use_step_to_raw_node_use() -> None:
|
||||||
|
plan = compile_workflow_draft(_draft_with_steps([_use_step()]))
|
||||||
|
|
||||||
|
assert plan["name"] == "echo_probe"
|
||||||
|
assert plan["start"] == "echo"
|
||||||
|
assert plan["nodes"][0]["id"] == "echo"
|
||||||
|
assert plan["nodes"][0]["type"] == "node"
|
||||||
|
assert plan["nodes"][0]["node"] == "everything.echo"
|
||||||
|
assert plan["nodes"][0]["in_map"]["input.message"] == "message"
|
||||||
|
assert plan["nodes"][0]["out_map"]["content"] == "state.content"
|
||||||
|
assert plan["edges"][0]["to"] == "__end__"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_draft_maps_condition_foreach_interrupt_and_join() -> None:
|
||||||
|
plan = compile_workflow_draft(
|
||||||
|
_draft_with_steps(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "route",
|
||||||
|
"kind": "condition",
|
||||||
|
"check": {"op": "exists", "path": "input.items"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "each",
|
||||||
|
"kind": "foreach",
|
||||||
|
"over": "input.items",
|
||||||
|
"as": "item",
|
||||||
|
"mode": "serial",
|
||||||
|
"on_item_error": "collect",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "approval",
|
||||||
|
"kind": "interrupt",
|
||||||
|
"interrupt_kind": "approval",
|
||||||
|
"request": {"input.message": "message"},
|
||||||
|
"resume": {"approved": "state.approved"},
|
||||||
|
"outcomes": ["submitted"],
|
||||||
|
},
|
||||||
|
{"id": "joined", "kind": "join"},
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
{"from": "route", "outcome": "true", "to": "each"},
|
||||||
|
{"from": "each", "outcome": "done", "to": "approval"},
|
||||||
|
{"from": "approval", "outcome": "submitted", "to": "joined"},
|
||||||
|
{"from": "joined", "outcome": "done", "to": "__end__"},
|
||||||
|
],
|
||||||
|
start="route",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert plan["nodes"][0]["type"] == "condition"
|
||||||
|
assert plan["nodes"][0]["check"]["op"] == "exists"
|
||||||
|
assert plan["nodes"][1]["type"] == "foreach"
|
||||||
|
assert plan["nodes"][1]["as"] == "item"
|
||||||
|
assert plan["nodes"][1]["on_item_error"] == "collect"
|
||||||
|
assert plan["nodes"][2]["type"] == "interrupt"
|
||||||
|
assert plan["nodes"][2]["kind"] == "approval"
|
||||||
|
assert plan["nodes"][2]["request_map"]["input.message"] == "message"
|
||||||
|
assert plan["nodes"][2]["out_map"]["approved"] == "state.approved"
|
||||||
|
assert plan["nodes"][3]["type"] == "join"
|
||||||
|
assert plan["edges"][3]["from"] == "joined"
|
||||||
|
|
||||||
|
|
||||||
|
def test_compile_draft_requires_explicit_step_ids() -> None:
|
||||||
|
draft = _draft_with_steps([{"kind": "join"}])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError) as exc_info:
|
||||||
|
compile_workflow_draft(draft)
|
||||||
|
|
||||||
|
assert "steps[0].id" in str(exc_info.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_workflow_draft_applies_json_patch_and_validates_result() -> None:
|
||||||
|
patched = patch_workflow_draft(
|
||||||
|
_draft_with_steps([_use_step()]),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/steps/0/in/input.message",
|
||||||
|
"value": "text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"op": "add",
|
||||||
|
"path": "/edges/-",
|
||||||
|
"value": {"from": "echo", "outcome": "error", "to": "__end__"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert patched["status"] == "valid"
|
||||||
|
assert patched["draft"]["steps"][0]["in"]["input.message"] == "text"
|
||||||
|
assert patched["compiled_plan"]["edges"][1]["outcome"] == "error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_patch_workflow_draft_reports_invalid_patch_without_partial_result() -> None:
|
||||||
|
patched = patch_workflow_draft(
|
||||||
|
_draft_with_steps([_use_step()]),
|
||||||
|
[{"op": "replace", "path": "/steps/99/id", "value": "missing"}],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert patched["status"] == "invalid"
|
||||||
|
assert patched["diagnostics"][0]["code"] == "patch_invalid"
|
||||||
|
assert "draft" not in patched
|
||||||
|
|
||||||
|
|
||||||
|
def _draft_with_steps(
|
||||||
|
steps: list[dict[str, object]],
|
||||||
|
*,
|
||||||
|
edges: list[dict[str, str]] | None = None,
|
||||||
|
start: str = "echo",
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"name": "echo_probe",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"state_schema": {"fields": {"content": {"type": "string"}}},
|
||||||
|
"output_schema": {"type": "object", "properties": {}},
|
||||||
|
"start": start,
|
||||||
|
"steps": steps,
|
||||||
|
"edges": edges or [{"from": "echo", "outcome": "ok", "to": "__end__"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _use_step() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": "echo",
|
||||||
|
"kind": "use",
|
||||||
|
"capability": "everything.echo",
|
||||||
|
"in": {"input.message": "message"},
|
||||||
|
"out": {"content": "state.content"},
|
||||||
|
}
|
||||||
@@ -93,7 +93,10 @@ def test_create_workflow_artifact_from_plan_snapshots_observed_node_spec() -> No
|
|||||||
"demo.personal.echo_tool": NodeSpecInventory(
|
"demo.personal.echo_tool": NodeSpecInventory(
|
||||||
name="demo.personal.echo_tool",
|
name="demo.personal.echo_tool",
|
||||||
outcomes=("ok",),
|
outcomes=("ok",),
|
||||||
input_schema={"type": "object", "properties": {"text": {"type": "string"}}},
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"text": {"type": "string"}},
|
||||||
|
},
|
||||||
output_schema={
|
output_schema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {"echoed": {"type": "string"}},
|
"properties": {"echoed": {"type": "string"}},
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.list_capabilities" in names
|
assert "wf.workflow.list_capabilities" in names
|
||||||
assert "wf.workflow.inspect_capability" in names
|
assert "wf.workflow.inspect_capability" in names
|
||||||
assert "wf.workflow.call_capability" in names
|
assert "wf.workflow.call_capability" in names
|
||||||
|
assert "wf.workflow.validate_draft" in names
|
||||||
|
assert "wf.workflow.compile_draft" in names
|
||||||
|
assert "wf.workflow.create_artifact_from_draft" in names
|
||||||
|
assert "wf.workflow.patch_draft" in names
|
||||||
assert "wf.workflow.run_deployment" in names
|
assert "wf.workflow.run_deployment" in names
|
||||||
|
|
||||||
echo_result = await client.call_tool(
|
echo_result = await client.call_tool(
|
||||||
@@ -80,8 +84,7 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert _structured(capability_result)["outcome"] == "ok"
|
assert _structured(capability_result)["outcome"] == "ok"
|
||||||
assert _structured(capability_result)["output"] == {"value": "hello"}
|
assert _structured(capability_result)["output"] == {"value": "hello"}
|
||||||
source_ids = {
|
source_ids = {
|
||||||
source["id"]
|
source["id"] for source in _structured(sources_result)["sources"]
|
||||||
for source in _structured(sources_result)["sources"]
|
|
||||||
}
|
}
|
||||||
assert "wf.admin" in source_ids
|
assert "wf.admin" in source_ids
|
||||||
assert "wf.docs" in source_ids
|
assert "wf.docs" in source_ids
|
||||||
@@ -152,6 +155,8 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
|||||||
assert "wf.admin.list_proxy_tools" in names
|
assert "wf.admin.list_proxy_tools" in names
|
||||||
assert "wf.admin.get_proxy_tool" in names
|
assert "wf.admin.get_proxy_tool" in names
|
||||||
assert "wf.workflow.list_artifacts" in names
|
assert "wf.workflow.list_artifacts" in names
|
||||||
|
assert "wf.workflow.validate_draft" in names
|
||||||
|
assert "wf.workflow.create_artifact_from_draft" in names
|
||||||
assert "wf.workflow.call_capability" in names
|
assert "wf.workflow.call_capability" in names
|
||||||
assert "wf.workflow.inspect_artifact" in names
|
assert "wf.workflow.inspect_artifact" in names
|
||||||
assert "wf.workflow.list_deployments" in names
|
assert "wf.workflow.list_deployments" in names
|
||||||
@@ -206,6 +211,39 @@ def test_create_artifact_from_plan_exposes_plan_as_plain_object() -> None:
|
|||||||
asyncio.run(run_proxy())
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|
||||||
|
def test_draft_tools_expose_plain_object_and_patch_array_schemas() -> None:
|
||||||
|
config = BrokerConfig(
|
||||||
|
store_root=local_temp_root() / "unified_draft_schema_store",
|
||||||
|
connections=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def run_proxy() -> None:
|
||||||
|
client = create_server_client(config, admin_tools=False)
|
||||||
|
async with client:
|
||||||
|
tools = await client.list_tools()
|
||||||
|
by_name = {tool.name: tool for tool in tools}
|
||||||
|
|
||||||
|
validate_schema = by_name["wf.workflow.validate_draft"].inputSchema
|
||||||
|
validate_draft_schema = validate_schema["properties"]["draft"]
|
||||||
|
create_schema = by_name[
|
||||||
|
"wf.workflow.create_artifact_from_draft"
|
||||||
|
].inputSchema
|
||||||
|
create_draft_schema = create_schema["properties"]["draft"]
|
||||||
|
patch_schema = by_name["wf.workflow.patch_draft"].inputSchema
|
||||||
|
patch_draft_schema = patch_schema["properties"]["draft"]
|
||||||
|
patch_patch_schema = patch_schema["properties"]["patch"]
|
||||||
|
|
||||||
|
assert validate_draft_schema["type"] == "object"
|
||||||
|
assert validate_draft_schema.get("additionalProperties") is True
|
||||||
|
assert create_draft_schema["type"] == "object"
|
||||||
|
assert create_draft_schema.get("additionalProperties") is True
|
||||||
|
assert patch_draft_schema["type"] == "object"
|
||||||
|
assert patch_patch_schema["type"] == "array"
|
||||||
|
assert "$defs" not in patch_patch_schema
|
||||||
|
|
||||||
|
asyncio.run(run_proxy())
|
||||||
|
|
||||||
|
|
||||||
def test_server_exposes_platform_documentation_resources() -> None:
|
def test_server_exposes_platform_documentation_resources() -> None:
|
||||||
config = BrokerConfig(
|
config = BrokerConfig(
|
||||||
store_root=local_temp_root() / "unified_docs_resource_store",
|
store_root=local_temp_root() / "unified_docs_resource_store",
|
||||||
@@ -306,8 +344,7 @@ def test_server_reload_syncs_service_connection_source_enabled_state() -> None:
|
|||||||
{"source_id": "fixture.personal"},
|
{"source_id": "fixture.personal"},
|
||||||
)
|
)
|
||||||
names = [
|
names = [
|
||||||
capability["name"]
|
capability["name"] for capability in _structured(after)["capabilities"]
|
||||||
for capability in _structured(after)["capabilities"]
|
|
||||||
]
|
]
|
||||||
assert "fixture.personal.echo_tool" in names
|
assert "fixture.personal.echo_tool" in names
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,115 @@ def test_workflow_surface_creates_artifact_with_logical_node_refs() -> None:
|
|||||||
assert artifact.required_capabilities["demo.echo_tool"].logical_source == "demo"
|
assert artifact.required_capabilities["demo.echo_tool"].logical_source == "demo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_validates_draft_without_saving() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_draft_validate"
|
||||||
|
)
|
||||||
|
handlers = _handlers(artifact_store)
|
||||||
|
|
||||||
|
payload = asyncio.run(handlers.validate_draft(draft=_echo_draft()))
|
||||||
|
|
||||||
|
assert payload["status"] == "valid"
|
||||||
|
assert payload["diagnostics"] == []
|
||||||
|
assert payload["compiled_plan"]["nodes"][0]["type"] == "node"
|
||||||
|
assert artifact_store.list_artifacts() == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions() -> (
|
||||||
|
None
|
||||||
|
):
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_draft_create"
|
||||||
|
)
|
||||||
|
service = WfMcpService(
|
||||||
|
store=FileStore(local_temp_root() / "surface_draft_create_mcp"),
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
)
|
||||||
|
service.register_connection(
|
||||||
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
|
)
|
||||||
|
service.register_specs("demo.personal", echo_tool)
|
||||||
|
handlers = WorkflowSurfaceHandlers(service)
|
||||||
|
draft = _echo_draft()
|
||||||
|
draft["steps"][0]["capability"] = "demo.personal.echo_tool"
|
||||||
|
|
||||||
|
payload = asyncio.run(
|
||||||
|
handlers.create_artifact_from_draft(
|
||||||
|
artifact_id="draft_echo",
|
||||||
|
version=1,
|
||||||
|
title="Draft Echo",
|
||||||
|
draft=draft,
|
||||||
|
outcomes=("completed",),
|
||||||
|
source_bindings={"demo": "demo.personal"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
artifact = artifact_store.get_artifact("draft_echo", 1)
|
||||||
|
|
||||||
|
assert payload["saved"] is True
|
||||||
|
assert payload["required_logical_sources"] == ["demo", "wf.std"]
|
||||||
|
assert payload["suggested_bindings"]["wf.std"] == "wf.std"
|
||||||
|
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
|
||||||
|
assert artifact.required_capabilities["demo.echo_tool"].logical_source == "demo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_draft_artifact_requires_std_self_binding() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_draft_missing_std"
|
||||||
|
)
|
||||||
|
handlers = _handlers(artifact_store)
|
||||||
|
|
||||||
|
asyncio.run(
|
||||||
|
handlers.create_artifact_from_draft(
|
||||||
|
artifact_id="draft_echo_missing_std",
|
||||||
|
version=1,
|
||||||
|
title="Draft Echo Missing Std",
|
||||||
|
draft=_echo_draft(),
|
||||||
|
outcomes=("completed",),
|
||||||
|
source_bindings={"demo": "demo.personal"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
artifact_store.save_deployment(
|
||||||
|
WorkflowDeployment(
|
||||||
|
id="draft_echo_missing_std.personal",
|
||||||
|
artifact_id="draft_echo_missing_std",
|
||||||
|
artifact_version=1,
|
||||||
|
bindings={"demo": "demo.personal"},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = asyncio.run(
|
||||||
|
handlers.validate_deployment(deployment_id="draft_echo_missing_std.personal")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload["status"] == "unrunnable"
|
||||||
|
assert payload["diagnostics"][0]["code"] == "binding_missing"
|
||||||
|
assert payload["diagnostics"][0]["logical_ref"] == "wf.std.replace"
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_patches_draft_without_saving() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_draft_patch"
|
||||||
|
)
|
||||||
|
handlers = _handlers(artifact_store)
|
||||||
|
|
||||||
|
payload = asyncio.run(
|
||||||
|
handlers.patch_draft(
|
||||||
|
draft=_echo_draft(),
|
||||||
|
patch=[
|
||||||
|
{
|
||||||
|
"op": "replace",
|
||||||
|
"path": "/steps/0/in/input.text",
|
||||||
|
"value": "message",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload["status"] == "valid"
|
||||||
|
assert payload["draft"]["steps"][0]["in"]["input.text"] == "message"
|
||||||
|
assert artifact_store.list_artifacts() == []
|
||||||
|
|
||||||
|
|
||||||
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
|
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
|
||||||
plan = RawWorkflowPlan.model_validate(_echo_artifact().plan)
|
plan = RawWorkflowPlan.model_validate(_echo_artifact().plan)
|
||||||
|
|
||||||
@@ -595,6 +704,34 @@ def _echo_artifact() -> WorkflowArtifact:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _echo_draft() -> dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"name": "echo",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"text": {"type": "string"}},
|
||||||
|
"required": ["text"],
|
||||||
|
},
|
||||||
|
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"echoed": {"type": "string"}},
|
||||||
|
"required": ["echoed"],
|
||||||
|
},
|
||||||
|
"start": "echo",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "echo",
|
||||||
|
"kind": "use",
|
||||||
|
"capability": "demo.personal.echo_tool",
|
||||||
|
"in": {"input.text": "text"},
|
||||||
|
"out": {"echoed": "state.echoed"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _logical_echo_artifact() -> WorkflowArtifact:
|
def _logical_echo_artifact() -> WorkflowArtifact:
|
||||||
artifact = _echo_artifact()
|
artifact = _echo_artifact()
|
||||||
plan = dict(artifact.plan)
|
plan = dict(artifact.plan)
|
||||||
|
|||||||
@@ -476,6 +476,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" },
|
{ url = "https://files.pythonhosted.org/packages/b6/f7/210b27752e972edb36d239315b08d3eb6b14824cc4a590da2337d195260b/joserfc-1.6.4-py3-none-any.whl", hash = "sha256:3e4a22b509b41908989237a045e25c8308d5fd47ab96bdae2dd8057c6451003a", size = 70464, upload-time = "2026-04-13T13:15:39.259Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpatch"
|
||||||
|
version = "1.33"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jsonpointer" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonpointer"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jsonref"
|
name = "jsonref"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -549,6 +570,7 @@ version = "0.0.1"
|
|||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
|
{ name = "jsonpatch" },
|
||||||
{ name = "jsonschema" },
|
{ name = "jsonschema" },
|
||||||
{ name = "mcp", extra = ["cli", "rich"] },
|
{ name = "mcp", extra = ["cli", "rich"] },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
@@ -562,6 +584,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastmcp", specifier = ">=3.2.4" },
|
{ name = "fastmcp", specifier = ">=3.2.4" },
|
||||||
|
{ name = "jsonpatch", specifier = ">=1.33" },
|
||||||
{ name = "jsonschema", specifier = ">=4.26" },
|
{ name = "jsonschema", specifier = ">=4.26" },
|
||||||
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
{ name = "mcp", extras = ["cli", "rich"], specifier = ">=1" },
|
||||||
{ name = "pydantic", specifier = ">=2" },
|
{ name = "pydantic", specifier = ">=2" },
|
||||||
|
|||||||
Reference in New Issue
Block a user