flyteplugins-agents-google 2.5.10__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.
- flyteplugins/agents/google/__init__.py +23 -0
- flyteplugins/agents/google/_durable.py +82 -0
- flyteplugins/agents/google/_memory.py +39 -0
- flyteplugins/agents/google/_run.py +205 -0
- flyteplugins_agents_google-2.5.10.dist-info/METADATA +96 -0
- flyteplugins_agents_google-2.5.10.dist-info/RECORD +8 -0
- flyteplugins_agents_google-2.5.10.dist-info/WHEEL +5 -0
- flyteplugins_agents_google-2.5.10.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Google ADK (Agent Development Kit) adapter for Flyte.
|
|
2
|
+
|
|
3
|
+
Bring your own ``google-adk`` agent and run it durably on Flyte. ADK's ``Runner``
|
|
4
|
+
owns the loop; Flyte is the runtime underneath: the tools you expose are Flyte tasks
|
|
5
|
+
(durable child actions), each model turn is recorded for replay (``durable=True``),
|
|
6
|
+
the run timeline renders into the task report, and ``memory_key`` gives cross-run
|
|
7
|
+
conversation memory.
|
|
8
|
+
|
|
9
|
+
- :func:`tool` — turn an ``@env.task`` into a Google ADK tool.
|
|
10
|
+
- :func:`run_agent` — run the ADK agent loop inside your task and return the answer.
|
|
11
|
+
- :func:`durable_model` — wrap a model so its turns are durable, for hand-built agent
|
|
12
|
+
trees (e.g. sub-agent transfers) passed to ``run_agent`` via ``agent=``.
|
|
13
|
+
|
|
14
|
+
Set the model provider's API key in the environment (e.g. ``GOOGLE_API_KEY`` for
|
|
15
|
+
Gemini) — wire it as a Flyte secret.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from flyteplugins.agents.core import tool
|
|
19
|
+
|
|
20
|
+
from ._durable import FlyteLlm, durable_model
|
|
21
|
+
from ._run import run_agent, run_agent_sync
|
|
22
|
+
|
|
23
|
+
__all__ = ["FlyteLlm", "durable_model", "run_agent", "run_agent_sync", "tool"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Durable model turns for Google ADK — trace the seam below the loop.
|
|
2
|
+
|
|
3
|
+
ADK's ``Runner`` owns the loop, but every model turn flows through the agent's
|
|
4
|
+
``BaseLlm.generate_content_async``. :class:`FlyteLlm` wraps that method so each
|
|
5
|
+
(non-streaming) turn is recorded as a ``durable_step`` (a ``flyte.trace`` leaf):
|
|
6
|
+
on a crash/retry the completed turns replay from their recorded ``LlmResponse``
|
|
7
|
+
instead of re-calling (and re-billing) the model — while ADK still drives the loop.
|
|
8
|
+
|
|
9
|
+
Streaming turns are passed through unmemoized; tool calls remain durable
|
|
10
|
+
Flyte actions regardless.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import typing
|
|
17
|
+
|
|
18
|
+
from flyteplugins.agents.core import durable_step, fingerprint, jsonable
|
|
19
|
+
|
|
20
|
+
from google.adk.models import BaseLlm
|
|
21
|
+
from google.adk.models.llm_response import LlmResponse
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class FlyteLlm(BaseLlm):
|
|
25
|
+
"""A ``BaseLlm`` that records each model turn via ``durable_step`` for replay.
|
|
26
|
+
|
|
27
|
+
Wraps an inner ``BaseLlm`` (resolved from the agent's ``model``); ``model`` is set
|
|
28
|
+
to the inner model name so ADK behaves identically. Construct via
|
|
29
|
+
:func:`durable_model`.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
inner: typing.Any = None
|
|
33
|
+
|
|
34
|
+
async def generate_content_async(
|
|
35
|
+
self, llm_request: typing.Any, stream: bool = False
|
|
36
|
+
) -> typing.AsyncGenerator[typing.Any, None]:
|
|
37
|
+
if stream:
|
|
38
|
+
# Streamed turns are not memoized per-turn in this version.
|
|
39
|
+
async for response in self.inner.generate_content_async(llm_request, stream=True):
|
|
40
|
+
yield response
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
key = fingerprint({"model": self.model, "request": _request_key(llm_request)})
|
|
44
|
+
|
|
45
|
+
async def _run() -> list[dict]:
|
|
46
|
+
collected: list[dict] = []
|
|
47
|
+
async for response in self.inner.generate_content_async(llm_request, stream=False):
|
|
48
|
+
collected.append(response.model_dump(mode="json", exclude_none=True))
|
|
49
|
+
return collected
|
|
50
|
+
|
|
51
|
+
recorded = await durable_step(key, _run, name="adk_model_turn", dumps=json.dumps, loads=json.loads)
|
|
52
|
+
for payload in recorded:
|
|
53
|
+
yield LlmResponse.model_validate(payload)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _request_key(llm_request: typing.Any) -> typing.Any:
|
|
57
|
+
"""A deterministic, JSON-able view of the request to key the durable turn."""
|
|
58
|
+
try:
|
|
59
|
+
return jsonable(llm_request.model_dump(mode="json", exclude_none=True))
|
|
60
|
+
except Exception: # pragma: no cover - fall back to a coarse key
|
|
61
|
+
return jsonable(getattr(llm_request, "contents", None))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def durable_model(model: typing.Any) -> typing.Any:
|
|
65
|
+
"""Wrap ``model`` (a name string or ``BaseLlm``) so its turns are durable.
|
|
66
|
+
|
|
67
|
+
Returns a :class:`FlyteLlm` over the resolved inner model, or ``model`` unchanged
|
|
68
|
+
when it can't be wrapped (durability is best-effort, never fatal).
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
from google.adk.models import BaseLlm as _BaseLlm
|
|
72
|
+
from google.adk.models import LLMRegistry
|
|
73
|
+
|
|
74
|
+
if isinstance(model, str):
|
|
75
|
+
inner = LLMRegistry.new_llm(model)
|
|
76
|
+
elif isinstance(model, _BaseLlm):
|
|
77
|
+
inner = model
|
|
78
|
+
else:
|
|
79
|
+
return model
|
|
80
|
+
return FlyteLlm(model=inner.model, inner=inner)
|
|
81
|
+
except Exception: # pragma: no cover - never break a run over durability wiring
|
|
82
|
+
return model
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Cross-run memory for Google ADK — persist and restore the session transcript.
|
|
2
|
+
|
|
3
|
+
ADK keeps the conversation as a list of ``Event``s on the session. For cross-run
|
|
4
|
+
memory we persist those events to a keyed ``MemoryStore`` and replay them into a
|
|
5
|
+
fresh session on the next run, so the agent continues the conversation. Keyed by a
|
|
6
|
+
stable ``memory_key`` (a user/thread id); best-effort and never fatal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typing
|
|
12
|
+
|
|
13
|
+
from flyteplugins.agents.core import resolve_memory
|
|
14
|
+
|
|
15
|
+
# Path-addressed slot holding the thread's ADK event transcript in the MemoryStore.
|
|
16
|
+
_EVENTS_PATH = "google/events.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def load_memory(memory_key: str | None) -> tuple[typing.Any, list[typing.Any]]:
|
|
20
|
+
"""Return ``(store, prior_events)``; ``store`` is ``None`` when memory is off/unavailable."""
|
|
21
|
+
from google.adk.events import Event
|
|
22
|
+
|
|
23
|
+
store = await resolve_memory(memory_key)
|
|
24
|
+
if store is None:
|
|
25
|
+
return None, []
|
|
26
|
+
|
|
27
|
+
raw = await store.read_json.aio(_EVENTS_PATH)
|
|
28
|
+
events = [Event.model_validate(e) for e in (raw or [])]
|
|
29
|
+
return store, events
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def save_memory(store: typing.Any, events: typing.Sequence[typing.Any]) -> None:
|
|
33
|
+
"""Persist the session's events to the keyed store (no-op when ``store`` is ``None``)."""
|
|
34
|
+
if store is None:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
payload = [e.model_dump(mode="json", exclude_none=True) for e in events]
|
|
38
|
+
await store.write_json.aio(_EVENTS_PATH, payload, actor="google-agent")
|
|
39
|
+
await store.save.aio()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""``run_agent`` — run a Google ADK agent on Flyte using the SDK's own loop.
|
|
2
|
+
|
|
3
|
+
ADK's ``Runner`` owns the agent loop (it drives the model + tools and yields
|
|
4
|
+
``Event``s). ``run_agent`` runs that loop inside your ``@env.task``: it builds an
|
|
5
|
+
``LlmAgent`` with Flyte-task tools, drives ``Runner.run_async``, renders the events
|
|
6
|
+
into the Flyte report, and returns the final answer.
|
|
7
|
+
|
|
8
|
+
Durability via the seam below the loop: with ``durable=True`` the agent's model is
|
|
9
|
+
wrapped (:class:`FlyteLlm`) so each turn is recorded for replay. Cross-run memory
|
|
10
|
+
via ``memory_key``: the session transcript is persisted to a keyed ``MemoryStore``
|
|
11
|
+
and restored on the next run.
|
|
12
|
+
|
|
13
|
+
API keys are read from the environment (e.g. ``GOOGLE_API_KEY``) — wire them as
|
|
14
|
+
Flyte secrets.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import typing
|
|
20
|
+
import uuid
|
|
21
|
+
|
|
22
|
+
from flyte._task import AsyncFunctionTaskTemplate
|
|
23
|
+
from flyteplugins.agents.core import ReportTimeline, abbrev, flush_report, sync_variant, tool
|
|
24
|
+
|
|
25
|
+
from ._durable import durable_model
|
|
26
|
+
from ._memory import load_memory, save_memory
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _coerce_tool(t: typing.Any) -> typing.Any:
|
|
30
|
+
return tool(t) if isinstance(t, AsyncFunctionTaskTemplate) else t
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _content_text(content: typing.Any) -> str:
|
|
34
|
+
parts = getattr(content, "parts", None) or []
|
|
35
|
+
return "".join(getattr(p, "text", "") or "" for p in parts)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _render(timeline: ReportTimeline, event: typing.Any) -> None:
|
|
39
|
+
content = getattr(event, "content", None)
|
|
40
|
+
for part in getattr(content, "parts", None) or []:
|
|
41
|
+
call = getattr(part, "function_call", None)
|
|
42
|
+
resp = getattr(part, "function_response", None)
|
|
43
|
+
text = getattr(part, "text", None)
|
|
44
|
+
if call is not None:
|
|
45
|
+
timeline.row(
|
|
46
|
+
icon="🛠️",
|
|
47
|
+
label=getattr(call, "name", ""),
|
|
48
|
+
meta="tool",
|
|
49
|
+
detail=abbrev(getattr(call, "args", ""), 160),
|
|
50
|
+
)
|
|
51
|
+
elif resp is not None:
|
|
52
|
+
timeline.row(
|
|
53
|
+
icon="🔧",
|
|
54
|
+
label=getattr(resp, "name", ""),
|
|
55
|
+
meta="tool result",
|
|
56
|
+
detail=abbrev(getattr(resp, "response", ""), 160),
|
|
57
|
+
)
|
|
58
|
+
elif text and text.strip():
|
|
59
|
+
timeline.row(icon="💬", label="assistant", detail=abbrev(text, 200))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _run_config(max_llm_calls: int | None) -> typing.Any:
|
|
63
|
+
"""Build an ADK ``RunConfig`` that caps model calls; ``None`` → ADK's default (500)."""
|
|
64
|
+
if max_llm_calls is None:
|
|
65
|
+
return None
|
|
66
|
+
from google.adk.agents.run_config import RunConfig
|
|
67
|
+
|
|
68
|
+
return RunConfig(max_llm_calls=max_llm_calls)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _UsageSink:
|
|
72
|
+
"""Tally model-turn count + token usage across ADK's event stream.
|
|
73
|
+
|
|
74
|
+
Each model-response ``Event`` carries genai ``usage_metadata`` (tool-result events
|
|
75
|
+
don't), so events with usage = model turns and we sum their token counts. Gemini's
|
|
76
|
+
``cached_content_token_count`` is surfaced as ``cached`` (its context cache, like
|
|
77
|
+
Claude's cache-read tokens), ``thoughts_token_count`` as ``thinking`` for those models.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self.turns = self.prompt = self.completion = self.total = self.cached = self.thinking = 0
|
|
82
|
+
|
|
83
|
+
def add(self, event: typing.Any) -> None:
|
|
84
|
+
um = getattr(event, "usage_metadata", None)
|
|
85
|
+
if um is None:
|
|
86
|
+
return
|
|
87
|
+
self.turns += 1
|
|
88
|
+
self.prompt += getattr(um, "prompt_token_count", 0) or 0
|
|
89
|
+
self.completion += getattr(um, "candidates_token_count", 0) or 0
|
|
90
|
+
self.total += getattr(um, "total_token_count", 0) or 0
|
|
91
|
+
self.cached += getattr(um, "cached_content_token_count", 0) or 0
|
|
92
|
+
self.thinking += getattr(um, "thoughts_token_count", 0) or 0
|
|
93
|
+
|
|
94
|
+
def detail(self) -> str:
|
|
95
|
+
out = (
|
|
96
|
+
f"{self.turns} model turns · {self.prompt} prompt · "
|
|
97
|
+
f"{self.completion} completion · {self.total} total tokens"
|
|
98
|
+
)
|
|
99
|
+
if self.thinking:
|
|
100
|
+
out += f" · {self.thinking} thinking"
|
|
101
|
+
if self.cached:
|
|
102
|
+
out += f" · {self.cached} cached"
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def run_agent(
|
|
107
|
+
input: str,
|
|
108
|
+
*,
|
|
109
|
+
agent: typing.Any = None,
|
|
110
|
+
tools: typing.Sequence[typing.Any] = (),
|
|
111
|
+
model: str = "gemini-2.0-flash",
|
|
112
|
+
instructions: str | None = None,
|
|
113
|
+
name: str = "assistant",
|
|
114
|
+
max_llm_calls: int | None = None,
|
|
115
|
+
durable: bool = True,
|
|
116
|
+
observability: bool = True,
|
|
117
|
+
memory_key: str | None = None,
|
|
118
|
+
app_name: str = "flyte-agent",
|
|
119
|
+
user_id: str = "flyte-user",
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Run a Google ADK agent with the given tools and prompt; return the final text.
|
|
122
|
+
|
|
123
|
+
Await this from an async task as ``await run_agent(...)``; from a sync task
|
|
124
|
+
use :func:`run_agent_sync` instead.
|
|
125
|
+
|
|
126
|
+
Call this from inside an ``@env.task`` — that task is the durable parent, and each
|
|
127
|
+
tool the agent calls runs as a durable Flyte child action. Provide either a
|
|
128
|
+
pre-built ``agent`` (an ADK ``LlmAgent``/``BaseAgent``) or ``tools`` + ``model`` +
|
|
129
|
+
``instructions`` to have one built.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input: The user prompt.
|
|
133
|
+
agent: A pre-built ADK agent. Mutually exclusive with ``tools``.
|
|
134
|
+
tools: ``tool``-wrapped tools or bare ``@env.task`` templates.
|
|
135
|
+
model: Model name for the built agent (e.g. ``gemini-2.0-flash``).
|
|
136
|
+
instructions: System instruction for the built agent.
|
|
137
|
+
name: Agent name (a valid Python identifier). ADK injects this into the system
|
|
138
|
+
prompt as the model's "internal name", so it can surface in replies — keep it
|
|
139
|
+
natural (defaults to ``"assistant"``; avoid a brand-y/internal label).
|
|
140
|
+
max_llm_calls: Cap on model (LLM) calls before ADK raises
|
|
141
|
+
``LlmCallsLimitExceededError`` (its runaway-loop guard, via
|
|
142
|
+
``RunConfig.max_llm_calls``); ``None`` uses ADK's default of 500. Counts LLM
|
|
143
|
+
calls, not conversational turns (a tool round is ~2 calls). For a wall-clock
|
|
144
|
+
bound on the whole run, set ``timeout=`` on the enclosing ``@env.task``.
|
|
145
|
+
durable: Wrap the model so each turn is recorded/replayed via ``flyte.trace``.
|
|
146
|
+
observability: Render the run timeline into the Flyte task report.
|
|
147
|
+
memory_key: Stable id (user/thread) for cross-run memory. When set, the session
|
|
148
|
+
transcript is persisted and restored so a later run continues the conversation.
|
|
149
|
+
app_name: ADK app name (namespacing).
|
|
150
|
+
user_id: ADK user id.
|
|
151
|
+
"""
|
|
152
|
+
from google.adk.agents import LlmAgent
|
|
153
|
+
from google.adk.runners import Runner
|
|
154
|
+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
|
155
|
+
from google.genai import types
|
|
156
|
+
|
|
157
|
+
if agent is not None and tools:
|
|
158
|
+
raise ValueError("Pass either `agent` (with its own tools) or `tools`, not both.")
|
|
159
|
+
|
|
160
|
+
if agent is None:
|
|
161
|
+
agent = LlmAgent(
|
|
162
|
+
name=name,
|
|
163
|
+
model=durable_model(model) if durable else model,
|
|
164
|
+
instruction=instructions or "You are a helpful assistant.",
|
|
165
|
+
tools=[_coerce_tool(t) for t in tools],
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
session_service = InMemorySessionService()
|
|
169
|
+
store, prior_events = await load_memory(memory_key)
|
|
170
|
+
session_id = memory_key or uuid.uuid4().hex
|
|
171
|
+
session = await session_service.create_session(app_name=app_name, user_id=user_id, session_id=session_id)
|
|
172
|
+
for event in prior_events:
|
|
173
|
+
await session_service.append_event(session, event)
|
|
174
|
+
|
|
175
|
+
runner = Runner(agent=agent, app_name=app_name, session_service=session_service)
|
|
176
|
+
timeline = ReportTimeline() if observability else None
|
|
177
|
+
usage = _UsageSink() if observability else None
|
|
178
|
+
if timeline is not None:
|
|
179
|
+
timeline.heading("Google ADK agent")
|
|
180
|
+
|
|
181
|
+
final = ""
|
|
182
|
+
message = types.Content(role="user", parts=[types.Part.from_text(text=input)])
|
|
183
|
+
run_config = _run_config(max_llm_calls)
|
|
184
|
+
async for event in runner.run_async(
|
|
185
|
+
user_id=user_id, session_id=session_id, new_message=message, run_config=run_config
|
|
186
|
+
):
|
|
187
|
+
if timeline is not None:
|
|
188
|
+
_render(timeline, event)
|
|
189
|
+
if usage is not None:
|
|
190
|
+
usage.add(event)
|
|
191
|
+
if event.is_final_response() and event.content is not None:
|
|
192
|
+
final = _content_text(event.content) or final
|
|
193
|
+
|
|
194
|
+
if memory_key:
|
|
195
|
+
latest = await session_service.get_session(app_name=app_name, user_id=user_id, session_id=session_id)
|
|
196
|
+
await save_memory(store, getattr(latest, "events", session.events))
|
|
197
|
+
|
|
198
|
+
if timeline is not None and usage is not None and usage.turns:
|
|
199
|
+
timeline.row(icon="📊", label="usage", meta="model", detail=usage.detail())
|
|
200
|
+
if observability:
|
|
201
|
+
await flush_report()
|
|
202
|
+
return final
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
run_agent_sync = sync_variant(run_agent)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flyteplugins-agents-google
|
|
3
|
+
Version: 2.5.10
|
|
4
|
+
Summary: Run Google ADK (Agent Development Kit) agents on Flyte.
|
|
5
|
+
Author-email: Samhita Alla <samhita@union.ai>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: flyteplugins-agents-core
|
|
9
|
+
Requires-Dist: google-adk>=2.0
|
|
10
|
+
|
|
11
|
+
# flyteplugins-agents-google
|
|
12
|
+
|
|
13
|
+
Run [Google ADK](https://github.com/google/adk-python) (Agent Development Kit)
|
|
14
|
+
agents on Flyte. You keep writing ADK agents; Flyte is the runtime underneath.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install flyteplugins-agents-google
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import flyte
|
|
22
|
+
from flyteplugins.agents.google import tool, run_agent
|
|
23
|
+
|
|
24
|
+
env = flyte.TaskEnvironment(
|
|
25
|
+
"google-agent",
|
|
26
|
+
secrets=[flyte.Secret(key="google_api_key", as_env_var="GOOGLE_API_KEY")],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@tool
|
|
30
|
+
@env.task(cache="auto", retries=3)
|
|
31
|
+
async def get_weather(city: str) -> str:
|
|
32
|
+
"""Get the current weather for a city."""
|
|
33
|
+
return f"The weather in {city} is sunny, 22°C."
|
|
34
|
+
|
|
35
|
+
@env.task(report=True, retries=3)
|
|
36
|
+
async def city_agent(question: str) -> str:
|
|
37
|
+
return await run_agent(question, tools=[get_weather], model="gemini-2.0-flash")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How it maps to Flyte
|
|
41
|
+
|
|
42
|
+
- The SDK owns the loop — we don't reimplement it. ADK's `Runner` drives the
|
|
43
|
+
agent loop (model + tools, yielding `Event`s); `run_agent` builds an `LlmAgent`,
|
|
44
|
+
runs `Runner.run_async` inside your `@env.task`, and returns the final answer.
|
|
45
|
+
- Tools as durable child actions. `tool` wraps an `@env.task` as the
|
|
46
|
+
Python function ADK calls; its body dispatches to `task.aio()`, so each tool call
|
|
47
|
+
runs as a durable Flyte child action. ADK derives the tool declaration from the
|
|
48
|
+
task signature.
|
|
49
|
+
- Durable, replayable model turns. With `durable=True`, the agent's model is
|
|
50
|
+
wrapped (`FlyteLlm`) so each turn through `BaseLlm.generate_content_async` — the
|
|
51
|
+
seam below the loop — is recorded via `flyte.trace`. On a crash/retry, completed
|
|
52
|
+
turns replay from their recorded `LlmResponse` and tools are cache hits. (Same idea
|
|
53
|
+
as swapping OpenAI's `ModelProvider`: trace the model-call seam, not the loop.)
|
|
54
|
+
- Observability: the turns and tool calls render into the task report.
|
|
55
|
+
|
|
56
|
+
The API key is read from the environment (e.g. `GOOGLE_API_KEY` for Gemini, or your
|
|
57
|
+
Vertex AI config), so it can't leak into task inputs — wire it as a Flyte secret.
|
|
58
|
+
|
|
59
|
+
## Memory
|
|
60
|
+
|
|
61
|
+
Pass `memory_key` (a user/thread id) for cross-run memory — the agent continues
|
|
62
|
+
the same conversation across separate runs:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
await run_agent(message, model="gemini-2.0-flash", memory_key="user-alice")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
ADK keeps the conversation as a list of `Event`s on the session; we persist those to a
|
|
69
|
+
durable, keyed `MemoryStore` and restore them into a fresh session on the next run
|
|
70
|
+
with the same key.
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
See [`examples/`](examples/):
|
|
75
|
+
|
|
76
|
+
- [`google_durable_agent.py`](examples/google_durable_agent.py) — a single durable
|
|
77
|
+
agent: tools as Flyte tasks, traced model turns, agent timeline in the report.
|
|
78
|
+
- [`google_multi_agent.py`](examples/google_multi_agent.py) — multi-agent
|
|
79
|
+
orchestration: a planner agent decomposes a topic, researcher agents fan out in
|
|
80
|
+
parallel, an editor agent synthesizes — each agent its own durable action.
|
|
81
|
+
- [`google_crash_resume.py`](examples/google_crash_resume.py) — crash & resume: the
|
|
82
|
+
task crashes on its first attempt; on retry the completed model turns replay from
|
|
83
|
+
their `flyte.trace` records and the tool calls are cache hits. Run on a backend.
|
|
84
|
+
- [`google_memory.py`](examples/google_memory.py) — cross-run memory: two separate
|
|
85
|
+
runs share a `memory_key`; the agent learns a fact in run 1 and recalls it in run 2.
|
|
86
|
+
- [`google_handoffs.py`](examples/google_handoffs.py) — native agent transfer: a triage
|
|
87
|
+
agent transfers to a billing or technical-support sub-agent, the whole agent tree
|
|
88
|
+
durable on Flyte. The specialist can pause on a Flyte condition (`flyte.new_condition`)
|
|
89
|
+
to have a human share details mid-conversation, then resume with them.
|
|
90
|
+
|
|
91
|
+
## Conformance
|
|
92
|
+
|
|
93
|
+
This adapter passes the shared `flyteplugins.agents.core.testing.assert_adapter_conforms`
|
|
94
|
+
check — the same one every adapter runs — so it follows the common format
|
|
95
|
+
(`tool` + `run_agent`, tool tasks wired to the resolver), shared with the
|
|
96
|
+
OpenAI, Claude and Mistral adapters.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
flyteplugins/agents/google/__init__.py,sha256=GJ2Fmx2Gu5bKpZ8xikc8rqwehgMhFv6XT8O3OReTe_A,1067
|
|
2
|
+
flyteplugins/agents/google/_durable.py,sha256=_Omx14XTEZePtv4KduxdfmrXhqFq5LsG38k5yNGkScs,3240
|
|
3
|
+
flyteplugins/agents/google/_memory.py,sha256=J4nqiV8npRNyHjRc28H7kqtAOxfC-dv6dojTS-pdcQY,1484
|
|
4
|
+
flyteplugins/agents/google/_run.py,sha256=9Ja9CfiqAhiZNaOpsjc_qXu-ypEPgPRdWca778jGMSs,8610
|
|
5
|
+
flyteplugins_agents_google-2.5.10.dist-info/METADATA,sha256=MWz_OzW7s40bE964JEy85KGrBHS5ARJ4DxWmXuSrlqI,4219
|
|
6
|
+
flyteplugins_agents_google-2.5.10.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
7
|
+
flyteplugins_agents_google-2.5.10.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
|
|
8
|
+
flyteplugins_agents_google-2.5.10.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flyteplugins
|