Skip to content

Commit aa4e770

Browse files
committed
Add builtin tools
1 parent 3ad6d38 commit aa4e770

File tree

12 files changed

+364
-163
lines changed

12 files changed

+364
-163
lines changed

pydantic_ai_slim/pydantic_ai/builtin_tools.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations as _annotations
22

33
from abc import ABC
4-
from dataclasses import dataclass, field
4+
from dataclasses import dataclass
55
from typing import Literal
66

77
from typing_extensions import TypedDict
@@ -19,20 +19,6 @@ class AbstractBuiltinTool(ABC):
1919
"""
2020

2121

22-
class UserLocation(TypedDict, total=False):
23-
"""Allows you to localize search results based on a user's location.
24-
25-
Supported by:
26-
* Anthropic
27-
* OpenAI
28-
"""
29-
30-
city: str
31-
country: str
32-
region: str
33-
timezone: str
34-
35-
3622
@dataclass
3723
class WebSearchTool(AbstractBuiltinTool):
3824
"""A builtin tool that allows your agent to search the web for information.
@@ -47,7 +33,7 @@ class WebSearchTool(AbstractBuiltinTool):
4733
* OpenAI
4834
"""
4935

50-
user_location: UserLocation = field(default_factory=UserLocation)
36+
user_location: UserLocation | None = None
5137
"""The `user_location` parameter allows you to localize search results based on a user's location.
5238
5339
Supported by:
@@ -82,3 +68,26 @@ class WebSearchTool(AbstractBuiltinTool):
8268
Supported by:
8369
* Anthropic
8470
"""
71+
72+
73+
class UserLocation(TypedDict, total=False):
74+
"""Allows you to localize search results based on a user's location.
75+
76+
Supported by:
77+
* Anthropic
78+
* OpenAI
79+
"""
80+
81+
city: str
82+
country: str
83+
region: str
84+
timezone: str
85+
86+
87+
class CodeExecutionTool(AbstractBuiltinTool):
88+
"""A builtin tool that allows your agent to execute code.
89+
90+
Supported by:
91+
* Anthropic
92+
* OpenAI
93+
"""

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
from datetime import datetime, timezone
88
from typing import Any, Literal, Union, cast, overload
99

10-
from anthropic.types import ServerToolUseBlock, ToolUnionParam, WebSearchTool20250305Param, WebSearchToolResultBlock
11-
from anthropic.types.web_search_tool_20250305_param import UserLocation
10+
from anthropic.types.beta import BetaMessage, BetaRawMessageStreamEvent, BetaToolUnionParam
1211
from typing_extensions import assert_never
1312

14-
from pydantic_ai.builtin_tools import WebSearchTool
13+
from pydantic_ai.builtin_tools import CodeExecutionTool, WebSearchTool
1514

1615
from .. import ModelHTTPError, UnexpectedModelBehavior, _utils, usage
1716
from .._utils import guard_tool_call_id as _guard_tool_call_id
@@ -61,15 +60,21 @@
6160
RawMessageStartEvent,
6261
RawMessageStopEvent,
6362
RawMessageStreamEvent,
63+
ServerToolUseBlock,
6464
TextBlock,
6565
TextBlockParam,
6666
TextDelta,
6767
ToolChoiceParam,
6868
ToolParam,
6969
ToolResultBlockParam,
70+
ToolUnionParam,
7071
ToolUseBlock,
7172
ToolUseBlockParam,
73+
WebSearchTool20250305Param,
74+
WebSearchToolResultBlock,
7275
)
76+
from anthropic.types.beta.beta_code_execution_tool_20250522_param import BetaCodeExecutionTool20250522Param
77+
from anthropic.types.web_search_tool_20250305_param import UserLocation
7378
except ImportError as _import_error:
7479
raise ImportError(
7580
'Please install `anthropic` to use the Anthropic model, '
@@ -207,10 +212,11 @@ async def _messages_create(
207212
stream: bool,
208213
model_settings: AnthropicModelSettings,
209214
model_request_parameters: ModelRequestParameters,
210-
) -> AnthropicMessage | AsyncStream[RawMessageStreamEvent]:
215+
) -> AnthropicMessage | AsyncStream[RawMessageStreamEvent] | BetaMessage | AsyncStream[BetaRawMessageStreamEvent]:
211216
# standalone function to make it easier to override
212217
tools = self._get_tools(model_request_parameters)
213218
tools += self._get_builtin_tools(model_request_parameters)
219+
beta_tools = self._get_beta_tools(model_request_parameters)
214220
tool_choice: ToolChoiceParam | None
215221

216222
if not tools:
@@ -229,22 +235,30 @@ async def _messages_create(
229235
try:
230236
extra_headers = model_settings.get('extra_headers', {})
231237
extra_headers.setdefault('User-Agent', get_user_agent())
232-
return await self.client.messages.create(
233-
max_tokens=model_settings.get('max_tokens', 1024),
234-
system=system_prompt or NOT_GIVEN,
235-
messages=anthropic_messages,
236-
model=self._model_name,
237-
tools=tools or NOT_GIVEN,
238-
tool_choice=tool_choice or NOT_GIVEN,
239-
stream=stream,
240-
stop_sequences=model_settings.get('stop_sequences', NOT_GIVEN),
241-
temperature=model_settings.get('temperature', NOT_GIVEN),
242-
top_p=model_settings.get('top_p', NOT_GIVEN),
243-
timeout=model_settings.get('timeout', NOT_GIVEN),
244-
metadata=model_settings.get('anthropic_metadata', NOT_GIVEN),
245-
extra_headers=extra_headers,
246-
extra_body=model_settings.get('extra_body'),
247-
)
238+
if beta_tools:
239+
return await self.client.beta.messages.create(
240+
max_tokens=model_settings.get('max_tokens', 1024),
241+
system=system_prompt or NOT_GIVEN,
242+
messages=anthropic_messages,
243+
model=self._model_name,
244+
)
245+
else:
246+
return await self.client.messages.create(
247+
max_tokens=model_settings.get('max_tokens', 1024),
248+
system=system_prompt or NOT_GIVEN,
249+
messages=anthropic_messages,
250+
model=self._model_name,
251+
tools=tools or NOT_GIVEN,
252+
tool_choice=tool_choice or NOT_GIVEN,
253+
stream=stream,
254+
stop_sequences=model_settings.get('stop_sequences', NOT_GIVEN),
255+
temperature=model_settings.get('temperature', NOT_GIVEN),
256+
top_p=model_settings.get('top_p', NOT_GIVEN),
257+
timeout=model_settings.get('timeout', NOT_GIVEN),
258+
metadata=model_settings.get('anthropic_metadata', NOT_GIVEN),
259+
extra_headers=extra_headers,
260+
extra_body=model_settings.get('extra_body'),
261+
)
248262
except APIStatusError as e:
249263
if (status_code := e.status_code) >= 400:
250264
raise ModelHTTPError(status_code=status_code, model_name=self.model_name, body=e.body) from e
@@ -254,10 +268,12 @@ def _process_response(self, response: AnthropicMessage) -> ModelResponse:
254268
"""Process a non-streamed response, and prepare a message to return."""
255269
items: list[ModelResponsePart] = []
256270
for item in response.content:
271+
breakpoint()
257272
if isinstance(item, TextBlock):
258273
items.append(TextPart(content=item.text))
259-
if isinstance(item, WebSearchToolResultBlock):
274+
elif isinstance(item, WebSearchToolResultBlock):
260275
# TODO(Marcelo): We should send something back to the user, because we need to send it back on the next request.
276+
# Do we need a new part type here? All the content is encrypted anyway - but it needs to go back to Anthropic.
261277
...
262278
else:
263279
assert isinstance(item, (ToolUseBlock, ServerToolUseBlock)), f'unexpected item type {type(item)}'
@@ -305,6 +321,13 @@ def _get_builtin_tools(self, model_request_parameters: ModelRequestParameters) -
305321
)
306322
return tools
307323

324+
def _get_beta_tools(self, model_request_parameters: ModelRequestParameters) -> list[BetaToolUnionParam]:
325+
tools: list[BetaToolUnionParam] = []
326+
for tool in model_request_parameters.builtin_tools:
327+
if isinstance(tool, CodeExecutionTool):
328+
tools.append(BetaCodeExecutionTool20250522Param(name='code_execution', type='code_execution_20250522'))
329+
return tools
330+
308331
async def _map_message(self, messages: list[ModelMessage]) -> tuple[str, list[MessageParam]]:
309332
"""Just maps a `pydantic_ai.Message` to a `anthropic.types.MessageParam`."""
310333
system_prompt_parts: list[str] = []

pydantic_ai_slim/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ openai = ["openai>=1.75.0"]
6565
cohere = ["cohere>=5.13.11; platform_system != 'Emscripten'"]
6666
vertexai = ["google-auth>=2.36.0", "requests>=2.32.2"]
6767
google = ["google-genai>=1.15.0"]
68-
anthropic = ["anthropic>=0.49.0"]
68+
anthropic = ["anthropic>=0.52.0"]
6969
groq = ["groq>=0.15.0"]
7070
mistral = ["mistralai>=1.2.5"]
7171
bedrock = ["boto3>=1.35.74"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
interactions:
2+
- request:
3+
headers:
4+
accept:
5+
- application/json
6+
accept-encoding:
7+
- gzip, deflate
8+
connection:
9+
- keep-alive
10+
content-length:
11+
- '257'
12+
content-type:
13+
- application/json
14+
host:
15+
- api.anthropic.com
16+
method: POST
17+
parsed_body:
18+
max_tokens: 1024
19+
messages:
20+
- content:
21+
- text: How much is 3 * 12390?
22+
type: text
23+
role: user
24+
model: claude-3-5-sonnet-latest
25+
stream: false
26+
tool_choice:
27+
type: auto
28+
tools:
29+
- name: code_execution
30+
type: code_execution_20250522
31+
uri: https://api.anthropic.com/v1/messages
32+
response:
33+
headers:
34+
connection:
35+
- keep-alive
36+
content-length:
37+
- '271'
38+
content-type:
39+
- application/json
40+
strict-transport-security:
41+
- max-age=31536000; includeSubDomains; preload
42+
parsed_body:
43+
error:
44+
message: 'tools.0: Input tag ''code_execution_20250522'' found using ''type'' does not match any of the expected tags:
45+
''bash_20250124'', ''custom'', ''text_editor_20250124'', ''text_editor_20250429'', ''web_search_20250305'''
46+
type: invalid_request_error
47+
type: error
48+
status:
49+
code: 400
50+
message: Bad Request
51+
version: 1

0 commit comments

Comments
 (0)