temporalio-google-genai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- temporalio_google_genai-0.1.0/LICENSE +21 -0
- temporalio_google_genai-0.1.0/PKG-INFO +318 -0
- temporalio_google_genai-0.1.0/README.md +292 -0
- temporalio_google_genai-0.1.0/pyproject.toml +127 -0
- temporalio_google_genai-0.1.0/pyproject.toml.orig +111 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/__init__.py +119 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_compat.py +24 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_errors.py +16 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_gemini_activity.py +358 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_google_genai_plugin.py +159 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_mcp.py +226 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_models.py +173 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_agents.py +143 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_api_client.py +319 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_async_client.py +291 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_file_search_stores.py +109 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_files.py +217 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_interactions.py +258 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/_temporal_mcp.py +116 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/py.typed +1 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/testing.py +177 -0
- temporalio_google_genai-0.1.0/src/temporalio/google_genai/workflow.py +111 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 temporal.io
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: temporalio-google-genai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Temporal integration for google genai
|
|
5
|
+
Author: Temporal Technologies Inc
|
|
6
|
+
Author-email: Temporal Technologies Inc <sdk@temporal.io>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Dist: temporalio>=1.34.0,<2
|
|
17
|
+
Requires-Dist: google-genai>=2.21.0,<3
|
|
18
|
+
Requires-Dist: pydantic>=2.0.0,<3
|
|
19
|
+
Requires-Dist: mcp>=1.24,<2 ; extra == 'mcp'
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Project-URL: Homepage, https://github.com/temporalio/ai-integrations/tree/main/python/google_genai
|
|
22
|
+
Project-URL: Repository, https://github.com/temporalio/ai-integrations
|
|
23
|
+
Project-URL: Documentation, https://docs.temporal.io/develop/python/integrations/google-genai
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# Google Gemini SDK Integration for Temporal
|
|
28
|
+
|
|
29
|
+
> Release stage: [Public Preview](https://docs.temporal.io/develop/python/integrations/google-genai).
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
This plugin lets you use the [Google Gemini SDK](https://googleapis.github.io/python-genai/)
|
|
34
|
+
(`google-genai`) inside Temporal workflows with durable execution. Every Gemini
|
|
35
|
+
API call becomes a **Temporal activity**, so model calls, tool calls, file
|
|
36
|
+
operations, interactions, and managed agents are retried, recorded in history,
|
|
37
|
+
and survive worker restarts.
|
|
38
|
+
|
|
39
|
+
Key properties:
|
|
40
|
+
|
|
41
|
+
- **Credentials never enter the workflow.** The real `genai.Client` lives only
|
|
42
|
+
on the worker, inside activities; no API keys or tokens appear in event
|
|
43
|
+
history.
|
|
44
|
+
- **The SDK's automatic function calling (AFC) loop runs in the workflow**, so
|
|
45
|
+
tool wrappers (`activity_as_tool`) work naturally — no manual agent loop.
|
|
46
|
+
- **Temporal owns retries.** Configure them via the activity `retry_policy`; the
|
|
47
|
+
SDK's own retry loop is rejected to avoid double-retry (see
|
|
48
|
+
[Retries & errors](#retries--errors)).
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv add temporalio-google-genai
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Hello World
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import os
|
|
60
|
+
from datetime import timedelta
|
|
61
|
+
|
|
62
|
+
from google import genai
|
|
63
|
+
from google.genai import types
|
|
64
|
+
|
|
65
|
+
from temporalio import activity, workflow
|
|
66
|
+
from temporalio.client import Client
|
|
67
|
+
from temporalio.google_genai import (
|
|
68
|
+
GoogleGenAIPlugin,
|
|
69
|
+
TemporalAsyncClient,
|
|
70
|
+
activity_as_tool,
|
|
71
|
+
)
|
|
72
|
+
from temporalio.worker import Worker
|
|
73
|
+
from temporalio.workflow import ActivityConfig
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---- a tool, as a normal Temporal activity (runs on the worker) ----
|
|
77
|
+
@activity.defn
|
|
78
|
+
async def get_weather(city: str) -> str:
|
|
79
|
+
return f"It's sunny in {city}."
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---- the workflow (runs in the Temporal sandbox) ----
|
|
83
|
+
@workflow.defn
|
|
84
|
+
class WeatherAgent:
|
|
85
|
+
@workflow.run
|
|
86
|
+
async def run(self, prompt: str) -> str:
|
|
87
|
+
client = TemporalAsyncClient()
|
|
88
|
+
response = await client.models.generate_content(
|
|
89
|
+
model="gemini-2.5-flash",
|
|
90
|
+
contents=prompt,
|
|
91
|
+
config=types.GenerateContentConfig(
|
|
92
|
+
tools=[
|
|
93
|
+
activity_as_tool(
|
|
94
|
+
get_weather,
|
|
95
|
+
activity_config=ActivityConfig(
|
|
96
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
],
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
return response.text or ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---- worker setup (outside the sandbox: real client + credentials) ----
|
|
106
|
+
async def main() -> None:
|
|
107
|
+
gemini = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
|
|
108
|
+
plugin = GoogleGenAIPlugin(gemini)
|
|
109
|
+
|
|
110
|
+
client = await Client.connect("localhost:7233", plugins=[plugin])
|
|
111
|
+
async with Worker(
|
|
112
|
+
client,
|
|
113
|
+
task_queue="gemini",
|
|
114
|
+
workflows=[WeatherAgent],
|
|
115
|
+
activities=[get_weather],
|
|
116
|
+
):
|
|
117
|
+
result = await client.execute_workflow(
|
|
118
|
+
WeatherAgent.run,
|
|
119
|
+
"What's the weather in Tokyo?",
|
|
120
|
+
id="weather-1",
|
|
121
|
+
task_queue="gemini",
|
|
122
|
+
)
|
|
123
|
+
print(result)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Construct `TemporalAsyncClient` **inside** the workflow; construct the real
|
|
127
|
+
`genai.Client` and `GoogleGenAIPlugin` **on the worker**.
|
|
128
|
+
|
|
129
|
+
## What this plugin gives you
|
|
130
|
+
|
|
131
|
+
| Surface | Workflow API | Runs as |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| Model calls | `client.models.generate_content` / `generate_content_stream` | activity (AFC loop in workflow) |
|
|
134
|
+
| Tools | `activity_as_tool(fn, ...)` | one activity per tool call |
|
|
135
|
+
| Files | `client.files.upload` / `download` | activity |
|
|
136
|
+
| File search | `client.file_search_stores.upload_to_file_search_store` | activity |
|
|
137
|
+
| Interactions | `client.interactions.create` / `get` / `cancel` / `delete` | whole-operation activity |
|
|
138
|
+
| Managed agents | `client.agents.create` / `get` / `list` / `delete` | whole-operation activity |
|
|
139
|
+
| MCP (client-side) | `TemporalMcpClientSession(name)` in `tools=[...]` | `list_tools` / `call_tool` activities |
|
|
140
|
+
|
|
141
|
+
Streamed responses are batched: the activity drains the stream and the workflow
|
|
142
|
+
iterates the collected chunks/events. `client.webhooks` is not supported in
|
|
143
|
+
workflows and raises.
|
|
144
|
+
|
|
145
|
+
## Tool calling
|
|
146
|
+
|
|
147
|
+
`activity_as_tool` wraps any `@activity.defn` function as a Gemini tool. When the
|
|
148
|
+
model calls it, the AFC loop (running in the workflow) dispatches it as a
|
|
149
|
+
durable activity:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
activity_as_tool(
|
|
153
|
+
get_weather,
|
|
154
|
+
activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
A timeout is required — `activity_config` must set `start_to_close_timeout` or
|
|
159
|
+
`schedule_to_close_timeout` (Temporal needs one; there is no default for tools).
|
|
160
|
+
|
|
161
|
+
## MCP support
|
|
162
|
+
|
|
163
|
+
Install the optional dependency for client-side MCP:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
uv add "temporalio-google-genai[mcp]"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
This adapter requires MCP Python SDK v1. The base package does not require MCP
|
|
170
|
+
and can be installed alongside MCP v2 integrations. Server-side MCP on Vertex AI
|
|
171
|
+
and the Interactions API do not require this extra.
|
|
172
|
+
|
|
173
|
+
Client-side MCP (Gemini Developer API) is wired through the plugin: register the
|
|
174
|
+
server on the worker and reference it by name in the workflow.
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
from contextlib import asynccontextmanager
|
|
178
|
+
import sys
|
|
179
|
+
|
|
180
|
+
from mcp import ClientSession, StdioServerParameters
|
|
181
|
+
from mcp.client.stdio import stdio_client
|
|
182
|
+
|
|
183
|
+
from temporalio.google_genai import TemporalMcpClientSession
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---- worker: a factory yielding a connected, initialized session ----
|
|
187
|
+
@asynccontextmanager
|
|
188
|
+
async def weather_mcp():
|
|
189
|
+
params = StdioServerParameters(command=sys.executable, args=["weather_server.py"])
|
|
190
|
+
async with stdio_client(params) as (read, write):
|
|
191
|
+
async with ClientSession(read, write) as session:
|
|
192
|
+
await session.initialize()
|
|
193
|
+
yield session
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
plugin = GoogleGenAIPlugin(
|
|
197
|
+
genai.Client(api_key=os.environ["GOOGLE_API_KEY"]),
|
|
198
|
+
mcp_servers={"weather": weather_mcp},
|
|
199
|
+
mcp_connection_idle_timeout=timedelta(minutes=5),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---- workflow: reference the server by name in the tools list ----
|
|
204
|
+
@workflow.defn
|
|
205
|
+
class McpAgent:
|
|
206
|
+
@workflow.run
|
|
207
|
+
async def run(self, prompt: str) -> str:
|
|
208
|
+
client = TemporalAsyncClient()
|
|
209
|
+
session = TemporalMcpClientSession(
|
|
210
|
+
"weather",
|
|
211
|
+
activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
|
|
212
|
+
)
|
|
213
|
+
response = await client.models.generate_content(
|
|
214
|
+
model="gemini-2.5-flash",
|
|
215
|
+
contents=prompt,
|
|
216
|
+
config=types.GenerateContentConfig(tools=[session]),
|
|
217
|
+
)
|
|
218
|
+
return response.text or ""
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The MCP connection lives on the worker (pooled, idle-evicted); the workflow only
|
|
222
|
+
carries the server name. Tool discovery and calls run as `{name}-list-tools` /
|
|
223
|
+
`{name}-call-tool` activities, so the full tool parameter schema reaches the
|
|
224
|
+
model. Set `cache_tools=True` to list a server's tools once per workflow instead
|
|
225
|
+
of per turn.
|
|
226
|
+
|
|
227
|
+
## Streaming
|
|
228
|
+
|
|
229
|
+
`generate_content_stream` works as usual — the workflow iterates chunks (batched
|
|
230
|
+
from the activity). To let an **external** consumer (a chat UI) observe chunks in
|
|
231
|
+
real time while the workflow runs durably, set `streaming_topic` on the client
|
|
232
|
+
and host a [`WorkflowStream`](https://github.com/temporalio/sdk-python/tree/6adc0d84290a79952dee3ef02c36f6ed9334874a/temporalio/contrib/workflow_streams/) in the workflow. Each
|
|
233
|
+
streamed `GenerateContentResponse` is published to that topic as it arrives:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from temporalio.contrib.workflow_streams import WorkflowStream
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@workflow.defn
|
|
240
|
+
class StreamingAgent:
|
|
241
|
+
@workflow.init
|
|
242
|
+
def __init__(self, prompt: str) -> None:
|
|
243
|
+
self.stream = WorkflowStream() # required when streaming_topic is set
|
|
244
|
+
|
|
245
|
+
@workflow.run
|
|
246
|
+
async def run(self, prompt: str) -> str:
|
|
247
|
+
client = TemporalAsyncClient(streaming_topic="gemini")
|
|
248
|
+
text = []
|
|
249
|
+
async for chunk in await client.models.generate_content_stream(
|
|
250
|
+
model="gemini-2.5-flash", contents=prompt,
|
|
251
|
+
):
|
|
252
|
+
text.append(chunk.text or "")
|
|
253
|
+
return "".join(text)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Consume the stream from outside the workflow:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from temporalio.contrib.workflow_streams import WorkflowStreamClient
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def consume(client, workflow_id):
|
|
263
|
+
stream = WorkflowStreamClient.create(client, workflow_id)
|
|
264
|
+
async for item in stream.subscribe(
|
|
265
|
+
["gemini"], result_type=types.GenerateContentResponse,
|
|
266
|
+
):
|
|
267
|
+
print(item.data.text, end="", flush=True)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The workflow's own iteration is unchanged (it still receives batched chunks for
|
|
271
|
+
the SDK to parse); the topic is purely for external real-time observation. If
|
|
272
|
+
`streaming_topic` is set but the workflow hosts no `WorkflowStream`, the call
|
|
273
|
+
raises `GoogleGenAIError`. Tune flush cadence with
|
|
274
|
+
`TemporalAsyncClient(streaming_topic=..., streaming_batch_interval=...)`
|
|
275
|
+
(default 100ms).
|
|
276
|
+
|
|
277
|
+
## Retries & errors
|
|
278
|
+
|
|
279
|
+
Temporal owns retries. Configure them with the activity `retry_policy` via
|
|
280
|
+
`activity_config`. The plugin **rejects** the SDK's own retry config so retries
|
|
281
|
+
don't compound:
|
|
282
|
+
|
|
283
|
+
- Constructing the plugin with a `genai.Client` that has
|
|
284
|
+
`http_options.retry_options` raises `ValueError`.
|
|
285
|
+
- Setting `http_options.retry_options` on a per-request call raises
|
|
286
|
+
`GoogleGenAIError`.
|
|
287
|
+
|
|
288
|
+
API-call activities classify failures: transient statuses (408, 429, 5xx) stay
|
|
289
|
+
retryable (the activity's `retry_policy` applies); other statuses (e.g. 4xx) are
|
|
290
|
+
non-retryable so the workflow fails fast.
|
|
291
|
+
|
|
292
|
+
## Vertex AI
|
|
293
|
+
|
|
294
|
+
Pass `vertexai=True` to both the worker-side `genai.Client` and the
|
|
295
|
+
workflow-side `TemporalAsyncClient`. On the workflow side you must also set
|
|
296
|
+
`project` and `location` **explicitly**:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
# worker
|
|
300
|
+
genai.Client(vertexai=True, project="my-project", location="us-central1")
|
|
301
|
+
|
|
302
|
+
# workflow
|
|
303
|
+
TemporalAsyncClient(vertexai=True, project="my-project", location="us-central1")
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Normally the SDK auto-discovers `project`/`location` from the environment
|
|
307
|
+
(credentials, ADC, metadata server). That discovery
|
|
308
|
+
would be non-deterministic and break replay. Setting them by hand
|
|
309
|
+
keeps it deterministic.
|
|
310
|
+
|
|
311
|
+
## Composing with other plugins
|
|
312
|
+
|
|
313
|
+
`GoogleGenAIPlugin` is a `temporalio.plugin.SimplePlugin`; pass it in the
|
|
314
|
+
`plugins=[...]` list alongside others (e.g. OpenTelemetry). It contributes a
|
|
315
|
+
Pydantic data converter, the Gemini activities, a sandbox-passthrough config for
|
|
316
|
+
`google.genai` (and `mcp`), and registers `GoogleGenAIError` as a workflow
|
|
317
|
+
failure type. When composing data converters, construct the plugins so their
|
|
318
|
+
converters are compatible.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# Google Gemini SDK Integration for Temporal
|
|
2
|
+
|
|
3
|
+
> Release stage: [Public Preview](https://docs.temporal.io/develop/python/integrations/google-genai).
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin lets you use the [Google Gemini SDK](https://googleapis.github.io/python-genai/)
|
|
8
|
+
(`google-genai`) inside Temporal workflows with durable execution. Every Gemini
|
|
9
|
+
API call becomes a **Temporal activity**, so model calls, tool calls, file
|
|
10
|
+
operations, interactions, and managed agents are retried, recorded in history,
|
|
11
|
+
and survive worker restarts.
|
|
12
|
+
|
|
13
|
+
Key properties:
|
|
14
|
+
|
|
15
|
+
- **Credentials never enter the workflow.** The real `genai.Client` lives only
|
|
16
|
+
on the worker, inside activities; no API keys or tokens appear in event
|
|
17
|
+
history.
|
|
18
|
+
- **The SDK's automatic function calling (AFC) loop runs in the workflow**, so
|
|
19
|
+
tool wrappers (`activity_as_tool`) work naturally — no manual agent loop.
|
|
20
|
+
- **Temporal owns retries.** Configure them via the activity `retry_policy`; the
|
|
21
|
+
SDK's own retry loop is rejected to avoid double-retry (see
|
|
22
|
+
[Retries & errors](#retries--errors)).
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add temporalio-google-genai
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Hello World
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import os
|
|
34
|
+
from datetime import timedelta
|
|
35
|
+
|
|
36
|
+
from google import genai
|
|
37
|
+
from google.genai import types
|
|
38
|
+
|
|
39
|
+
from temporalio import activity, workflow
|
|
40
|
+
from temporalio.client import Client
|
|
41
|
+
from temporalio.google_genai import (
|
|
42
|
+
GoogleGenAIPlugin,
|
|
43
|
+
TemporalAsyncClient,
|
|
44
|
+
activity_as_tool,
|
|
45
|
+
)
|
|
46
|
+
from temporalio.worker import Worker
|
|
47
|
+
from temporalio.workflow import ActivityConfig
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---- a tool, as a normal Temporal activity (runs on the worker) ----
|
|
51
|
+
@activity.defn
|
|
52
|
+
async def get_weather(city: str) -> str:
|
|
53
|
+
return f"It's sunny in {city}."
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---- the workflow (runs in the Temporal sandbox) ----
|
|
57
|
+
@workflow.defn
|
|
58
|
+
class WeatherAgent:
|
|
59
|
+
@workflow.run
|
|
60
|
+
async def run(self, prompt: str) -> str:
|
|
61
|
+
client = TemporalAsyncClient()
|
|
62
|
+
response = await client.models.generate_content(
|
|
63
|
+
model="gemini-2.5-flash",
|
|
64
|
+
contents=prompt,
|
|
65
|
+
config=types.GenerateContentConfig(
|
|
66
|
+
tools=[
|
|
67
|
+
activity_as_tool(
|
|
68
|
+
get_weather,
|
|
69
|
+
activity_config=ActivityConfig(
|
|
70
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
return response.text or ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---- worker setup (outside the sandbox: real client + credentials) ----
|
|
80
|
+
async def main() -> None:
|
|
81
|
+
gemini = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
|
|
82
|
+
plugin = GoogleGenAIPlugin(gemini)
|
|
83
|
+
|
|
84
|
+
client = await Client.connect("localhost:7233", plugins=[plugin])
|
|
85
|
+
async with Worker(
|
|
86
|
+
client,
|
|
87
|
+
task_queue="gemini",
|
|
88
|
+
workflows=[WeatherAgent],
|
|
89
|
+
activities=[get_weather],
|
|
90
|
+
):
|
|
91
|
+
result = await client.execute_workflow(
|
|
92
|
+
WeatherAgent.run,
|
|
93
|
+
"What's the weather in Tokyo?",
|
|
94
|
+
id="weather-1",
|
|
95
|
+
task_queue="gemini",
|
|
96
|
+
)
|
|
97
|
+
print(result)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Construct `TemporalAsyncClient` **inside** the workflow; construct the real
|
|
101
|
+
`genai.Client` and `GoogleGenAIPlugin` **on the worker**.
|
|
102
|
+
|
|
103
|
+
## What this plugin gives you
|
|
104
|
+
|
|
105
|
+
| Surface | Workflow API | Runs as |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| Model calls | `client.models.generate_content` / `generate_content_stream` | activity (AFC loop in workflow) |
|
|
108
|
+
| Tools | `activity_as_tool(fn, ...)` | one activity per tool call |
|
|
109
|
+
| Files | `client.files.upload` / `download` | activity |
|
|
110
|
+
| File search | `client.file_search_stores.upload_to_file_search_store` | activity |
|
|
111
|
+
| Interactions | `client.interactions.create` / `get` / `cancel` / `delete` | whole-operation activity |
|
|
112
|
+
| Managed agents | `client.agents.create` / `get` / `list` / `delete` | whole-operation activity |
|
|
113
|
+
| MCP (client-side) | `TemporalMcpClientSession(name)` in `tools=[...]` | `list_tools` / `call_tool` activities |
|
|
114
|
+
|
|
115
|
+
Streamed responses are batched: the activity drains the stream and the workflow
|
|
116
|
+
iterates the collected chunks/events. `client.webhooks` is not supported in
|
|
117
|
+
workflows and raises.
|
|
118
|
+
|
|
119
|
+
## Tool calling
|
|
120
|
+
|
|
121
|
+
`activity_as_tool` wraps any `@activity.defn` function as a Gemini tool. When the
|
|
122
|
+
model calls it, the AFC loop (running in the workflow) dispatches it as a
|
|
123
|
+
durable activity:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
activity_as_tool(
|
|
127
|
+
get_weather,
|
|
128
|
+
activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A timeout is required — `activity_config` must set `start_to_close_timeout` or
|
|
133
|
+
`schedule_to_close_timeout` (Temporal needs one; there is no default for tools).
|
|
134
|
+
|
|
135
|
+
## MCP support
|
|
136
|
+
|
|
137
|
+
Install the optional dependency for client-side MCP:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv add "temporalio-google-genai[mcp]"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This adapter requires MCP Python SDK v1. The base package does not require MCP
|
|
144
|
+
and can be installed alongside MCP v2 integrations. Server-side MCP on Vertex AI
|
|
145
|
+
and the Interactions API do not require this extra.
|
|
146
|
+
|
|
147
|
+
Client-side MCP (Gemini Developer API) is wired through the plugin: register the
|
|
148
|
+
server on the worker and reference it by name in the workflow.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from contextlib import asynccontextmanager
|
|
152
|
+
import sys
|
|
153
|
+
|
|
154
|
+
from mcp import ClientSession, StdioServerParameters
|
|
155
|
+
from mcp.client.stdio import stdio_client
|
|
156
|
+
|
|
157
|
+
from temporalio.google_genai import TemporalMcpClientSession
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---- worker: a factory yielding a connected, initialized session ----
|
|
161
|
+
@asynccontextmanager
|
|
162
|
+
async def weather_mcp():
|
|
163
|
+
params = StdioServerParameters(command=sys.executable, args=["weather_server.py"])
|
|
164
|
+
async with stdio_client(params) as (read, write):
|
|
165
|
+
async with ClientSession(read, write) as session:
|
|
166
|
+
await session.initialize()
|
|
167
|
+
yield session
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
plugin = GoogleGenAIPlugin(
|
|
171
|
+
genai.Client(api_key=os.environ["GOOGLE_API_KEY"]),
|
|
172
|
+
mcp_servers={"weather": weather_mcp},
|
|
173
|
+
mcp_connection_idle_timeout=timedelta(minutes=5),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ---- workflow: reference the server by name in the tools list ----
|
|
178
|
+
@workflow.defn
|
|
179
|
+
class McpAgent:
|
|
180
|
+
@workflow.run
|
|
181
|
+
async def run(self, prompt: str) -> str:
|
|
182
|
+
client = TemporalAsyncClient()
|
|
183
|
+
session = TemporalMcpClientSession(
|
|
184
|
+
"weather",
|
|
185
|
+
activity_config=ActivityConfig(start_to_close_timeout=timedelta(seconds=30)),
|
|
186
|
+
)
|
|
187
|
+
response = await client.models.generate_content(
|
|
188
|
+
model="gemini-2.5-flash",
|
|
189
|
+
contents=prompt,
|
|
190
|
+
config=types.GenerateContentConfig(tools=[session]),
|
|
191
|
+
)
|
|
192
|
+
return response.text or ""
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The MCP connection lives on the worker (pooled, idle-evicted); the workflow only
|
|
196
|
+
carries the server name. Tool discovery and calls run as `{name}-list-tools` /
|
|
197
|
+
`{name}-call-tool` activities, so the full tool parameter schema reaches the
|
|
198
|
+
model. Set `cache_tools=True` to list a server's tools once per workflow instead
|
|
199
|
+
of per turn.
|
|
200
|
+
|
|
201
|
+
## Streaming
|
|
202
|
+
|
|
203
|
+
`generate_content_stream` works as usual — the workflow iterates chunks (batched
|
|
204
|
+
from the activity). To let an **external** consumer (a chat UI) observe chunks in
|
|
205
|
+
real time while the workflow runs durably, set `streaming_topic` on the client
|
|
206
|
+
and host a [`WorkflowStream`](https://github.com/temporalio/sdk-python/tree/6adc0d84290a79952dee3ef02c36f6ed9334874a/temporalio/contrib/workflow_streams/) in the workflow. Each
|
|
207
|
+
streamed `GenerateContentResponse` is published to that topic as it arrives:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from temporalio.contrib.workflow_streams import WorkflowStream
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@workflow.defn
|
|
214
|
+
class StreamingAgent:
|
|
215
|
+
@workflow.init
|
|
216
|
+
def __init__(self, prompt: str) -> None:
|
|
217
|
+
self.stream = WorkflowStream() # required when streaming_topic is set
|
|
218
|
+
|
|
219
|
+
@workflow.run
|
|
220
|
+
async def run(self, prompt: str) -> str:
|
|
221
|
+
client = TemporalAsyncClient(streaming_topic="gemini")
|
|
222
|
+
text = []
|
|
223
|
+
async for chunk in await client.models.generate_content_stream(
|
|
224
|
+
model="gemini-2.5-flash", contents=prompt,
|
|
225
|
+
):
|
|
226
|
+
text.append(chunk.text or "")
|
|
227
|
+
return "".join(text)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Consume the stream from outside the workflow:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from temporalio.contrib.workflow_streams import WorkflowStreamClient
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def consume(client, workflow_id):
|
|
237
|
+
stream = WorkflowStreamClient.create(client, workflow_id)
|
|
238
|
+
async for item in stream.subscribe(
|
|
239
|
+
["gemini"], result_type=types.GenerateContentResponse,
|
|
240
|
+
):
|
|
241
|
+
print(item.data.text, end="", flush=True)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The workflow's own iteration is unchanged (it still receives batched chunks for
|
|
245
|
+
the SDK to parse); the topic is purely for external real-time observation. If
|
|
246
|
+
`streaming_topic` is set but the workflow hosts no `WorkflowStream`, the call
|
|
247
|
+
raises `GoogleGenAIError`. Tune flush cadence with
|
|
248
|
+
`TemporalAsyncClient(streaming_topic=..., streaming_batch_interval=...)`
|
|
249
|
+
(default 100ms).
|
|
250
|
+
|
|
251
|
+
## Retries & errors
|
|
252
|
+
|
|
253
|
+
Temporal owns retries. Configure them with the activity `retry_policy` via
|
|
254
|
+
`activity_config`. The plugin **rejects** the SDK's own retry config so retries
|
|
255
|
+
don't compound:
|
|
256
|
+
|
|
257
|
+
- Constructing the plugin with a `genai.Client` that has
|
|
258
|
+
`http_options.retry_options` raises `ValueError`.
|
|
259
|
+
- Setting `http_options.retry_options` on a per-request call raises
|
|
260
|
+
`GoogleGenAIError`.
|
|
261
|
+
|
|
262
|
+
API-call activities classify failures: transient statuses (408, 429, 5xx) stay
|
|
263
|
+
retryable (the activity's `retry_policy` applies); other statuses (e.g. 4xx) are
|
|
264
|
+
non-retryable so the workflow fails fast.
|
|
265
|
+
|
|
266
|
+
## Vertex AI
|
|
267
|
+
|
|
268
|
+
Pass `vertexai=True` to both the worker-side `genai.Client` and the
|
|
269
|
+
workflow-side `TemporalAsyncClient`. On the workflow side you must also set
|
|
270
|
+
`project` and `location` **explicitly**:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# worker
|
|
274
|
+
genai.Client(vertexai=True, project="my-project", location="us-central1")
|
|
275
|
+
|
|
276
|
+
# workflow
|
|
277
|
+
TemporalAsyncClient(vertexai=True, project="my-project", location="us-central1")
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Normally the SDK auto-discovers `project`/`location` from the environment
|
|
281
|
+
(credentials, ADC, metadata server). That discovery
|
|
282
|
+
would be non-deterministic and break replay. Setting them by hand
|
|
283
|
+
keeps it deterministic.
|
|
284
|
+
|
|
285
|
+
## Composing with other plugins
|
|
286
|
+
|
|
287
|
+
`GoogleGenAIPlugin` is a `temporalio.plugin.SimplePlugin`; pass it in the
|
|
288
|
+
`plugins=[...]` list alongside others (e.g. OpenTelemetry). It contributes a
|
|
289
|
+
Pydantic data converter, the Gemini activities, a sandbox-passthrough config for
|
|
290
|
+
`google.genai` (and `mcp`), and registers `GoogleGenAIError` as a workflow
|
|
291
|
+
failure type. When composing data converters, construct the plugins so their
|
|
292
|
+
converters are compatible.
|