Skip to content

Commit 7c00439

Browse files
committed
Merge branch 'main' into feat/google-enhanced-json-schema
2 parents 2b9e2d0 + 6fe5146 commit 7c00439

File tree

12 files changed

+1768
-161
lines changed

12 files changed

+1768
-161
lines changed

docs/gateway.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,34 +41,35 @@ print(result.output)
4141
The first known use of "hello, world" was in a 1974 textbook about the C programming language.
4242
"""
4343
```
44-
# Quick Start
44+
45+
## Quick Start
4546

4647
This section contains instructions on how to set up your account and run your app with Pydantic AI Gateway credentials.
4748

48-
## Create an account
49+
### Create an account
4950

5051
Using your GitHub or Google account, sign in at [gateway.pydantic.dev](https://gateway.pydantic.dev).
5152
Choose a name for your organization (or accept the default). You will automatically be assigned the Admin role.
5253

5354
A default project will be created for you. You can choose to use it, or create a new one on the [Projects](https://gateway.pydantic.dev/admin/projects) page.
5455

55-
## Add **Providers**
56+
### Add **Providers**
5657
There are two ways to use Providers in the Pydantic AI Gateway: you can bring your own key (BYOK) or buy inference through the platform.
5758

58-
### Bringing your own API key (BYOK)
59+
#### Bringing your own API key (BYOK)
5960

6061
On the [Providers](https://gateway.pydantic.dev/admin/providers) page, fill in the form to add a provider. Paste your API key into the form under Credentials, and make sure to **select the Project that will be associated to this provider**. It is possible to add multiple keys from the same provider.
6162

62-
### Use Built-in Providers
63+
#### Use Built-in Providers
6364
Go to the Billing page, add a payment method, and purchase $15 in credits to activate built-in providers. This gives you single-key access to all available models from OpenAI, Anthropic, Google Vertex, AWS Bedrock, and Groq.
6465

65-
## Grant access to your team
66+
### Grant access to your team
6667
On the [Users](https://gateway.pydantic.dev/admin/users) page, create an invitation and share the URL with your team to allow them to access the project.
6768

68-
## Create Gateway project keys
69+
### Create Gateway project keys
6970
On the Keys page, Admins can create project keys which are not affected by spending limits. Users can only create personal keys, that will inherit spending caps from both User and Project levels, whichever is more restrictive.
7071

71-
# Usage
72+
## Usage
7273
After setting up your account with the instructions above, you will be able to make an AI model request with the Pydantic AI Gateway.
7374
The code snippets below show how you can use PAIG with different frameworks and SDKs.
7475
You can add `gateway/` as prefix on every known provider that
@@ -85,7 +86,7 @@ Examples of providers and models that can be used are:
8586
| Groq | `groq` | `gateway/groq:openai/gpt-oss-120b` |
8687
| AWS Bedrock | `bedrock` | `gateway/bedrock:amazon.nova-micro-v1:0` |
8788

88-
## Pydantic AI
89+
### Pydantic AI
8990
Before you start, make sure you are on version 1.16 or later of `pydantic-ai`. To update to the latest version run:
9091

9192
=== "uv"
@@ -121,7 +122,7 @@ The first known use of "hello, world" was in a 1974 textbook about the C program
121122
```
122123

123124

124-
## Claude Code
125+
### Claude Code
125126
Before you start, log out of Claude Code using `/logout`.
126127

127128
Set your gateway credentials as environment variables:
@@ -135,9 +136,9 @@ Replace `YOUR_PAIG_TOKEN` with the API key from the Keys page.
135136

136137
Launch Claude Code by typing `claude`. All requests will now route through the Pydantic AI Gateway.
137138

138-
## SDKs
139+
### SDKs
139140

140-
### OpenAI SDK
141+
#### OpenAI SDK
141142

142143
```python {title="openai_sdk.py" test="skip"}
143144
import openai
@@ -155,7 +156,7 @@ print(response.choices[0].message.content)
155156
#> Hello user
156157
```
157158

158-
### Anthropic SDK
159+
#### Anthropic SDK
159160

160161
```python {title="anthropic_sdk.py" test="skip"}
161162
import anthropic

docs/mcp/client.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,37 @@ The configuration file should be a JSON file with an `mcpServers` object contain
187187

188188
We made this decision given that the SSE transport is deprecated.
189189

190+
### Environment Variables
191+
192+
The configuration file supports environment variable expansion using the `${VAR}` and `${VAR:-default}` syntax,
193+
[like Claude Code](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json).
194+
This is useful for keeping sensitive information like API keys or host names out of your configuration files:
195+
196+
```json {title="mcp_config_with_env.json"}
197+
{
198+
"mcpServers": {
199+
"python-runner": {
200+
"command": "${PYTHON_CMD:-python3}",
201+
"args": ["run", "${MCP_MODULE}", "stdio"],
202+
"env": {
203+
"API_KEY": "${MY_API_KEY}"
204+
}
205+
},
206+
"weather-api": {
207+
"url": "https://${SERVER_HOST:-localhost}:${SERVER_PORT:-8080}/sse"
208+
}
209+
}
210+
}
211+
```
212+
213+
When loading this configuration with [`load_mcp_servers()`][pydantic_ai.mcp.load_mcp_servers]:
214+
215+
- `${VAR}` references will be replaced with the corresponding environment variable values.
216+
- `${VAR:-default}` references will use the environment variable value if set, otherwise the default value.
217+
218+
!!! warning
219+
If a referenced environment variable using `${VAR}` syntax is not defined, a `ValueError` will be raised. Use the `${VAR:-default}` syntax to provide a fallback value.
220+
190221
### Usage
191222

192223
```python {title="mcp_config_loader.py" test="skip"}

pydantic_ai_slim/pydantic_ai/durable_exec/temporal/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def configure_worker(self, config: WorkerConfig) -> WorkerConfig:
7575
'httpx',
7676
'anyio',
7777
'httpcore',
78+
# Used by fastmcp via py-key-value-aio
79+
'beartype',
7880
# Imported inside `logfire._internal.json_encoder` when running `logfire.info` inside an activity with attributes to serialize
7981
'attrs',
8082
# Imported inside `logfire._internal.json_schema` when running `logfire.info` inside an activity with attributes to serialize
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from __future__ import annotations
2+
3+
from typing import Literal
4+
5+
from temporalio.workflow import ActivityConfig
6+
7+
from pydantic_ai import ToolsetTool
8+
from pydantic_ai.tools import AgentDepsT, ToolDefinition
9+
from pydantic_ai.toolsets.fastmcp import FastMCPToolset
10+
11+
from ._mcp import TemporalMCPToolset
12+
from ._run_context import TemporalRunContext
13+
14+
15+
class TemporalFastMCPToolset(TemporalMCPToolset[AgentDepsT]):
16+
def __init__(
17+
self,
18+
toolset: FastMCPToolset[AgentDepsT],
19+
*,
20+
activity_name_prefix: str,
21+
activity_config: ActivityConfig,
22+
tool_activity_config: dict[str, ActivityConfig | Literal[False]],
23+
deps_type: type[AgentDepsT],
24+
run_context_type: type[TemporalRunContext[AgentDepsT]] = TemporalRunContext[AgentDepsT],
25+
):
26+
super().__init__(
27+
toolset,
28+
activity_name_prefix=activity_name_prefix,
29+
activity_config=activity_config,
30+
tool_activity_config=tool_activity_config,
31+
deps_type=deps_type,
32+
run_context_type=run_context_type,
33+
)
34+
35+
def tool_for_tool_def(self, tool_def: ToolDefinition) -> ToolsetTool[AgentDepsT]:
36+
assert isinstance(self.wrapped, FastMCPToolset)
37+
return self.wrapped.tool_for_tool_def(tool_def)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from collections.abc import Callable
5+
from dataclasses import dataclass
6+
from typing import Any, Literal
7+
8+
from pydantic import ConfigDict, with_config
9+
from temporalio import activity, workflow
10+
from temporalio.workflow import ActivityConfig
11+
from typing_extensions import Self
12+
13+
from pydantic_ai import ToolsetTool
14+
from pydantic_ai.exceptions import UserError
15+
from pydantic_ai.tools import AgentDepsT, RunContext, ToolDefinition
16+
from pydantic_ai.toolsets import AbstractToolset
17+
18+
from ._run_context import TemporalRunContext
19+
from ._toolset import (
20+
CallToolParams,
21+
CallToolResult,
22+
TemporalWrapperToolset,
23+
)
24+
25+
26+
@dataclass
27+
@with_config(ConfigDict(arbitrary_types_allowed=True))
28+
class _GetToolsParams:
29+
serialized_run_context: Any
30+
31+
32+
class TemporalMCPToolset(TemporalWrapperToolset[AgentDepsT], ABC):
33+
def __init__(
34+
self,
35+
toolset: AbstractToolset[AgentDepsT],
36+
*,
37+
activity_name_prefix: str,
38+
activity_config: ActivityConfig,
39+
tool_activity_config: dict[str, ActivityConfig | Literal[False]],
40+
deps_type: type[AgentDepsT],
41+
run_context_type: type[TemporalRunContext[AgentDepsT]] = TemporalRunContext[AgentDepsT],
42+
):
43+
super().__init__(toolset)
44+
self.activity_config = activity_config
45+
46+
self.tool_activity_config: dict[str, ActivityConfig] = {}
47+
for tool_name, tool_config in tool_activity_config.items():
48+
if tool_config is False:
49+
raise UserError(
50+
f'Temporal activity config for MCP tool {tool_name!r} has been explicitly set to `False` (activity disabled), '
51+
'but MCP tools require the use of IO and so cannot be run outside of an activity.'
52+
)
53+
self.tool_activity_config[tool_name] = tool_config
54+
55+
self.run_context_type = run_context_type
56+
57+
async def get_tools_activity(params: _GetToolsParams, deps: AgentDepsT) -> dict[str, ToolDefinition]:
58+
run_context = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
59+
tools = await self.wrapped.get_tools(run_context)
60+
# ToolsetTool is not serializable as it holds a SchemaValidator (which is also the same for every MCP tool so unnecessary to pass along the wire every time),
61+
# so we just return the ToolDefinitions and wrap them in ToolsetTool outside of the activity.
62+
return {name: tool.tool_def for name, tool in tools.items()}
63+
64+
# Set type hint explicitly so that Temporal can take care of serialization and deserialization
65+
get_tools_activity.__annotations__['deps'] = deps_type
66+
67+
self.get_tools_activity = activity.defn(name=f'{activity_name_prefix}__mcp_server__{self.id}__get_tools')(
68+
get_tools_activity
69+
)
70+
71+
async def call_tool_activity(params: CallToolParams, deps: AgentDepsT) -> CallToolResult:
72+
run_context = self.run_context_type.deserialize_run_context(params.serialized_run_context, deps=deps)
73+
assert isinstance(params.tool_def, ToolDefinition)
74+
return await self._wrap_call_tool_result(
75+
self.wrapped.call_tool(
76+
params.name,
77+
params.tool_args,
78+
run_context,
79+
self.tool_for_tool_def(params.tool_def),
80+
)
81+
)
82+
83+
# Set type hint explicitly so that Temporal can take care of serialization and deserialization
84+
call_tool_activity.__annotations__['deps'] = deps_type
85+
86+
self.call_tool_activity = activity.defn(name=f'{activity_name_prefix}__mcp_server__{self.id}__call_tool')(
87+
call_tool_activity
88+
)
89+
90+
@abstractmethod
91+
def tool_for_tool_def(self, tool_def: ToolDefinition) -> ToolsetTool[AgentDepsT]:
92+
raise NotImplementedError
93+
94+
@property
95+
def temporal_activities(self) -> list[Callable[..., Any]]:
96+
return [self.get_tools_activity, self.call_tool_activity]
97+
98+
async def __aenter__(self) -> Self:
99+
# The wrapped MCPServer enters itself around listing and calling tools
100+
# so we don't need to enter it here (nor could we because we're not inside a Temporal activity).
101+
return self
102+
103+
async def __aexit__(self, *args: Any) -> bool | None:
104+
return None
105+
106+
async def get_tools(self, ctx: RunContext[AgentDepsT]) -> dict[str, ToolsetTool[AgentDepsT]]:
107+
if not workflow.in_workflow():
108+
return await super().get_tools(ctx)
109+
110+
serialized_run_context = self.run_context_type.serialize_run_context(ctx)
111+
tool_defs = await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
112+
activity=self.get_tools_activity,
113+
args=[
114+
_GetToolsParams(serialized_run_context=serialized_run_context),
115+
ctx.deps,
116+
],
117+
**self.activity_config,
118+
)
119+
return {name: self.tool_for_tool_def(tool_def) for name, tool_def in tool_defs.items()}
120+
121+
async def call_tool(
122+
self,
123+
name: str,
124+
tool_args: dict[str, Any],
125+
ctx: RunContext[AgentDepsT],
126+
tool: ToolsetTool[AgentDepsT],
127+
) -> CallToolResult:
128+
if not workflow.in_workflow():
129+
return await super().call_tool(name, tool_args, ctx, tool)
130+
131+
tool_activity_config = self.activity_config | self.tool_activity_config.get(name, {})
132+
serialized_run_context = self.run_context_type.serialize_run_context(ctx)
133+
return self._unwrap_call_tool_result(
134+
await workflow.execute_activity( # pyright: ignore[reportUnknownMemberType]
135+
activity=self.call_tool_activity,
136+
args=[
137+
CallToolParams(
138+
name=name,
139+
tool_args=tool_args,
140+
serialized_run_context=serialized_run_context,
141+
tool_def=tool.tool_def,
142+
),
143+
ctx.deps,
144+
],
145+
**tool_activity_config,
146+
)
147+
)

0 commit comments

Comments
 (0)