Files
lda-wf/tests/wf_mcp/workflow_surface/conftest.py
T

389 lines
11 KiB
Python

from __future__ import annotations
from typing import Any
from pydantic import BaseModel
from wf_artifacts import (
FileDraftWorkspaceStore,
FileRunStore,
FileWorkflowArtifactStore,
RequiredCapability,
WorkflowArtifact,
)
from wf_authoring import node, reducer
from wf_mcp.broker import WfMcpService
from wf_mcp.capabilities import DiscoveredTool
from wf_mcp.sdk import ToolCallResult
from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from ..test_support import input_binding, output_binding
class AmountInput(BaseModel):
amount: int
class AmountOutput(BaseModel):
amount: int
class ChangedEchoInput(BaseModel):
message: str
class ChangedEchoOutput(BaseModel):
echoed: str
@node()
async def amount_tool(payload: AmountInput) -> AmountOutput:
return AmountOutput(amount=payload.amount)
@node(name="echo_tool")
def changed_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
return ChangedEchoOutput(echoed=payload.message)
@node(name="mcp_echo_tool", outcomes=("ok", "error"))
def mcp_echo_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
"""Test fixture that mirrors naive MCP wrappers with ok/error outcomes."""
return ChangedEchoOutput(echoed=payload.message)
@node(name="failing_tool")
def failing_tool(payload: ChangedEchoInput) -> ChangedEchoOutput:
raise RuntimeError("upstream exploded")
class ContentOnlyOutputAdapter:
"""MCP-like adapter whose tool exposes raw content blocks as output schema."""
async def list_tools(
self,
connection,
auth,
) -> list[DiscoveredTool]:
return [
DiscoveredTool(
name="echo",
title="Echo",
description="Echo a message as an MCP text content block.",
input_schema={
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
output_schema={
"type": "object",
"properties": {"content": {"type": "array"}},
"required": ["content"],
},
)
]
async def list_resources(
self,
connection,
auth,
) -> list[Any]:
return []
async def list_prompts(
self,
connection,
auth,
) -> list[Any]:
return []
async def get_connection_metadata(
self,
connection,
auth,
) -> dict[str, Any]:
return {
"server": getattr(
connection, "provider", getattr(connection, "server", None)
)
}
async def call_tool(
self,
connection,
auth,
tool_name: str,
payload: dict[str, Any],
) -> ToolCallResult:
message = payload.get("message", "")
return ToolCallResult(
outcome="ok",
output={"content": [{"type": "text", "text": f"Echo: {message}"}]},
)
async def read_resource(
self,
connection,
auth,
uri: str,
) -> dict[str, Any]:
raise KeyError(uri)
async def get_prompt(
self,
connection,
auth,
prompt_name: str,
arguments: dict[str, str] | None = None,
) -> dict[str, Any]:
raise KeyError(prompt_name)
async def invoke_method(
self,
connection,
auth,
method: str,
params: dict[str, Any] | None = None,
) -> dict[str, Any]:
raise KeyError(method)
async def send_notification(
self,
connection,
auth,
method: str,
params: dict[str, Any] | None = None,
) -> None:
raise KeyError(method)
@reducer(name="custom.default.multiply")
def multiply(current: int | None, incoming: int) -> int:
return (current or 1) * incoming
def handlers(artifact_store: FileWorkflowArtifactStore) -> WorkflowSurfaceHandlers:
mcp_root = artifact_store.root / "surface_mcp" / str(id(artifact_store))
service = WfMcpService(
store=FileStore(mcp_root),
artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore(mcp_root),
run_store=FileRunStore(mcp_root),
)
return WorkflowSurfaceHandlers(service)
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:
plan: dict[str, Any] = {
"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__"}],
}
return WorkflowArtifact(
id="echo",
version=1,
title="Echo",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.echo_tool": RequiredCapability(
ref="demo.echo_tool",
kind="node_spec",
)
},
)
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": {
"echo": {
"use": "demo.personal.echo_tool",
"input": [
{
"target": {"root": "local", "parts": ["text"]},
"path": {"root": "input", "parts": ["text"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["echoed"]},
"target": {"root": "state", "parts": ["echoed"]},
}
],
}
},
"routes": {"echo": {"ok": "__end__"}},
}
def logical_echo_artifact() -> WorkflowArtifact:
a = echo_artifact()
plan = dict(a.plan)
nodes = [dict(node) for node in plan["nodes"]]
nodes[0]["node"] = "demo.echo_tool"
plan["nodes"] = nodes
return a.model_copy(
update={
"id": "logical_echo",
"plan": plan,
}
)
def failing_artifact() -> WorkflowArtifact:
plan: dict[str, Any] = {
"name": "fail",
"input_schema": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
"state_schema": {"fields": {"echoed": {"type": "string"}}},
"output_schema": {
"type": "object",
"properties": {"echoed": {"type": "string"}},
},
"start": "fail",
"nodes": [
{
"id": "fail",
"type": "node",
"node": "demo.personal.failing_tool",
"input": [input_binding("input.message", "message")],
"output": [output_binding("echoed", "state.echoed")],
}
],
"edges": [{"from": "fail", "outcome": "ok", "to": "__end__"}],
}
return WorkflowArtifact(
id="fail",
version=1,
title="Fail",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.failing_tool": RequiredCapability(
ref="demo.failing_tool",
kind="node_spec",
)
},
)
def custom_reducer_artifact() -> WorkflowArtifact:
plan: dict[str, Any] = {
"name": "multiply",
"input_schema": {
"type": "object",
"properties": {
"total": {"type": "integer"},
"amount": {"type": "integer"},
},
"required": ["total", "amount"],
},
"state_schema": {
"type": "object",
"properties": {
"total": {
"type": "integer",
"reducer": "custom.multiply",
}
},
},
"output_schema": {
"type": "object",
"properties": {"total": {"type": "integer"}},
"required": ["total"],
},
"start": "amount",
"nodes": [
{
"id": "amount",
"type": "node",
"node": "demo.personal.amount_tool",
"input": [input_binding("input.amount", "amount")],
"output": [output_binding("amount", "state.total")],
}
],
"edges": [{"from": "amount", "outcome": "ok", "to": "__end__"}],
}
return WorkflowArtifact(
id="multiply",
version=1,
title="Multiply",
input_schema=plan["input_schema"],
output_schema=plan["output_schema"],
outcomes=("completed",),
plan=plan,
required_capabilities={
"demo.amount_tool": RequiredCapability(
ref="demo.amount_tool",
kind="node_spec",
),
"custom.multiply": RequiredCapability(
ref="custom.multiply",
kind="reducer",
),
},
)