717 lines
24 KiB
Python
717 lines
24 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
from mcp.types import CallToolResult, InputRequiredResult
|
|
|
|
from wf_artifacts import (
|
|
FileDraftWorkspaceStore,
|
|
FileRunStore,
|
|
FileWorkflowArtifactStore,
|
|
RequiredCapability,
|
|
WorkflowArtifact,
|
|
WorkflowDeployment,
|
|
)
|
|
from wf_mcp.broker import (
|
|
WfMcpService,
|
|
build_service_from_config,
|
|
create_broker_server,
|
|
load_broker_config,
|
|
)
|
|
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
|
from wf_mcp.source_registry import (
|
|
FileSourceRegistryStore,
|
|
McpSourceRegistryEntry,
|
|
SourceRegistryFile,
|
|
StdioSourceTransport,
|
|
)
|
|
from wf_mcp.storage import FileStore
|
|
|
|
from .test_support import (
|
|
FailingDiscoveryAdapter,
|
|
FakeAdapter,
|
|
echo_tool,
|
|
input_binding,
|
|
output_binding,
|
|
)
|
|
|
|
|
|
def _structured_content(
|
|
result: CallToolResult | InputRequiredResult,
|
|
) -> dict[str, Any]:
|
|
"""Return completed tool output, narrowing away MRTR interim results.
|
|
|
|
Broker admin/workflow tools always complete inline in these tests; an
|
|
`InputRequiredResult` here would mean the tool unexpectedly asked for
|
|
mid-call input.
|
|
"""
|
|
assert isinstance(result, CallToolResult)
|
|
return cast(dict[str, Any], result.structured_content)
|
|
|
|
|
|
def test_load_broker_config_resolves_relative_store_root(tmp_path: Path) -> None:
|
|
tmp_path = tmp_path / "broker_config_test"
|
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
|
config_path = tmp_path / "wf_mcp.config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"store_root": ".broker-store",
|
|
"connections": [
|
|
{
|
|
"id": "demo.personal",
|
|
"server": "demo",
|
|
"account": "personal",
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = load_broker_config(config_path)
|
|
|
|
assert config.store_root == (tmp_path / ".broker-store").resolve()
|
|
assert [connection.id for connection in config.connections] == ["demo.personal"]
|
|
|
|
|
|
def test_create_broker_server_exposes_tools_resources_and_prompts(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
service = WfMcpService(store=FileStore(tmp_path / "broker_server_store"))
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_adapter("demo", FakeAdapter())
|
|
asyncio.run(service.refresh_connection_catalog("demo.personal"))
|
|
|
|
server = create_broker_server(service)
|
|
|
|
tools = asyncio.run(server.list_tools())
|
|
resources = asyncio.run(server.list_resources())
|
|
prompts = asyncio.run(server.list_prompts())
|
|
|
|
tool_names = {tool.name for tool in tools}
|
|
resource_names = {resource.name for resource in resources}
|
|
prompt_names = {prompt.name for prompt in prompts}
|
|
|
|
assert "get_connection_statuses" in tool_names
|
|
assert "refresh_connection_catalog" in tool_names
|
|
assert "get_planner_catalog" in tool_names
|
|
assert "list_sources" in tool_names
|
|
assert "invoke_broker_method" in tool_names
|
|
assert "catalog.all" in resource_names
|
|
assert "events.all" in resource_names
|
|
assert "status.all" in resource_names
|
|
assert "workflow_authoring_guide" in prompt_names
|
|
assert "plan_with_catalog" not in prompt_names
|
|
|
|
planner_catalog = asyncio.run(server.call_tool("get_planner_catalog", {}))
|
|
planner_catalog = _structured_content(planner_catalog)
|
|
planner_names = [node["qualified_name"] for node in planner_catalog["nodes"]]
|
|
assert "demo.personal.echo_tool" in planner_names
|
|
assert "wf.std.runtime_error" in planner_names
|
|
|
|
all_sources = asyncio.run(server.call_tool("list_sources", {}))
|
|
all_sources_payload = _structured_content(all_sources)
|
|
all_sources = all_sources_payload["sources"]
|
|
all_source_ids = {source["id"] for source in all_sources}
|
|
assert "wf.admin" in all_source_ids
|
|
assert "demo.personal" in all_source_ids
|
|
|
|
|
|
def test_broker_admin_tools_are_backed_by_wf_admin_source(tmp_path: Path) -> None:
|
|
service = WfMcpService(store=FileStore(tmp_path / "broker_admin_source"))
|
|
server = create_broker_server(service)
|
|
|
|
tools = asyncio.run(server.list_tools())
|
|
tool_names = {tool.name for tool in tools}
|
|
|
|
assert "get_planner_catalog" in tool_names
|
|
assert (
|
|
"wf.admin.list_sources"
|
|
in service.capability_sources["wf.admin"].capabilities.tools
|
|
)
|
|
|
|
|
|
def test_build_service_from_config_registers_connections(tmp_path: Path) -> None:
|
|
config = BrokerConfig(
|
|
store_root=tmp_path / "broker_config_store",
|
|
connections=[
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal"),
|
|
ConnectionConfig(id="demo.work", server="demo", account="work"),
|
|
],
|
|
)
|
|
|
|
service = build_service_from_config(config)
|
|
|
|
ids = [connection.id for connection in service.connections.list_all()]
|
|
assert ids == ["demo.personal", "demo.work"]
|
|
|
|
|
|
def test_broker_refresh_tool_returns_structured_error(tmp_path: Path) -> None:
|
|
service = WfMcpService(store=FileStore(tmp_path / "broker_fail_store"))
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_adapter("demo", FailingDiscoveryAdapter())
|
|
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"refresh_connection_catalog", {"connection_id": "demo.personal"}
|
|
)
|
|
)
|
|
assert _structured_content(result) == {
|
|
"connection_id": "demo.personal",
|
|
"refreshed": False,
|
|
"error_type": "PermissionError",
|
|
"error": "Access is denied",
|
|
}
|
|
|
|
|
|
def test_broker_lists_workflow_artifacts_from_artifact_store(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_artifacts")
|
|
artifact_store.save_artifact(_artifact())
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_artifacts_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(server.call_tool("list_workflow_artifacts", {}))
|
|
payload = _structured_content(result)
|
|
|
|
nodes = payload["nodes"]
|
|
assert len(nodes) == 1
|
|
assert nodes[0]["name"] == "workflow.summarize_docs.v1"
|
|
assert nodes[0]["required_sources"] == ["context7"]
|
|
assert "plan" not in nodes[0]
|
|
|
|
|
|
def test_broker_inspects_workflow_artifact_from_artifact_store(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_inspect_artifacts")
|
|
artifact_store.save_artifact(_artifact())
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_inspect_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"inspect_workflow_artifact",
|
|
{"artifact_id": "summarize_docs", "version": 1},
|
|
)
|
|
)
|
|
artifact = _structured_content(result)
|
|
|
|
assert artifact["id"] == "summarize_docs"
|
|
assert artifact["version"] == 1
|
|
assert artifact["plan"]["name"] == "summarize_docs"
|
|
|
|
|
|
def test_broker_validates_workflow_deployment_from_artifact_store(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_validate_artifacts")
|
|
artifact_store.save_artifact(_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="summarize_docs.personal",
|
|
artifact_id="summarize_docs",
|
|
artifact_version=1,
|
|
bindings=[
|
|
{"logical_source": "context7", "concrete_source": "context7.personal"}
|
|
],
|
|
)
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_validate_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"validate_workflow_deployment",
|
|
{"deployment_id": "summarize_docs.personal"},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
|
|
assert payload["deployment_id"] == "summarize_docs.personal"
|
|
assert payload["artifact_id"] == "summarize_docs"
|
|
assert payload["status"] == "unrunnable"
|
|
assert payload["diagnostics"][0]["code"] == "source_missing"
|
|
|
|
|
|
def test_broker_saves_workflow_artifact(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_save_artifacts")
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_save_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"save_workflow_artifact",
|
|
{"artifact": _artifact().model_dump(mode="json")},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
loaded = artifact_store.get_artifact("summarize_docs", 1)
|
|
|
|
assert payload["artifact_id"] == "summarize_docs"
|
|
assert payload["version"] == 1
|
|
assert loaded.title == "Summarize Docs"
|
|
|
|
|
|
def test_broker_creates_workflow_artifact_from_plan(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(
|
|
tmp_path / "broker_create_artifact_from_plan"
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_create_artifact_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"create_workflow_artifact_from_plan",
|
|
{
|
|
"artifact_id": "echo",
|
|
"version": 1,
|
|
"title": "Echo",
|
|
"description": "Echo through a saved plan.",
|
|
"plan": _echo_artifact().plan,
|
|
"outcomes": ["done"],
|
|
"required_capabilities": {
|
|
"demo.echo_tool": {
|
|
"logical_source": "demo",
|
|
"capability_name": "echo_tool",
|
|
"kind": "node_spec",
|
|
}
|
|
},
|
|
"created_from_catalog_version": "catalog-1",
|
|
},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
loaded = artifact_store.get_artifact("echo", 1)
|
|
|
|
assert payload["artifact_id"] == "echo"
|
|
assert payload["version"] == 1
|
|
assert payload["saved"] is True
|
|
assert loaded.input_schema["properties"]["text"]["type"] == "string"
|
|
assert loaded.output_schema["properties"]["echoed"]["type"] == "string"
|
|
assert loaded.required_capability_map()["demo.echo_tool"].logical_source == "demo"
|
|
|
|
|
|
def test_broker_saves_and_lists_workflow_deployments(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_save_deployments")
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_save_deployments_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
save_result = asyncio.run(
|
|
server.call_tool(
|
|
"save_workflow_deployment",
|
|
{
|
|
"deployment": WorkflowDeployment(
|
|
id="summarize_docs.personal",
|
|
artifact_id="summarize_docs",
|
|
artifact_version=1,
|
|
bindings=[
|
|
{
|
|
"logical_source": "context7",
|
|
"concrete_source": "context7.personal",
|
|
}
|
|
],
|
|
).model_dump(mode="json")
|
|
},
|
|
)
|
|
)
|
|
save_payload = _structured_content(save_result)
|
|
list_result = asyncio.run(server.call_tool("list_workflow_deployments", {}))
|
|
list_payload = _structured_content(list_result)
|
|
|
|
assert save_payload["deployment_id"] == "summarize_docs.personal"
|
|
assert list_payload["deployments"][0]["id"] == "summarize_docs.personal"
|
|
assert "bindings" not in list_payload["deployments"][0]
|
|
|
|
|
|
def test_broker_runs_non_interrupting_workflow_deployment(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_run_artifacts")
|
|
artifact_store.save_artifact(_echo_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_run_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
run_store=FileRunStore(tmp_path / "broker_run_mcp_store"),
|
|
)
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_specs("demo.personal", echo_tool)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"run_workflow_deployment",
|
|
{
|
|
"deployment_id": "echo.personal",
|
|
"workflow_input": {"text": "hello"},
|
|
},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
|
|
assert payload["deployment_id"] == "echo.personal"
|
|
assert payload["artifact_id"] == "echo"
|
|
assert payload["status"] == "completed"
|
|
assert payload["output"]["echoed"] == "hello"
|
|
assert payload["diagnostics"] == []
|
|
assert payload["trace_count"] > 0
|
|
|
|
|
|
def test_broker_run_deployment_returns_unrunnable_for_dependency_errors(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(
|
|
tmp_path / "broker_run_unrunnable_artifacts"
|
|
)
|
|
artifact_store.save_artifact(_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="summarize_docs.personal",
|
|
artifact_id="summarize_docs",
|
|
artifact_version=1,
|
|
bindings=[
|
|
{"logical_source": "context7", "concrete_source": "context7.personal"}
|
|
],
|
|
)
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_run_unrunnable_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"run_workflow_deployment",
|
|
{
|
|
"deployment_id": "summarize_docs.personal",
|
|
"workflow_input": {},
|
|
},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
|
|
assert payload["status"] == "unrunnable"
|
|
assert payload["output"] is None
|
|
assert payload["diagnostics"][0]["code"] == "source_missing"
|
|
|
|
|
|
def test_broker_run_deployment_pauses_and_resumes_interrupting_artifacts(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(
|
|
tmp_path / "broker_run_interrupt_artifacts"
|
|
)
|
|
artifact_store.save_artifact(_interrupt_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="approval.personal",
|
|
artifact_id="approval",
|
|
artifact_version=1,
|
|
bindings=[],
|
|
)
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(tmp_path / "broker_run_interrupt_mcp_store"),
|
|
artifact_store=artifact_store,
|
|
run_store=FileRunStore(tmp_path / "broker_run_interrupt_mcp_store"),
|
|
)
|
|
server = create_broker_server(service)
|
|
|
|
result = asyncio.run(
|
|
server.call_tool(
|
|
"run_workflow_deployment",
|
|
{
|
|
"deployment_id": "approval.personal",
|
|
"workflow_input": {"message": "send?"},
|
|
},
|
|
)
|
|
)
|
|
payload = _structured_content(result)
|
|
|
|
assert payload["status"] == "interrupted"
|
|
assert payload["output"] == {}
|
|
assert isinstance(payload["run_id"], str)
|
|
assert payload["interrupt"]["payload"]["message"] == "send?"
|
|
|
|
resumed = asyncio.run(
|
|
server.call_tool(
|
|
"resume_workflow_run",
|
|
{
|
|
"run_id": payload["run_id"],
|
|
"resume_payload": {},
|
|
},
|
|
)
|
|
)
|
|
resumed = _structured_content(resumed)
|
|
|
|
assert resumed["status"] == "completed"
|
|
assert resumed["outcome"] == "submitted"
|
|
assert resumed["run_id"] == payload["run_id"]
|
|
assert resumed["resume_readiness"] == "not_applicable"
|
|
|
|
|
|
def test_build_service_from_config_uses_store_root_for_workflow_stores(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
store_root = tmp_path / "broker_config_workflow_stores"
|
|
config = BrokerConfig(store_root=store_root, connections=[])
|
|
|
|
service = build_service_from_config(config)
|
|
|
|
assert isinstance(service.artifact_store, FileWorkflowArtifactStore)
|
|
assert isinstance(service.draft_workspace_store, FileDraftWorkspaceStore)
|
|
assert isinstance(service.run_store, FileRunStore)
|
|
assert service.artifact_store.root == store_root
|
|
assert service.draft_workspace_store.root == store_root
|
|
assert service.run_store.root == store_root
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Source registry integration tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _registry_entry(
|
|
source_id: str = "demo.registry",
|
|
*,
|
|
enabled: bool = True,
|
|
) -> McpSourceRegistryEntry:
|
|
return McpSourceRegistryEntry(
|
|
id=source_id,
|
|
kind="mcp",
|
|
enabled=enabled,
|
|
provider="demo",
|
|
account=source_id.rsplit(".", 1)[-1],
|
|
transport=StdioSourceTransport(command="demo-server"),
|
|
)
|
|
|
|
|
|
def test_build_service_from_config_loads_source_registry_entries(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
tmp_path = tmp_path / "broker_config_registry_load"
|
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
|
config = BrokerConfig(store_root=tmp_path, connections=[])
|
|
FileSourceRegistryStore(tmp_path).save_registry(
|
|
SourceRegistryFile(sources=[_registry_entry("fixture.registry")])
|
|
)
|
|
|
|
service = build_service_from_config(config)
|
|
|
|
assert service.connections.get("fixture.registry").server == "demo"
|
|
assert "demo" in service.adapters
|
|
assert "fixture.registry" in service.capability_sources
|
|
|
|
|
|
def test_build_service_from_config_config_shadows_registry(tmp_path: Path) -> None:
|
|
tmp_path = tmp_path / "broker_config_registry_shadow"
|
|
tmp_path.mkdir(parents=True, exist_ok=True)
|
|
config = BrokerConfig(
|
|
store_root=tmp_path,
|
|
connections=[
|
|
ConnectionConfig(id="fixture.same", server="fixture", account="config"),
|
|
],
|
|
)
|
|
FileSourceRegistryStore(tmp_path).save_registry(
|
|
SourceRegistryFile(sources=[_registry_entry("fixture.same")])
|
|
)
|
|
|
|
service = build_service_from_config(config)
|
|
|
|
assert service.connections.get("fixture.same").account == "config"
|
|
assert any(
|
|
event.kind == "source_registry_ignored_config_shadow"
|
|
and event.connection_id == "fixture.same"
|
|
for event in service.list_events()
|
|
)
|
|
|
|
|
|
def test_broker_config_connection_defaults_to_locked(tmp_path: Path) -> None:
|
|
config_path = tmp_path / "wf_mcp.config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"store_root": str(tmp_path / "store"),
|
|
"connections": [
|
|
{"id": "demo.default", "server": "demo", "account": "default"}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = load_broker_config(config_path)
|
|
|
|
assert config.connections[0].source_config_ownership == "locked"
|
|
|
|
|
|
def test_broker_config_connection_accepts_seed_policy(tmp_path: Path) -> None:
|
|
config_path = tmp_path / "wf_mcp.config.json"
|
|
config_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"store_root": str(tmp_path / "store"),
|
|
"connections": [
|
|
{
|
|
"id": "demo.default",
|
|
"server": "demo",
|
|
"account": "default",
|
|
"source_config_ownership": "seed",
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
config = load_broker_config(config_path)
|
|
|
|
assert config.connections[0].source_config_ownership == "seed"
|
|
|
|
|
|
def _artifact() -> WorkflowArtifact:
|
|
return WorkflowArtifact(
|
|
id="summarize_docs",
|
|
version=1,
|
|
title="Summarize Docs",
|
|
description="Summarize retrieved documentation.",
|
|
input_schema={"type": "object", "properties": {}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
outcomes=("done",),
|
|
plan={"name": "summarize_docs", "nodes": [], "edges": []},
|
|
required_capabilities={
|
|
"context7.query-docs": RequiredCapability(
|
|
ref="context7.query-docs",
|
|
kind="tool",
|
|
input_schema_hash="sha256:input",
|
|
output_schema_hash="sha256:output",
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
def _echo_artifact() -> WorkflowArtifact:
|
|
return WorkflowArtifact(
|
|
id="echo",
|
|
version=1,
|
|
title="Echo",
|
|
description="Echo text through a demo capability.",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {"text": {"type": "string"}},
|
|
"required": ["text"],
|
|
},
|
|
output_schema={
|
|
"type": "object",
|
|
"properties": {"echoed": {"type": "string"}},
|
|
"required": ["echoed"],
|
|
},
|
|
outcomes=("completed",),
|
|
plan={
|
|
"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",
|
|
"nodes": [
|
|
{
|
|
"id": "echo",
|
|
"type": "node",
|
|
"node": "demo.personal.echo_tool",
|
|
"input": [input_binding("input.text", "text")],
|
|
"output": [output_binding("echoed", "state.echoed")],
|
|
}
|
|
],
|
|
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
|
|
},
|
|
required_capabilities={
|
|
"demo.echo_tool": RequiredCapability(
|
|
ref="demo.echo_tool",
|
|
kind="node_spec",
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
def _interrupt_artifact() -> WorkflowArtifact:
|
|
return WorkflowArtifact(
|
|
id="approval",
|
|
version=1,
|
|
title="Approval",
|
|
input_schema={
|
|
"type": "object",
|
|
"properties": {"message": {"type": "string"}},
|
|
"required": ["message"],
|
|
},
|
|
output_schema={"type": "object", "properties": {}},
|
|
outcomes=("submitted",),
|
|
plan={
|
|
"name": "approval",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {"message": {"type": "string"}},
|
|
"required": ["message"],
|
|
},
|
|
"state_schema": {"fields": {}},
|
|
"output_schema": {"type": "object", "properties": {}},
|
|
"outcomes": ["submitted"],
|
|
"start": "approval",
|
|
"nodes": [
|
|
{
|
|
"id": "approval",
|
|
"type": "interrupt",
|
|
"kind": "approval",
|
|
"request": [input_binding("input.message", "message")],
|
|
"resume": [],
|
|
"outcomes": ["submitted"],
|
|
},
|
|
{"id": "end_submitted", "type": "end", "outcome": "submitted"},
|
|
],
|
|
"edges": [
|
|
{"from": "approval", "outcome": "submitted", "to": "end_submitted"}
|
|
],
|
|
},
|
|
)
|