Skip to content

Commit e22754d

Browse files
authored
Merge branch 'main' into feat/deferred-tool-metadata
2 parents 37f5790 + 359c6d2 commit e22754d

File tree

101 files changed

+5848
-1393
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+5848
-1393
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- run: make docs
8181

8282
- run: make docs-insiders
83-
if: github.event.pull_request.head.repo.full_name == github.repository || github.ref == 'refs/heads/main'
83+
if: (github.event.pull_request.head.repo.full_name == github.repository || github.ref == 'refs/heads/main') && github.repository == 'pydantic/pydantic-ai'
8484
env:
8585
PPPR_TOKEN: ${{ secrets.PPPR_TOKEN }}
8686

@@ -103,7 +103,7 @@ jobs:
103103
test-live:
104104
runs-on: ubuntu-latest
105105
timeout-minutes: 5
106-
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
106+
if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push') && github.repository == 'pydantic/pydantic-ai'
107107
steps:
108108
- uses: actions/checkout@v4
109109

@@ -202,7 +202,8 @@ jobs:
202202
strategy:
203203
fail-fast: false
204204
matrix:
205-
python-version: ["3.10", "3.11", "3.12", "3.13"]
205+
# TODO(Marcelo): Enable 3.11 again.
206+
python-version: ["3.10", "3.12", "3.13"]
206207
env:
207208
CI: true
208209
COVERAGE_PROCESS_START: ./pyproject.toml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ node_modules/
2121
/test_tmp/
2222
.mcp.json
2323
.claude/
24+
/.cursor/
25+
/.devcontainer/

docs/.hooks/main.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
1818
"""Called on each file after it is read and before it is converted to HTML."""
19-
markdown = inject_snippets(markdown, (DOCS_ROOT / page.file.src_uri).parent)
19+
relative_path_root = (DOCS_ROOT / page.file.src_uri).parent
20+
markdown = inject_snippets(markdown, relative_path_root)
2021
markdown = replace_uv_python_run(markdown)
2122
markdown = render_examples(markdown)
2223
markdown = render_video(markdown)
24+
markdown = create_gateway_toggle(markdown, relative_path_root)
2325
return markdown
2426

2527

@@ -39,6 +41,7 @@ def on_env(env: Environment, config: Config, files: Files) -> Environment:
3941

4042
def on_post_build(config: Config) -> None:
4143
"""Inject extra CSS into mermaid styles to avoid titles being the same color as the background in dark mode."""
44+
assert bundle_path is not None
4245
if bundle_path.exists():
4346
content = bundle_path.read_text()
4447
content, _ = re.subn(r'}(\.statediagram)', '}.statediagramTitleText{fill:#888}\1', content, count=1)
@@ -115,3 +118,109 @@ def sub_cf_video(m: re.Match[str]) -> str:
115118
></iframe>
116119
</div>
117120
"""
121+
122+
123+
def create_gateway_toggle(markdown: str, relative_path_root: Path) -> str:
124+
"""Transform Python code blocks with Agent() calls to show both Pydantic AI and Gateway versions."""
125+
# Pattern matches Python code blocks with or without attributes, and optional annotation definitions after
126+
# Annotation definitions are numbered list items like "1. Some text" that follow the code block
127+
return re.sub(
128+
r'```py(?:thon)?(?: *\{?([^}\n]*)\}?)?\n(.*?)\n```(\n\n(?:\d+\..+?\n)+?\n)?',
129+
lambda m: transform_gateway_code_block(m, relative_path_root),
130+
markdown,
131+
flags=re.MULTILINE | re.DOTALL,
132+
)
133+
134+
135+
# Models that should get gateway transformation
136+
GATEWAY_MODELS = ('anthropic', 'openai', 'openai-responses', 'openai-chat', 'bedrock', 'google-vertex', 'groq')
137+
138+
139+
def transform_gateway_code_block(m: re.Match[str], relative_path_root: Path) -> str:
140+
"""Transform a single code block to show both versions if it contains Agent() calls."""
141+
attrs = m.group(1) or ''
142+
code = m.group(2)
143+
annotations = m.group(3) or '' # Capture annotation definitions if present
144+
145+
# Simple check: does the code contain both "Agent(" and a quoted string?
146+
if 'Agent(' not in code:
147+
attrs_str = f' {{{attrs}}}' if attrs else ''
148+
return f'```python{attrs_str}\n{code}\n```{annotations}'
149+
150+
# Check if code contains Agent() with a model that should be transformed
151+
# Look for Agent(...'model:...' or Agent(..."model:..."
152+
agent_pattern = r'Agent\((?:(?!["\']).)*([\"\'])([^"\']+)\1'
153+
agent_match = re.search(agent_pattern, code, flags=re.DOTALL)
154+
155+
if not agent_match:
156+
# No Agent() with string literal found
157+
attrs_str = f' {{{attrs}}}' if attrs else ''
158+
return f'```python{attrs_str}\n{code}\n```{annotations}'
159+
160+
model_string = agent_match.group(2)
161+
# Check if model starts with one of the gateway-supported models
162+
should_transform = any(model_string.startswith(f'{model}:') for model in GATEWAY_MODELS)
163+
164+
if not should_transform:
165+
# Model doesn't match gateway models, return original
166+
attrs_str = f' {{{attrs}}}' if attrs else ''
167+
return f'```python{attrs_str}\n{code}\n```{annotations}'
168+
169+
# Transform the code for gateway version
170+
def replace_agent_model(match: re.Match[str]) -> str:
171+
"""Replace model string with gateway/ prefix."""
172+
full_match = match.group(0)
173+
quote = match.group(1)
174+
model = match.group(2)
175+
176+
# Replace the model string while preserving the rest
177+
return full_match.replace(f'{quote}{model}{quote}', f'{quote}gateway/{model}{quote}', 1)
178+
179+
# This pattern finds: "Agent(" followed by anything (lazy), then the first quoted string
180+
gateway_code = re.sub(
181+
agent_pattern,
182+
replace_agent_model,
183+
code,
184+
flags=re.DOTALL,
185+
)
186+
187+
# Build attributes string
188+
docs_path = DOCS_ROOT / 'gateway'
189+
relative_path = docs_path.relative_to(relative_path_root, walk_up=True)
190+
link = f"<a href='{relative_path}' style='float: right;'>Learn about Gateway</a>"
191+
192+
attrs_str = f' {{{attrs}}}' if attrs else ''
193+
194+
if 'title="' in attrs:
195+
gateway_attrs = attrs.replace('title="', f'title="{link} ', 1)
196+
else:
197+
gateway_attrs = attrs + f' title="{link}"'
198+
gateway_attrs_str = f' {{{gateway_attrs}}}'
199+
200+
# Indent code lines for proper markdown formatting within tabs
201+
# Always add 4 spaces to every line (even empty ones) to preserve annotations
202+
code_lines = code.split('\n')
203+
indented_code = '\n'.join(' ' + line for line in code_lines)
204+
205+
gateway_code_lines = gateway_code.split('\n')
206+
indented_gateway_code = '\n'.join(' ' + line for line in gateway_code_lines)
207+
208+
# Indent annotation definitions if present (need to be inside tabs for Material to work)
209+
indented_annotations = ''
210+
if annotations:
211+
# Remove surrounding newlines and indent each line with 4 spaces
212+
annotation_lines = annotations.strip().split('\n')
213+
indented_annotations = '\n\n' + '\n'.join(' ' + line for line in annotation_lines) + '\n\n'
214+
215+
return f"""\
216+
=== "With Pydantic AI Gateway"
217+
218+
```python{gateway_attrs_str}
219+
{indented_gateway_code}
220+
```{indented_annotations}
221+
222+
=== "Directly to Provider API"
223+
224+
```python{attrs_str}
225+
{indented_code}
226+
```{indented_annotations}"""

docs/.overrides/main.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
3+
{% block announce %}
4+
<strong>
5+
<a href="/gateway">Pydantic AI Gateway</a> is now available! 🚀
6+
Enterprise-ready AI model routing: One key for all your models with real-time monitoring and budget control that works.
7+
</strong>
8+
{% endblock %}

docs/agents.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ async def main():
320320
content='What is the capital of France?',
321321
timestamp=datetime.datetime(...),
322322
)
323-
]
323+
],
324+
run_id='...',
324325
)
325326
),
326327
CallToolsNode(
@@ -329,6 +330,7 @@ async def main():
329330
usage=RequestUsage(input_tokens=56, output_tokens=7),
330331
model_name='gpt-5',
331332
timestamp=datetime.datetime(...),
333+
run_id='...',
332334
)
333335
),
334336
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -382,7 +384,8 @@ async def main():
382384
content='What is the capital of France?',
383385
timestamp=datetime.datetime(...),
384386
)
385-
]
387+
],
388+
run_id='...',
386389
)
387390
),
388391
CallToolsNode(
@@ -391,6 +394,7 @@ async def main():
391394
usage=RequestUsage(input_tokens=56, output_tokens=7),
392395
model_name='gpt-5',
393396
timestamp=datetime.datetime(...),
397+
run_id='...',
394398
)
395399
),
396400
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -1044,7 +1048,8 @@ with capture_run_messages() as messages: # (2)!
10441048
content='Please get me the volume of a box with size 6.',
10451049
timestamp=datetime.datetime(...),
10461050
)
1047-
]
1051+
],
1052+
run_id='...',
10481053
),
10491054
ModelResponse(
10501055
parts=[
@@ -1057,6 +1062,7 @@ with capture_run_messages() as messages: # (2)!
10571062
usage=RequestUsage(input_tokens=62, output_tokens=4),
10581063
model_name='gpt-5',
10591064
timestamp=datetime.datetime(...),
1065+
run_id='...',
10601066
),
10611067
ModelRequest(
10621068
parts=[
@@ -1066,7 +1072,8 @@ with capture_run_messages() as messages: # (2)!
10661072
tool_call_id='pyd_ai_tool_call_id',
10671073
timestamp=datetime.datetime(...),
10681074
)
1069-
]
1075+
],
1076+
run_id='...',
10701077
),
10711078
ModelResponse(
10721079
parts=[
@@ -1079,6 +1086,7 @@ with capture_run_messages() as messages: # (2)!
10791086
usage=RequestUsage(input_tokens=72, output_tokens=8),
10801087
model_name='gpt-5',
10811088
timestamp=datetime.datetime(...),
1089+
run_id='...',
10821090
),
10831091
]
10841092
"""

docs/api/models/function.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ async def model_function(
2929
content='Testing my agent...',
3030
timestamp=datetime.datetime(...),
3131
)
32-
]
32+
],
33+
run_id='...',
3334
)
3435
]
3536
"""

docs/deferred-tools.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ print(result.all_messages())
107107
content='Delete `__init__.py`, write `Hello, world!` to `README.md`, and clear `.env`',
108108
timestamp=datetime.datetime(...),
109109
)
110-
]
110+
],
111+
run_id='...',
111112
),
112113
ModelResponse(
113114
parts=[
@@ -130,6 +131,7 @@ print(result.all_messages())
130131
usage=RequestUsage(input_tokens=63, output_tokens=21),
131132
model_name='gpt-5',
132133
timestamp=datetime.datetime(...),
134+
run_id='...',
133135
),
134136
ModelRequest(
135137
parts=[
@@ -139,7 +141,8 @@ print(result.all_messages())
139141
tool_call_id='update_file_readme',
140142
timestamp=datetime.datetime(...),
141143
)
142-
]
144+
],
145+
run_id='...',
143146
),
144147
ModelRequest(
145148
parts=[
@@ -155,7 +158,8 @@ print(result.all_messages())
155158
tool_call_id='delete_file',
156159
timestamp=datetime.datetime(...),
157160
),
158-
]
161+
],
162+
run_id='...',
159163
),
160164
ModelResponse(
161165
parts=[
@@ -166,6 +170,7 @@ print(result.all_messages())
166170
usage=RequestUsage(input_tokens=79, output_tokens=39),
167171
model_name='gpt-5',
168172
timestamp=datetime.datetime(...),
173+
run_id='...',
169174
),
170175
]
171176
"""
@@ -277,7 +282,8 @@ async def main():
277282
content='Calculate the answer to the ultimate question of life, the universe, and everything',
278283
timestamp=datetime.datetime(...),
279284
)
280-
]
285+
],
286+
run_id='...',
281287
),
282288
ModelResponse(
283289
parts=[
@@ -292,6 +298,7 @@ async def main():
292298
usage=RequestUsage(input_tokens=63, output_tokens=13),
293299
model_name='gpt-5',
294300
timestamp=datetime.datetime(...),
301+
run_id='...',
295302
),
296303
ModelRequest(
297304
parts=[
@@ -301,7 +308,8 @@ async def main():
301308
tool_call_id='pyd_ai_tool_call_id',
302309
timestamp=datetime.datetime(...),
303310
)
304-
]
311+
],
312+
run_id='...',
305313
),
306314
ModelResponse(
307315
parts=[
@@ -312,6 +320,7 @@ async def main():
312320
usage=RequestUsage(input_tokens=64, output_tokens=28),
313321
model_name='gpt-5',
314322
timestamp=datetime.datetime(...),
323+
run_id='...',
315324
),
316325
]
317326
"""

docs/dependencies.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Pydantic AI uses a dependency injection system to provide data and services to your agent's [system prompts](agents.md#system-prompts), [tools](tools.md) and [output validators](output.md#output-validator-functions).
44

5-
Matching Pydantic AI's design philosophy, our dependency system tries to use existing best practice in Python development rather than inventing esoteric "magic", this should make dependencies type-safe, understandable easier to test and ultimately easier to deploy in production.
5+
Matching Pydantic AI's design philosophy, our dependency system tries to use existing best practice in Python development rather than inventing esoteric "magic", this should make dependencies type-safe, understandable, easier to test, and ultimately easier to deploy in production.
66

77
## Defining Dependencies
88

@@ -103,11 +103,11 @@ _(This example is complete, it can be run "as is" — you'll need to add `asynci
103103
[System prompt functions](agents.md#system-prompts), [function tools](tools.md) and [output validators](output.md#output-validator-functions) are all run in the async context of an agent run.
104104

105105
If these functions are not coroutines (e.g. `async def`) they are called with
106-
[`run_in_executor`][asyncio.loop.run_in_executor] in a thread pool, it's therefore marginally preferable
106+
[`run_in_executor`][asyncio.loop.run_in_executor] in a thread pool. It's therefore marginally preferable
107107
to use `async` methods where dependencies perform IO, although synchronous dependencies should work fine too.
108108

109109
!!! note "`run` vs. `run_sync` and Asynchronous vs. Synchronous dependencies"
110-
Whether you use synchronous or asynchronous dependencies, is completely independent of whether you use `run` or `run_sync``run_sync` is just a wrapper around `run` and agents are always run in an async context.
110+
Whether you use synchronous or asynchronous dependencies is completely independent of whether you use `run` or `run_sync``run_sync` is just a wrapper around `run` and agents are always run in an async context.
111111

112112
Here's the same example as above, but with a synchronous dependency:
113113

docs/durable_execution/temporal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ As workflows and activities run in separate processes, any values passed between
172172

173173
To account for these limitations, tool functions and the [event stream handler](#streaming) running inside activities receive a limited version of the agent's [`RunContext`][pydantic_ai.tools.RunContext], and it's your responsibility to make sure that the [dependencies](../dependencies.md) object provided to [`TemporalAgent.run()`][pydantic_ai.durable_exec.temporal.TemporalAgent.run] can be serialized using Pydantic.
174174

175-
Specifically, only the `deps`, `retries`, `tool_call_id`, `tool_name`, `tool_call_approved`, `retry`, `max_retries`, `run_step` and `partial_output` fields are available by default, and trying to access `model`, `usage`, `prompt`, `messages`, or `tracer` will raise an error.
175+
Specifically, only the `deps`, `run_id`, `retries`, `tool_call_id`, `tool_name`, `tool_call_approved`, `retry`, `max_retries`, `run_step` and `partial_output` fields are available by default, and trying to access `model`, `usage`, `prompt`, `messages`, or `tracer` will raise an error.
176176
If you need one or more of these attributes to be available inside activities, you can create a [`TemporalRunContext`][pydantic_ai.durable_exec.temporal.TemporalRunContext] subclass with custom `serialize_run_context` and `deserialize_run_context` class methods and pass it to [`TemporalAgent`][pydantic_ai.durable_exec.temporal.TemporalAgent] as `run_context_type`.
177177

178178
### Streaming

docs/examples/ag-ui.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Agent User Interaction (AG-UI)
22

3-
Example of using Pydantic AI agents with the [AG-UI Dojo](https://github.com/ag-ui-protocol/ag-ui/tree/main/typescript-sdk/apps/dojo) example app.
3+
Example of using Pydantic AI agents with the [AG-UI Dojo](https://github.com/ag-ui-protocol/ag-ui/tree/main/apps/dojo) example app.
44

55
See the [AG-UI docs](../ui/ag-ui.md) for more information about the AG-UI integration.
66

@@ -48,7 +48,7 @@ Next run the AG-UI Dojo example frontend.
4848
cd ag-ui/sdks/typescript
4949
```
5050

51-
3. Run the Dojo app following the [official instructions](https://github.com/ag-ui-protocol/ag-ui/tree/main/typescript-sdk/apps/dojo#development-setup)
51+
3. Run the Dojo app following the [official instructions](https://github.com/ag-ui-protocol/ag-ui/tree/main/apps/dojo#development-setup)
5252
4. Visit <http://localhost:3000/pydantic-ai>
5353
5. Select View `Pydantic AI` from the sidebar
5454

0 commit comments

Comments
 (0)