Skip to content

Commit 6763054

Browse files
Add file search stores, register_files, chat support, and full test coverage
- Add TemporalAsyncFileSearchStores: overrides upload_to_file_search_store to dispatch through a Temporal activity on the worker. - Add TemporalAsyncFiles.register_files(): dispatches through activity, auth param ignored — uses extra_credentials from GeminiPlugin or the client's own credentials. Raises ValueError when neither is available. - Add extra_credentials param to GeminiPlugin for operations needing explicit auth (e.g. GCS file registration). - Wire TemporalAsyncFileSearchStores into TemporalAsyncClient. - Extract all Pydantic models into _models.py; move _validate_http_options to _temporal_api_client.py. - Remove all AFC disabling from tests. - Add chat_example for multi-turn conversation via client.chats. - Add FileSearchStoreWorkflow exercising full RAG flow with file_search tool. - Add full integration test running real activities with mocked client. - Add tests for file upload/download, store upload, chat, register_files credential error, TemporalAsyncClient wiring, and low-level raises. - Fix all lint issues: pydocstyle, mypy, basedpyright, ruff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b770922 commit 6763054

11 files changed

Lines changed: 1504 additions & 234 deletions

File tree

temporalio/contrib/google_gemini_sdk/_gemini_activity.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99
from __future__ import annotations
1010

1111
from collections.abc import Sequence
12-
from typing import Any, Callable, Optional
12+
from typing import Any, Callable
1313

1414
import google.auth.credentials
15-
from google.genai import Client as GeminiClient, types
15+
from google.genai import Client as GeminiClient
16+
from google.genai import types
1617
from google.genai.types import HttpOptions
1718
from google.genai.types import HttpResponse as SdkHttpResponse
1819

1920
from temporalio import activity
20-
from temporalio.contrib.google_gemini_sdk._temporal_api_client import (
21+
from temporalio.contrib.google_gemini_sdk._models import (
2122
_GeminiApiRequest,
2223
_GeminiApiResponse,
2324
_GeminiApiStreamedResponse,
@@ -48,8 +49,9 @@ class GeminiApiCaller:
4849
def __init__(
4950
self,
5051
client: GeminiClient,
51-
credentials: Optional[google.auth.credentials.Credentials] = None,
52+
credentials: google.auth.credentials.Credentials | None = None,
5253
) -> None:
54+
"""Initialize with a genai.Client and optional extra credentials."""
5355
self._client = client
5456
self._credentials = credentials
5557

@@ -107,9 +109,7 @@ async def gemini_files_upload(
107109
else:
108110
file_arg = req.file_path
109111

110-
return await self._client.aio.files.upload(
111-
file=file_arg, config=req.config
112-
)
112+
return await self._client.aio.files.upload(file=file_arg, config=req.config)
113113

114114
@activity.defn(name="gemini_files_download")
115115
async def gemini_files_download(
@@ -131,8 +131,15 @@ async def gemini_files_register(
131131
Token refresh happens here on the worker side, so no auth
132132
material enters the workflow event history.
133133
"""
134+
auth = self._credentials or self._client._api_client._credentials
135+
if auth is None:
136+
raise ValueError(
137+
"No credentials available for register_files(). "
138+
"Pass extra_credentials to GeminiPlugin or initialize "
139+
"the genai.Client with credentials."
140+
)
134141
return await self._client.aio.files.register_files(
135-
auth=self._credentials or self._client._api_client._credentials,
142+
auth=auth,
136143
uris=req.uris,
137144
config=req.config,
138145
)
@@ -149,10 +156,12 @@ async def gemini_file_search_stores_upload(
149156
else:
150157
file_arg = req.file_path
151158

152-
return await self._client.aio.file_search_stores.upload_to_file_search_store(
153-
file_search_store_name=req.file_search_store_name,
154-
file=file_arg,
155-
config=req.config,
159+
return (
160+
await self._client.aio.file_search_stores.upload_to_file_search_store(
161+
file_search_store_name=req.file_search_store_name,
162+
file=file_arg,
163+
config=req.config,
164+
)
156165
)
157166

158167
return [

temporalio/contrib/google_gemini_sdk/_gemini_plugin.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import dataclasses
6-
from typing import Optional
76

87
import google.auth.credentials
98
from google.genai import Client as GeminiClient
@@ -64,7 +63,7 @@ class GeminiPlugin(SimplePlugin):
6463
def __init__(
6564
self,
6665
client: GeminiClient,
67-
extra_credentials: Optional[google.auth.credentials.Credentials] = None,
66+
extra_credentials: google.auth.credentials.Credentials | None = None,
6867
) -> None:
6968
"""Initialize the Gemini plugin.
7069
@@ -77,9 +76,7 @@ def __init__(
7776
``files.register_files()``). If not provided, the
7877
client's own credentials are used.
7978
"""
80-
self._api_caller = GeminiApiCaller(
81-
client, credentials=extra_credentials
82-
)
79+
self._api_caller = GeminiApiCaller(client, credentials=extra_credentials)
8380

8481
def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner:
8582
if not runner:
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Serializable Pydantic models for the Gemini SDK Temporal integration.
2+
3+
These models cross the activity boundary — they're constructed on the
4+
workflow side and deserialized on the activity side (or vice versa).
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from typing import Any
10+
11+
from google.genai import types
12+
from pydantic import BaseModel
13+
14+
__all__ = [
15+
"_GeminiApiRequest",
16+
"_GeminiApiResponse",
17+
"_GeminiApiStreamedResponse",
18+
"_GeminiDownloadFileRequest",
19+
"_GeminiRegisterFilesRequest",
20+
"_GeminiUploadFileRequest",
21+
"_GeminiUploadToFileSearchStoreRequest",
22+
"_SerializableHttpOptions",
23+
]
24+
25+
26+
class _SerializableHttpOptions(BaseModel):
27+
"""Per-request HTTP options that can be serialized across the activity boundary.
28+
29+
Non-serializable fields (httpx_client, httpx_async_client, aiohttp_client,
30+
client_args, async_client_args) must be configured at GeminiPlugin init.
31+
32+
``timeout`` is excluded because Temporal owns timeouts/retries — configure
33+
via ``ActivityConfig`` instead.
34+
"""
35+
36+
base_url: str | None = None
37+
base_url_resource_scope: str | None = None
38+
api_version: str | None = None
39+
headers: dict[str, str] | None = None
40+
extra_body: dict[str, Any] | None = None
41+
42+
43+
# ── async_request models ──────────────────────────────────────────────────
44+
45+
46+
class _GeminiApiRequest(BaseModel):
47+
"""Serializable activity input for a Gemini SDK API call."""
48+
49+
http_method: str
50+
path: str
51+
request_dict: dict[str, object]
52+
http_options_overrides: _SerializableHttpOptions | None = None
53+
54+
55+
class _GeminiApiResponse(BaseModel):
56+
"""Serializable activity output for a Gemini SDK API call."""
57+
58+
headers: dict[str, str]
59+
body: str
60+
61+
62+
class _GeminiApiStreamedResponse(BaseModel):
63+
"""Serializable activity output for a batched streamed API call.
64+
65+
The activity collects all streamed chunks and returns them as a list.
66+
The ``TemporalApiClient`` then yields them one at a time to the SDK.
67+
"""
68+
69+
chunks: list[_GeminiApiResponse]
70+
71+
72+
# ── files upload/download models ──────────────────────────────────────────
73+
74+
75+
class _GeminiUploadFileRequest(BaseModel):
76+
"""Serializable activity input for a file upload.
77+
78+
For file path uploads the path is resolved on the worker. For
79+
in-memory uploads the raw bytes are sent across the activity boundary.
80+
"""
81+
82+
file_bytes: bytes | None = None
83+
file_path: str | None = None
84+
config: types.UploadFileConfig | None = None
85+
86+
87+
class _GeminiDownloadFileRequest(BaseModel):
88+
"""Serializable activity input for a file download."""
89+
90+
file: str
91+
config: types.DownloadFileConfig | None = None
92+
93+
94+
class _GeminiRegisterFilesRequest(BaseModel):
95+
"""Serializable activity input for registering GCS files."""
96+
97+
uris: list[str]
98+
config: types.RegisterFilesConfig | None = None
99+
100+
101+
class _GeminiUploadToFileSearchStoreRequest(BaseModel):
102+
"""Serializable activity input for uploading a file to a file search store."""
103+
104+
file_search_store_name: str
105+
file_bytes: bytes | None = None
106+
file_path: str | None = None
107+
config: types.UploadToFileSearchStoreConfig | None = None

0 commit comments

Comments
 (0)