-
Notifications
You must be signed in to change notification settings - Fork 114
Add Google ADK chatbot sample #330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
|
|
||
| # @@@SNIPEND | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.