Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Some examples require extra dependencies. See each sample's directory for specif
* [encryption](encryption) - Apply end-to-end encryption for all input/output.
* [env_config](env_config) - Load client configuration from TOML files with programmatic overrides.
* [gevent_async](gevent_async) - Combine gevent and Temporal.
* [google_adk](google_adk) - Orchestrate durable AI agent workflows with Google ADK (Gemini) including tools, multi-agent orchestration, and human-in-the-loop approval.
* [hello_standalone_activity](hello_standalone_activity) - Use activities without using a workflow.
* [langchain](langchain) - Orchestrate workflows for LangChain.
* [message_passing/introduction](message_passing/introduction/) - Introduction to queries, signals, and updates.
Expand Down
85 changes: 85 additions & 0 deletions google_adk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Temporal Google ADK Integration

This directory contains samples demonstrating how to use [Google's Agent Development Kit (ADK)](https://github.com/google/adk-python) with Temporal's durable execution engine.

See the [module documentation](https://github.com/temporalio/sdk-python/blob/main/temporalio/contrib/google_adk_agents/README.md) for more information.

## Overview

The integration combines:
- **Temporal workflows** for durable orchestration, retries, and state management
- **Google ADK** for AI agent creation with Gemini models and tool interactions

Every LLM call and tool execution is wrapped in a Temporal Activity, making agent workflows replay-safe, retryable, and observable.

## Prerequisites

- Temporal server [running locally](https://docs.temporal.io/cli/server#start-dev)
- Required dependencies installed via `uv sync --group google-adk`
- Google API key set as environment variable: `export GOOGLE_API_KEY=your_key_here`

## Examples

Each directory contains a complete example with its own instructions:

### [Basic Examples](./basic/)

Simple agent examples demonstrating core integration patterns:

- **Hello World** — A minimal agent that responds in haikus
- **Tools** — An agent with activity-backed tools (weather, web search)

```bash
# Terminal 1: Start the worker
uv run google_adk/basic/run_worker.py

# Terminal 2: Run a workflow
uv run google_adk/basic/run_hello_world_workflow.py
uv run google_adk/basic/run_tools_workflow.py
```

### [Orchestration](./orchestration/)

Multi-agent orchestration patterns — sequential pipelines, parallel fan-out, and iterative loops:

- **Sequential** — Researcher → Writer → Editor pipeline via chained agent calls
- **Parallel** — Multiple agents answer the same question from different perspectives simultaneously
- **Loop** — Agent iterates on its own output until a termination condition is met

```bash
# Terminal 1: Start the worker
uv run google_adk/orchestration/run_worker.py

# Terminal 2: Run an orchestration
uv run google_adk/orchestration/run_sequential_workflow.py
uv run google_adk/orchestration/run_parallel_workflow.py
uv run google_adk/orchestration/run_loop_workflow.py
```

### [Human-in-the-Loop](./human_in_the_loop/)

Demonstrates pausing agent execution for human approval before sensitive tool calls:

- Agent attempts a sensitive action (send email, delete record)
- Workflow pauses and exposes pending approvals via `@workflow.query`
- External process approves/rejects via `@workflow.signal`
- Agent resumes or reports rejection

```bash
# Terminal 1: Start the worker
uv run google_adk/human_in_the_loop/run_worker.py

# Terminal 2: Run the HITL workflow (starts workflow, polls for approvals, approves)
uv run google_adk/human_in_the_loop/run_hitl_workflow.py
```

## Key Integration Patterns

| Pattern | How It Works |
|---------|-------------|
| **Plugin** | `GoogleAdkPlugin` on `Client.connect()` — handles ADK/Pydantic serialization |
| **TemporalModel** | Wraps Gemini model calls as Temporal Activities for durability |
| **activity_tool** | Wraps `@activity.defn` functions as ADK-compatible tools |
| **InMemoryRunner** | ADK's runner executes the agent within the workflow |
| **Signals for HITL** | `@workflow.signal` enables external approval/rejection of tool calls |
| **Queries for observability** | `@workflow.query` exposes pending state without advancing the workflow |
Empty file added google_adk/__init__.py
Empty file.
7 changes: 7 additions & 0 deletions google_adk/basic/activities/get_weather_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from temporalio import activity


@activity.defn
async def get_weather(city: str) -> str:
"""Get current weather for a city (mock implementation)."""
return f"72°F and sunny in {city}"
7 changes: 7 additions & 0 deletions google_adk/basic/activities/search_web_activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from temporalio import activity


@activity.defn
async def search_web(query: str) -> str:
"""Search the web for information (mock implementation)."""
return f'Search results for "{query}": [Result 1: Overview] [Result 2: Details] [Result 3: Examples]'
25 changes: 25 additions & 0 deletions google_adk/basic/run_hello_world_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import asyncio

from temporalio.client import Client
from temporalio.contrib.google_adk_agents import GoogleAdkPlugin

from google_adk.basic.workflows.hello_world_workflow import HelloWorldWorkflow


async def main():
client = await Client.connect(
"localhost:7233",
plugins=[GoogleAdkPlugin()],
)

result = await client.execute_workflow(
HelloWorldWorkflow.run,
"Tell me about recursion in programming.",
id="google-adk-hello-world",
task_queue="google-adk-basic-task-queue",
)
print(f"Result: {result}")


if __name__ == "__main__":
asyncio.run(main())
25 changes: 25 additions & 0 deletions google_adk/basic/run_tools_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import asyncio

from temporalio.client import Client
from temporalio.contrib.google_adk_agents import GoogleAdkPlugin

from google_adk.basic.workflows.tools_workflow import ToolsWorkflow


async def main():
client = await Client.connect(
"localhost:7233",
plugins=[GoogleAdkPlugin()],
)

result = await client.execute_workflow(
ToolsWorkflow.run,
"What is the weather in Tokyo?",
id="google-adk-tools-workflow",
task_queue="google-adk-basic-task-queue",
)
print(f"Result: {result}")


if __name__ == "__main__":
asyncio.run(main())
32 changes: 32 additions & 0 deletions google_adk/basic/run_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from __future__ import annotations

import asyncio

from temporalio.client import Client
from temporalio.contrib.google_adk_agents import GoogleAdkPlugin
from temporalio.worker import Worker

from google_adk.basic.activities.get_weather_activity import get_weather
from google_adk.basic.activities.search_web_activity import search_web
from google_adk.basic.workflows.hello_world_workflow import HelloWorldWorkflow
from google_adk.basic.workflows.tools_workflow import ToolsWorkflow


async def main():
client = await Client.connect(
"localhost:7233",
plugins=[GoogleAdkPlugin()],
)

worker = Worker(
client,
task_queue="google-adk-basic-task-queue",
workflows=[HelloWorldWorkflow, ToolsWorkflow],
activities=[get_weather, search_web],
)
print("Worker started on task queue: google-adk-basic-task-queue")
await worker.run()


if __name__ == "__main__":
asyncio.run(main())
46 changes: 46 additions & 0 deletions google_adk/basic/workflows/hello_world_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from __future__ import annotations

from contextlib import aclosing

from temporalio import workflow
from temporalio.contrib.google_adk_agents import TemporalModel

with workflow.unsafe.imports_passed_through():
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.genai import types


@workflow.defn
class HelloWorldWorkflow:
@workflow.run
async def run(self, prompt: str) -> str:
agent = Agent(
name="Assistant",
model=TemporalModel("gemini-2.5-flash"),
instruction="You only respond in haikus.",
)

runner = InMemoryRunner(agent=agent, app_name="hello_world")
session = await runner.session_service.create_session(
user_id="user", app_name="hello_world"
)

result = ""
async with aclosing(
runner.run_async(
user_id="user",
session_id=session.id,
new_message=types.Content(
role="user",
parts=[types.Part.from_text(text=prompt)],
),
)
) as events:
async for event in events:
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
result = part.text

return result
59 changes: 59 additions & 0 deletions google_adk/basic/workflows/tools_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from contextlib import aclosing
from datetime import timedelta

from temporalio import workflow
from temporalio.contrib.google_adk_agents import TemporalModel
from temporalio.contrib.google_adk_agents.workflow import activity_tool

with workflow.unsafe.imports_passed_through():
from google.adk.agents import Agent
from google.adk.runners import InMemoryRunner
from google.genai import types

from google_adk.basic.activities.get_weather_activity import get_weather
from google_adk.basic.activities.search_web_activity import search_web


@workflow.defn
class ToolsWorkflow:
@workflow.run
async def run(self, question: str) -> str:
weather_tool = activity_tool(
get_weather, start_to_close_timeout=timedelta(seconds=10)
)
search_tool = activity_tool(
search_web, start_to_close_timeout=timedelta(seconds=10)
)

agent = Agent(
name="ToolsAgent",
model=TemporalModel("gemini-2.5-flash"),
instruction="You are a helpful agent. Use tools to answer questions.",
tools=[weather_tool, search_tool],
)

runner = InMemoryRunner(agent=agent, app_name="tools_agent")
session = await runner.session_service.create_session(
user_id="user", app_name="tools_agent"
)

result = ""
async with aclosing(
runner.run_async(
user_id="user",
session_id=session.id,
new_message=types.Content(
role="user",
parts=[types.Part.from_text(text=question)],
),
)
) as events:
async for event in events:
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
result = part.text

return result
13 changes: 13 additions & 0 deletions google_adk/human_in_the_loop/activities/sensitive_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from temporalio import activity


@activity.defn
async def send_email(to: str, subject: str, body: str) -> str:
"""Send an email (mock implementation)."""
return f"Email sent to {to} with subject '{subject}'"


@activity.defn
async def delete_record(record_id: str) -> str:
"""Delete a record from the database (mock implementation)."""
return f"Record {record_id} deleted successfully"
71 changes: 71 additions & 0 deletions google_adk/human_in_the_loop/run_hitl_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Human-in-the-loop example — demonstrates approval workflow.

This starter:
1. Starts the workflow with a prompt that will trigger a sensitive tool
2. Polls for pending approvals
3. Approves the tool call via signal
4. Waits for the final result
"""

import asyncio

from temporalio.client import Client
from temporalio.contrib.google_adk_agents import GoogleAdkPlugin

from google_adk.human_in_the_loop.workflows.hitl_workflow import (
ApprovalSignal,
HumanInTheLoopWorkflow,
)

WORKFLOW_ID = "google-adk-hitl-workflow"


async def main():
client = await Client.connect(
"localhost:7233",
plugins=[GoogleAdkPlugin()],
)

# Start the workflow (don't await result yet)
handle = await client.start_workflow(
HumanInTheLoopWorkflow.run,
"Send an email to alice@example.com with subject 'Hello' and body 'How are you?'",
id=WORKFLOW_ID,
task_queue="google-adk-hitl-task-queue",
)
print(f"Workflow started: {handle.id}")

# Poll for pending approvals
print("Waiting for tool call to require approval...")
pending = []
for _ in range(30):
pending = await handle.query(HumanInTheLoopWorkflow.get_pending_approvals)
if pending:
break
await asyncio.sleep(1)

if not pending:
print("No pending approvals found (agent may not have used a sensitive tool)")
result = await handle.result()
print(f"Result: {result}")
return

# Show pending approval and approve it
for call in pending:
print("\nPending approval:")
print(f" Tool: {call.tool_name}")
print(f" Arguments: {call.arguments}")
print(f" Approving call {call.call_id}...")

await handle.signal(
HumanInTheLoopWorkflow.approve,
ApprovalSignal(call_id=call.call_id, approved=True),
)

# Wait for final result
result = await handle.result()
print(f"\nFinal result: {result}")


if __name__ == "__main__":
asyncio.run(main())
Loading