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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ flyteplugins