Commit 4ec9ab0
Add First-Class Google Gen AI SDK Integration to Contrib (#1378)
* Add First-Class Gemini SDK Integration to Contrib
# Temporal Integration for the Google Gemini SDK
This adds a first-class integration that lets users call the Gemini SDK's `AsyncClient` directly from within Temporal workflows. Every API call and tool invocation becomes a durable Temporal activity — giving full crash recovery, visibility in workflow event history, and replay safety — while keeping credentials entirely on the worker side.
## How it works
The integration shims three layers of the Gemini SDK so that workflows can use `client.models`, `client.files`, `client.file_search_stores`, `client.chats`, and all other SDK modules naturally:
### `TemporalApiClient` (`_temporal_api_client.py`)
A `BaseApiClient` subclass that replaces the SDK's HTTP layer. Instead of making network calls, `async_request` and `async_request_streamed` serialize the request and dispatch it through `workflow.execute_activity`. The real HTTP call happens inside the activity on the worker, where the actual `genai.Client` with real credentials lives. Sync methods raise immediately. Per-request `http_options` are validated (non-serializable fields like `httpx_client` are rejected), and `timeout` is mapped to Temporal's `start_to_close_timeout`.
### `TemporalAsyncFiles` / `TemporalAsyncFileSearchStores` (`_temporal_files.py`, `_temporal_file_search_stores.py`)
Subclasses of `AsyncFiles` and `AsyncFileSearchStores` that override `upload`, `download`, `register_files`, and `upload_to_file_search_store` to dispatch the entire operation as a Temporal activity. This avoids filesystem access (`os` module) and credential token refresh in the workflow sandbox. Methods like `get`, `delete`, `list` are inherited and work through the `TemporalApiClient`'s `async_request` activity. File uploads accept `str` paths (resolved on the worker), `os.PathLike`, or `io.IOBase` (bytes serialized across the activity boundary).
### `TemporalAsyncClient` (`_temporal_async_client.py`)
An `AsyncClient` subclass that wires in `TemporalAsyncFiles` and `TemporalAsyncFileSearchStores`. All other SDK modules (`models`, `tunings`, `caches`, `batches`, `live`, `tokens`, `operations`) are inherited unchanged since they only use `async_request` under the hood.
### `GeminiPlugin` (`_gemini_plugin.py`)
A `SimplePlugin` that registers all activities, configures the Pydantic data converter, and passes `google.genai` through the workflow sandbox. Users pass a fully configured `genai.Client` — the plugin never constructs one itself. An optional `extra_credentials` parameter supports operations like `register_files` that need separate GCS credentials.
### `activity_as_tool` (`workflow.py`)
Wraps any `@activity.defn` function so it looks like a regular async callable to Gemini's automatic function calling (AFC). When the model decides to call the tool, the SDK invokes the wrapper, which dispatches through `workflow.execute_activity`. Users can also pass plain workflow methods directly as tools — these run in-workflow without an activity.
### Batched streaming
`generate_content_stream` is supported via a batched approach: the `async_request_streamed` activity collects all chunks from the real streaming response and returns them as a list. The workflow-side `TemporalApiClient` yields them back as an async generator so the SDK sees the expected interface.
## Usage
```python
# Worker side
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
plugin = GeminiPlugin(client)
# Workflow side
@workflow.defn
class MyWorkflow:
@workflow.run
async def run(self, query: str) -> str:
client = gemini_client()
response = await client.models.generate_content(
model="gemini-2.5-flash",
contents=query,
config=types.GenerateContentConfig(
tools=[activity_as_tool(my_tool)],
),
)
return response.text
```
## Testing
31 integration tests covering:
- Basic `generate_content` and multi-chunk streaming
- AFC tool calling (single-arg, multi-arg, workflow methods, sequential multi-tool, failure propagation)
- Per-request `http_options` propagation (headers, api_version, base_url)
- File upload via str path and `io.BytesIO`, file download
- File search store upload
- Multi-turn chat via `client.chats`
- `TemporalAsyncClient` wiring verification
- `TemporalApiClient` error paths (sync raises, low-level upload/download raises)
- `activity_as_tool` validation and signature preservation
- A full end-to-end integration test that exercises all real activity implementations (generate, stream, file upload, download, store upload, RAG query, store delete) with a mocked `genai.Client` — ensuring the actual activity code in `_gemini_activity.py` is covered, not just the workflow-side shims.
* update lock
* address PR feedback
* upper bound google-genai dep
* move to `.../contrib/google_genai/`
* rename `gemini_client` -> `google_genai_client`
* Rename `GeminiPlugin` -> `GoogleGenAIPlugin`
* add to codeowners
* Fix docstring errors breaking poe gen-docs
pydoctor runs with warnings-as-errors: the bullet list needed a blank
line before it, and the :func: reference must use the unqualified name
since pydoctor relocates __all__ re-exports to the package page.
* google_genai: add MCP support, interactions/agents, and durability tests
Client-side MCP (Gemini Developer API): TemporalMcpClientSession subclasses
mcp.ClientSession and routes list_tools/call_tool through {server}-list-tools
and {server}-call-tool activities, so the SDK's in-workflow AFC loop drives MCP
tools while the real session lives on the worker. Servers register on the
plugin via mcp_servers={name: factory} with a pooled, idle-evicted worker-side
connection (mcp_connection_idle_timeout). Server-side MCP (Vertex McpServer
config and Interactions API MCP steps) flows through unchanged as data.
Also in this change:
- Interactions API and managed agents support, plus files/file-search activities
- Collapse activity_config defaults to a single documented 60s start_to_close
- activity_as_tool requires an explicit timeout (matches openai_agents/strands)
- Replay and side-effect (ActivityTaskScheduled count) tests, incl. MCP
- mcp is an optional dep: lazy import + TYPE_CHECKING so the package imports
without it; declared in the dev group for tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: add GoogleGenAIError and make Temporal own retries
- Add GoogleGenAIError(ApplicationError) and register it via the plugin's
workflow_failure_exception_types so it terminally fails the workflow rather
than retrying the task; activity_as_tool validation now raises it.
- Reject the SDK's own retry config instead of silently overriding it: the
plugin raises ValueError if the genai.Client has http_options.retry_options,
and the workflow-side client raises GoogleGenAIError on per-request
retry_options (previously dropped silently). Both point users to the
activity retry_policy via activity_config.
- Classify API-call activity errors: catch google.genai.errors.APIError and
re-raise as ApplicationError with non_retryable set by HTTP status (408/429/5xx
retryable, 4xx fail fast), with the SDK error class name as the type.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: add README, fix plugin name, document determinism, widen passthrough
- Add a user-facing README (overview, install, hello world, tool calling, MCP,
retries/errors, Vertex, composing).
- Rename the plugin to the conventional "google_genai.GoogleGenAIPlugin".
- Document the replay-determinism survey: the generate_content/AFC/MCP paths are
replay-safe; note the one in-workflow caveat (Vertex batches.create auto-naming).
- Add pydantic_core and annotated_types to sandbox passthrough so the SDK's
in-workflow Pydantic validation doesn't reimport them after workflow load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: README — explain Vertex project/location, trim MCP section
- Vertex AI: document that project/location must be set explicitly on the
workflow-side client; auto-discovery can't run in the sandbox and would make
in-workflow request formatting non-deterministic.
- MCP: drop the server-side/interactions bullet (works unchanged, no wiring) and
focus the section on the client-side path the plugin actually wires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: pin google-genai < 2.8.0 (in-workflow AFC regression)
google-genai 2.8.0 regressed automatic function calling for plain
workflow-method tools: the tool executes (its function response is correct) but
its in-workflow state mutation is no longer visible on replay/query, failing
test_workflow_method_as_tool. The activity_as_tool path is unaffected. Cap at
< 2.8.0 until a fixed release ships upstream.
Also simplify exclude-newer-package to disable the cutoff for google-adk
outright (= false) rather than a dated pin that needs later cleanup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: fix poe lint (exports, import order, type nits)
- Export GoogleGenAIError in __all__ (fixes unused-import in __init__ and the
"not exported" warning where tests import it from the package).
- Sort imports in _gemini_activity.py (ruff I001).
- Suppress reportUnusedClass on _TemporalApiClient (used in the sibling module)
and reportUnusedFunction on the autouse MCP fixture, matching repo convention.
- Use collections.abc.AsyncIterator and annotate the test helper parameter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: add public testing utilities
Add temporalio/contrib/google_genai/testing.py so users can test workflows that
use TemporalAsyncClient without real Gemini API calls — the agent-framework
"test fakes are table-stakes" expectation, matching openai_agents/testing.py.
- text_response / function_call_response build canned generate_content bodies
- GeminiTestServer scripts model responses (incl. AFC turns and streaming) and
exposes a GoogleGenAIPlugin via .plugin(); records requests for assertions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: stream generate_content_stream via Workflow Streams
Set TemporalAsyncClient(streaming_topic=...) and host a WorkflowStream in the
workflow's @workflow.init; each generate_content_stream chunk is then published
to that topic (as a parsed GenerateContentResponse) as it arrives, so external
WorkflowStreamClient consumers observe model output in real time while the
workflow runs durably. The workflow's own iteration is unchanged (still batched).
- _models: _GeminiApiRequest carries streaming_topic + streaming_batch_interval_ms
- TemporalAsyncClient/_TemporalApiClient: streaming_topic + streaming_batch_interval;
fail fast (GoogleGenAIError) if a topic is set but no WorkflowStream is hosted
- streamed activity publishes via WorkflowStreamClient.from_within_activity;
publishing is best-effort and never breaks the batched return
- README Streaming section + module docstring; tests in test_gemini_streaming.py
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: fix gen-docs cross-reference link targets
pydoctor (warnings-as-errors) couldn't resolve the `~`-prefixed
`:class:` cross-references added for streaming/testing docstrings. Drop the `~`
prefix to use the full dotted path, matching the form already used elsewhere
(e.g. openai_agents references temporalio.contrib.workflow_streams.WorkflowStream).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* google_genai: closure-wrap workflow-method tools for config deep-copy
google-genai >= 2.8.0 deep-copies the request config internally
(config.model_copy(deep=True)), which clones a bound-method tool's
__self__ — so a workflow-method tool runs against a throwaway clone and
its in-workflow state mutation is silently lost.
TemporalAsyncClient now closure-wraps bound-method tools before handing
the config to the SDK; copy.deepcopy leaves plain functions intact, so
the closure keeps the tool bound to the real workflow instance. Plain
functions and activity_as_tool wrappers are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* google_genai: migrate interactions/agents to public API; require google-genai 2.10
google-genai 2.9.0 regenerated the vendored interactions client
(google.genai._interactions -> google.genai._gaos), removing the private
symbols the interactions/agents shims imported (construct_type, AsyncStream,
and the AsyncInteractionsResource/AsyncAgentsResource base classes).
Retarget the shims to the public google.genai.interactions surface:
- TemporalAsyncInteractions/TemporalAsyncAgents are now standalone classes
(they already overrode every method), not private-resource subclasses.
- _deserialize uses public pydantic only: a TypeAdapter dispatches the
InteractionSSEEvent discriminated union (and nested unions), and
model_validate rehydrates plain models (recursing nested objects).
- _TemporalInteractionAsyncStream is a plain async iterator/context manager,
no longer subclassing the SDK's AsyncStream.
- Worker-side activities use public types; the stream drain is typed
structurally.
Bump the pin to google-genai>=2.10.0,<3 (with an exclude-newer-package
override so uv can select it past the cutoff) and update the tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix lint errors from google-genai 2.10 migration
- Mark the Agent import from google.genai.interactions with a
reportPrivateImportUsage ignore (pyright can't follow the module's
dynamically-built __all__), matching the source modules.
- Narrow types in the bound-method tool-wrapping unit tests so pyright
and mypy accept attribute/subscript access on the helper results.
- Add missing __init__ docstrings flagged by pydocstyle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix ADK multi-agent test mock under google-adk 2.4
google-adk 2.4 rewrites cross-agent history into "For context:" text
parts after a transfer, so ResearchModel's dedup-against-history never
matched and it re-served "transfer to researcher" to the researcher
itself, failing test-latest-deps with "Agent 'researcher' cannot
transfer to itself". Key the scripted responses off the calling
agent's instruction text instead, which is stateless (replay-safe)
and independent of ADK history rewriting. Passes on both the locked
google-adk 2.2.0 and latest 2.4.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Brian Strauch <brian@brianstrauch.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 59c1164 commit 4ec9ab0
25 files changed
Lines changed: 5651 additions & 1028 deletions
File tree
- .github
- temporalio/contrib/google_genai
- tests/contrib
- google_adk_agents
- google_genai
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| 21 | + | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
32 | | - | |
| 32 | + | |
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
43 | 44 | | |
44 | 45 | | |
45 | 46 | | |
| |||
88 | 89 | | |
89 | 90 | | |
90 | 91 | | |
| 92 | + | |
91 | 93 | | |
92 | 94 | | |
93 | 95 | | |
| |||
260 | 262 | | |
261 | 263 | | |
262 | 264 | | |
263 | | - | |
| 265 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
0 commit comments