Skip to content

Commit 558985f

Browse files
authored
Merge branch 'main' into clai-chat
2 parents 441d6a0 + c27e5e4 commit 558985f

File tree

62 files changed

+5867
-432
lines changed

Some content is hidden

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

62 files changed

+5867
-432
lines changed

docs/builtin-tools.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ The [`ImageGenerationTool`][pydantic_ai.builtin_tools.ImageGenerationTool] enabl
202202
| Provider | Supported | Notes |
203203
|----------|-----------|-------|
204204
| OpenAI Responses || Full feature support. Only supported by models newer than `gpt-5`. Metadata about the generated image, like the [`revised_prompt`](https://platform.openai.com/docs/guides/tools-image-generation#revised-prompt) sent to the underlying image model, is available on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] that's available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls]. |
205-
| Google || No parameter support. Only supported by [image generation models](https://ai.google.dev/gemini-api/docs/image-generation) like `gemini-2.5-flash-image`. These models do not support [structured output](output.md) or [function tools](tools.md). These models will always generate images, even if this built-in tool is not explicitly specified. |
205+
| Google || No parameter support. Only supported by [image generation models](https://ai.google.dev/gemini-api/docs/image-generation) like `gemini-2.5-flash-image` and `gemini-3-pro-image-preview`. These models do not support [function tools](tools.md). These models will always have the option of generating images, even if this built-in tool is not explicitly specified. |
206206
| Anthropic || |
207207
| Groq || |
208208
| Bedrock || |

docs/input.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,37 @@ print(result.output)
104104

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

107-
As a general rule, when you provide a URL using any of `ImageUrl`, `AudioUrl`, `VideoUrl` or `DocumentUrl`, Pydantic AI downloads the file content and then sends it as part of the API request.
107+
When you provide a URL using any of `ImageUrl`, `AudioUrl`, `VideoUrl` or `DocumentUrl`, Pydantic AI will typically send the URL directly to the model API so that the download happens on their side.
108108

109-
The situation is different for certain models:
109+
Some model APIs do not support file URLs at all or for specific file types. In the following cases, Pydantic AI will download the file content and send it as part of the API request instead:
110110

111-
- [`AnthropicModel`][pydantic_ai.models.anthropic.AnthropicModel]: if you provide a PDF document via `DocumentUrl`, the URL is sent directly in the API request, so no download happens on the user side.
111+
- [`OpenAIChatModel`][pydantic_ai.models.openai.OpenAIChatModel]: `AudioUrl` and `DocumentUrl`
112+
- [`OpenAIResponsesModel`][pydantic_ai.models.openai.OpenAIResponsesModel]: All URLs
113+
- [`AnthropicModel`][pydantic_ai.models.anthropic.AnthropicModel]: `DocumentUrl` with media type `text/plain`
114+
- [`GoogleModel`][pydantic_ai.models.google.GoogleModel] using GLA (Gemini Developer API): All URLs except YouTube video URLs and files uploaded to the [Files API](https://ai.google.dev/gemini-api/docs/files).
115+
- [`BedrockConverseModel`][pydantic_ai.models.bedrock.BedrockConverseModel]: All URLs
112116

113-
- [`GoogleModel`][pydantic_ai.models.google.GoogleModel] on Vertex AI: any URL provided using `ImageUrl`, `AudioUrl`, `VideoUrl`, or `DocumentUrl` is sent as-is in the API request and no data is downloaded beforehand.
117+
If the model API supports file URLs but may not be able to download a file because of crawling or access restrictions, you can instruct Pydantic AI to download the file content and send that instead of the URL by enabling the `force_download` flag on the URL object. For example, [`GoogleModel`][pydantic_ai.models.google.GoogleModel] on Vertex AI limits YouTube video URLs to one URL per request.
114118

115-
See the [Gemini API docs for Vertex AI](https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/inference#filedata) to learn more about supported URLs, formats and limitations:
119+
## Uploaded Files
116120

117-
- Cloud Storage bucket URIs (with protocol `gs://`)
118-
- Public HTTP(S) URLs
119-
- Public YouTube video URL (maximum one URL per request)
121+
Some model providers like Google's Gemini API support [uploading files](https://ai.google.dev/gemini-api/docs/files). You can upload a file to the model API using the client you can get from the provider and use the resulting URL as input:
120122

121-
However, because of crawling restrictions, it may happen that Gemini can't access certain URLs. In that case, you can instruct Pydantic AI to download the file content and send that instead of the URL by setting the boolean flag `force_download` to `True`. This attribute is available on all objects that inherit from [`FileUrl`][pydantic_ai.messages.FileUrl].
123+
```py {title="file_upload.py" test="skip"}
124+
from pydantic_ai import Agent, DocumentUrl
125+
from pydantic_ai.models.google import GoogleModel
126+
from pydantic_ai.providers.google import GoogleProvider
127+
128+
provider = GoogleProvider()
129+
file = provider.client.files.upload(file='pydantic-ai-logo.png')
130+
assert file.uri is not None
122131

123-
- [`GoogleModel`][pydantic_ai.models.google.GoogleModel] on GLA: YouTube video URLs are sent directly in the request to the model.
132+
agent = Agent(GoogleModel('gemini-2.5-flash', provider=provider))
133+
result = agent.run_sync(
134+
[
135+
'What company is this logo from?',
136+
DocumentUrl(url=file.uri, media_type=file.mime_type),
137+
]
138+
)
139+
print(result.output)
140+
```

docs/output.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Instead of plain text or structured data, you may want the output of your agent
121121

122122
Output functions are similar to [function tools](tools.md), but the model is forced to call one of them, the call ends the agent run, and the result is not passed back to the model.
123123

124-
As with tool functions, output function arguments provided by the model are validated using Pydantic, they can optionally take [`RunContext`][pydantic_ai.tools.RunContext] as the first argument, and they can raise [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] to ask the model to try again with modified arguments (or with a different output type).
124+
As with tool functions, output function arguments provided by the model are validated using Pydantic (with optional [validation context](#validation-context)), can optionally take [`RunContext`][pydantic_ai.tools.RunContext] as the first argument, and can raise [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] to ask the model to try again with modified arguments (or with a different output type).
125125

126126
To specify output functions, you set the agent's `output_type` to either a single function (or bound instance method), or a list of functions. The list can also contain other output types like simple scalars or entire Pydantic models.
127127
You typically do not want to also register your output function as a tool (using the `@agent.tool` decorator or `tools` argument), as this could confuse the model about which it should be calling.
@@ -308,7 +308,7 @@ _(This example is complete, it can be run "as is")_
308308

309309
#### Native Output
310310

311-
Native Output mode uses a model's native "Structured Outputs" feature (aka "JSON Schema response format"), where the model is forced to only output text matching the provided JSON schema. Note that this is not supported by all models, and sometimes comes with restrictions. For example, Anthropic does not support this at all, and Gemini cannot use tools at the same time as structured output, and attempting to do so will result in an error.
311+
Native Output mode uses a model's native "Structured Outputs" feature (aka "JSON Schema response format"), where the model is forced to only output text matching the provided JSON schema. Note that this is not supported by all models, and sometimes comes with restrictions. For example, Gemini cannot use tools at the same time as structured output, and attempting to do so will result in an error.
312312

313313
To use this mode, you can wrap the output type(s) in the [`NativeOutput`][pydantic_ai.output.NativeOutput] marker class that also lets you specify a `name` and `description` if the name and docstring of the type or function are not sufficient.
314314

@@ -416,6 +416,62 @@ result = agent.run_sync('Create a person')
416416
#> {'name': 'John Doe', 'age': 30}
417417
```
418418

419+
### Validation context {#validation-context}
420+
421+
Some validation relies on an extra Pydantic [context](https://docs.pydantic.dev/latest/concepts/validators/#validation-context) object. You can pass such an object to an `Agent` at definition-time via its [`validation_context`][pydantic_ai.Agent.__init__] parameter. It will be used in the validation of both structured outputs and [tool arguments](tools-advanced.md#tool-retries).
422+
423+
This validation context can be either:
424+
425+
- the context object itself (`Any`), used as-is to validate outputs, or
426+
- a function that takes the [`RunContext`][pydantic_ai.tools.RunContext] and returns a context object (`Any`). This function will be called automatically before each validation, allowing you to build a dynamic validation context.
427+
428+
!!! warning "Don't confuse this _validation_ context with the _LLM_ context"
429+
This Pydantic validation context object is only used internally by Pydantic AI for tool arg and output validation. In particular, it is **not** included in the prompts or messages sent to the language model.
430+
431+
```python {title="validation_context.py"}
432+
from dataclasses import dataclass
433+
434+
from pydantic import BaseModel, ValidationInfo, field_validator
435+
436+
from pydantic_ai import Agent
437+
438+
439+
class Value(BaseModel):
440+
x: int
441+
442+
@field_validator('x')
443+
def increment_value(cls, value: int, info: ValidationInfo):
444+
return value + (info.context or 0)
445+
446+
447+
agent = Agent(
448+
'google-gla:gemini-2.5-flash',
449+
output_type=Value,
450+
validation_context=10,
451+
)
452+
result = agent.run_sync('Give me a value of 5.')
453+
print(repr(result.output)) # 5 from the model + 10 from the validation context
454+
#> Value(x=15)
455+
456+
457+
@dataclass
458+
class Deps:
459+
increment: int
460+
461+
462+
agent = Agent(
463+
'google-gla:gemini-2.5-flash',
464+
output_type=Value,
465+
deps_type=Deps,
466+
validation_context=lambda ctx: ctx.deps.increment,
467+
)
468+
result = agent.run_sync('Give me a value of 5.', deps=Deps(increment=10))
469+
print(repr(result.output)) # 5 from the model + 10 from the validation context
470+
#> Value(x=15)
471+
```
472+
473+
_(This example is complete, it can be run "as is")_
474+
419475
### Output validators {#output-validator-functions}
420476

421477
Some validation is inconvenient or impossible to do in Pydantic validators, in particular when the validation requires IO and is asynchronous. Pydantic AI provides a way to add validation functions via the [`agent.output_validator`][pydantic_ai.Agent.output_validator] decorator.

docs/tools-advanced.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ If both per-tool `prepare` and agent-wide `prepare_tools` are used, the per-tool
353353

354354
## Tool Execution and Retries {#tool-retries}
355355

356-
When a tool is executed, its arguments (provided by the LLM) are first validated against the function's signature using Pydantic. If validation fails (e.g., due to incorrect types or missing required arguments), a `ValidationError` is raised, and the framework automatically generates a [`RetryPromptPart`][pydantic_ai.messages.RetryPromptPart] containing the validation details. This prompt is sent back to the LLM, informing it of the error and allowing it to correct the parameters and retry the tool call.
356+
When a tool is executed, its arguments (provided by the LLM) are first validated against the function's signature using Pydantic (with optional [validation context](output.md#validation-context)). If validation fails (e.g., due to incorrect types or missing required arguments), a `ValidationError` is raised, and the framework automatically generates a [`RetryPromptPart`][pydantic_ai.messages.RetryPromptPart] containing the validation details. This prompt is sent back to the LLM, informing it of the error and allowing it to correct the parameters and retry the tool call.
357357

358358
Beyond automatic validation errors, the tool's own internal logic can also explicitly request a retry by raising the [`ModelRetry`][pydantic_ai.exceptions.ModelRetry] exception. This is useful for situations where the parameters were technically valid, but an issue occurred during execution (like a transient network error, or the tool determining the initial attempt needs modification).
359359

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ class GraphAgentDeps(Generic[DepsT, OutputDataT]):
144144

145145
output_schema: _output.OutputSchema[OutputDataT]
146146
output_validators: list[_output.OutputValidator[DepsT, OutputDataT]]
147+
validation_context: Any | Callable[[RunContext[DepsT]], Any]
147148

148149
history_processors: Sequence[HistoryProcessor[DepsT]]
149150

@@ -745,7 +746,7 @@ async def _handle_text_response(
745746
) -> ModelRequestNode[DepsT, NodeRunEndT] | End[result.FinalResult[NodeRunEndT]]:
746747
run_context = build_run_context(ctx)
747748

748-
result_data = await text_processor.process(text, run_context)
749+
result_data = await text_processor.process(text, run_context=run_context)
749750

750751
for validator in ctx.deps.output_validators:
751752
result_data = await validator.validate(result_data, run_context)
@@ -790,12 +791,13 @@ async def run(
790791

791792
def build_run_context(ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, Any]]) -> RunContext[DepsT]:
792793
"""Build a `RunContext` object from the current agent graph run context."""
793-
return RunContext[DepsT](
794+
run_context = RunContext[DepsT](
794795
deps=ctx.deps.user_deps,
795796
model=ctx.deps.model,
796797
usage=ctx.state.usage,
797798
prompt=ctx.deps.prompt,
798799
messages=ctx.state.message_history,
800+
validation_context=None,
799801
tracer=ctx.deps.tracer,
800802
trace_include_content=ctx.deps.instrumentation_settings is not None
801803
and ctx.deps.instrumentation_settings.include_content,
@@ -805,6 +807,21 @@ def build_run_context(ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT
805807
run_step=ctx.state.run_step,
806808
run_id=ctx.state.run_id,
807809
)
810+
validation_context = build_validation_context(ctx.deps.validation_context, run_context)
811+
run_context = replace(run_context, validation_context=validation_context)
812+
return run_context
813+
814+
815+
def build_validation_context(
816+
validation_ctx: Any | Callable[[RunContext[DepsT]], Any],
817+
run_context: RunContext[DepsT],
818+
) -> Any:
819+
"""Build a Pydantic validation context, potentially from the current agent run context."""
820+
if callable(validation_ctx):
821+
fn = cast(Callable[[RunContext[DepsT]], Any], validation_ctx)
822+
return fn(run_context)
823+
else:
824+
return validation_ctx
808825

809826

810827
async def process_tool_calls( # noqa: C901

pydantic_ai_slim/pydantic_ai/_json_schema.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
class JsonSchemaTransformer(ABC):
1616
"""Walks a JSON schema, applying transformations to it at each level.
1717
18+
The transformer is called during a model's prepare_request() step to build the JSON schema
19+
before it is sent to the model provider.
20+
1821
Note: We may eventually want to rework tools to build the JSON schema from the type directly, using a subclass of
1922
pydantic.json_schema.GenerateJsonSchema, rather than making use of this machinery.
2023
"""
@@ -30,8 +33,15 @@ def __init__(
3033
self.schema = schema
3134

3235
self.strict = strict
33-
self.is_strict_compatible = True # Can be set to False by subclasses to set `strict` on `ToolDefinition` when set not set by user explicitly
36+
"""The `strict` parameter forces the conversion of the original JSON schema (`self.schema`) of a `ToolDefinition` or `OutputObjectDefinition` to a format supported by the model provider.
37+
38+
The "strict mode" offered by model providers ensures that the model's output adheres closely to the defined schema. However, not all model providers offer it, and their support for various schema features may differ. For example, a model provider's required schema may not support certain validation constraints like `minLength` or `pattern`.
39+
"""
40+
self.is_strict_compatible = True
41+
"""Whether the schema is compatible with strict mode.
3442
43+
This value is used to set `ToolDefinition.strict` or `OutputObjectDefinition.strict` when their values are `None`.
44+
"""
3545
self.prefer_inlined_defs = prefer_inlined_defs
3646
self.simplify_nullable_unions = simplify_nullable_unions
3747

pydantic_ai_slim/pydantic_ai/_output.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ class BaseOutputProcessor(ABC, Generic[OutputDataT]):
522522
async def process(
523523
self,
524524
data: str,
525+
*,
525526
run_context: RunContext[AgentDepsT],
526527
allow_partial: bool = False,
527528
wrap_validation_errors: bool = True,
@@ -609,6 +610,7 @@ def __init__(
609610
async def process(
610611
self,
611612
data: str | dict[str, Any] | None,
613+
*,
612614
run_context: RunContext[AgentDepsT],
613615
allow_partial: bool = False,
614616
wrap_validation_errors: bool = True,
@@ -628,7 +630,7 @@ async def process(
628630
data = _utils.strip_markdown_fences(data)
629631

630632
try:
631-
output = self.validate(data, allow_partial)
633+
output = self.validate(data, allow_partial=allow_partial, validation_context=run_context.validation_context)
632634
except ValidationError as e:
633635
if wrap_validation_errors:
634636
m = _messages.RetryPromptPart(
@@ -645,13 +647,19 @@ async def process(
645647
def validate(
646648
self,
647649
data: str | dict[str, Any] | None,
650+
*,
648651
allow_partial: bool = False,
652+
validation_context: Any | None = None,
649653
) -> dict[str, Any]:
650654
pyd_allow_partial: Literal['off', 'trailing-strings'] = 'trailing-strings' if allow_partial else 'off'
651655
if isinstance(data, str):
652-
return self.validator.validate_json(data or '{}', allow_partial=pyd_allow_partial)
656+
return self.validator.validate_json(
657+
data or '{}', allow_partial=pyd_allow_partial, context=validation_context
658+
)
653659
else:
654-
return self.validator.validate_python(data or {}, allow_partial=pyd_allow_partial)
660+
return self.validator.validate_python(
661+
data or {}, allow_partial=pyd_allow_partial, context=validation_context
662+
)
655663

656664
async def call(
657665
self,
@@ -770,12 +778,16 @@ def __init__(
770778
async def process(
771779
self,
772780
data: str,
781+
*,
773782
run_context: RunContext[AgentDepsT],
774783
allow_partial: bool = False,
775784
wrap_validation_errors: bool = True,
776785
) -> OutputDataT:
777786
union_object = await self._union_processor.process(
778-
data, run_context, allow_partial=allow_partial, wrap_validation_errors=wrap_validation_errors
787+
data,
788+
run_context=run_context,
789+
allow_partial=allow_partial,
790+
wrap_validation_errors=wrap_validation_errors,
779791
)
780792

781793
result = union_object.result
@@ -791,15 +803,20 @@ async def process(
791803
raise
792804

793805
return await processor.process(
794-
inner_data, run_context, allow_partial=allow_partial, wrap_validation_errors=wrap_validation_errors
806+
inner_data,
807+
run_context=run_context,
808+
allow_partial=allow_partial,
809+
wrap_validation_errors=wrap_validation_errors,
795810
)
796811

797812

798813
class TextOutputProcessor(BaseOutputProcessor[OutputDataT]):
799814
async def process(
800815
self,
801816
data: str,
817+
*,
802818
run_context: RunContext[AgentDepsT],
819+
validation_context: Any | None = None,
803820
allow_partial: bool = False,
804821
wrap_validation_errors: bool = True,
805822
) -> OutputDataT:
@@ -830,14 +847,22 @@ def __init__(
830847
async def process(
831848
self,
832849
data: str,
850+
*,
833851
run_context: RunContext[AgentDepsT],
852+
validation_context: Any | None = None,
834853
allow_partial: bool = False,
835854
wrap_validation_errors: bool = True,
836855
) -> OutputDataT:
837856
args = {self._str_argument_name: data}
838857
data = await execute_traced_output_function(self._function_schema, run_context, args, wrap_validation_errors)
839858

840-
return await super().process(data, run_context, allow_partial, wrap_validation_errors)
859+
return await super().process(
860+
data,
861+
run_context=run_context,
862+
validation_context=validation_context,
863+
allow_partial=allow_partial,
864+
wrap_validation_errors=wrap_validation_errors,
865+
)
841866

842867

843868
@dataclass(init=False)

0 commit comments

Comments
 (0)