Files
lda-wf/examples/authoring_control_flow.py
T

369 lines
11 KiB
Python

from __future__ import annotations
from typing import Any
from pydantic import BaseModel
from wf_authoring import (
NodeReturn,
NodeSpec,
WorkflowBuilder,
input_from,
input_path,
node,
outcome,
output_to,
state,
state_path,
)
from wf_core import END
class TextInput(BaseModel):
"""Common workflow input used by the authoring examples."""
text: str
class ExampleState(BaseModel):
"""Small shared state shape so examples can focus on control flow."""
message: str = ""
status: str = ""
length: int = 0
class MessageOutput(BaseModel):
"""Common workflow output payload."""
message: str
class StatusOutput(BaseModel):
"""Intermediate node output that records a status in workflow state."""
status: str
class MetricsOutput(BaseModel):
"""Intermediate node output used by condition examples."""
message: str
length: int
@node(outcomes=("send", "skip", "error"))
def classify_message(input: TextInput) -> NodeReturn[MessageOutput]:
"""Choose an outcome directly from node logic."""
if input.text == "bad":
return outcome("error", MessageOutput(message="classification failed"))
if "send" in input.text:
return outcome("send", MessageOutput(message=input.text))
return outcome("skip", MessageOutput(message=input.text))
@node(outcomes=("ok", "error"))
def lookup_message(input: TextInput) -> NodeReturn[MessageOutput]:
"""Pretend to fetch data and expose a business error outcome."""
if input.text == "bad":
return outcome("error", MessageOutput(message="lookup failed"))
return outcome("ok", MessageOutput(message=input.text))
@node(outcomes=("ok", "error"))
def deliver_message(input: MessageOutput) -> NodeReturn[MessageOutput]:
"""Pretend delivery can also fail with the same error outcome."""
if input.message == "undeliverable":
return outcome("error", MessageOutput(message="delivery failed"))
return outcome("ok", MessageOutput(message=f"delivered: {input.message}"))
@node
def mark_sent(input: MessageOutput) -> MessageOutput:
"""Normalize the branch payload for the send path."""
return MessageOutput(message=f"sent: {input.message}")
@node
def mark_skipped(input: MessageOutput) -> MessageOutput:
"""Normalize the branch payload for the skip path."""
return MessageOutput(message=f"skipped: {input.message}")
@node
def fail_safely(input: MessageOutput) -> MessageOutput:
"""Collapse several error outcomes into one workflow-facing payload."""
return MessageOutput(message="failed safely")
@node
def classify_status(input: TextInput) -> StatusOutput:
"""Write a status value for `match()` to inspect."""
if input.text == "approve":
return StatusOutput(status="approved")
if input.text == "reject":
return StatusOutput(status="rejected")
return StatusOutput(status="pending")
@node
def approved(input: StatusOutput) -> MessageOutput:
"""Target for the approved status."""
return MessageOutput(message="approved")
@node
def rejected(input: StatusOutput) -> MessageOutput:
"""Target for the rejected status."""
return MessageOutput(message="rejected")
@node
def pending(input: StatusOutput) -> MessageOutput:
"""Target for the pending/default status."""
return MessageOutput(message="pending")
@node
def measure_text(input: TextInput) -> MetricsOutput:
"""Record derived state for `when()` and `choose()` examples."""
return MetricsOutput(message=input.text, length=len(input.text))
@node
def enthusiastic(input: MetricsOutput) -> MessageOutput:
"""Target used when text is long enough to be considered excited."""
return MessageOutput(message="enthusiastic")
@node
def calm(input: MetricsOutput) -> MessageOutput:
"""Target used when text is not long enough for the positive branch."""
return MessageOutput(message="calm")
@node
def long_message(input: MetricsOutput) -> MessageOutput:
"""Target for the first true `choose()` clause."""
return MessageOutput(message="long")
@node
def medium_message(input: MetricsOutput) -> MessageOutput:
"""Target for a later `choose()` clause."""
return MessageOutput(message="medium")
@node
def short_message(input: MetricsOutput) -> MessageOutput:
"""Default target for the ordered predicate chain."""
return MessageOutput(message="short")
def _graph(name: str) -> WorkflowBuilder:
"""Create the shared example graph shell."""
return WorkflowBuilder(
name=name,
input_schema=TextInput,
state_schema=ExampleState,
output_schema=MessageOutput,
)
def _message_use(
graph: WorkflowBuilder,
spec: NodeSpec[Any, MessageOutput],
*,
id: str,
):
"""Use a message node with explicit state mappings for readability."""
return graph.use(
spec,
id=id,
input=[input_from(state_path("message"), "message")],
output=[output_to("message", state_path("message"))],
)
def _status_use(
graph: WorkflowBuilder,
spec: NodeSpec[Any, MessageOutput],
*,
id: str,
):
"""Use a status target with explicit state mappings for readability."""
return graph.use(
spec,
id=id,
input=[input_from(state_path("status"), "status")],
output=[output_to("message", state_path("message"))],
)
def _metrics_use(
graph: WorkflowBuilder,
spec: NodeSpec[Any, MessageOutput],
*,
id: str,
):
"""Use a metrics target with explicit state mappings for readability."""
return graph.use(
spec,
id=id,
input=[
input_from(state_path("message"), "message"),
input_from(state_path("length"), "length"),
],
output=[output_to("message", state_path("message"))],
)
def build_branch_workflow() -> WorkflowBuilder:
"""Build a workflow that demonstrates outcome routing with `branch()`."""
graph = _graph("branch_example")
router = graph.use(
classify_message,
id="classify",
input=[input_from(input_path("text"), "text")],
output=[output_to("message", state_path("message"))],
)
graph.branch(
router,
{
"send": _message_use(graph, mark_sent, id="sent"),
"skip": _message_use(graph, mark_skipped, id="skipped"),
"error": _message_use(graph, fail_safely, id="failed"),
},
)
graph.connect("sent", "ok", END)
graph.connect("skipped", "ok", END)
graph.connect("failed", "ok", END)
graph.set_entry_point(router)
return graph
def build_handle_workflow() -> WorkflowBuilder:
"""Build a workflow that demonstrates shared error handling."""
graph = _graph("handle_example")
lookup = graph.use(
lookup_message,
id="lookup",
input=[input_from(input_path("text"), "text")],
output=[output_to("message", state_path("message"))],
)
deliver = _message_use(graph, deliver_message, id="deliver")
failed = _message_use(graph, fail_safely, id="failed")
graph.connect(lookup, "ok", deliver)
graph.connect(deliver, "ok", END)
graph.handle((lookup, "error"), (deliver, "error"), to=failed)
graph.connect(failed, "ok", END)
graph.set_entry_point(lookup)
return graph
def build_match_workflow() -> WorkflowBuilder:
"""Build a workflow that demonstrates equality dispatch with `match()`."""
graph = _graph("match_example")
classifier = graph.use(
classify_status,
id="classify_status",
input=[input_from(input_path("text"), "text")],
output=[output_to("status", state_path("status"))],
)
decision = graph.match(
state("status"),
{
"approved": _status_use(graph, approved, id="approved"),
"rejected": _status_use(graph, rejected, id="rejected"),
},
default=_status_use(graph, pending, id="pending"),
id="status",
)
graph.connect(classifier, "ok", decision.entry)
graph.connect("approved", "ok", END)
graph.connect("rejected", "ok", END)
graph.connect("pending", "ok", END)
graph.set_entry_point(classifier)
return graph
def build_when_workflow() -> WorkflowBuilder:
"""Build a workflow that demonstrates one boolean condition with `when()`."""
graph = _graph("when_example")
measure = graph.use(
measure_text,
id="measure",
input=[input_from(input_path("text"), "text")],
output=[
output_to("message", state_path("message")),
output_to("length", state_path("length")),
],
)
decision = graph.when(
state("length").ge(6),
then=_metrics_use(graph, enthusiastic, id="enthusiastic"),
otherwise=_metrics_use(graph, calm, id="calm"),
id="long_enough",
)
graph.connect(measure, "ok", decision.entry)
graph.connect("enthusiastic", "ok", END)
graph.connect("calm", "ok", END)
graph.set_entry_point(measure)
return graph
def build_choose_workflow() -> WorkflowBuilder:
"""Build a workflow that demonstrates ordered predicates with `choose()`."""
graph = _graph("choose_example")
measure = graph.use(
measure_text,
id="measure",
input=[input_from(input_path("text"), "text")],
output=[
output_to("message", state_path("message")),
output_to("length", state_path("length")),
],
)
decision = graph.choose(
(state("length").ge(20), _metrics_use(graph, long_message, id="long")),
(state("length").ge(8), _metrics_use(graph, medium_message, id="medium")),
default=_metrics_use(graph, short_message, id="short"),
id="message_size",
)
graph.connect(measure, "ok", decision.entry)
graph.connect("long", "ok", END)
graph.connect("medium", "ok", END)
graph.connect("short", "ok", END)
graph.set_entry_point(measure)
return graph
def build_use_ref_workflow() -> WorkflowBuilder:
"""Compile a graph that references an externally resolved capability."""
graph = _graph("use_ref_example")
echo = graph.use_ref(
"demo.echo",
id="echo",
input=[input_from(input_path("text"), "message")],
output=[output_to("echoed", state_path("message"))],
)
graph.connect(echo, "ok", END)
graph.set_entry_point(echo)
return graph
def main() -> None:
"""Run a few examples directly from the command line."""
for build, payload in (
(build_branch_workflow, {"text": "send this"}),
(build_match_workflow, {"text": "approve"}),
(build_choose_workflow, {"text": "this is a very long message"}),
):
graph = build()
run = graph.execute(payload)
print(graph.name, run.status.value, run.output)
if __name__ == "__main__":
main()