Skip to content

Commit a7abf0a

Browse files
authored
Merge branch 'main' into examples/medical-triage-delegation
2 parents 62e0d26 + 1f3b100 commit a7abf0a

File tree

8 files changed

+75
-28
lines changed

8 files changed

+75
-28
lines changed

.github/workflows/after-ci.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ jobs:
4545
name: deploy-docs-preview
4646

4747
steps:
48+
- run: echo "$GITHUB_EVENT_JSON"
49+
env:
50+
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
51+
4852
- uses: actions/checkout@v4
4953

5054
- uses: actions/setup-node@v4
@@ -57,7 +61,8 @@ jobs:
5761
enable-cache: true
5862
cache-suffix: deploy-docs-preview
5963

60-
- uses: dawidd6/action-download-artifact@v6
64+
- id: download-artifact
65+
uses: dawidd6/action-download-artifact@v6
6166
with:
6267
workflow: ci.yml
6368
name: site
@@ -69,6 +74,7 @@ jobs:
6974

7075
- uses: cloudflare/wrangler-action@v3
7176
id: deploy
77+
if: steps.download-artifact.outputs.found_artifact == 'true'
7278
with:
7379
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
7480
environment: previews
@@ -78,12 +84,9 @@ jobs:
7884
--var GIT_COMMIT_SHA:${{ github.event.workflow_run.head_sha }}
7985
--var GIT_BRANCH:${{ github.event.workflow_run.head_branch }}
8086
81-
- run: echo "$GITHUB_EVENT_JSON"
82-
env:
83-
GITHUB_EVENT_JSON: ${{ toJSON(github.event) }}
84-
8587
- name: Set preview URL
8688
run: uv run --no-project --with httpx .github/set_docs_pr_preview_url.py
89+
if: steps.deploy.outcome == 'success'
8790
env:
8891
DEPLOY_OUTPUT: ${{ steps.deploy.outputs.command-output }}
8992
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

docs/changelog.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Upgrade Guide
22

3-
In September 2025, Pydantic AI reached V1, which means we're committed to API stability: we will not introduce changes that break your code until V2 (if we do, you can shout at us as it's definitely a mistake).
4-
Once we release V2, in April 2026 at the earliest, we'll continue to provide security fixes for V1 for another 6 months minimum, so you have time to upgrade your applications.
3+
In September 2025, Pydantic AI reached V1, which means we're committed to API stability: we will not introduce changes that break your code until V2. For more information, review our [Version Policy](version-policy.md).
54

65
## Breaking Changes
76

docs/version-policy.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Version Policy
2+
3+
We will not intentionally make breaking changes in minor releases of V1. V2 will be released in April 2026 at the earliest, 6 months after the release of V1 in September 2025.
4+
5+
Once we release V2, we'll continue to provide security fixes for V1 for another 6 months minimum, so you have time to upgrade your applications.
6+
7+
Functionality marked as deprecated will not be removed until V2.
8+
9+
Of course, some apparently safe changes and bug fixes will inevitably break some users' code — obligatory link to [xkcd](https://xkcd.com/1172/).
10+
11+
The following changes will **NOT** be considered breaking changes, and may occur in minor releases:
12+
13+
* Bug fixes that may result in existing code breaking, provided that such code was relying on undocumented features/constructs/assumptions.
14+
* Adding new [message parts][pydantic_ai.messages], [stream events][pydantic_ai.messages.AgentStreamEvent], or optional fields on existing message (part) and event types. Always code defensively when consuming message parts or event streams, and use the [`ModelMessagesTypeAdapter`][pydantic_ai.messages.ModelMessagesTypeAdapter] to (de)serialize message histories.
15+
* Changing OpenTelemetry span attributes. Because different [observability platforms](logfire.md#using-opentelemetry) support different versions of the [OpenTelemetry Semantic Conventions for Generative AI systems](https://opentelemetry.io/docs/specs/semconv/gen-ai/), Pydantic AI lets you configure the [instrumentation version](logfire.md#configuring-data-format), but the default version may change in a minor release. Span attributes for [Pydantic Evals](evals.md) may also change as we iterate on Evals support in [Pydantic Logfire](https://logfire.pydantic.dev/docs/guides/web-ui/evals/).
16+
* Changing how `__repr__` behaves, even of public classes.
17+
18+
In all cases we will aim to minimize churn and do so only when justified by the increase of quality of Pydantic AI for users.
19+
20+
## Beta Features
21+
22+
At Pydantic, we like to move quickly and innovate! To that end, minor releases may introduce beta features (indicated by a `beta` module) that are active works in progress. While in its beta phase, a feature's API and behaviors may not be stable, and it's very possible that changes made to the feature will not be backward-compatible. We aim to move beta features out of beta within a few months after initial release, once users have had a chance to provide feedback and test the feature in production.
23+
24+
## Support for Python versions
25+
26+
Pydantic will drop support for a Python version when the following conditions are met:
27+
28+
* The Python version has reached its [expected end of life](https://devguide.python.org/versions/).
29+
* less than 5% of downloads of the most recent minor release are using that version.

mkdocs.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ nav:
1414
- install.md
1515
- help.md
1616
- troubleshooting.md
17-
- changelog.md
1817

1918
- Documentation:
2019
- Core Concepts:
@@ -191,6 +190,8 @@ nav:
191190

192191
- Project:
193192
- contributing.md
193+
- changelog.md
194+
- version-policy.md
194195

195196
extra:
196197
# hide the "Made with Material for MkDocs" message
@@ -333,7 +334,6 @@ plugins:
333334
- install.md
334335
- help.md
335336
- troubleshooting.md
336-
- changelog.md
337337
Concepts documentation:
338338
- a2a.md
339339
- ag-ui.md
@@ -365,11 +365,15 @@ plugins:
365365
- durable_execution/*.md
366366
MCP:
367367
- mcp/*.md
368+
UI Event Streams:
369+
- ui/*.md
368370
Optional:
369371
- testing.md
370372
- cli.md
371373
- logfire.md
372374
- contributing.md
375+
- changelog.md
376+
- version-policy.md
373377
Examples:
374378
- examples/*.md
375379

pydantic_ai_slim/pydantic_ai/models/google.py

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,9 @@ def _process_response(self, response: GenerateContentResponse) -> ModelResponse:
471471
raise UnexpectedModelBehavior(
472472
f'Content filter {raw_finish_reason.value!r} triggered', response.model_dump_json()
473473
)
474-
else:
475-
raise UnexpectedModelBehavior(
476-
'Content field missing from Gemini response', response.model_dump_json()
477-
) # pragma: no cover
478-
parts = candidate.content.parts or []
474+
parts = [] # pragma: no cover
475+
else:
476+
parts = candidate.content.parts or []
479477

480478
usage = _metadata_as_usage(response)
481479
return _process_response_from_parts(
@@ -649,17 +647,12 @@ async def _get_event_iterator(self) -> AsyncIterator[ModelResponseStreamEvent]:
649647
# )
650648

651649
if candidate.content is None or candidate.content.parts is None:
652-
if self.finish_reason == 'stop': # pragma: no cover
653-
# Normal completion - skip this chunk
654-
continue
655-
elif self.finish_reason == 'content_filter' and raw_finish_reason: # pragma: no cover
650+
if self.finish_reason == 'content_filter' and raw_finish_reason: # pragma: no cover
656651
raise UnexpectedModelBehavior(
657652
f'Content filter {raw_finish_reason.value!r} triggered', chunk.model_dump_json()
658653
)
659654
else: # pragma: no cover
660-
raise UnexpectedModelBehavior(
661-
'Content field missing from streaming Gemini response', chunk.model_dump_json()
662-
)
655+
continue
663656

664657
parts = candidate.content.parts
665658
if not parts:

pydantic_ai_slim/pydantic_ai/ui/_adapter.py

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

33
from abc import ABC, abstractmethod
44
from collections.abc import AsyncIterator, Sequence
5-
from dataclasses import KW_ONLY, Field, dataclass, replace
5+
from dataclasses import KW_ONLY, Field, dataclass
66
from functools import cached_property
77
from http import HTTPStatus
88
from typing import (
@@ -238,7 +238,7 @@ def run_stream_native(
238238
else:
239239
state = raw_state
240240

241-
deps = replace(deps, state=state)
241+
deps.state = state
242242
elif self.state:
243243
raise UserError(
244244
f'State is provided but `deps` of type `{type(deps).__name__}` does not implement the `StateHandler` protocol: it needs to be a dataclass with a non-optional `state` field.'

pydantic_ai_slim/pydantic_ai/ui/ag_ui/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import Callable, Mapping, Sequence
6+
from dataclasses import replace
67
from typing import Any, Generic
78

89
from typing_extensions import Self
@@ -18,7 +19,7 @@
1819
from pydantic_ai.toolsets import AbstractToolset
1920
from pydantic_ai.usage import RunUsage, UsageLimits
2021

21-
from .. import OnCompleteFunc
22+
from .. import OnCompleteFunc, StateHandler
2223
from ._adapter import AGUIAdapter
2324

2425
try:
@@ -121,6 +122,12 @@ def __init__(
121122

122123
async def run_agent(request: Request) -> Response:
123124
"""Endpoint to run the agent with the provided input data."""
125+
# `dispatch_request` will store the frontend state from the request on `deps.state` (if it implements the `StateHandler` protocol),
126+
# so we need to copy the deps to avoid different requests mutating the same deps object.
127+
nonlocal deps
128+
if isinstance(deps, StateHandler): # pragma: no branch
129+
deps = replace(deps)
130+
124131
return await AGUIAdapter[AgentDepsT, OutputDataT].dispatch_request(
125132
request,
126133
agent=agent,

tests/test_ag_ui.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,15 +1154,21 @@ async def store_state(
11541154
),
11551155
]
11561156

1157-
deps = StateDeps(StateInt(value=0))
1157+
seen_deps_states: list[int] = []
11581158

11591159
for run_input in run_inputs:
11601160
events = list[dict[str, Any]]()
1161-
async for event in run_ag_ui(agent, run_input, deps=deps):
1161+
deps = StateDeps(StateInt(value=0))
1162+
1163+
async def on_complete(result: AgentRunResult[Any]):
1164+
seen_deps_states.append(deps.state.value)
1165+
1166+
async for event in run_ag_ui(agent, run_input, deps=deps, on_complete=on_complete):
11621167
events.append(json.loads(event.removeprefix('data: ')))
11631168

11641169
assert events == simple_result()
11651170
assert seen_states == snapshot([41, 0, 0, 42])
1171+
assert seen_deps_states == snapshot([42, 1, 1, 43])
11661172

11671173

11681174
async def test_request_with_state_without_handler() -> None:
@@ -1275,8 +1281,10 @@ async def get_state(ctx: RunContext[StateDeps[StateInt]]) -> int:
12751281
async def test_to_ag_ui() -> None:
12761282
"""Test the agent.to_ag_ui method."""
12771283

1278-
agent = Agent(model=FunctionModel(stream_function=simple_stream))
1279-
app = agent.to_ag_ui()
1284+
agent = Agent(model=FunctionModel(stream_function=simple_stream), deps_type=StateDeps[StateInt])
1285+
1286+
deps = StateDeps(StateInt(value=0))
1287+
app = agent.to_ag_ui(deps=deps)
12801288
async with LifespanManager(app):
12811289
transport = httpx.ASGITransport(app)
12821290
async with httpx.AsyncClient(transport=transport) as client:
@@ -1286,6 +1294,7 @@ async def test_to_ag_ui() -> None:
12861294
id='msg_1',
12871295
content='Hello, world!',
12881296
),
1297+
state=StateInt(value=42),
12891298
)
12901299
async with client.stream(
12911300
'POST',
@@ -1301,6 +1310,9 @@ async def test_to_ag_ui() -> None:
13011310

13021311
assert events == simple_result()
13031312

1313+
# Verify the state was not mutated by the run
1314+
assert deps.state.value == 0
1315+
13041316

13051317
async def test_callback_sync() -> None:
13061318
"""Test that sync callbacks work correctly."""

0 commit comments

Comments
 (0)