Skip to content

Commit b5c5cb1

Browse files
docs(examples): add per-example pages with mermaid diagrams
New top-level Examples section in the docs nav, sitting between Concepts and Model Providers. One page per example (00 through 09) plus an index, each following the same structure: overview / what it teaches / how to run / mermaid graph / reading the output. Mermaid is wired into pymdownx.superfences so the per-example graph shapes render inline. Cross-links to the relevant concepts pages appear in each "What it teaches" section. extra.css gets a small left-pad on nested primary-nav links so the section hierarchy reads at a glance now that the Examples section runs 11 entries deep. The llms.txt / llms-full.txt plugin config picks up the new Examples section plus Model Providers (which had been omitted since the plugin was first added) so AI-assistant ingestion covers the full public docs surface.
1 parent 7a7ef04 commit b5c5cb1

13 files changed

Lines changed: 1384 additions & 1 deletion

docs/examples/00-hello-world.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# 00 - Hello, world
2+
3+
The smallest possible LLM-routed pipeline: classify a query, then
4+
either plan research on it or summarize it in one sentence. Sets the
5+
shape every other example builds on.
6+
7+
## Overview
8+
9+
You ask a question. A classifier LLM decides whether the question
10+
wants new information or a summary of known material. Depending on
11+
the answer, the run either calls a research-planner node (returns
12+
topics to investigate plus follow-up questions) or a summarizer node
13+
(returns one sentence plus a confidence score).
14+
15+
The demo query is *"why did Apollo 13 abort its lunar landing?"*,
16+
which the model usually routes to `summarize` because the facts are
17+
well-established.
18+
19+
## What it teaches
20+
21+
- A typed [`State`](../concepts/state-and-reducers.md) holding query
22+
plus per-node artifacts, with three reducer policies in one model
23+
(`last_write_wins`, `append`, `merge`).
24+
- The [`OpenAIProvider`](../concepts/llms.md) talking to any
25+
OpenAI-compatible endpoint.
26+
- Both forms of [structured output](../concepts/llms.md): pass a
27+
Pydantic class as `response_schema` (`Classification`, `Summary`)
28+
and get an instance back on `Response.parsed`; pass a JSON Schema
29+
dict (`research`) and get a raw dict.
30+
- `RuntimeConfig` for per-call sampling knobs. Every `complete()`
31+
passes `RuntimeConfig(temperature=0.0)` so the run is as
32+
reproducible as the API allows.
33+
- A [conditional edge](../concepts/graphs.md) reading a parsed field
34+
off state (`route` returns `state.classification.intent`).
35+
- A function-shaped [observer](../concepts/observability.md) attached
36+
after compile.
37+
38+
## How to run
39+
40+
```bash
41+
uv sync --group examples
42+
LLM_API_KEY=sk-... uv run python examples/00-hello-world/main.py
43+
```
44+
45+
To point at a local OpenAI-compatible server, override `LLM_BASE_URL`
46+
and (often) `LLM_MODEL`:
47+
48+
```bash
49+
LLM_BASE_URL=http://localhost:8000 LLM_MODEL=Qwen2.5-7B-Instruct \
50+
LLM_API_KEY= \
51+
uv run python examples/00-hello-world/main.py
52+
```
53+
54+
## The graph
55+
56+
```mermaid
57+
flowchart TD
58+
start([start])
59+
classify[classify]
60+
research[research]
61+
summarize[summarize]
62+
stop([end])
63+
64+
start --> classify
65+
classify -->|"intent == 'research'"| research
66+
classify -->|"intent == 'summarize'"| summarize
67+
research --> stop
68+
summarize --> stop
69+
```
70+
71+
Three nodes, one conditional edge. `classify` is the entry; `route`
72+
inspects `state.classification.intent` and returns the name of the
73+
next node.
74+
75+
## Reading the output
76+
77+
A clean run prints two lines from the observer and then the final
78+
state:
79+
80+
```
81+
classify: sources=[]
82+
summarize: sources=['cache']
83+
84+
classification: intent='summarize' rationale='...'
85+
summary: one_liner='...' confidence=0.92
86+
sources: ['cache']
87+
metadata: {'classified_by': 'llm', 'tool': 'summarize'}
88+
```
89+
90+
- `classify: sources=[]` - the classifier ran, no sources have been
91+
appended yet because only the chosen follow-up node adds them.
92+
- `summarize: sources=['cache']` - the second node ran (since the
93+
classifier picked `summarize`). The `append` reducer on the
94+
`sources` field merged the new entry into the existing list.
95+
- `classification` and `summary` are the parsed Pydantic instances,
96+
not raw model output. Compare with `research_plan`, which would
97+
show as a plain dict if the classifier had picked `research`.
98+
- `metadata: {...}` shows the `merge` reducer in action. Each node
99+
contributed one key (`classified_by`, `tool`); the final map has
100+
both.
101+
102+
If the classifier picks `research` instead, you'll see `research`
103+
in the second observer line and a `research_plan` dict (with
104+
`topics` and `follow_up_questions`) in the final printout.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# 01 - Routing and subgraphs
2+
3+
A question-answering assistant. Classify the question, then either
4+
give a one-shot quick answer or run a multi-step research
5+
sub-pipeline, then lightly copy-edit the result.
6+
7+
## Overview
8+
9+
You ask a question. A classifier LLM decides whether it can be
10+
answered in one or two sentences ("quick") or whether it benefits
11+
from considering multiple angles ("research"). Quick questions go
12+
through a single `quick_answer` node. Research questions descend
13+
into a subgraph that plans three angles, gathers a short note for
14+
each, and synthesizes them into a paragraph. Either way, a final
15+
`format_final` node copy-edits the answer.
16+
17+
Demo questions: *"what year did the moon landing happen"*
18+
(usually routes to quick) and *"why is the lunar south pole
19+
strategically important?"* (usually routes to research).
20+
21+
## What it teaches
22+
23+
- [Conditional edges](../concepts/graphs.md) routing on a state
24+
field. `classify` writes `state.route`; the conditional edge
25+
function reads it and returns the next node's name.
26+
- [Subgraph composition](../concepts/composition.md). The research
27+
pipeline is itself a compiled graph, wrapped as a single node in
28+
the outer graph via `add_subgraph_node`.
29+
- A custom
30+
[`ProjectionStrategy`](../concepts/composition.md). The default
31+
`FieldNameMatching` only carries fields back *out* of a subgraph;
32+
carrying the parent's question *in* requires writing a small
33+
`ProjectionStrategy` class. The `QuestionProjection` here is the
34+
canonical pattern for non-trivial subgraph boundaries.
35+
- The [`merge` reducer](../concepts/state-and-reducers.md) for dict
36+
accumulation. Every node returns a small `tallies` fragment; the
37+
reducer accumulates them into one dict on the final state.
38+
39+
## How to run
40+
41+
```bash
42+
uv sync --group examples
43+
LLM_API_KEY=sk-... uv run python examples/01-routing-and-subgraphs/main.py \
44+
"why is the lunar south pole strategically important?"
45+
```
46+
47+
The first positional arg becomes the question. With no arg, it falls
48+
back to the lunar-south-pole question above.
49+
50+
## The graph
51+
52+
```mermaid
53+
flowchart TD
54+
start([start])
55+
classify[classify]
56+
quick_answer[quick_answer]
57+
format_final[format_final]
58+
stop([end])
59+
60+
subgraph research [research subgraph]
61+
direction TB
62+
plan_research[plan_research]
63+
gather[gather]
64+
synthesize[synthesize]
65+
plan_research --> gather --> synthesize
66+
end
67+
68+
start --> classify
69+
classify -->|"route == 'quick'"| quick_answer
70+
classify -->|"route == 'research'"| research
71+
quick_answer --> format_final
72+
research --> format_final
73+
format_final --> stop
74+
```
75+
76+
The research box is a separate compiled graph with its own state
77+
schema (`ResearchState`). The `QuestionProjection` carries
78+
`parent.question` in as `subgraph.question`, and brings
79+
`subgraph.answer` plus `subgraph.trace` back out.
80+
81+
## Reading the output
82+
83+
For a research-route run, expect:
84+
85+
```
86+
question: why is the lunar south pole strategically important?
87+
route: research
88+
89+
answer:
90+
<paragraph synthesized from three angles>
91+
92+
trace: ['classify', 'plan_research', 'gather', 'synthesize', 'format_final']
93+
tallies: {'classify_calls': 1, 'research_runs': 1, 'formatted': 1}
94+
```
95+
96+
- `route` is the field `classify` wrote that the conditional edge
97+
read.
98+
- `trace` lists nodes in invocation order. Subgraph nodes appear
99+
inline; that's the projection's `trace` field flowing back out
100+
through the parent's `append` reducer.
101+
- `tallies` has one entry per node that contributed: `classify` set
102+
`classify_calls`, the subgraph projection's `project_out` set
103+
`research_runs`, `format_final` set `formatted`. `quick_answer`
104+
would have contributed `quick_answers: 1` if the run had gone the
105+
other way.
106+
107+
For a quick-route run, `trace` drops to `['classify',
108+
'quick_answer', 'format_final']` and `tallies` has
109+
`quick_answers: 1` in place of `research_runs: 1`.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# 02 - Explicit subgraph mapping
2+
3+
Compare two topics by running the *same* compiled analysis subgraph
4+
on each, with each call site writing into disjoint parent fields.
5+
This is the canonical use of `ExplicitMapping`.
6+
7+
## Overview
8+
9+
You give the pipeline two topics ("Apollo 11" vs "Apollo 17", or
10+
"Apollo program vs Artemis program"). One compiled subgraph
11+
(`summarize → score`) is registered twice in the outer graph. The
12+
first registration analyzes topic A and writes its results into
13+
`a_summary` / `a_score`; the second analyzes topic B and writes
14+
into `b_summary` / `b_score`. A final `synthesize` node reads both
15+
sides and renders a verdict.
16+
17+
Without explicit mapping the two sites would both write to a single
18+
`parent.summary` field under default name matching, and the second
19+
call would clobber the first.
20+
21+
## What it teaches
22+
23+
- [`ExplicitMapping`](../concepts/composition.md) for reusing one
24+
compiled subgraph at multiple parent sites with disjoint parent
25+
fields. Each site declares its own `inputs` and `outputs` dicts;
26+
the same compiled subgraph value is registered twice.
27+
- The encapsulation property that makes this work: the subgraph
28+
speaks in neutral field names (`topic`, `summary`, `score`) and
29+
has no idea which side of the comparison it's running for. The
30+
mapping at each call site is what wires the subgraph's neutral
31+
names to the parent's per-side fields.
32+
- The contrast with example 01: there a custom
33+
`ProjectionStrategy` carried one field in. Here the two sites
34+
need to be similar-but-different, and `ExplicitMapping` is the
35+
zero-boilerplate way to express that.
36+
37+
## How to run
38+
39+
```bash
40+
uv sync --group examples
41+
LLM_API_KEY=sk-... uv run python examples/02-explicit-subgraph-mapping/main.py "Apollo 11" "Apollo 17"
42+
```
43+
44+
Or pass a single `"X vs Y"` arg, or no args (defaults to
45+
`"Apollo 11"` vs `"Apollo 17"`).
46+
47+
## The graph
48+
49+
```mermaid
50+
flowchart TD
51+
start([start])
52+
synthesize[synthesize]
53+
stop([end])
54+
55+
subgraph analysis_a [analyze_a: analysis subgraph]
56+
direction TB
57+
sa[summarize]
58+
ra[score]
59+
sa --> ra
60+
end
61+
62+
subgraph analysis_b [analyze_b: analysis subgraph]
63+
direction TB
64+
sb[summarize]
65+
rb[score]
66+
sb --> rb
67+
end
68+
69+
start --> analysis_a --> analysis_b --> synthesize --> stop
70+
```
71+
72+
Both subgraph boxes are the *same* compiled value, registered twice
73+
under different names with different mappings. The `analyze_a` site
74+
maps `parent.topic_a → subgraph.topic` and back out as `summary →
75+
a_summary`, `score → a_score`. The `analyze_b` site does the same
76+
thing on the B-side parent fields.
77+
78+
## Reading the output
79+
80+
```
81+
topic A: Apollo 11
82+
summary: <one-sentence summary of Apollo 11>
83+
score: 8/10
84+
85+
topic B: Apollo 17
86+
summary: <one-sentence summary of Apollo 17>
87+
score: 7/10
88+
89+
verdict:
90+
<paragraph picking a winner or calling it a tie, citing the summaries>
91+
92+
trace: ['summarize', 'score', 'summarize', 'score', 'synthesize']
93+
```
94+
95+
- The per-side summary and score fields are populated by separate
96+
invocations of the same subgraph, routed by the mappings.
97+
- `trace` shows the subgraph's nodes running **twice**, interleaved
98+
with the outer `synthesize`. Both invocations contribute to the
99+
same parent `trace` list because each `outputs` mapping includes
100+
`"trace": "trace"`.
101+
- `verdict` is whatever `synthesize` produced from reading both
102+
sides. The outer node knows nothing about which side ran first;
103+
it just reads four parent fields.

0 commit comments

Comments
 (0)