Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ opentelemetry = ["opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2"]
pydantic = ["pydantic>=2.0.0,<3"]
openai-agents = ["openai-agents>=0.3,<0.7", "mcp>=1.9.4, <2"]
google-adk = ["google-adk>=1.27.0,<2"]
google-gemini = [
"google-genai>=1.66.0",
]

[project.urls]
Homepage = "https://github.com/temporalio/sdk-python"
Expand Down
121 changes: 121 additions & 0 deletions temporalio/contrib/google_gemini_sdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""First-class Temporal integration for the Google Gemini SDK.

.. warning::
This module is experimental and may change in future versions.
Use with caution in production environments.

This integration lets you use the Gemini SDK **exactly as you normally would**
while making every network call and every tool invocation **durable Temporal
activities**.

- :class:`GeminiPlugin` — creates and owns the ``genai.Client``, registers
the HTTP transport activity, and configures the worker. Pass the same args
you would pass to ``genai.Client()`` — the plugin handles ``http_options``
internally.
- :func:`activity_as_tool` — convert any ``@activity.defn`` function into a
Gemini tool callable; Gemini's AFC invokes it as a Temporal activity.
- :func:`get_gemini_client` — retrieve the client inside a workflow.

Quickstart::

# ---- worker setup (outside sandbox) ----
plugin = GeminiPlugin(api_key=os.environ["GOOGLE_API_KEY"])

@activity.defn
async def get_weather(state: str) -> str: ...

# ---- workflow (sandbox-safe) ----
@workflow.defn
class AgentWorkflow:
@workflow.run
async def run(self, query: str) -> str:
client = get_gemini_client()
response = await client.aio.models.generate_content(
model="gemini-2.5-flash",
contents=query,
config=types.GenerateContentConfig(
tools=[
activity_as_tool(
get_weather,
start_to_close_timeout=timedelta(seconds=30),
),
],
),
)
return response.text
"""

from __future__ import annotations

from typing import TYPE_CHECKING

# --- Type-checking imports (never executed at runtime) ---
# These give IDEs and type checkers full visibility into the lazy-loaded
# symbols so that autocomplete, go-to-definition, and hover docs work.
if TYPE_CHECKING:
from temporalio.contrib.google_gemini_sdk._gemini_plugin import (
GeminiPlugin as GeminiPlugin,
)
from temporalio.contrib.google_gemini_sdk._temporal_httpx_client import (
TemporalHttpxClient as TemporalHttpxClient,
temporal_http_options as temporal_http_options,
)

# --- Sandbox-safe imports (loaded eagerly at runtime) ---
# These modules have NO httpx / google.genai imports and are safe to load
# inside the Temporal workflow sandbox.
from temporalio.contrib.google_gemini_sdk._client_store import get_gemini_client
from temporalio.contrib.google_gemini_sdk.workflow import (
GeminiAgentWorkflowError,
GeminiToolSerializationError,
activity_as_tool,
)

__all__ = [
"DEFAULT_SENSITIVE_HEADER_KEYS",
"GeminiAgentWorkflowError",
"GeminiPlugin",
"GeminiToolSerializationError",
"TemporalHttpxClient",
"activity_as_tool",
"get_gemini_client",
"temporal_http_options",
]


# --- Lazy imports for httpx-dependent symbols ---
# GeminiPlugin, TemporalHttpxClient, and temporal_http_options all transitively
# import httpx (via _gemini_plugin → _http_activity → httpx, and via
# _temporal_httpx_client → httpx). They must NOT be loaded inside the workflow
# sandbox. They are imported lazily so that sandbox-safe imports like
# ``from temporalio.contrib.google_gemini_sdk import activity_as_tool``
# never trigger an httpx import.
def __getattr__(name: str): # type: ignore[override]
_lazy = {
"DEFAULT_SENSITIVE_HEADER_KEYS": (
"temporalio.contrib.google_gemini_sdk._gemini_plugin",
"DEFAULT_SENSITIVE_HEADER_KEYS",
),
"GeminiPlugin": (
"temporalio.contrib.google_gemini_sdk._gemini_plugin",
"GeminiPlugin",
),
"TemporalHttpxClient": (
"temporalio.contrib.google_gemini_sdk._temporal_httpx_client",
"TemporalHttpxClient",
),
"temporal_http_options": (
"temporalio.contrib.google_gemini_sdk._temporal_httpx_client",
"temporal_http_options",
),
}
if name in _lazy:
import importlib

module_path, attr = _lazy[name]
mod = importlib.import_module(module_path)
value = getattr(mod, attr)
# Cache on the module so __getattr__ is only called once per name.
globals()[name] = value
return value
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
46 changes: 46 additions & 0 deletions temporalio/contrib/google_gemini_sdk/_client_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Shared storage for the pre-built ``genai.Client`` instance.
This module is added to the Temporal sandbox passthrough list by
:class:`~temporalio.contrib.google_gemini_sdk.GeminiPlugin`. Because it is
passthrough'd, the module-level ``_gemini_client`` variable is shared between
the real runtime (where the worker sets it) and the sandboxed workflow (where
:func:`get_gemini_client` reads it).
This module intentionally has **no** runtime ``httpx`` or ``google.genai``
imports so that it can also be loaded safely by the sandbox's restricted
importer if the passthrough hasn't been configured yet.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from google.genai import Client as _GeminiClient

# Set by GeminiPlugin.__init__ before the worker starts.
_gemini_client: _GeminiClient | None = None


def get_gemini_client() -> _GeminiClient:
"""Return the ``genai.Client`` stored by :class:`GeminiPlugin`.
.. warning::
This function is experimental and may change in future versions.
Use with caution in production environments.
Call this inside a workflow to obtain the pre-built Gemini client that was
passed to :class:`~temporalio.contrib.google_gemini_sdk.GeminiPlugin` at
worker setup time. The client is created **once** outside the workflow
sandbox, so ``os.environ`` access, SSL cert loading, etc. happen at startup
— not during workflow execution.
Raises:
RuntimeError: If no client has been configured (i.e. ``GeminiPlugin``
was not initialised with a ``gemini_client``).
"""
if _gemini_client is None:
raise RuntimeError(
"No Gemini client configured. Pass gemini_client= to GeminiPlugin()."
)
return _gemini_client
Loading
Loading