temporal-agent-harness 0.3.0__py3-none-any.whl
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.
- temporal_agent_harness/__init__.py +15 -0
- temporal_agent_harness/ai_sdks/__init__.py +11 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/__init__.py +67 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_gemini_activity.py +235 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_google_genai_plugin.py +98 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_interactions_activity.py +411 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_interactions_models.py +31 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_interactions_workflow.py +56 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_models.py +121 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_temporal_api_client.py +292 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_temporal_async_client.py +64 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_temporal_file_search_stores.py +109 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_temporal_files.py +169 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/_temporal_interactions.py +143 -0
- temporal_agent_harness/ai_sdks/google_genai_plugin/workflow.py +96 -0
- temporal_agent_harness/ai_sdks/integration_helpers/__init__.py +33 -0
- temporal_agent_harness/ai_sdks/integration_helpers/_stream_observer.py +170 -0
- temporal_agent_harness/ai_sdks/openai_agents/__init__.py +33 -0
- temporal_agent_harness/ai_sdks/openai_agents/_heartbeat_decorator.py +38 -0
- temporal_agent_harness/ai_sdks/openai_agents/_invoke_model_activity.py +451 -0
- temporal_agent_harness/ai_sdks/openai_agents/_mcp.py +537 -0
- temporal_agent_harness/ai_sdks/openai_agents/_model_parameters.py +127 -0
- temporal_agent_harness/ai_sdks/openai_agents/_nexus_mcp.py +194 -0
- temporal_agent_harness/ai_sdks/openai_agents/_openai_runner.py +378 -0
- temporal_agent_harness/ai_sdks/openai_agents/_otel_trace_interceptor.py +88 -0
- temporal_agent_harness/ai_sdks/openai_agents/_temporal_model_stub.py +317 -0
- temporal_agent_harness/ai_sdks/openai_agents/_temporal_openai_agents.py +426 -0
- temporal_agent_harness/ai_sdks/openai_agents/_temporal_trace_provider.py +211 -0
- temporal_agent_harness/ai_sdks/openai_agents/_trace_interceptor.py +445 -0
- temporal_agent_harness/ai_sdks/openai_agents/sandbox/__init__.py +1 -0
- temporal_agent_harness/ai_sdks/openai_agents/sandbox/_sandbox_client_provider.py +285 -0
- temporal_agent_harness/ai_sdks/openai_agents/sandbox/_temporal_activity_models.py +218 -0
- temporal_agent_harness/ai_sdks/openai_agents/sandbox/_temporal_sandbox_client.py +124 -0
- temporal_agent_harness/ai_sdks/openai_agents/sandbox/_temporal_sandbox_session.py +239 -0
- temporal_agent_harness/ai_sdks/openai_agents/testing.py +262 -0
- temporal_agent_harness/ai_sdks/openai_agents/workflow.py +435 -0
- temporal_agent_harness/ai_sdks/openai_agents_harness.py +512 -0
- temporal_agent_harness/ai_sdks/pydantic_ai_harness.py +512 -0
- temporal_agent_harness/harness/__init__.py +18 -0
- temporal_agent_harness/harness/agent.py +78 -0
- temporal_agent_harness/harness/agent_client.py +650 -0
- temporal_agent_harness/harness/agent_protocol/__init__.py +166 -0
- temporal_agent_harness/harness/agent_protocol/agent_interface.py +553 -0
- temporal_agent_harness/harness/agent_protocol/events.py +896 -0
- temporal_agent_harness/harness/agent_protocol/subagent_interface.py +103 -0
- temporal_agent_harness/harness/agent_workflow.py +2904 -0
- temporal_agent_harness/harness/code_mode/__init__.py +30 -0
- temporal_agent_harness/harness/code_mode/activities.py +123 -0
- temporal_agent_harness/harness/code_mode/batch_models.py +84 -0
- temporal_agent_harness/harness/code_mode/driver.py +203 -0
- temporal_agent_harness/harness/code_mode/monty_stepper.py +176 -0
- temporal_agent_harness/harness/code_mode/stubs.py +325 -0
- temporal_agent_harness/harness/code_mode/tool.py +197 -0
- temporal_agent_harness/harness/slash_commands.py +378 -0
- temporal_agent_harness/harness/stream_context.py +30 -0
- temporal_agent_harness/harness/stream_merge/__init__.py +32 -0
- temporal_agent_harness/harness/stream_merge/cursor.py +166 -0
- temporal_agent_harness/harness/stream_merge/gates.py +148 -0
- temporal_agent_harness/harness/stream_merge/merge.py +494 -0
- temporal_agent_harness/harness/subagent_activities.py +317 -0
- temporal_agent_harness/harness/subagent_toolset.py +203 -0
- temporal_agent_harness/nexus_agent_adapter/__init__.py +0 -0
- temporal_agent_harness/nexus_agent_adapter/generated/__init__.py +65 -0
- temporal_agent_harness/nexus_agent_adapter/generated/_definitions.py +514 -0
- temporal_agent_harness/nexus_agent_adapter/generated/models.py +2192 -0
- temporal_agent_harness/nexus_agent_adapter/generated/services.py +101 -0
- temporal_agent_harness/nexus_agent_adapter/handler.py +369 -0
- temporal_agent_harness/nexus_agent_adapter/worker.py +72 -0
- temporal_agent_harness/plugin.py +255 -0
- temporal_agent_harness/ui/__init__.py +12 -0
- temporal_agent_harness/ui/dist/assets/index-CSosuMGP.js +53 -0
- temporal_agent_harness/ui/dist/assets/index-DNXWrDgV.css +1 -0
- temporal_agent_harness/ui/dist/index.html +13 -0
- temporal_agent_harness/ui/dist/temporal-logo.svg +3 -0
- temporal_agent_harness/utils/__init__.py +6 -0
- temporal_agent_harness/utils/large_payload.py +227 -0
- temporal_agent_harness/web/__init__.py +50 -0
- temporal_agent_harness/web/app.py +682 -0
- temporal_agent_harness/web/cli.py +173 -0
- temporal_agent_harness/web/client.py +87 -0
- temporal_agent_harness/web/registry.py +82 -0
- temporal_agent_harness/web/serve.py +66 -0
- temporal_agent_harness/web/session_manager.py +124 -0
- temporal_agent_harness/web/session_manager_worker.py +90 -0
- temporal_agent_harness/web/task_queue_status.py +158 -0
- temporal_agent_harness-0.3.0.dist-info/METADATA +566 -0
- temporal_agent_harness-0.3.0.dist-info/RECORD +91 -0
- temporal_agent_harness-0.3.0.dist-info/WHEEL +5 -0
- temporal_agent_harness-0.3.0.dist-info/entry_points.txt +2 -0
- temporal_agent_harness-0.3.0.dist-info/licenses/LICENSE +21 -0
- temporal_agent_harness-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Temporal-native agent harness (experimental).
|
|
2
|
+
|
|
3
|
+
A Temporal-native outer agent harness that gives you the full power of Temporal's
|
|
4
|
+
durable-execution primitives while letting you keep using the AI SDKs you already know
|
|
5
|
+
(the inner harness), via first-class integrations under :mod:`temporal_agent_harness.ai_sdks`.
|
|
6
|
+
|
|
7
|
+
Subpackages:
|
|
8
|
+
* :mod:`temporal_agent_harness.harness` — the core harness: the agent workflow runner,
|
|
9
|
+
the agent/subagent protocol, tool definitions, and human-in-the-loop tool approvals.
|
|
10
|
+
* :mod:`temporal_agent_harness.ai_sdks` — integrations that make AI SDK calls durable
|
|
11
|
+
Temporal activities (currently the Google Gemini SDK).
|
|
12
|
+
* :mod:`temporal_agent_harness.plugin` — ``AgentHarnessPlugin``, the one Temporal plugin
|
|
13
|
+
that wires a client + worker for all of the above: it supplies the harness's data
|
|
14
|
+
converter and registers its activities, so a worker declares only its workflows.
|
|
15
|
+
"""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""First-class integrations between the Temporal-native harness and AI SDKs.
|
|
2
|
+
|
|
3
|
+
Each integration turns an AI SDK's calls into durable Temporal activities, so workflow code
|
|
4
|
+
can use the SDK it already knows while inheriting Temporal's durability, retries, and
|
|
5
|
+
observability. One subpackage per SDK; more are expected over time.
|
|
6
|
+
|
|
7
|
+
* :mod:`temporal_agent_harness.ai_sdks.google_genai_plugin` — the Google Gemini SDK.
|
|
8
|
+
* :mod:`temporal_agent_harness.ai_sdks.openai_agents` — the OpenAI Agents SDK.
|
|
9
|
+
* :mod:`temporal_agent_harness.ai_sdks.pydantic_ai_harness` — Pydantic AI (glue for the
|
|
10
|
+
unmodified upstream ``pydantic_ai.durable_exec.temporal`` plugin).
|
|
11
|
+
"""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""First-class Temporal integration for the Google Gemini SDK.
|
|
2
|
+
|
|
3
|
+
.. warning::
|
|
4
|
+
This module is experimental and may change in future versions.
|
|
5
|
+
Use with caution in production environments.
|
|
6
|
+
|
|
7
|
+
This integration lets you use the Gemini SDK's async client with full
|
|
8
|
+
automatic function calling (AFC) support, plus a Temporal-aware shim of
|
|
9
|
+
the Interactions API. Every API call becomes a **durable Temporal
|
|
10
|
+
activity**. Define tools with :func:`harness.agent.tool_defn` (inline,
|
|
11
|
+
deterministic in-workflow) or :func:`harness.agent.activity_tool_defn`
|
|
12
|
+
(durable, activity-backed); for the latter, :func:`harness.agent.tool_activity`
|
|
13
|
+
returns the activity to register on the worker.
|
|
14
|
+
|
|
15
|
+
No credentials are fetched in the workflow, and no auth material appears in
|
|
16
|
+
Temporal's event history.
|
|
17
|
+
|
|
18
|
+
- :class:`GoogleGenAIPlugin` — registers all of the Gemini activities
|
|
19
|
+
using a caller-provided ``genai.Client`` on the worker side.
|
|
20
|
+
- :func:`google_genai_client` — call from a workflow to get an
|
|
21
|
+
``AsyncClient`` whose ``models.generate_content*``, ``files``,
|
|
22
|
+
``file_search_stores``, and ``interactions`` modules all route through
|
|
23
|
+
Temporal activities.
|
|
24
|
+
- :func:`function_param` — derive an Interactions-API ``FunctionParam``
|
|
25
|
+
tool declaration from a tool's model-facing signature. The Interactions
|
|
26
|
+
API has no AFC, so the workflow drives the tool-calling loop itself;
|
|
27
|
+
pair this helper with the ``gemini.interactions.create(...)`` call on
|
|
28
|
+
the client returned by :func:`google_genai_client`.
|
|
29
|
+
|
|
30
|
+
Quickstart::
|
|
31
|
+
|
|
32
|
+
# ---- worker setup (outside the Temporal Python Sandbox) ----
|
|
33
|
+
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
|
|
34
|
+
plugin = GoogleGenAIPlugin(client)
|
|
35
|
+
|
|
36
|
+
@agent.activity_tool_defn()
|
|
37
|
+
async def get_weather(state: str) -> str: ...
|
|
38
|
+
|
|
39
|
+
Worker(..., activities=[agent.tool_activity(get_weather), ...])
|
|
40
|
+
|
|
41
|
+
# ---- workflow (inside the Temporal Python Sandbox) ----
|
|
42
|
+
@workflow.defn
|
|
43
|
+
class AgentWorkflow:
|
|
44
|
+
@workflow.run
|
|
45
|
+
async def run(self, query: str) -> str:
|
|
46
|
+
client = google_genai_client()
|
|
47
|
+
response = await client.models.generate_content(
|
|
48
|
+
model="gemini-2.5-flash",
|
|
49
|
+
contents=query,
|
|
50
|
+
config=types.GenerateContentConfig(tools=[get_weather]),
|
|
51
|
+
)
|
|
52
|
+
return response.text
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
from ._google_genai_plugin import GoogleGenAIPlugin
|
|
58
|
+
from ._interactions_workflow import function_param
|
|
59
|
+
from .workflow import (
|
|
60
|
+
google_genai_client,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
__all__ = [
|
|
64
|
+
"GoogleGenAIPlugin",
|
|
65
|
+
"function_param",
|
|
66
|
+
"google_genai_client",
|
|
67
|
+
]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Temporal activity that executes Gemini SDK API calls with real credentials.
|
|
2
|
+
|
|
3
|
+
The ``TemporalApiClient`` in the workflow dispatches calls here. This
|
|
4
|
+
activity holds a user-provided ``genai.Client`` and forwards structured
|
|
5
|
+
requests. Credentials are fetched/refreshed only within the activity —
|
|
6
|
+
they never appear in workflow event history.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from contextlib import AsyncExitStack
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
import google.auth.credentials
|
|
17
|
+
from google.genai import Client as GeminiClient
|
|
18
|
+
from google.genai import types
|
|
19
|
+
from google.genai.types import HttpOptions
|
|
20
|
+
from google.genai.types import HttpResponse as SdkHttpResponse
|
|
21
|
+
|
|
22
|
+
from temporalio import activity
|
|
23
|
+
|
|
24
|
+
from temporal_agent_harness.harness.agent_protocol import ReplyDelta
|
|
25
|
+
from temporal_agent_harness.harness.agent_workflow import AgentWorkflowRunner
|
|
26
|
+
from ._interactions_activity import make_gemini_interactions_create_streamed
|
|
27
|
+
from ._models import (
|
|
28
|
+
_GeminiApiRequest,
|
|
29
|
+
_GeminiApiResponse,
|
|
30
|
+
_GeminiApiStreamedResponse,
|
|
31
|
+
_GeminiDownloadFileRequest,
|
|
32
|
+
_GeminiRegisterFilesRequest,
|
|
33
|
+
_GeminiUploadFileRequest,
|
|
34
|
+
_GeminiUploadToFileSearchStoreRequest,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _extract_text_delta(body: str) -> str:
|
|
39
|
+
"""Pull the prose text out of a single streamed Gemini response chunk.
|
|
40
|
+
|
|
41
|
+
Chunks come in as raw HTTP response bodies — usually a JSON object
|
|
42
|
+
representing one partial ``GenerateContentResponse``, sometimes
|
|
43
|
+
prefixed with an SSE ``data:`` field. Tolerant of both shapes;
|
|
44
|
+
returns ``""`` for non-text chunks (function-call parts, malformed
|
|
45
|
+
bodies, etc.).
|
|
46
|
+
"""
|
|
47
|
+
body = body.strip()
|
|
48
|
+
if body.startswith("data:"):
|
|
49
|
+
body = body[5:].strip()
|
|
50
|
+
if not body:
|
|
51
|
+
return ""
|
|
52
|
+
try:
|
|
53
|
+
data = json.loads(body)
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
return ""
|
|
56
|
+
text = ""
|
|
57
|
+
for cand in data.get("candidates", []) or []:
|
|
58
|
+
content = cand.get("content") or {}
|
|
59
|
+
for part in content.get("parts") or []:
|
|
60
|
+
if isinstance(part, dict):
|
|
61
|
+
t = part.get("text")
|
|
62
|
+
if isinstance(t, str):
|
|
63
|
+
text += t
|
|
64
|
+
return text
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _resolve_http_options(
|
|
68
|
+
overrides: Any,
|
|
69
|
+
) -> HttpOptions | None:
|
|
70
|
+
"""Reconstruct ``HttpOptions`` from serializable overrides, or None."""
|
|
71
|
+
if overrides is None:
|
|
72
|
+
return None
|
|
73
|
+
return HttpOptions.model_validate(overrides.model_dump(exclude_none=True))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GeminiApiCaller:
|
|
77
|
+
"""Wraps a ``genai.Client`` and exposes Temporal activities for SDK calls.
|
|
78
|
+
|
|
79
|
+
The caller owns a reference to the user-provided ``genai.Client``.
|
|
80
|
+
All credential management, HTTP client configuration, etc. is the
|
|
81
|
+
responsibility of whoever constructs the client.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
client: GeminiClient,
|
|
87
|
+
credentials: google.auth.credentials.Credentials | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Initialize with a genai.Client and optional extra credentials."""
|
|
90
|
+
self._client = client
|
|
91
|
+
self._credentials = credentials
|
|
92
|
+
|
|
93
|
+
def activities(self) -> Sequence[Callable]:
|
|
94
|
+
"""Return activities that route SDK calls through this client."""
|
|
95
|
+
|
|
96
|
+
@activity.defn
|
|
97
|
+
async def gemini_api_client_async_request(
|
|
98
|
+
req: _GeminiApiRequest,
|
|
99
|
+
) -> _GeminiApiResponse:
|
|
100
|
+
"""Execute a Gemini SDK API call with real credentials."""
|
|
101
|
+
response: SdkHttpResponse = (
|
|
102
|
+
await self._client.aio._api_client.async_request(
|
|
103
|
+
http_method=req.http_method,
|
|
104
|
+
path=req.path,
|
|
105
|
+
request_dict=req.request_dict,
|
|
106
|
+
http_options=_resolve_http_options(req.http_options_overrides),
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
return _GeminiApiResponse(
|
|
110
|
+
headers=response.headers or {},
|
|
111
|
+
body=response.body or "",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@activity.defn
|
|
115
|
+
async def gemini_api_client_async_request_streamed(
|
|
116
|
+
req: _GeminiApiRequest,
|
|
117
|
+
) -> _GeminiApiStreamedResponse:
|
|
118
|
+
"""Execute a streamed Gemini SDK API call, collecting all chunks.
|
|
119
|
+
|
|
120
|
+
When ``req.stream_turn_id`` is set, each chunk's text content is
|
|
121
|
+
republished as a ``reply_delta`` event on the parent workflow's
|
|
122
|
+
:class:`WorkflowStream` as soon as it arrives — fine-grained,
|
|
123
|
+
visible to the UI in real time. Function-call chunks (no text
|
|
124
|
+
parts) are silently skipped.
|
|
125
|
+
"""
|
|
126
|
+
stream = await self._client.aio._api_client.async_request_streamed(
|
|
127
|
+
http_method=req.http_method,
|
|
128
|
+
path=req.path,
|
|
129
|
+
request_dict=req.request_dict,
|
|
130
|
+
http_options=_resolve_http_options(req.http_options_overrides),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
chunks: list[_GeminiApiResponse] = []
|
|
134
|
+
|
|
135
|
+
# If a stream context rode in on the request, hand it to the
|
|
136
|
+
# harness's publisher helper. The activity never unpacks
|
|
137
|
+
# turn_id/turn_number itself — it just forwards the opaque
|
|
138
|
+
# carrier and calls ``publish`` per chunk.
|
|
139
|
+
async with AsyncExitStack() as stack:
|
|
140
|
+
publisher = None
|
|
141
|
+
if req.stream_context is not None:
|
|
142
|
+
publisher = await stack.enter_async_context(
|
|
143
|
+
AgentWorkflowRunner.publisher_from_activity(
|
|
144
|
+
req.stream_context,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async for chunk in stream:
|
|
149
|
+
body = chunk.body or ""
|
|
150
|
+
chunks.append(
|
|
151
|
+
_GeminiApiResponse(headers=chunk.headers or {}, body=body)
|
|
152
|
+
)
|
|
153
|
+
if publisher is not None:
|
|
154
|
+
delta = _extract_text_delta(body)
|
|
155
|
+
if delta:
|
|
156
|
+
publisher.publish(ReplyDelta(text=delta))
|
|
157
|
+
|
|
158
|
+
return _GeminiApiStreamedResponse(chunks=chunks)
|
|
159
|
+
|
|
160
|
+
@activity.defn
|
|
161
|
+
async def gemini_files_upload(
|
|
162
|
+
req: _GeminiUploadFileRequest,
|
|
163
|
+
) -> types.File:
|
|
164
|
+
"""Upload a file using the real genai.Client on the worker."""
|
|
165
|
+
if req.file_bytes is not None:
|
|
166
|
+
import io
|
|
167
|
+
|
|
168
|
+
file_arg: Any = io.BytesIO(req.file_bytes)
|
|
169
|
+
else:
|
|
170
|
+
file_arg = req.file_path
|
|
171
|
+
|
|
172
|
+
return await self._client.aio.files.upload(file=file_arg, config=req.config)
|
|
173
|
+
|
|
174
|
+
@activity.defn
|
|
175
|
+
async def gemini_files_download(
|
|
176
|
+
req: _GeminiDownloadFileRequest,
|
|
177
|
+
) -> bytes:
|
|
178
|
+
"""Download a file using the real genai.Client on the worker."""
|
|
179
|
+
return await self._client.aio.files.download(
|
|
180
|
+
file=req.file, config=req.config
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@activity.defn
|
|
184
|
+
async def gemini_files_register(
|
|
185
|
+
req: _GeminiRegisterFilesRequest,
|
|
186
|
+
) -> types.RegisterFilesResponse:
|
|
187
|
+
"""Register GCS files using the real genai.Client on the worker.
|
|
188
|
+
|
|
189
|
+
Uses ``credentials`` if provided at plugin init,
|
|
190
|
+
otherwise falls back to the client's own credentials.
|
|
191
|
+
Token refresh happens here on the worker side, so no auth
|
|
192
|
+
material enters the workflow event history.
|
|
193
|
+
"""
|
|
194
|
+
auth = self._credentials or self._client._api_client._credentials
|
|
195
|
+
if auth is None:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"No credentials available for register_files(). "
|
|
198
|
+
"Pass extra_credentials to GoogleGenAIPlugin or initialize "
|
|
199
|
+
"the genai.Client with credentials."
|
|
200
|
+
)
|
|
201
|
+
return await self._client.aio.files.register_files(
|
|
202
|
+
auth=auth,
|
|
203
|
+
uris=req.uris,
|
|
204
|
+
config=req.config,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
@activity.defn
|
|
208
|
+
async def gemini_file_search_stores_upload(
|
|
209
|
+
req: _GeminiUploadToFileSearchStoreRequest,
|
|
210
|
+
) -> types.UploadToFileSearchStoreOperation:
|
|
211
|
+
"""Upload a file to a file search store on the worker."""
|
|
212
|
+
if req.file_bytes is not None:
|
|
213
|
+
import io
|
|
214
|
+
|
|
215
|
+
file_arg: Any = io.BytesIO(req.file_bytes)
|
|
216
|
+
else:
|
|
217
|
+
file_arg = req.file_path
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
await self._client.aio.file_search_stores.upload_to_file_search_store(
|
|
221
|
+
file_search_store_name=req.file_search_store_name,
|
|
222
|
+
file=file_arg,
|
|
223
|
+
config=req.config,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
return [
|
|
228
|
+
gemini_api_client_async_request,
|
|
229
|
+
gemini_api_client_async_request_streamed,
|
|
230
|
+
gemini_files_upload,
|
|
231
|
+
gemini_files_download,
|
|
232
|
+
gemini_files_register,
|
|
233
|
+
gemini_file_search_stores_upload,
|
|
234
|
+
make_gemini_interactions_create_streamed(self._client),
|
|
235
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Temporal plugin for Google Gemini SDK integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
|
|
7
|
+
import google.auth.credentials
|
|
8
|
+
from google.genai import Client as GeminiClient
|
|
9
|
+
|
|
10
|
+
from ._gemini_activity import GeminiApiCaller
|
|
11
|
+
from temporalio.contrib.pydantic import PydanticPayloadConverter
|
|
12
|
+
from temporalio.converter import DataConverter, DefaultPayloadConverter
|
|
13
|
+
from temporalio.plugin import SimplePlugin
|
|
14
|
+
from temporalio.worker import WorkflowRunner
|
|
15
|
+
from temporalio.worker.workflow_sandbox import SandboxedWorkflowRunner
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _data_converter(converter: DataConverter | None) -> DataConverter:
|
|
19
|
+
if converter is None:
|
|
20
|
+
return DataConverter(payload_converter_class=PydanticPayloadConverter)
|
|
21
|
+
elif converter.payload_converter_class is DefaultPayloadConverter:
|
|
22
|
+
return dataclasses.replace(
|
|
23
|
+
converter, payload_converter_class=PydanticPayloadConverter
|
|
24
|
+
)
|
|
25
|
+
return converter
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GoogleGenAIPlugin(SimplePlugin):
|
|
29
|
+
"""A Temporal Worker Plugin configured for the Google Gemini SDK.
|
|
30
|
+
|
|
31
|
+
.. warning::
|
|
32
|
+
This class is experimental and may change in future versions.
|
|
33
|
+
Use with caution in production environments.
|
|
34
|
+
|
|
35
|
+
This plugin registers the ``gemini_api_client_async_request`` activity
|
|
36
|
+
using the provided ``genai.Client`` with real credentials. Workflows use
|
|
37
|
+
:func:`~temporalio.contrib.google_genai.workflow.google_genai_client` to
|
|
38
|
+
get an ``AsyncClient`` backed by a ``TemporalApiClient`` that routes all
|
|
39
|
+
API calls through this activity.
|
|
40
|
+
|
|
41
|
+
No credentials are passed to or from the workflow. Auth material never
|
|
42
|
+
appears in Temporal's event history.
|
|
43
|
+
|
|
44
|
+
Example (Gemini Developer API)::
|
|
45
|
+
|
|
46
|
+
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
|
|
47
|
+
plugin = GoogleGenAIPlugin(client)
|
|
48
|
+
|
|
49
|
+
Example (Vertex AI)::
|
|
50
|
+
|
|
51
|
+
client = genai.Client(
|
|
52
|
+
vertexai=True, project="my-project", location="us-central1",
|
|
53
|
+
)
|
|
54
|
+
plugin = GoogleGenAIPlugin(client)
|
|
55
|
+
|
|
56
|
+
Example (with separate GCS credentials for file registration)::
|
|
57
|
+
|
|
58
|
+
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
|
|
59
|
+
gcs_creds, _ = google.auth.default()
|
|
60
|
+
plugin = GoogleGenAIPlugin(client, extra_credentials=gcs_creds)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
client: GeminiClient,
|
|
66
|
+
extra_credentials: google.auth.credentials.Credentials | None = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Initialize the Gemini plugin.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
client: A fully configured ``genai.Client`` instance.
|
|
72
|
+
All credential management, HTTP client configuration, etc.
|
|
73
|
+
is the responsibility of the caller.
|
|
74
|
+
extra_credentials: Optional Google Cloud credentials used for
|
|
75
|
+
operations that require explicit auth (e.g.
|
|
76
|
+
``files.register_files()``). If not provided, the
|
|
77
|
+
client's own credentials are used.
|
|
78
|
+
"""
|
|
79
|
+
self._api_caller = GeminiApiCaller(client, credentials=extra_credentials)
|
|
80
|
+
|
|
81
|
+
def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner:
|
|
82
|
+
if not runner:
|
|
83
|
+
raise ValueError("No WorkflowRunner provided to GoogleGenAIPlugin.")
|
|
84
|
+
if isinstance(runner, SandboxedWorkflowRunner):
|
|
85
|
+
return dataclasses.replace(
|
|
86
|
+
runner,
|
|
87
|
+
restrictions=runner.restrictions.with_passthrough_modules(
|
|
88
|
+
"google.genai"
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
return runner
|
|
92
|
+
|
|
93
|
+
super().__init__(
|
|
94
|
+
name="GoogleGenAIPlugin",
|
|
95
|
+
data_converter=_data_converter,
|
|
96
|
+
activities=self._api_caller.activities(),
|
|
97
|
+
workflow_runner=workflow_runner,
|
|
98
|
+
)
|