Skip to content

Commit dd0f644

Browse files
Merge branch 'main' into update-versins
2 parents 749c71d + 98650cd commit dd0f644

28 files changed

+468
-67
lines changed

.github/workflows/stale.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
1-
name: 'Close stale issues'
1+
name: 'Close stale issues and PRs'
22
on:
33
schedule:
44
- cron: '0 14 * * *'
55

66
permissions:
77
issues: write
8+
pull-requests: write
89

910
jobs:
1011
stale:
1112
runs-on: ubuntu-latest
1213
steps:
1314
- uses: actions/stale@v9
1415
with:
15-
stale-issue-message: 'This issue is stale, and will be closed in 3 days if no reply is received.'
16-
close-issue-message: 'Closing this issue as it has been inactive for 10 days.'
17-
any-of-labels: 'question,more info'
1816
days-before-stale: 7
1917
days-before-close: 3
18+
19+
any-of-issue-labels: 'question,more info'
20+
stale-issue-message: 'This issue is stale, and will be closed in 3 days if no reply is received.'
21+
close-issue-message: 'Closing this issue as it has been inactive for 10 days.'
22+
23+
any-of-pr-labels: 'awaiting author revision'
24+
stale-pr-message: 'This PR is stale, and will be closed in 3 days if no reply is received.'
25+
close-pr-message: 'Closing this PR as it has been inactive for 10 days.'

docs/agents.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ This structure allows you to configure common parameters that influence the mode
467467
`timeout`, and more.
468468

469469
There are two ways to apply these settings:
470+
470471
1. Passing to `run{_sync,_stream}` functions via the `model_settings` argument. This allows for fine-tuning on a per-request basis.
471472
2. Setting during [`Agent`][pydantic_ai.agent.Agent] initialization via the `model_settings` argument. These settings will be applied by default to all subsequent run calls using said agent. However, `model_settings` provided during a specific run call will override the agent's default settings.
472473

docs/tools.md

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -721,11 +721,19 @@ def my_flaky_tool(query: str) -> str:
721721
```
722722
Raising `ModelRetry` also generates a `RetryPromptPart` containing the exception message, which is sent back to the LLM to guide its next attempt. Both `ValidationError` and `ModelRetry` respect the `retries` setting configured on the `Tool` or `Agent`.
723723

724-
## Use LangChain Tools {#langchain-tools}
724+
## Third-Party Tools
725725

726-
If you'd like to use a tool from LangChain's [community tool library](https://python.langchain.com/docs/integrations/tools/) with PydanticAI, you can use the `pydancic_ai.ext.langchain.tool_from_langchain` convenience method. Note that PydanticAI will not validate the arguments in this case -- it's up to the model to provide arguments matching the schema specified by the LangChain tool, and up to the LangChain tool to raise an error if the arguments are invalid.
726+
### MCP Tools {#mcp-tools}
727727

728-
Here is how you can use it to augment model responses using a LangChain web search tool. This tool will need you to install the `langchain-community` and `duckduckgo-search` dependencies to work properly.
728+
See the [MCP Client](./mcp/client.md) documentation for how to use MCP servers with Pydantic AI.
729+
730+
### LangChain Tools {#langchain-tools}
731+
732+
If you'd like to use a tool from LangChain's [community tool library](https://python.langchain.com/docs/integrations/tools/) with Pydantic AI, you can use the `pydancic_ai.ext.langchain.tool_from_langchain` convenience method. Note that Pydantic AI will not validate the arguments in this case -- it's up to the model to provide arguments matching the schema specified by the LangChain tool, and up to the LangChain tool to raise an error if the arguments are invalid.
733+
734+
You will need to install the `langchain-community` package and any others required by the tool in question.
735+
736+
Here is how you can use the LangChain `DuckDuckGoSearchRun` tool, which requires the `duckduckgo-search` package:
729737

730738
```python {test="skip"}
731739
from langchain_community.tools import DuckDuckGoSearchRun
@@ -737,15 +745,44 @@ search = DuckDuckGoSearchRun()
737745
search_tool = tool_from_langchain(search)
738746

739747
agent = Agent(
740-
'google-gla:gemini-2.0-flash', # (1)!
748+
'google-gla:gemini-2.0-flash',
741749
tools=[search_tool],
742750
)
743751

744-
result = agent.run_sync('What is the release date of Elden Ring Nightreign?') # (2)!
752+
result = agent.run_sync('What is the release date of Elden Ring Nightreign?') # (1)!
745753
print(result.output)
746754
#> Elden Ring Nightreign is planned to be released on May 30, 2025.
747755
```
748756

757+
1. The release date of this game is the 30th of May 2025, which is after the knowledge cutoff for Gemini 2.0 (August 2024).
758+
759+
### ACI.dev Tools {#aci-tools}
760+
761+
If you'd like to use a tool from the [ACI.dev tool library](https://www.aci.dev/tools) with Pydantic AI, you can use the `pydancic_ai.ext.aci.tool_from_aci` convenience method. Note that Pydantic AI will not validate the arguments in this case -- it's up to the model to provide arguments matching the schema specified by the ACI tool, and up to the ACI tool to raise an error if the arguments are invalid.
762+
763+
You will need to install the `aci-sdk` package, set your ACI API key in the `ACI_API_KEY` environment variable, and pass your ACI "linked account owner ID" to the function.
764+
765+
Here is how you can use the ACI.dev `TAVILY__SEARCH` tool:
766+
767+
```python {test="skip"}
768+
import os
769+
770+
from pydantic_ai import Agent
771+
from pydantic_ai.ext.aci import tool_from_aci
772+
773+
tavily_search = tool_from_aci(
774+
'TAVILY__SEARCH',
775+
linked_account_owner_id=os.getenv('LINKED_ACCOUNT_OWNER_ID')
776+
)
777+
778+
agent = Agent(
779+
'google-gla:gemini-2.0-flash',
780+
tools=[tavily_search]
781+
)
782+
783+
result = agent.run_sync('What is the release date of Elden Ring Nightreign?') # (1)!
784+
print(result.output)
785+
#> Elden Ring Nightreign is planned to be released on May 30, 2025.
786+
```
749787

750-
1. While this task is simple Gemini 1.5 didn't want to use the provided tool. Gemini 2.0 is still fast and cheap.
751-
2. The release date of this game is the 30th of May 2025, which was confirmed after the knowledge cutoff for Gemini 2.0 (August 2024).
788+
1. The release date of this game is the 30th of May 2025, which is after the knowledge cutoff for Gemini 2.0 (August 2024).

pydantic_ai_slim/pydantic_ai/_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,8 +315,11 @@ def dataclasses_no_defaults_repr(self: Any) -> str:
315315
return f'{self.__class__.__qualname__}({", ".join(kv_pairs)})'
316316

317317

318+
_datetime_ta = TypeAdapter(datetime)
319+
320+
318321
def number_to_datetime(x: int | float) -> datetime:
319-
return TypeAdapter(datetime).validate_python(x)
322+
return _datetime_ta.validate_python(x)
320323

321324

322325
AwaitableCallable = Callable[..., Awaitable[T]]

pydantic_ai_slim/pydantic_ai/agent.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def __init__(
296296
if 'result_type' in _deprecated_kwargs:
297297
if output_type is not str: # pragma: no cover
298298
raise TypeError('`result_type` and `output_type` cannot be set at the same time.')
299-
warnings.warn('`result_type` is deprecated, use `output_type` instead', DeprecationWarning)
299+
warnings.warn('`result_type` is deprecated, use `output_type` instead', DeprecationWarning, stacklevel=2)
300300
output_type = _deprecated_kwargs.pop('result_type')
301301

302302
self.output_type = output_type
@@ -310,19 +310,23 @@ def __init__(
310310
warnings.warn(
311311
'`result_tool_name` is deprecated, use `output_type` with `ToolOutput` instead',
312312
DeprecationWarning,
313+
stacklevel=2,
313314
)
314315

315316
self._deprecated_result_tool_description = _deprecated_kwargs.pop('result_tool_description', None)
316317
if self._deprecated_result_tool_description is not None:
317318
warnings.warn(
318319
'`result_tool_description` is deprecated, use `output_type` with `ToolOutput` instead',
319320
DeprecationWarning,
321+
stacklevel=2,
320322
)
321323
result_retries = _deprecated_kwargs.pop('result_retries', None)
322324
if result_retries is not None:
323325
if output_retries is not None: # pragma: no cover
324326
raise TypeError('`output_retries` and `result_retries` cannot be set at the same time.')
325-
warnings.warn('`result_retries` is deprecated, use `max_result_retries` instead', DeprecationWarning)
327+
warnings.warn(
328+
'`result_retries` is deprecated, use `max_result_retries` instead', DeprecationWarning, stacklevel=2
329+
)
326330
output_retries = result_retries
327331

328332
default_output_mode = (
@@ -472,7 +476,7 @@ async def main():
472476
if 'result_type' in _deprecated_kwargs: # pragma: no cover
473477
if output_type is not str:
474478
raise TypeError('`result_type` and `output_type` cannot be set at the same time.')
475-
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning)
479+
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning, stacklevel=2)
476480
output_type = _deprecated_kwargs.pop('result_type')
477481

478482
_utils.validate_empty_kwargs(_deprecated_kwargs)
@@ -640,7 +644,7 @@ async def main():
640644
if 'result_type' in _deprecated_kwargs: # pragma: no cover
641645
if output_type is not str:
642646
raise TypeError('`result_type` and `output_type` cannot be set at the same time.')
643-
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning)
647+
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning, stacklevel=2)
644648
output_type = _deprecated_kwargs.pop('result_type')
645649

646650
_utils.validate_empty_kwargs(_deprecated_kwargs)
@@ -879,7 +883,7 @@ def run_sync(
879883
if 'result_type' in _deprecated_kwargs: # pragma: no cover
880884
if output_type is not str:
881885
raise TypeError('`result_type` and `output_type` cannot be set at the same time.')
882-
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning)
886+
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning, stacklevel=2)
883887
output_type = _deprecated_kwargs.pop('result_type')
884888

885889
_utils.validate_empty_kwargs(_deprecated_kwargs)
@@ -997,7 +1001,7 @@ async def main():
9971001
if 'result_type' in _deprecated_kwargs: # pragma: no cover
9981002
if output_type is not str:
9991003
raise TypeError('`result_type` and `output_type` cannot be set at the same time.')
1000-
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning)
1004+
warnings.warn('`result_type` is deprecated, use `output_type` instead.', DeprecationWarning, stacklevel=2)
10011005
output_type = _deprecated_kwargs.pop('result_type')
10021006

10031007
_utils.validate_empty_kwargs(_deprecated_kwargs)
@@ -1336,7 +1340,11 @@ async def output_validator_deps(ctx: RunContext[str], data: str) -> str:
13361340
return func
13371341

13381342
@deprecated('`result_validator` is deprecated, use `output_validator` instead.')
1339-
def result_validator(self, func: Any, /) -> Any: ...
1343+
def result_validator(self, func: Any, /) -> Any:
1344+
warnings.warn(
1345+
'`result_validator` is deprecated, use `output_validator` instead.', DeprecationWarning, stacklevel=2
1346+
)
1347+
return self.output_validator(func) # type: ignore
13401348

13411349
@overload
13421350
def tool(self, func: ToolFuncContext[AgentDepsT, ToolParams], /) -> ToolFuncContext[AgentDepsT, ToolParams]: ...
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Checking whether aci-sdk is installed
2+
try:
3+
from aci import ACI
4+
except ImportError as _import_error:
5+
raise ImportError('Please install `aci-sdk` to use ACI.dev tools') from _import_error
6+
7+
from typing import Any
8+
9+
from aci import ACI
10+
11+
from pydantic_ai import Tool
12+
13+
14+
def _clean_schema(schema):
15+
if isinstance(schema, dict):
16+
# Remove non-standard keys (e.g., 'visible')
17+
return {k: _clean_schema(v) for k, v in schema.items() if k not in {'visible'}}
18+
elif isinstance(schema, list):
19+
return [_clean_schema(item) for item in schema]
20+
else:
21+
return schema
22+
23+
24+
def tool_from_aci(aci_function: str, linked_account_owner_id: str) -> Tool:
25+
"""Creates a Pydantic AI tool proxy from an ACI function.
26+
27+
Args:
28+
aci_function: The ACI function to wrao.
29+
linked_account_owner_id: The ACI user ID to execute the function on behalf of.
30+
31+
Returns:
32+
A Pydantic AI tool that corresponds to the ACI.dev tool.
33+
"""
34+
aci = ACI()
35+
function_definition = aci.functions.get_definition(aci_function)
36+
function_name = function_definition['function']['name']
37+
function_description = function_definition['function']['description']
38+
inputs = function_definition['function']['parameters']
39+
40+
json_schema = {
41+
'additionalProperties': inputs.get('additionalProperties', False),
42+
'properties': inputs.get('properties', {}),
43+
'required': inputs.get('required', []),
44+
# Default to 'object' if not specified
45+
'type': inputs.get('type', 'object'),
46+
}
47+
48+
# Clean the schema
49+
json_schema = _clean_schema(json_schema)
50+
51+
def implementation(*args: Any, **kwargs: Any) -> str:
52+
if args:
53+
raise TypeError('Positional arguments are not allowed')
54+
return aci.handle_function_call(
55+
function_name,
56+
kwargs,
57+
linked_account_owner_id=linked_account_owner_id,
58+
allowed_apps_only=True,
59+
)
60+
61+
return Tool.from_schema(
62+
function=implementation,
63+
name=function_name,
64+
description=function_description,
65+
json_schema=json_schema,
66+
)

pydantic_ai_slim/pydantic_ai/ext/langchain.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ def run(self, *args: Any, **kwargs: Any) -> str: ...
2727

2828

2929
def tool_from_langchain(langchain_tool: LangChainTool) -> Tool:
30-
"""Creates a Pydantic tool proxy from a LangChain tool.
30+
"""Creates a Pydantic AI tool proxy from a LangChain tool.
3131
3232
Args:
3333
langchain_tool: The LangChain tool to wrap.
3434
3535
Returns:
36-
A Pydantic tool that corresponds to the LangChain tool.
36+
A Pydantic AI tool that corresponds to the LangChain tool.
3737
"""
3838
function_name = langchain_tool.name
3939
function_description = langchain_tool.description

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from .models.instrumented import InstrumentationSettings
2626

2727

28-
AudioMediaType: TypeAlias = Literal['audio/wav', 'audio/mpeg']
28+
AudioMediaType: TypeAlias = Literal['audio/wav', 'audio/mpeg', 'audio/ogg', 'audio/flac', 'audio/aiff', 'audio/aac']
2929
ImageMediaType: TypeAlias = Literal['image/jpeg', 'image/png', 'image/gif', 'image/webp']
3030
DocumentMediaType: TypeAlias = Literal[
3131
'application/pdf',
@@ -48,7 +48,7 @@
4848
'video/3gpp',
4949
]
5050

51-
AudioFormat: TypeAlias = Literal['wav', 'mp3']
51+
AudioFormat: TypeAlias = Literal['wav', 'mp3', 'oga', 'flac', 'aiff', 'aac']
5252
ImageFormat: TypeAlias = Literal['jpeg', 'png', 'gif', 'webp']
5353
DocumentFormat: TypeAlias = Literal['csv', 'doc', 'docx', 'html', 'md', 'pdf', 'txt', 'xls', 'xlsx']
5454
VideoFormat: TypeAlias = Literal['mkv', 'mov', 'mp4', 'webm', 'flv', 'mpeg', 'mpg', 'wmv', 'three_gp']
@@ -99,6 +99,13 @@ class FileUrl(ABC):
9999
* If False, the URL is sent directly to the model and no download is performed.
100100
"""
101101

102+
vendor_metadata: dict[str, Any] | None = None
103+
"""Vendor-specific metadata for the file.
104+
105+
Supported by:
106+
- `GoogleModel`: `VideoUrl.vendor_metadata` is used as `video_metadata`: https://ai.google.dev/gemini-api/docs/video-understanding#customize-video-processing
107+
"""
108+
102109
@property
103110
@abstractmethod
104111
def media_type(self) -> str:
@@ -175,13 +182,25 @@ class AudioUrl(FileUrl):
175182

176183
@property
177184
def media_type(self) -> AudioMediaType:
178-
"""Return the media type of the audio file, based on the url."""
185+
"""Return the media type of the audio file, based on the url.
186+
187+
References:
188+
- Gemini: https://ai.google.dev/gemini-api/docs/audio#supported-formats
189+
"""
179190
if self.url.endswith('.mp3'):
180191
return 'audio/mpeg'
181-
elif self.url.endswith('.wav'):
192+
if self.url.endswith('.wav'):
182193
return 'audio/wav'
183-
else:
184-
raise ValueError(f'Unknown audio file extension: {self.url}')
194+
if self.url.endswith('.flac'):
195+
return 'audio/flac'
196+
if self.url.endswith('.oga'):
197+
return 'audio/ogg'
198+
if self.url.endswith('.aiff'):
199+
return 'audio/aiff'
200+
if self.url.endswith('.aac'):
201+
return 'audio/aac'
202+
203+
raise ValueError(f'Unknown audio file extension: {self.url}')
185204

186205
@property
187206
def format(self) -> AudioFormat:
@@ -263,6 +282,13 @@ class BinaryContent:
263282
media_type: AudioMediaType | ImageMediaType | DocumentMediaType | str
264283
"""The media type of the binary data."""
265284

285+
vendor_metadata: dict[str, Any] | None = None
286+
"""Vendor-specific metadata for the file.
287+
288+
Supported by:
289+
- `GoogleModel`: `BinaryContent.vendor_metadata` is used as `video_metadata`: https://ai.google.dev/gemini-api/docs/video-understanding#customize-video-processing
290+
"""
291+
266292
kind: Literal['binary'] = 'binary'
267293
"""Type identifier, this is available on all parts as a discriminator."""
268294

@@ -344,6 +370,10 @@ class ToolReturn:
344370
_audio_format_lookup: dict[str, AudioFormat] = {
345371
'audio/mpeg': 'mp3',
346372
'audio/wav': 'wav',
373+
'audio/flac': 'flac',
374+
'audio/ogg': 'oga',
375+
'audio/aiff': 'aiff',
376+
'audio/aac': 'aac',
347377
}
348378
_image_format_lookup: dict[str, ImageFormat] = {
349379
'image/jpeg': 'jpeg',

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,9 @@
9090

9191

9292
class AnthropicModelSettings(ModelSettings, total=False):
93-
"""Settings used for an Anthropic model request.
93+
"""Settings used for an Anthropic model request."""
9494

95-
ALL FIELDS MUST BE `anthropic_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
96-
"""
95+
# ALL FIELDS MUST BE `anthropic_` PREFIXED SO YOU CAN MERGE THEM WITH OTHER MODELS.
9796

9897
anthropic_metadata: BetaMetadataParam
9998
"""An object describing metadata about the request.

0 commit comments

Comments
 (0)