Skip to content
Merged
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 google_adk_agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Each directory contains a complete example with its own README:
| Scenario | What it shows |
| --- | --- |
| [basic](./basic/README.md) | A single ADK agent with `TemporalModel` and one model call — no tools. The minimal end-to-end example. |
| [chatbot](./chatbot/README.md) | A multi-turn conversation over one persisted ADK session, with each turn driven by a workflow Update handler that returns the assistant's reply. |
| [tools](./tools/README.md) | A Temporal activity wrapped as an ADK tool with `activity_tool`, so tool calls run as their own activities. |
| [agent_patterns](./agent_patterns/README.md) | A coordinator `LlmAgent` with `sub_agents`, each a `TemporalModel` with a per-agent activity summary. |
| [mcp](./mcp/README.md) | A local echo MCP toolset via `TemporalMcpToolSet` / `TemporalMcpToolSetProvider`, running MCP tools as activities. Self-contained, no Node required. |
Expand Down
43 changes: 43 additions & 0 deletions google_adk_agents/chatbot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Chatbot — Multi-Turn Conversation via Updates

A no-frills conversational chatbot: an ADK `Agent` whose
`model=TemporalModel("gemini-2.5-flash")`, driven by an `InMemoryRunner` inside a
workflow that stays alive across turns. Unlike the [basic](../basic/README.md)
single-shot sample, one ADK session persists for the life of the workflow, so
the assistant remembers earlier turns.

Each conversational turn arrives as a Temporal **Update**: the `message` update
handler feeds the user's text into `runner.run_async` on the persisted session
and returns the assistant's reply as the update result. The handler has a noop
validator that accepts every message. Every model turn still runs as its own
`invoke_model` activity.

Before running, review the [prerequisites in the suite README](../README.md)
(Temporal dev server, `uv sync --group google-adk`, and
`export GOOGLE_API_KEY=...`).

## Running

Start the worker in one terminal:

```bash
uv run python -m google_adk_agents.chatbot.run_worker
```

Then start the interactive client in another terminal:

```bash
uv run python -m google_adk_agents.chatbot.run_chatbot_workflow
```

## What to expect

The client starts the workflow, then reads messages from stdin. Each line is
sent as an update and the assistant's reply is printed. Enter an empty line or
`/quit` to end the session, which terminates the workflow.

## In the Temporal UI

Open the workflow `google-adk-agents-chatbot-workflow-id`. In the history you
will see the workflow stay running to accept updates, with one `invoke_model`
activity per turn. The workflow itself stays deterministic and replay-safe.
Empty file.
34 changes: 34 additions & 0 deletions google_adk_agents/chatbot/run_chatbot_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import asyncio

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

from google_adk_agents.chatbot.workflows.chatbot_workflow import (
ChatbotAgentWorkflow,
)


async def main():
# @@@SNIPSTART google-adk-agents-chatbot-starter
client = await Client.connect("localhost:7233", plugins=[GoogleAdkPlugin()])

handle = await client.start_workflow(
ChatbotAgentWorkflow.run,
id="google-adk-agents-chatbot-workflow-id",
task_queue="google-adk-agents-chatbot",
)

print('Chat with the assistant. Enter an empty line or "/quit" to exit.')
while True:
message = input("> ").strip()
if not message or message == "/quit":
break
reply = await handle.execute_update(ChatbotAgentWorkflow.message, message)
print(f"Assistant: {reply}")

await handle.terminate()
# @@@SNIPEND


if __name__ == "__main__":
asyncio.run(main())
31 changes: 31 additions & 0 deletions google_adk_agents/chatbot/run_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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_agents.chatbot.workflows.chatbot_workflow import (
ChatbotAgentWorkflow,
)


async def main():
# @@@SNIPSTART google-adk-agents-chatbot-worker
plugin = GoogleAdkPlugin()

client = await Client.connect("localhost:7233", plugins=[plugin])

worker = Worker(
client,
task_queue="google-adk-agents-chatbot",
workflows=[ChatbotAgentWorkflow],
plugins=[plugin],
)
await worker.run()
# @@@SNIPEND


if __name__ == "__main__":
asyncio.run(main())
Empty file.
71 changes: 71 additions & 0 deletions google_adk_agents/chatbot/workflows/chatbot_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import asyncio

from google.adk import Agent
from google.adk.runners import InMemoryRunner
from google.adk.utils.context_utils import Aclosing
from google.genai import types
from temporalio import workflow
from temporalio.contrib.google_adk_agents import TemporalModel


# @@@SNIPSTART google-adk-agents-chatbot-agent-workflow
@workflow.defn
class ChatbotAgentWorkflow:
def __init__(self) -> None:
self._ready = False
self._runner: InMemoryRunner | None = None
self._session_id: str | None = None

@workflow.run
async def run(self) -> str:
agent = Agent(
name="chatbot_agent",
model=TemporalModel("gemini-2.5-flash"),
instruction="You are a helpful assistant.",
)

# The plugin points ADK's session-id generation at workflow.uuid4(), so
# creating a session here is replay-safe.
self._runner = InMemoryRunner(agent=agent, app_name="chatbot_app")
session = await self._runner.session_service.create_session(
app_name="chatbot_app", user_id="user"
)
self._session_id = session.id
self._ready = True

# Block forever to stay alive serving update turns; the client
# terminates the workflow when the user quits.
return await asyncio.Future()

@workflow.update
async def message(self, message: str) -> str:
# An update can arrive before run() has created the runner and session,
# so wait until they are ready before using them.
await workflow.wait_condition(lambda: self._ready)
assert self._runner is not None and self._session_id is not None

final_text = ""
async with Aclosing(
self._runner.run_async(
user_id="user",
session_id=self._session_id,
new_message=types.Content(
role="user", parts=[types.Part(text=message)]
),
)
) as event_stream:
async for event in event_stream:
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
final_text = part.text

return final_text

@message.validator
def validate_message(self, message: str) -> None:
# Verify user messages here
pass
Comment thread
brianstrauch marked this conversation as resolved.


# @@@SNIPEND
3 changes: 3 additions & 0 deletions tests/google_adk_agents/_mock_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def patch_model(
responses: list[LlmResponse],
*,
stream_chunks: bool = False,
captured: list[LlmRequest] | None = None,
) -> None:
script = list(responses)
orig_new_llm = LLMRegistry.new_llm # staticmethod
Expand All @@ -38,6 +39,8 @@ class _Mock(BaseLlm):
async def generate_content_async(
self, llm_request: LlmRequest, stream: bool = False
) -> AsyncGenerator[LlmResponse, None]:
if captured is not None:
captured.append(llm_request)
if stream_chunks:
# The streaming sample is single-turn, so yield every scripted
# chunk on this one call.
Expand Down
57 changes: 57 additions & 0 deletions tests/google_adk_agents/chatbot_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import uuid

import pytest
from google.adk.models.llm_request import LlmRequest
from temporalio.client import Client
from temporalio.contrib.google_adk_agents import GoogleAdkPlugin
from temporalio.worker import Worker

from google_adk_agents.chatbot.workflows.chatbot_workflow import (
ChatbotAgentWorkflow,
)
from tests.google_adk_agents._mock_model import patch_model, text


async def test_chatbot(client: Client, monkeypatch: pytest.MonkeyPatch) -> None:
captured: list[LlmRequest] = []
patch_model(
monkeypatch, [text("first reply"), text("second reply")], captured=captured
)

task_queue = f"google-adk-agents-chatbot-{uuid.uuid4()}"
plugin = GoogleAdkPlugin()

config = client.config()
config["plugins"] = [*config["plugins"], plugin]
client = Client(**config)

async with Worker(
client,
task_queue=task_queue,
workflows=[ChatbotAgentWorkflow],
max_cached_workflows=0,
):
handle = await client.start_workflow(
ChatbotAgentWorkflow.run,
id=f"google-adk-agents-chatbot-{uuid.uuid4()}",
task_queue=task_queue,
)

first = await handle.execute_update(ChatbotAgentWorkflow.message, "Hello")
second = await handle.execute_update(ChatbotAgentWorkflow.message, "Again")

await handle.terminate()

assert first == "first reply"
assert second == "second reply"

# The second turn reuses the same session, so its request carries the first
# turn's history.
turn_two_history = "\n".join(
part.text
for content in captured[1].contents
for part in (content.parts or [])
if part.text
)
assert "Hello" in turn_two_history
assert "first reply" in turn_two_history
Loading