hypothetical demonstration of conversion to workflow friendly tools
This commit is contained in:
@@ -35,6 +35,9 @@ This repository has three main packages plus examples and tests.
|
|||||||
`wf_core` so the kernel package does not carry fixture/demo code.
|
`wf_core` so the kernel package does not carry fixture/demo code.
|
||||||
- `examples/authoring_control_flow.py` demonstrates `WorkflowBuilder.branch`,
|
- `examples/authoring_control_flow.py` demonstrates `WorkflowBuilder.branch`,
|
||||||
`handle`, `match`, `when`, `choose`, and `use_ref` with executable examples.
|
`handle`, `match`, `when`, `choose`, and `use_ref` with executable examples.
|
||||||
|
- `examples/wrapper_status_route.py` and `examples/wrapper_normalization.py`
|
||||||
|
show two wrapper styles: routing on provider status fields, and converting
|
||||||
|
provider status fields into workflow outcomes.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from wf_authoring import NodeReturn, WorkflowBuilder, node, outcome
|
||||||
|
from wf_core import END
|
||||||
|
|
||||||
|
|
||||||
|
class RawToolInput(BaseModel):
|
||||||
|
"""Input accepted by the raw provider-shaped tool."""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
class RawToolOutput(BaseModel):
|
||||||
|
"""Provider-shaped result that hides business state in fields."""
|
||||||
|
|
||||||
|
status: Literal["done", "needs_input", "failed"]
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class WrapperState(BaseModel):
|
||||||
|
"""State used to pass the raw provider result into the normalizer."""
|
||||||
|
|
||||||
|
status: str
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class WrapperOutput(BaseModel):
|
||||||
|
"""Workflow-facing output after normalization."""
|
||||||
|
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
@node
|
||||||
|
def raw_status_tool(input: RawToolInput) -> RawToolOutput:
|
||||||
|
"""Stand in for an MCP tool whose output is not workflow-friendly yet."""
|
||||||
|
if input.text.endswith("?"):
|
||||||
|
return RawToolOutput(status="needs_input", message="Need clarification")
|
||||||
|
if not input.text.strip():
|
||||||
|
return RawToolOutput(status="failed", message="No text supplied")
|
||||||
|
return RawToolOutput(status="done", message=input.text.upper())
|
||||||
|
|
||||||
|
|
||||||
|
@node(outcomes=("done", "needs_input", "failed"))
|
||||||
|
def normalize_status(input: RawToolOutput) -> NodeReturn[WrapperOutput]:
|
||||||
|
"""Convert provider status fields into explicit workflow outcomes.
|
||||||
|
|
||||||
|
This is the key wrapper move: downstream graph code branches on outcomes
|
||||||
|
instead of re-parsing provider-specific result envelopes.
|
||||||
|
"""
|
||||||
|
return outcome(input.status, WrapperOutput(message=input.message))
|
||||||
|
|
||||||
|
|
||||||
|
def build_normalized_wrapper() -> WorkflowBuilder:
|
||||||
|
"""Build a wrapper graph around a provider-shaped raw tool result."""
|
||||||
|
graph = WorkflowBuilder(
|
||||||
|
name="normalized_status_wrapper",
|
||||||
|
input_schema=RawToolInput,
|
||||||
|
state_schema=WrapperState,
|
||||||
|
output_schema=WrapperOutput,
|
||||||
|
)
|
||||||
|
raw = graph.use(
|
||||||
|
raw_status_tool,
|
||||||
|
id="raw_tool",
|
||||||
|
in_map={"input.text": "text"},
|
||||||
|
out_map={
|
||||||
|
"status": "state.status",
|
||||||
|
"message": "state.message",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
normalizer = graph.use(
|
||||||
|
normalize_status,
|
||||||
|
id="normalize",
|
||||||
|
in_map={
|
||||||
|
"state.status": "status",
|
||||||
|
"state.message": "message",
|
||||||
|
},
|
||||||
|
out_map={"message": "state.message"},
|
||||||
|
)
|
||||||
|
graph.connect(raw, "ok", normalizer)
|
||||||
|
graph.connect(normalizer, "done", END)
|
||||||
|
graph.connect(normalizer, "needs_input", END)
|
||||||
|
graph.connect(normalizer, "failed", END)
|
||||||
|
graph.set_entry_point(raw)
|
||||||
|
return graph
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
workflow = build_normalized_wrapper()
|
||||||
|
for text in ("hello", "clarify?", ""):
|
||||||
|
run = workflow.execute({"text": text})
|
||||||
|
print(text, run.status.value, run.output)
|
||||||
@@ -62,16 +62,17 @@ def build_wrapper() -> WorkflowBuilder:
|
|||||||
output_schema=WrapperOutput,
|
output_schema=WrapperOutput,
|
||||||
)
|
)
|
||||||
tool = graph.use(raw_tool)
|
tool = graph.use(raw_tool)
|
||||||
graph.route(
|
decision = graph.match(
|
||||||
state("status"),
|
state("status"),
|
||||||
{
|
{
|
||||||
"done": graph.use(done, id="done"),
|
"done": graph.use(done, id="done"),
|
||||||
"needs_input": graph.use(needs_input, id="needs_input"),
|
"needs_input": graph.use(needs_input, id="needs_input"),
|
||||||
},
|
},
|
||||||
default=graph.use(failed, id="failed"),
|
default=graph.use(failed, id="failed"),
|
||||||
|
id="status",
|
||||||
)
|
)
|
||||||
graph.set_entry_point(tool)
|
graph.set_entry_point(tool)
|
||||||
graph.connect(tool, "ok", "condition")
|
graph.connect(tool, "ok", decision.entry)
|
||||||
graph.connect("done", "ok", "__end__")
|
graph.connect("done", "ok", "__end__")
|
||||||
graph.connect("needs_input", "ok", "__end__")
|
graph.connect("needs_input", "ok", "__end__")
|
||||||
graph.connect("failed", "ok", "__end__")
|
graph.connect("failed", "ok", "__end__")
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from wf_core import RunStatus
|
||||||
|
|
||||||
|
from examples.wrapper_status_route import build_wrapper
|
||||||
|
from examples.wrapper_normalization import build_normalized_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def test_status_wrapper_uses_match_without_deprecation_warning() -> None:
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("error", DeprecationWarning)
|
||||||
|
workflow = build_wrapper()
|
||||||
|
|
||||||
|
run = workflow.execute({"text": "hello"})
|
||||||
|
|
||||||
|
assert run.status == RunStatus.COMPLETED
|
||||||
|
assert run.output["message"] == "HELLO"
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalized_wrapper_maps_raw_status_to_workflow_outcome() -> None:
|
||||||
|
workflow = build_normalized_wrapper()
|
||||||
|
|
||||||
|
success = workflow.execute({"text": "hello"})
|
||||||
|
needs_input = workflow.execute({"text": "clarify?"})
|
||||||
|
failed = workflow.execute({"text": ""})
|
||||||
|
|
||||||
|
assert success.status == RunStatus.COMPLETED
|
||||||
|
assert success.trace[-1].outcome == "done"
|
||||||
|
assert success.output["message"] == "HELLO"
|
||||||
|
assert needs_input.status == RunStatus.COMPLETED
|
||||||
|
assert needs_input.trace[-1].outcome == "needs_input"
|
||||||
|
assert needs_input.output["message"] == "Need clarification"
|
||||||
|
assert failed.status == RunStatus.COMPLETED
|
||||||
|
assert failed.trace[-1].outcome == "failed"
|
||||||
|
assert failed.output["message"] == "No text supplied"
|
||||||
Reference in New Issue
Block a user