Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions docs/examples/00-hello-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# 00 - Hello, world

The smallest possible LLM-routed pipeline: classify a query, then
either plan research on it or summarize it in one sentence. Sets the
shape every other example builds on.

## Overview

You ask a question. A classifier LLM decides whether the question
wants new information or a summary of known material. Depending on
the answer, the run either calls a research-planner node (returns
topics to investigate plus follow-up questions) or a summarizer node
(returns one sentence plus a confidence score).

The demo query is *"why did Apollo 13 abort its lunar landing?"*,
which the model usually routes to `summarize` because the facts are
well-established.

## What it teaches

- A typed [`State`](../concepts/state-and-reducers.md) holding query
plus per-node artifacts, with three reducer policies in one model
(`last_write_wins`, `append`, `merge`).
- The [`OpenAIProvider`](../concepts/llms.md) talking to any
OpenAI-compatible endpoint.
- Both forms of [structured output](../concepts/llms.md): pass a
Pydantic class as `response_schema` (`Classification`, `Summary`)
and get an instance back on `Response.parsed`; pass a JSON Schema
dict (`research`) and get a raw dict.
- `RuntimeConfig` for per-call sampling knobs. Every `complete()`
passes `RuntimeConfig(temperature=0.0)` so the run is as
reproducible as the API allows.
- A [conditional edge](../concepts/graphs.md) reading a parsed field
off state (`route` returns `state.classification.intent`).
- A function-shaped [observer](../concepts/observability.md) attached
after compile.

## How to run

```bash
uv sync --group examples
LLM_API_KEY=sk-... uv run python examples/00-hello-world/main.py
```

To point at a local OpenAI-compatible server, override `LLM_BASE_URL`
and (often) `LLM_MODEL`:

```bash
LLM_BASE_URL=http://localhost:8000 LLM_MODEL=Qwen2.5-7B-Instruct \
LLM_API_KEY= \
uv run python examples/00-hello-world/main.py
```

## The graph

```mermaid
flowchart TD
start([start])
classify[classify]
research[research]
summarize[summarize]
stop([end])

start --> classify
classify -->|"intent == 'research'"| research
classify -->|"intent == 'summarize'"| summarize
research --> stop
summarize --> stop
```

Three nodes, one conditional edge. `classify` is the entry; `route`
inspects `state.classification.intent` and returns the name of the
next node.

## Reading the output

A clean run prints two lines from the observer and then the final
state:

```
classify: sources=[]
summarize: sources=['cache']

classification: intent='summarize' rationale='...'
summary: one_liner='...' confidence=0.92
sources: ['cache']
metadata: {'classified_by': 'llm', 'tool': 'summarize'}
```

- `classify: sources=[]` - the classifier ran, no sources have been
appended yet because only the chosen follow-up node adds them.
- `summarize: sources=['cache']` - the second node ran (since the
classifier picked `summarize`). The `append` reducer on the
`sources` field merged the new entry into the existing list.
- `classification` and `summary` are the parsed Pydantic instances,
not raw model output. Compare with `research_plan`, which would
show as a plain dict if the classifier had picked `research`.
- `metadata: {...}` shows the `merge` reducer in action. Each node
contributed one key (`classified_by`, `tool`); the final map has
both.

If the classifier picks `research` instead, you'll see `research`
in the second observer line and a `research_plan` dict (with
`topics` and `follow_up_questions`) in the final printout.
109 changes: 109 additions & 0 deletions docs/examples/01-routing-and-subgraphs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# 01 - Routing and subgraphs

A question-answering assistant. Classify the question, then either
give a one-shot quick answer or run a multi-step research
sub-pipeline, then lightly copy-edit the result.

## Overview

You ask a question. A classifier LLM decides whether it can be
answered in one or two sentences ("quick") or whether it benefits
from considering multiple angles ("research"). Quick questions go
through a single `quick_answer` node. Research questions descend
into a subgraph that plans three angles, gathers a short note for
each, and synthesizes them into a paragraph. Either way, a final
`format_final` node copy-edits the answer.

Demo questions: *"what year did the moon landing happen"*
(usually routes to quick) and *"why is the lunar south pole
strategically important?"* (usually routes to research).

## What it teaches

- [Conditional edges](../concepts/graphs.md) routing on a state
field. `classify` writes `state.route`; the conditional edge
function reads it and returns the next node's name.
- [Subgraph composition](../concepts/composition.md). The research
pipeline is itself a compiled graph, wrapped as a single node in
the outer graph via `add_subgraph_node`.
- A custom
[`ProjectionStrategy`](../concepts/composition.md). The default
`FieldNameMatching` only carries fields back *out* of a subgraph;
carrying the parent's question *in* requires writing a small
`ProjectionStrategy` class. The `QuestionProjection` here is the
canonical pattern for non-trivial subgraph boundaries.
- The [`merge` reducer](../concepts/state-and-reducers.md) for dict
accumulation. Every node returns a small `tallies` fragment; the
reducer accumulates them into one dict on the final state.

## How to run

```bash
uv sync --group examples
LLM_API_KEY=sk-... uv run python examples/01-routing-and-subgraphs/main.py \
"why is the lunar south pole strategically important?"
```

The first positional arg becomes the question. With no arg, it falls
back to the lunar-south-pole question above.

## The graph

```mermaid
flowchart TD
start([start])
classify[classify]
quick_answer[quick_answer]
format_final[format_final]
stop([end])

subgraph research [research subgraph]
direction TB
plan_research[plan_research]
gather[gather]
synthesize[synthesize]
plan_research --> gather --> synthesize
end

start --> classify
classify -->|"route == 'quick'"| quick_answer
classify -->|"route == 'research'"| research
quick_answer --> format_final
research --> format_final
format_final --> stop
```

The research box is a separate compiled graph with its own state
schema (`ResearchState`). The `QuestionProjection` carries
`parent.question` in as `subgraph.question`, and brings
`subgraph.answer` plus `subgraph.trace` back out.

## Reading the output

For a research-route run, expect:

```
question: why is the lunar south pole strategically important?
route: research

answer:
<paragraph synthesized from three angles>

trace: ['classify', 'plan_research', 'gather', 'synthesize', 'format_final']
tallies: {'classify_calls': 1, 'research_runs': 1, 'formatted': 1}
```

- `route` is the field `classify` wrote that the conditional edge
read.
- `trace` lists nodes in invocation order. Subgraph nodes appear
inline; that's the projection's `trace` field flowing back out
through the parent's `append` reducer.
- `tallies` has one entry per node that contributed: `classify` set
`classify_calls`, the subgraph projection's `project_out` set
`research_runs`, `format_final` set `formatted`. `quick_answer`
would have contributed `quick_answers: 1` if the run had gone the
other way.

For a quick-route run, `trace` drops to `['classify',
'quick_answer', 'format_final']` and `tallies` has
`quick_answers: 1` in place of `research_runs: 1`.
103 changes: 103 additions & 0 deletions docs/examples/02-explicit-subgraph-mapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 02 - Explicit subgraph mapping

Compare two topics by running the *same* compiled analysis subgraph
on each, with each call site writing into disjoint parent fields.
This is the canonical use of `ExplicitMapping`.

## Overview

You give the pipeline two topics ("Apollo 11" vs "Apollo 17", or
"Apollo program vs Artemis program"). One compiled subgraph
(`summarize → score`) is registered twice in the outer graph. The
first registration analyzes topic A and writes its results into
`a_summary` / `a_score`; the second analyzes topic B and writes
into `b_summary` / `b_score`. A final `synthesize` node reads both
sides and renders a verdict.

Without explicit mapping the two sites would both write to a single
`parent.summary` field under default name matching, and the second
call would clobber the first.

## What it teaches

- [`ExplicitMapping`](../concepts/composition.md) for reusing one
compiled subgraph at multiple parent sites with disjoint parent
fields. Each site declares its own `inputs` and `outputs` dicts;
the same compiled subgraph value is registered twice.
- The encapsulation property that makes this work: the subgraph
speaks in neutral field names (`topic`, `summary`, `score`) and
has no idea which side of the comparison it's running for. The
mapping at each call site is what wires the subgraph's neutral
names to the parent's per-side fields.
- The contrast with example 01: there a custom
`ProjectionStrategy` carried one field in. Here the two sites
need to be similar-but-different, and `ExplicitMapping` is the
zero-boilerplate way to express that.

## How to run

```bash
uv sync --group examples
LLM_API_KEY=sk-... uv run python examples/02-explicit-subgraph-mapping/main.py "Apollo 11" "Apollo 17"
```

Or pass a single `"X vs Y"` arg, or no args (defaults to
`"Apollo 11"` vs `"Apollo 17"`).

## The graph

```mermaid
flowchart TD
start([start])
synthesize[synthesize]
stop([end])

subgraph analysis_a [analyze_a: analysis subgraph]
direction TB
sa[summarize]
ra[score]
sa --> ra
end

subgraph analysis_b [analyze_b: analysis subgraph]
direction TB
sb[summarize]
rb[score]
sb --> rb
end

start --> analysis_a --> analysis_b --> synthesize --> stop
```

Both subgraph boxes are the *same* compiled value, registered twice
under different names with different mappings. The `analyze_a` site
maps `parent.topic_a → subgraph.topic` and back out as `summary →
a_summary`, `score → a_score`. The `analyze_b` site does the same
thing on the B-side parent fields.

## Reading the output

```
topic A: Apollo 11
summary: <one-sentence summary of Apollo 11>
score: 8/10

topic B: Apollo 17
summary: <one-sentence summary of Apollo 17>
score: 7/10

verdict:
<paragraph picking a winner or calling it a tie, citing the summaries>

trace: ['summarize', 'score', 'summarize', 'score', 'synthesize']
```

- The per-side summary and score fields are populated by separate
invocations of the same subgraph, routed by the mappings.
- `trace` shows the subgraph's nodes running **twice**, interleaved
with the outer `synthesize`. Both invocations contribute to the
same parent `trace` list because each `outputs` mapping includes
`"trace": "trace"`.
- `verdict` is whatever `synthesize` produced from reading both
sides. The outer node knows nothing about which side ran first;
it just reads four parent fields.
Loading