Skip to content

Commit 849aa4c

Browse files
authored
Toolsets (#2024)
1 parent 4d755d2 commit 849aa4c

Some content is hidden

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

49 files changed

+4617
-1413
lines changed

docs/agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ with capture_run_messages() as messages: # (2)!
826826
result = agent.run_sync('Please get me the volume of a box with size 6.')
827827
except UnexpectedModelBehavior as e:
828828
print('An error occurred:', e)
829-
#> An error occurred: Tool exceeded max retries count of 1
829+
#> An error occurred: Tool 'calc_volume' exceeded max retries count of 1
830830
print('cause:', repr(e.__cause__))
831831
#> cause: ModelRetry('Please try again.')
832832
print('messages:', messages)

docs/api/ext.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# `pydantic_ai.ext`
2+
3+
::: pydantic_ai.ext.langchain
4+
5+
::: pydantic_ai.ext.aci

docs/api/output.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
- PromptedOutput
1111
- TextOutput
1212
- StructuredDict
13+
- DeferredToolCalls

docs/api/toolsets.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# `pydantic_ai.toolsets`
2+
3+
::: pydantic_ai.toolsets
4+
options:
5+
members:
6+
- AbstractToolset
7+
- CombinedToolset
8+
- DeferredToolset
9+
- FilteredToolset
10+
- FunctionToolset
11+
- PrefixedToolset
12+
- RenamedToolset
13+
- PreparedToolset
14+
- WrapperToolset

docs/mcp/client.md

Lines changed: 61 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -16,42 +16,54 @@ pip/uv-add "pydantic-ai-slim[mcp]"
1616

1717
## Usage
1818

19-
PydanticAI comes with two ways to connect to MCP servers:
19+
PydanticAI comes with three ways to connect to MCP servers:
2020

21-
- [`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] which connects to an MCP server using the [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport
2221
- [`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] which connects to an MCP server using the [Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport
22+
- [`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] which connects to an MCP server using the [HTTP SSE](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) transport
2323
- [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] which runs the server as a subprocess and connects to it using the [stdio](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) transport
2424

25-
Examples of both are shown below; [mcp-run-python](run-python.md) is used as the MCP server in both examples.
25+
Examples of all three are shown below; [mcp-run-python](run-python.md) is used as the MCP server in all examples.
2626

27-
### SSE Client
27+
Each MCP server instance is a [toolset](../toolsets.md) and can be registered with an [`Agent`][pydantic_ai.Agent] using the `toolsets` argument.
2828

29-
[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] connects over HTTP using the [HTTP + Server Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) to a server.
29+
You can use the [`async with agent`][pydantic_ai.Agent.__aenter__] context manager to open and close connections to all registered servers (and in the case of stdio servers, start and stop the subprocesses) around the context where they'll be used in agent runs. You can also use [`async with server`][pydantic_ai.mcp.MCPServer.__aenter__] to manage the connection or subprocess of a specific server, for example if you'd like to use it with multiple agents. If you don't explicitly enter one of these context managers to set up the server, this will be done automatically when it's needed (e.g. to list the available tools or call a specific tool), but it's more efficient to do so around the entire context where you expect the servers to be used.
30+
31+
### Streamable HTTP Client
32+
33+
[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] connects over HTTP using the
34+
[Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport to a server.
3035

3136
!!! note
32-
[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] requires an MCP server to be running and accepting HTTP connections before calling [`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers]. Running the server is not managed by PydanticAI.
37+
[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] requires an MCP server to be
38+
running and accepting HTTP connections before running the agent. Running the server is not
39+
managed by Pydantic AI.
3340

34-
The name "HTTP" is used since this implementation will be adapted in future to use the new
35-
[Streamable HTTP](https://github.com/modelcontextprotocol/specification/pull/206) currently in development.
41+
Before creating the Streamable HTTP client, we need to run a server that supports the Streamable HTTP transport.
3642

37-
Before creating the SSE client, we need to run the server (docs [here](run-python.md)):
43+
```python {title="streamable_http_server.py" py="3.10" dunder_name="not_main"}
44+
from mcp.server.fastmcp import FastMCP
3845

39-
```bash {title="terminal (run sse server)"}
40-
deno run \
41-
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
42-
jsr:@pydantic/mcp-run-python sse
46+
app = FastMCP()
47+
48+
@app.tool()
49+
def add(a: int, b: int) -> int:
50+
return a + b
51+
52+
if __name__ == '__main__':
53+
app.run(transport='streamable-http')
4354
```
4455

45-
```python {title="mcp_sse_client.py" py="3.10"}
46-
from pydantic_ai import Agent
47-
from pydantic_ai.mcp import MCPServerSSE
56+
Then we can create the client:
4857

49-
server = MCPServerSSE(url='http://localhost:3001/sse') # (1)!
50-
agent = Agent('openai:gpt-4o', mcp_servers=[server]) # (2)!
58+
```python {title="mcp_streamable_http_client.py" py="3.10"}
59+
from pydantic_ai import Agent
60+
from pydantic_ai.mcp import MCPServerStreamableHTTP
5161

62+
server = MCPServerStreamableHTTP('http://localhost:8000/mcp') # (1)!
63+
agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)!
5264

5365
async def main():
54-
async with agent.run_mcp_servers(): # (3)!
66+
async with agent: # (3)!
5567
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
5668
print(result.output)
5769
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
@@ -85,43 +97,34 @@ Will display as follows:
8597

8698
![Logfire run python code](../img/logfire-run-python-code.png)
8799

88-
### Streamable HTTP Client
100+
### SSE Client
89101

90-
[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] connects over HTTP using the
91-
[Streamable HTTP](https://modelcontextprotocol.io/introduction#streamable-http) transport to a server.
102+
[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] connects over HTTP using the [HTTP + Server Sent Events transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse) to a server.
92103

93104
!!! note
94-
[`MCPServerStreamableHTTP`][pydantic_ai.mcp.MCPServerStreamableHTTP] requires an MCP server to be
95-
running and accepting HTTP connections before calling
96-
[`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers]. Running the server is not
97-
managed by PydanticAI.
98-
99-
Before creating the Streamable HTTP client, we need to run a server that supports the Streamable HTTP transport.
100-
101-
```python {title="streamable_http_server.py" py="3.10" dunder_name="not_main"}
102-
from mcp.server.fastmcp import FastMCP
105+
[`MCPServerSSE`][pydantic_ai.mcp.MCPServerSSE] requires an MCP server to be running and accepting HTTP connections before running the agent. Running the server is not managed by Pydantic AI.
103106

104-
app = FastMCP()
107+
The name "HTTP" is used since this implementation will be adapted in future to use the new
108+
[Streamable HTTP](https://github.com/modelcontextprotocol/specification/pull/206) currently in development.
105109

106-
@app.tool()
107-
def add(a: int, b: int) -> int:
108-
return a + b
110+
Before creating the SSE client, we need to run the server (docs [here](run-python.md)):
109111

110-
if __name__ == '__main__':
111-
app.run(transport='streamable-http')
112+
```bash {title="terminal (run sse server)"}
113+
deno run \
114+
-N -R=node_modules -W=node_modules --node-modules-dir=auto \
115+
jsr:@pydantic/mcp-run-python sse
112116
```
113117

114-
Then we can create the client:
115-
116-
```python {title="mcp_streamable_http_client.py" py="3.10"}
118+
```python {title="mcp_sse_client.py" py="3.10"}
117119
from pydantic_ai import Agent
118-
from pydantic_ai.mcp import MCPServerStreamableHTTP
120+
from pydantic_ai.mcp import MCPServerSSE
121+
122+
server = MCPServerSSE(url='http://localhost:3001/sse') # (1)!
123+
agent = Agent('openai:gpt-4o', toolsets=[server]) # (2)!
119124

120-
server = MCPServerStreamableHTTP('http://localhost:8000/mcp') # (1)!
121-
agent = Agent('openai:gpt-4o', mcp_servers=[server]) # (2)!
122125

123126
async def main():
124-
async with agent.run_mcp_servers(): # (3)!
127+
async with agent: # (3)!
125128
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
126129
print(result.output)
127130
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
@@ -137,9 +140,6 @@ _(This example is complete, it can be run "as is" with Python 3.10+ — you'll n
137140

138141
The other transport offered by MCP is the [stdio transport](https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#stdio) where the server is run as a subprocess and communicates with the client over `stdin` and `stdout`. In this case, you'd use the [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] class.
139142

140-
!!! note
141-
When using [`MCPServerStdio`][pydantic_ai.mcp.MCPServerStdio] servers, the [`agent.run_mcp_servers()`][pydantic_ai.Agent.run_mcp_servers] context manager is responsible for starting and stopping the server.
142-
143143
```python {title="mcp_stdio_client.py" py="3.10"}
144144
from pydantic_ai import Agent
145145
from pydantic_ai.mcp import MCPServerStdio
@@ -156,11 +156,11 @@ server = MCPServerStdio( # (1)!
156156
'stdio',
157157
]
158158
)
159-
agent = Agent('openai:gpt-4o', mcp_servers=[server])
159+
agent = Agent('openai:gpt-4o', toolsets=[server])
160160

161161

162162
async def main():
163-
async with agent.run_mcp_servers():
163+
async with agent:
164164
result = await agent.run('How many days between 2000-01-01 and 2025-03-18?')
165165
print(result.output)
166166
#> There are 9,208 days between January 1, 2000, and March 18, 2025.
@@ -188,23 +188,23 @@ from pydantic_ai.tools import RunContext
188188
async def process_tool_call(
189189
ctx: RunContext[int],
190190
call_tool: CallToolFunc,
191-
tool_name: str,
192-
args: dict[str, Any],
191+
name: str,
192+
tool_args: dict[str, Any],
193193
) -> ToolResult:
194194
"""A tool call processor that passes along the deps."""
195-
return await call_tool(tool_name, args, metadata={'deps': ctx.deps})
195+
return await call_tool(name, tool_args, {'deps': ctx.deps})
196196

197197

198198
server = MCPServerStdio('python', ['mcp_server.py'], process_tool_call=process_tool_call)
199199
agent = Agent(
200200
model=TestModel(call_tools=['echo_deps']),
201201
deps_type=int,
202-
mcp_servers=[server]
202+
toolsets=[server]
203203
)
204204

205205

206206
async def main():
207-
async with agent.run_mcp_servers():
207+
async with agent:
208208
result = await agent.run('Echo with deps set to 42', deps=42)
209209
print(result.output)
210210
#> {"echo_deps":{"echo":"This is an echo message","deps":42}}
@@ -214,15 +214,7 @@ async def main():
214214

215215
When connecting to multiple MCP servers that might provide tools with the same name, you can use the `tool_prefix` parameter to avoid naming conflicts. This parameter adds a prefix to all tool names from a specific server.
216216

217-
### How It Works
218-
219-
- If `tool_prefix` is set, all tools from that server will be prefixed with `{tool_prefix}_`
220-
- When listing tools, the prefixed names are shown to the model
221-
- When calling tools, the prefix is automatically removed before sending the request to the server
222-
223-
This allows you to use multiple servers that might have overlapping tool names without conflicts.
224-
225-
### Example with HTTP Server
217+
This allows you to use multiple servers that might have overlapping tool names without conflicts:
226218

227219
```python {title="mcp_tool_prefix_http_client.py" py="3.10"}
228220
from pydantic_ai import Agent
@@ -242,41 +234,9 @@ calculator_server = MCPServerSSE(
242234
# Both servers might have a tool named 'get_data', but they'll be exposed as:
243235
# - 'weather_get_data'
244236
# - 'calc_get_data'
245-
agent = Agent('openai:gpt-4o', mcp_servers=[weather_server, calculator_server])
246-
```
247-
248-
### Example with Stdio Server
249-
250-
```python {title="mcp_tool_prefix_stdio_client.py" py="3.10"}
251-
from pydantic_ai import Agent
252-
from pydantic_ai.mcp import MCPServerStdio
253-
254-
python_server = MCPServerStdio(
255-
'deno',
256-
args=[
257-
'run',
258-
'-N',
259-
'jsr:@pydantic/mcp-run-python',
260-
'stdio',
261-
],
262-
tool_prefix='py' # Tools will be prefixed with 'py_'
263-
)
264-
265-
js_server = MCPServerStdio(
266-
'node',
267-
args=[
268-
'run',
269-
'mcp-js-server.js',
270-
'stdio',
271-
],
272-
tool_prefix='js' # Tools will be prefixed with 'js_'
273-
)
274-
275-
agent = Agent('openai:gpt-4o', mcp_servers=[python_server, js_server])
237+
agent = Agent('openai:gpt-4o', toolsets=[weather_server, calculator_server])
276238
```
277239

278-
When the model interacts with these servers, it will see the prefixed tool names, but the prefixes will be automatically handled when making tool calls.
279-
280240
## MCP Sampling
281241

282242
!!! info "What is MCP Sampling?"
@@ -312,6 +272,8 @@ Pydantic AI supports sampling as both a client and server. See the [server](./se
312272

313273
Sampling is automatically supported by Pydantic AI agents when they act as a client.
314274

275+
To be able to use sampling, an MCP server instance needs to have a [`sampling_model`][pydantic_ai.mcp.MCPServerStdio.sampling_model] set. This can be done either directly on the server using the constructor keyword argument or the property, or by using [`agent.set_mcp_sampling_model()`][pydantic_ai.Agent.set_mcp_sampling_model] to set the agent's model or one specified as an argument as the sampling model on all MCP servers registered with that agent.
276+
315277
Let's say we have an MCP server that wants to use sampling (in this case to generate an SVG as per the tool arguments).
316278

317279
??? example "Sampling MCP Server"
@@ -359,11 +321,12 @@ from pydantic_ai import Agent
359321
from pydantic_ai.mcp import MCPServerStdio
360322

361323
server = MCPServerStdio(command='python', args=['generate_svg.py'])
362-
agent = Agent('openai:gpt-4o', mcp_servers=[server])
324+
agent = Agent('openai:gpt-4o', toolsets=[server])
363325

364326

365327
async def main():
366-
async with agent.run_mcp_servers():
328+
async with agent:
329+
agent.set_mcp_sampling_model()
367330
result = await agent.run('Create an image of a robot in a punk style.')
368331
print(result.output)
369332
#> Image file written to robot_punk.svg.

docs/models/huggingface.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ agent = Agent(model)
6969
## Custom Hugging Face client
7070

7171
[`HuggingFaceProvider`][pydantic_ai.providers.huggingface.HuggingFaceProvider] also accepts a custom
72-
[`AsyncInferenceClient`][huggingface_hub.AsyncInferenceClient] client via the `hf_client` parameter, so you can customise
72+
[`AsyncInferenceClient`](https://huggingface.co/docs/huggingface_hub/v0.29.3/en/package_reference/inference_client#huggingface_hub.AsyncInferenceClient) client via the `hf_client` parameter, so you can customise
7373
the `headers`, `bill_to` (billing to an HF organization you're a member of), `base_url` etc. as defined in the
7474
[Hugging Face Hub python library docs](https://huggingface.co/docs/huggingface_hub/package_reference/inference_client).
7575

docs/output.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,8 @@ async def hand_off_to_sql_agent(ctx: RunContext, query: str) -> list[Row]:
199199
return output
200200
except UnexpectedModelBehavior as e:
201201
# Bubble up potentially retryable errors to the router agent
202-
if (cause := e.__cause__) and hasattr(cause, 'tool_retry'):
203-
raise ModelRetry(f'SQL agent failed: {cause.tool_retry.content}') from e
202+
if (cause := e.__cause__) and isinstance(cause, ModelRetry):
203+
raise ModelRetry(f'SQL agent failed: {cause.message}') from e
204204
else:
205205
raise
206206

@@ -276,6 +276,8 @@ In the default Tool Output mode, the output JSON schema of each output type (or
276276

277277
If you'd like to change the name of the output tool, pass a custom description to aid the model, or turn on or off strict mode, you can wrap the type(s) in the [`ToolOutput`][pydantic_ai.output.ToolOutput] marker class and provide the appropriate arguments. Note that by default, the description is taken from the docstring specified on a Pydantic model or output function, so specifying it using the marker class is typically not necessary.
278278

279+
To dynamically modify or filter the available output tools during an agent run, you can define an agent-wide `prepare_output_tools` function that will be called ahead of each step of a run. This function should be of type [`ToolsPrepareFunc`][pydantic_ai.tools.ToolsPrepareFunc], which takes the [`RunContext`][pydantic_ai.tools.RunContext] and a list of [`ToolDefinition`][pydantic_ai.tools.ToolDefinition], and returns a new list of tool definitions (or `None` to disable all tools for that step). This is analogous to the [`prepare_tools` function](tools.md#prepare-tools) for non-output tools.
280+
279281
```python {title="tool_output.py"}
280282
from pydantic import BaseModel
281283

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Unless you're really sure you know better, you'll probably want to follow roughl
1010
* If you find yourself typing out long assertions, use [inline-snapshot](https://15r10nk.github.io/inline-snapshot/latest/)
1111
* Similarly, [dirty-equals](https://dirty-equals.helpmanual.io/latest/) can be useful for comparing large data structures
1212
* Use [`TestModel`][pydantic_ai.models.test.TestModel] or [`FunctionModel`][pydantic_ai.models.function.FunctionModel] in place of your actual model to avoid the usage, latency and variability of real LLM calls
13-
* Use [`Agent.override`][pydantic_ai.agent.Agent.override] to replace your model inside your application logic
13+
* Use [`Agent.override`][pydantic_ai.agent.Agent.override] to replace an agent's model, dependencies, or toolsets inside your application logic
1414
* Set [`ALLOW_MODEL_REQUESTS=False`][pydantic_ai.models.ALLOW_MODEL_REQUESTS] globally to block any requests from being made to non-test models accidentally
1515

1616
### Unit testing with `TestModel`

0 commit comments

Comments
 (0)