Skip to content

Commit edd8880

Browse files
committed
WIP
1 parent e2b4db2 commit edd8880

23 files changed

+275
-235
lines changed

docs/builtin-tools.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ agent = Agent(
8787
)
8888

8989
result = agent.run_sync('Use the web to get the current time.')
90-
# > In San Francisco, it's 8:21:41 pm PDT on Wednesday, August 6, 2025.
90+
#> In San Francisco, it's 8:21:41 pm PDT on Wednesday, August 6, 2025.
9191
```
9292

9393
### Parameter Support by Provider
@@ -129,7 +129,7 @@ from pydantic_ai import Agent, CodeExecutionTool
129129
agent = Agent('anthropic:claude-sonnet-4-0', builtin_tools=[CodeExecutionTool()])
130130

131131
result = agent.run_sync('Calculate the factorial of 15 and show your work')
132-
# > The factorial of 15 is **1,307,674,368,000**.
132+
#> The factorial of 15 is **1,307,674,368,000**.
133133
```
134134

135135
## URL Context Tool
@@ -158,7 +158,7 @@ from pydantic_ai import Agent, UrlContextTool
158158
agent = Agent('google-gla:gemini-2.5-flash', builtin_tools=[UrlContextTool()])
159159

160160
result = agent.run_sync('What is this? https://ai.pydantic.dev')
161-
# > A Python agent framework for building Generative AI applications.
161+
#> A Python agent framework for building Generative AI applications.
162162
```
163163

164164
## Memory Tool

docs/graph/beta/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Here's an example showcasing parallel execution with a map operation:
123123
```python {title="parallel_processing.py"}
124124
from dataclasses import dataclass
125125

126-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
126+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
127127

128128

129129
@dataclass
@@ -147,7 +147,7 @@ async def main():
147147
return ctx.inputs * ctx.inputs
148148

149149
# Create a join to collect results
150-
collect_results = g.join(ListReducer[int])
150+
collect_results = g.join(ListAppendReducer[int])
151151

152152
# Build the graph with map operation
153153
g.add(
@@ -172,7 +172,7 @@ In this example:
172172

173173
1. The start node receives a list of integers
174174
2. The `.map()` operation fans out each item to a separate parallel execution of the `square` step
175-
3. All results are collected back together using a [`ListReducer`][pydantic_graph.beta.join.ListReducer]
175+
3. All results are collected back together using a [`ListAppendReducer`][pydantic_graph.beta.join.ListAppendReducer]
176176
4. The joined results flow to the end node
177177

178178
## Next Steps

docs/graph/beta/joins.md

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Create a join using [`g.join()`][pydantic_graph.beta.graph_builder.GraphBuilder.
1717
```python {title="basic_join.py"}
1818
from dataclasses import dataclass
1919

20-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
20+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
2121

2222

2323
@dataclass
@@ -36,7 +36,7 @@ async def square(ctx: StepContext[SimpleState, None, int]) -> int:
3636
return ctx.inputs * ctx.inputs
3737

3838
# Create a join to collect all squared values
39-
collect = g.join(ListReducer[int])
39+
collect = g.join(ListAppendReducer[int])
4040

4141
g.add(
4242
g.edge_from(g.start_node).to(generate_numbers),
@@ -59,14 +59,14 @@ _(This example is complete, it can be run "as is" — you'll need to add `import
5959

6060
Pydantic Graph provides several common reducer types out of the box:
6161

62-
### ListReducer
62+
### ListAppendReducer
6363

64-
[`ListReducer`][pydantic_graph.beta.join.ListReducer] collects all inputs into a list:
64+
[`ListAppendReducer`][pydantic_graph.beta.join.ListAppendReducer] collects all inputs into a list:
6565

6666
```python {title="list_reducer.py"}
6767
from dataclasses import dataclass
6868

69-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
69+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
7070

7171

7272
@dataclass
@@ -85,7 +85,7 @@ async def main():
8585
async def to_string(ctx: StepContext[SimpleState, None, int]) -> str:
8686
return f'value-{ctx.inputs}'
8787

88-
collect = g.join(ListReducer[str])
88+
collect = g.join(ListAppendReducer[str])
8989

9090
g.add(
9191
g.edge_from(g.start_node).to(generate),
@@ -109,7 +109,7 @@ _(This example is complete, it can be run "as is" — you'll need to add `import
109109
```python {title="dict_reducer.py"}
110110
from dataclasses import dataclass
111111

112-
from pydantic_graph.beta import DictReducer, GraphBuilder, StepContext
112+
from pydantic_graph.beta import DictUpdateReducer, GraphBuilder, StepContext
113113

114114

115115
@dataclass
@@ -128,7 +128,7 @@ async def main():
128128
async def create_entry(ctx: StepContext[SimpleState, None, str]) -> dict[str, int]:
129129
return {ctx.inputs: len(ctx.inputs)}
130130

131-
merge = g.join(DictReducer[str, int])
131+
merge = g.join(DictUpdateReducer[str, int])
132132

133133
g.add(
134134
g.edge_from(g.start_node).to(generate_keys),
@@ -339,7 +339,7 @@ A graph can have multiple independent joins:
339339
```python {title="multiple_joins.py"}
340340
from dataclasses import dataclass, field
341341

342-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
342+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
343343

344344

345345
@dataclass
@@ -366,8 +366,8 @@ async def main():
366366
async def process_b(ctx: StepContext[MultiState, None, int]) -> int:
367367
return ctx.inputs * 3
368368

369-
join_a = g.join(ListReducer[int], node_id='join_a')
370-
join_b = g.join(ListReducer[int], node_id='join_b')
369+
join_a = g.join(ListAppendReducer[int], node_id='join_a')
370+
join_b = g.join(ListAppendReducer[int], node_id='join_b')
371371

372372
@g.step
373373
async def store_a(ctx: StepContext[MultiState, None, list[int]]) -> None:
@@ -412,9 +412,11 @@ _(This example is complete, it can be run "as is" — you'll need to add `import
412412
Like steps, joins can have custom IDs:
413413

414414
```python {title="join_custom_id.py" requires="basic_join.py"}
415-
from basic_join import ListReducer, g
415+
from pydantic_graph.beta import ListAppendReducer
416416

417-
my_join = g.join(ListReducer[int], node_id='my_custom_join_id')
417+
from basic_join import g
418+
419+
my_join = g.join(ListAppendReducer[int], node_id='my_custom_join_id')
418420
```
419421

420422
## How Joins Work

docs/graph/beta/parallel.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Broadcasting sends identical data to multiple destinations simultaneously:
1616
```python {title="basic_broadcast.py"}
1717
from dataclasses import dataclass
1818

19-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
19+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
2020

2121

2222
@dataclass
@@ -43,7 +43,7 @@ async def main():
4343
async def add_three(ctx: StepContext[SimpleState, None, int]) -> int:
4444
return ctx.inputs + 3
4545

46-
collect = g.join(ListReducer[int])
46+
collect = g.join(ListAppendReducer[int])
4747

4848
# Broadcasting: send the value from source to all three steps
4949
g.add(
@@ -70,7 +70,7 @@ Spreading fans out elements from an iterable, processing each element in paralle
7070
```python {title="basic_map.py"}
7171
from dataclasses import dataclass
7272

73-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
73+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
7474

7575

7676
@dataclass
@@ -89,7 +89,7 @@ async def main():
8989
async def square(ctx: StepContext[SimpleState, None, int]) -> int:
9090
return ctx.inputs * ctx.inputs
9191

92-
collect = g.join(ListReducer[int])
92+
collect = g.join(ListAppendReducer[int])
9393

9494
# Spreading: each item in the list gets its own parallel execution
9595
g.add(
@@ -114,7 +114,7 @@ The convenience method [`add_mapping_edge()`][pydantic_graph.beta.graph_builder.
114114
```python {title="mapping_convenience.py"}
115115
from dataclasses import dataclass
116116

117-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext, Reducer
117+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
118118

119119

120120
@dataclass
@@ -133,7 +133,7 @@ async def main():
133133
async def stringify(ctx: StepContext[SimpleState, None, int]) -> str:
134134
return f'Value: {ctx.inputs}'
135135

136-
collect = g.join(ListReducer[str])
136+
collect = g.join(ListAppendReducer[str])
137137

138138
g.add(g.edge_from(g.start_node).to(generate_numbers))
139139
g.add_mapping_edge(generate_numbers, stringify)
@@ -157,7 +157,7 @@ When mapping an empty iterable, you can specify a `downstream_join_id` to ensure
157157
```python {title="empty_map.py"}
158158
from dataclasses import dataclass
159159

160-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
160+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
161161

162162

163163
@dataclass
@@ -176,7 +176,7 @@ async def main():
176176
async def double(ctx: StepContext[SimpleState, None, int]) -> int:
177177
return ctx.inputs * 2
178178

179-
collect = g.join(ListReducer[int])
179+
collect = g.join(ListAppendReducer[int])
180180

181181
g.add(g.edge_from(g.start_node).to(generate_empty))
182182
g.add_mapping_edge(generate_empty, double, downstream_join_id=collect.id)
@@ -202,7 +202,7 @@ You can nest broadcasts and maps for complex parallel patterns:
202202
```python {title="map_then_broadcast.py"}
203203
from dataclasses import dataclass
204204

205-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
205+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
206206

207207

208208
@dataclass
@@ -225,7 +225,7 @@ async def main():
225225
async def add_two(ctx: StepContext[SimpleState, None, int]) -> int:
226226
return ctx.inputs + 2
227227

228-
collect = g.join(ListReducer[int])
228+
collect = g.join(ListAppendReducer[int])
229229

230230
g.add(
231231
g.edge_from(g.start_node).to(generate_list),
@@ -252,7 +252,7 @@ The result contains:
252252
```python {title="sequential_maps.py"}
253253
from dataclasses import dataclass
254254

255-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
255+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
256256

257257

258258
@dataclass
@@ -275,7 +275,7 @@ async def main():
275275
async def stringify(ctx: StepContext[SimpleState, None, int]) -> str:
276276
return f'num:{ctx.inputs}'
277277

278-
collect = g.join(ListReducer[str])
278+
collect = g.join(ListAppendReducer[str])
279279

280280
g.add(
281281
g.edge_from(g.start_node).to(generate_pairs),
@@ -302,7 +302,7 @@ Add labels to parallel edges for better documentation:
302302
```python {title="labeled_parallel.py"}
303303
from dataclasses import dataclass
304304

305-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
305+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
306306

307307

308308
@dataclass
@@ -321,7 +321,7 @@ async def main():
321321
async def process(ctx: StepContext[SimpleState, None, int]) -> str:
322322
return f'item-{ctx.inputs}'
323323

324-
collect = g.join(ListReducer[str])
324+
collect = g.join(ListAppendReducer[str])
325325

326326
g.add(g.edge_from(g.start_node).to(generate))
327327
g.add_mapping_edge(
@@ -350,7 +350,7 @@ All parallel tasks share the same graph state. Be careful with mutations:
350350
```python {title="parallel_state.py"}
351351
from dataclasses import dataclass, field
352352

353-
from pydantic_graph.beta import GraphBuilder, ListReducer, StepContext
353+
from pydantic_graph.beta import GraphBuilder, ListAppendReducer, StepContext
354354

355355

356356
@dataclass
@@ -371,7 +371,7 @@ async def main():
371371
ctx.state.values.append(ctx.inputs)
372372
return ctx.inputs * ctx.inputs
373373

374-
collect = g.join(ListReducer[int])
374+
collect = g.join(ListAppendReducer[int])
375375

376376
g.add(
377377
g.edge_from(g.start_node).to(generate),

docs/input.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ result = agent.run_sync(
2020
]
2121
)
2222
print(result.output)
23-
# > This is the logo for Pydantic, a data validation and settings management library in Python.
23+
#> This is the logo for Pydantic, a data validation and settings management library in Python.
2424
```
2525

2626
If you have the image locally, you can also use [`BinaryContent`][pydantic_ai.BinaryContent]:
@@ -40,7 +40,7 @@ result = agent.run_sync(
4040
]
4141
)
4242
print(result.output)
43-
# > This is the logo for Pydantic, a data validation and settings management library in Python.
43+
#> This is the logo for Pydantic, a data validation and settings management library in Python.
4444
```
4545

4646
1. To ensure the example is runnable we download this image from the web, but you can also use `Path().read_bytes()` to read a local file's contents.
@@ -79,7 +79,7 @@ result = agent.run_sync(
7979
]
8080
)
8181
print(result.output)
82-
# > This document is the technical report introducing Gemini 1.5, Google's latest large language model...
82+
#> This document is the technical report introducing Gemini 1.5, Google's latest large language model...
8383
```
8484

8585
The supported document formats vary by model.
@@ -99,7 +99,7 @@ result = agent.run_sync(
9999
]
100100
)
101101
print(result.output)
102-
# > The document discusses...
102+
#> The document discusses...
103103
```
104104

105105
## User-side download vs. direct file URL

pydantic_graph/pydantic_graph/beta/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010

1111
from .graph import Graph
1212
from .graph_builder import GraphBuilder
13-
from .join import DictReducer, ListReducer, NullReducer, Reducer
13+
from .join import DictUpdateReducer, ListAppendReducer, NullReducer, Reducer
1414
from .node import EndNode, StartNode
1515
from .step import StepContext, StepNode
1616
from .util import TypeExpression
1717

1818
__all__ = (
19-
'DictReducer',
19+
'DictUpdateReducer',
2020
'EndNode',
2121
'Graph',
2222
'GraphBuilder',
23-
'ListReducer',
23+
'ListAppendReducer',
2424
'NullReducer',
2525
'Reducer',
2626
'StartNode',

pydantic_graph/pydantic_graph/beta/graph.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ def render(self, *, title: str | None = None, direction: StateDiagramDirection |
251251
return build_mermaid_graph(self).render(title=title, direction=direction)
252252

253253
def __repr__(self) -> str:
254+
super_repr = super().__repr__() # include class and memory address
255+
# Insert the result of calling `__str__` before the final '>' in the repr
256+
return f'{super_repr[:-1]}\n{self}\n{super_repr[-1]}'
257+
258+
def __str__(self) -> str:
254259
"""Return a Mermaid diagram representation of the graph.
255260
256261
Returns:

pydantic_graph/pydantic_graph/beta/graph_builder.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
T = TypeVar('T', infer_variance=True)
5959

6060

61+
# TODO(P1): Should we make this method private? Not sure why it was public..
6162
@overload
6263
def join(
6364
*,
@@ -766,12 +767,13 @@ def _normalize_forks(
766767
if isinstance(item, MapMarker):
767768
assert item.fork_id in new_nodes
768769
new_edges[item.fork_id] = [path.next_path]
769-
if isinstance(item, BroadcastMarker):
770+
paths_to_handle.append(path.next_path)
771+
break
772+
elif isinstance(item, BroadcastMarker):
770773
assert item.fork_id in new_nodes
771-
# if item.fork_id not in new_nodes:
772-
# new_nodes[new_fork.id] = Fork[Any, Any](id=item.fork_id, is_map=False)
773774
new_edges[item.fork_id] = [*item.paths]
774775
paths_to_handle.extend(item.paths)
776+
break
775777

776778
return new_nodes, new_edges
777779

0 commit comments

Comments
 (0)