authoring exmaples

This commit is contained in:
lda
2026-05-19 03:07:37 +07:00 Verified
parent c7e87bc3c4
commit e5f8dd1c31
5 changed files with 669 additions and 5 deletions
+7 -3
View File
@@ -20,6 +20,8 @@ This repository has three main packages plus examples and tests.
- `wf_authoring`: public authoring facade.
- `wf_authoring.WorkflowBuilder`: graph construction.
- `wf_authoring.node`: typed Python function to `NodeSpec`.
- [`docs/wf_authoring_control_flow.md`](wf_authoring_control_flow.md): when to
use `branch`, `handle`, `match`, `when`, and `choose`.
- `wf_mcp`: public MCP facade.
- `wf-mcp`: CLI script from `pyproject.toml`.
- `wf_mcp.broker.WfMcpService.get_catalog()`: backend MCP catalog snapshots.
@@ -28,9 +30,11 @@ This repository has three main packages plus examples and tests.
## Examples
`examples/demo_workflow.py` contains the declared demo workflow and demo node
registry used by `main.py` and workflow tests. It is intentionally outside
`wf_core` so the kernel package does not carry fixture/demo code.
- `examples/demo_workflow.py` contains the declared demo workflow and demo node
registry used by `main.py` and workflow tests. It is intentionally outside
`wf_core` so the kernel package does not carry fixture/demo code.
- `examples/authoring_control_flow.py` demonstrates `WorkflowBuilder.branch`,
`handle`, `match`, `when`, `choose`, and `use_ref` with executable examples.
## Tests
@@ -21,7 +21,7 @@ Each public control-flow method should name one decision mechanism.
| `match` | one graph value | compare that value against equality cases |
| `when` | one boolean condition | route through `true` / `false` |
| `choose` | ordered boolean conditions | first true condition wins |
| future `handle` | several source/outcome pairs | send shared outcomes to one target |
| `handle` | several source/outcome pairs | send shared outcomes to one target |
Future fluent builders or operator sugar must call these methods rather than
constructing edges/conditions independently.
@@ -174,13 +174,36 @@ Recommended behavior during compatibility:
The public docs should prefer only:
- `branch`
- `handle`
- `match`
- `when`
- `choose`
## Shared Outcome Handlers
`handle()` is the reverse-shaped companion to `branch()`: it connects several
source/outcome pairs to one shared target.
```python
g.handle(
(lookup_user, "error"),
(charge_card, "error"),
to=fail,
)
```
Meaning:
```text
lookup_user.error -> fail
charge_card.error -> fail
```
It does not create a join, wait for multiple branches, or inspect state. It is
just outcome-edge sugar for the common "several things fail the same way" case.
## Not In This Pass
- reverse-branch/shared handlers (`handle`)
- fluent/cursor builder APIs
- operator overloading
- graph-as-node/subgraph support
+213
View File
@@ -0,0 +1,213 @@
# `wf_authoring` Control Flow
Use this document when choosing how to wire branches with
`WorkflowBuilder`.
The authoring API intentionally separates different control-flow ideas instead
of putting them all behind one overloaded method.
| Method | Use when | Creates condition nodes? |
| --- | --- | --- |
| `branch` | an existing step already returned an outcome label | no |
| `handle` | several source/outcome pairs should go to one target | no |
| `match` | one state/input/context value should equal one of several values | yes |
| `when` | one boolean expression chooses between two targets | yes |
| `choose` | ordered boolean expressions choose the first matching target | yes |
`route()` still exists only as deprecated compatibility sugar. New code should
use `match()` or `when()` directly.
## `branch`: Route Node Outcomes
Use `branch()` when a node already decides its own outcome.
```python
router = g.use(classify_message)
send = g.use(send_email)
skip = g.use(skip_email)
fail = g.use(runtime_error)
branches = g.branch(
router,
{
"send": send,
"skip": skip,
"error": fail,
},
)
```
This only adds edges:
```text
classify_message.send -> send_email
classify_message.skip -> skip_email
classify_message.error -> runtime_error
```
The return value is a `BranchResult`. It exposes the resolved source and lets
tests or later code retrieve targets by outcome:
```python
assert branches.source is router
assert branches["send"] is send
```
## `handle`: Shared Outcome Target
Use `handle()` when several steps should route the same kind of outcome to one
target.
```python
fail = g.use(runtime_error)
errors = g.handle(
(lookup_user, "error"),
(charge_card, "error"),
(send_receipt, "error"),
to=fail,
)
```
This is not a join and it does not wait for multiple branches. It only writes
edges:
```text
lookup_user.error -> fail
charge_card.error -> fail
send_receipt.error -> fail
```
The return value is a `HandleResult` with the shared target and the resolved
source/outcome pairs.
## `match`: Equality Dispatch
Use `match()` when one graph value chooses a target by equality.
```python
decision = g.match(
state("status"),
{
"approved": approve,
"rejected": reject,
"pending": wait,
},
default=fail,
)
```
This lowers to an ordered chain of generated condition nodes:
```text
if state.status == "approved": approve
elif state.status == "rejected": reject
elif state.status == "pending": wait
else: fail
```
Condition ids are source-derived by default, such as `state_status`,
`state_status_2`, and so on. Pass `id="status_choice"` when stable generated
ids matter.
The return value is a `DecisionResult`:
```python
g.set_entry_point(decision.entry)
assert decision["approved"] is approve
assert decision["default"] is fail
```
## `when`: Boolean Dispatch
Use `when()` when one boolean expression chooses between two targets.
```python
decision = g.when(
state("retry_count").lt(3),
then=retry,
otherwise=fail,
)
```
This lowers to one condition node with `true` and `false` edges.
The return value is also a `DecisionResult`:
```python
assert decision[True] is retry
assert decision[False] is fail
```
## `choose`: Ordered Predicate Chain
Use `choose()` when the graph should try several boolean expressions in order
and route to the first true target.
```python
decision = g.choose(
(state("score").ge(90), gold),
(state("score").ge(70), silver),
(state("score").ge(50), bronze),
default=fail,
id="score_tier",
)
```
This lowers to:
```text
if state.score >= 90: gold
elif state.score >= 70: silver
elif state.score >= 50: bronze
else: fail
```
`choose()` is still one explicit call. Fluent or operator-heavy syntax can be
built on top later, but should delegate to this API rather than rebuilding edge
logic itself.
## Defaults
`match()`, `when()`, and `choose()` default their fallback path to the standard
`runtime_error` node. This makes missing cases fail loudly instead of silently
ending or continuing with unclear state.
Pass an explicit `default=` or `otherwise=` when the fallback is valid business
logic.
## `NodeSpec` Targets
`connect()`, `branch()`, `handle()`, `match()`, `when()`, and `choose()` accept
either existing step refs or `NodeSpec` objects as targets. Passing a `NodeSpec`
creates a fresh `use()` step with auto-mapping and an auto id.
Use existing step refs when the same node use should be shared. Pass a
`NodeSpec` when you want a new use at that point in the graph.
## Deprecated `route`
`route()` is a compatibility shim:
- `route(state("x"), {"a": step})` forwards to `match(...)`.
- `route(state("x").exists(), {True: step})` forwards to `when(...)`.
It emits a `DeprecationWarning` and should not appear in new examples.
## Drafts
Workflow drafts currently expose only explicit outcome routes:
```json
{
"routes": {
"classify": {
"send": "send_email",
"skip": "__end__"
}
}
}
```
Draft JSON does not yet have `match`, `when`, or `choose` sugar. Add that only
after the Python authoring surface stays stable enough to be mirrored.