Compare commits

...

82 Commits

Author SHA1 Message Date
Yifan Wang 2f6ab08580 chore: Release 1.7.0
PiperOrigin-RevId: 783918022
2025-07-16 15:29:19 -07:00
Hangfei Lin 7415f2c094 feat: Add tool callback chaining support for live streaming
**Highlights:**

-   **Callback Chaining:** Now supports a list of before and after callbacks, as in the non-live version. Callbacks are executed in order, stopping when one returns a response.
-   **Unit Tests:** Added new unit tests for before and after callbacks, async and sync versions, callback chains and mixed callbacks.
-   **Sample Agent:** Introduced a new example agent with multiple callbacks showing various behaviors: audit, security, validation, and enhancement. This provides practical usage examples.

PiperOrigin-RevId: 783884562
2025-07-16 13:56:06 -07:00
Hangfei Lin 6071b34650 feat: Implement Activity Start and End signals in LiveRequestQueue and BaseLLMConnection
This change adds activity start and end signals to the LiveRequestQueue,
allowing clients to manually control the start and end of user input in
streaming sessions when automatic voice activity detection is disabled.

The LiveRequestQueue allows users to send messages to the model with the following semantics:
- `content`: sends turn-by-turn content.
- `blob`: sends a media blob for realtime streaming (e.g., audio).
- `activity_start`: indicates the beginning of an activity.
- `activity_end`: indicates the end of an activity.
- `close`: closes the connection.

GeminiLLMConnection has been updated to send the new activity signals to the backend.

This change is a necessary to support clients (e.g. voice assistants) that do not want to use automatic voice activity detection. In this case, the client will be responsible to send the `activity_start` signal when the user starts talking, and `activity_end` when the user finishes talking.

To test the change:
    run_config = RunConfig(
        realtime_input_config=types.RealtimeInputConfig(
            automatic_activity_detection=types.AutomaticActivityDetection(
                disabled=True,
            ),
        )
    )

    import threading  # Add this import

    def thread_target():
      # Define the async operations to run in the background.
      async def background_task():
        live_request_queue.send_activity_start()

        # live_request_queue.send_content(
        #     content=types.Content(
        #         role='user',
        #         parts=[types.Part.from_text(text="hi, what's the time?")],
        #     )
        # )

        await asyncio.sleep(3)
        live_request_queue.send_activity_end()

PiperOrigin-RevId: 783882447
2025-07-16 13:51:04 -07:00
Google Team Member b89aac9eb3 ADK changes
PiperOrigin-RevId: 783804355
2025-07-16 10:17:19 -07:00
Google Team Member 3f9f773d9b feat: Add ability to send state change with message
PiperOrigin-RevId: 783589708
2025-07-15 21:17:04 -07:00
Yifan Wang 7e8762896e chore: update adk-web
PiperOrigin-RevId: 783527209
2025-07-15 17:14:19 -07:00
Ariz Chang a4cbbf3e81 feat: Add ability to send state change with message
PiperOrigin-RevId: 783516738
2025-07-15 16:37:26 -07:00
Xiang (Sean) Zhou b977d12ea8 refactor: Add save_credential and load_credential in callback context for developer to access credential service
developer may want to save/load credentials themselves to/from credential service. see (https://github.com/google/adk-python/issues/1816)

PiperOrigin-RevId: 783487628
2025-07-15 15:09:19 -07:00
Xiang (Sean) Zhou b1fa383e73 docs: Update agent transfer related doc string and comments
This is to address https://github.com/google/adk-python/issues/1907

PiperOrigin-RevId: 783481177
2025-07-15 14:50:33 -07:00
Xiang (Sean) Zhou 1a75848391 refactor: Use CallbackContext in credential service for saving/loading credential
1. credential service may be accessed by callbacks
2. plan to add load_credential and save_credential method in CallbackContext (see cl/782158513) given customer has requirement to access credential service themselves. (see https://github.com/google/adk-python/issues/1816)

It's backward compatible given CallbackContext is parent class of ToolContext

PiperOrigin-RevId: 783480378
2025-07-15 14:48:38 -07:00
Xiang (Sean) Zhou bf7745f428 fix: Create correct object for image and video content in litellm
PiperOrigin-RevId: 783478779
2025-07-15 14:44:14 -07:00
Hangfei Lin c2058f3779 chore: adk Add more documentations for ADK live feature in ADK.md
Also added commit message guidelines.

PiperOrigin-RevId: 783450329
2025-07-15 13:27:38 -07:00
Alejandro Cruzado-Ruiz 94dc03761e fix: return empty list in place of raising FileNotFoundError when there are no eval sets
PiperOrigin-RevId: 783439932
2025-07-15 12:56:38 -07:00
Liang Wu 41f1888116 feat(config): support callbacks in BaseAgent and LlmAgent
PiperOrigin-RevId: 783422160
2025-07-15 12:05:10 -07:00
Xiang (Sean) Zhou 3c6e18fd1c fix: Don't set error_code and error_message when model stop response in normal case
PiperOrigin-RevId: 783415488
2025-07-15 11:47:53 -07:00
Liang Wu ce037ec888 feat(config): support built-in tools and referencing tools in code
PiperOrigin-RevId: 783415273
2025-07-15 11:46:04 -07:00
Copybara-Service 391cf3f320 Merge pull request #1669 from zshehov:use-starred-tuple-unpacking-on-gcs-artifact-blob-names-versions
PiperOrigin-RevId: 783413576
2025-07-15 11:43:14 -07:00
Xiang (Sean) Zhou a5d6f1e52e fix: Support project-based gemini model path for BuiltInCodeExecutor and all built-in tools
This is to support model path like :
projects/265104255505/locations/us-central1/publishers/google/models/gemini-2.0-flash-001"

PiperOrigin-RevId: 783413351
2025-07-15 11:41:37 -07:00
Xiang (Sean) Zhou 30d7b37069 refactor: Move list_artifacts from tool_context to callback_context
1. users may want to list_artifacts in callbacks
2. save/load artifacts are already in callback_context

PiperOrigin-RevId: 783399941
2025-07-15 11:08:17 -07:00
Copybara-Service 57043d3e7d Merge pull request #1955 from iwknow:func_fix
PiperOrigin-RevId: 783391431
2025-07-15 10:47:16 -07:00
Zdravko Shehov 4a2589a090 fix: Use starred tuple unpacking on GCS artifact blob names for versions function 2025-07-15 11:47:18 +03:00
Ankur Sharma c25911c912 feat: Implement in memory EvalSetsManager
This version of the EvalSetsManager is intended to support two main behaviors

1) The agent developer wants to bring in their own eval set file, which is usually the case with `adk eval` cli. Once their eval sets are uploaded into this version of the eval sets manager, the EvalSetManager could be handed over to the Eval system for running evals.

2) As a part of AgentEvaluator testing, we expect developers to supply Eval cases in json files. The in-memory version of the EvalSetsManager will help us run those test cases using LocalEvalService.

PiperOrigin-RevId: 783198788
2025-07-14 23:34:35 -07:00
Google Team Member a50419961c fix: Process ListEvents response correctly when getSession is called
When get session is being called on a session with more than 1 page of events (100+), the response of the subsequent listevents calls fails due to missing parsing logic. This change fixes the processing of the listevents responses.

PiperOrigin-RevId: 783166959
2025-07-14 21:21:51 -07:00
Xiang (Sean) Zhou 498ce906dd docs: Update doc string for GcsArtifactService
PiperOrigin-RevId: 783157778
2025-07-14 20:41:16 -07:00
Xuan Yang f52df2bdb6 chore: update ADK Answering Agent to label the discussion with "bot_responded" after responding
PiperOrigin-RevId: 783109120
2025-07-14 17:47:29 -07:00
Yifan Wang 5124252c94 feat: add testing endpoint for builder
PiperOrigin-RevId: 783090278
2025-07-14 16:44:50 -07:00
Xiang (Sean) Zhou dea1ee14ab docs: Fix docstring and update module public name list for generating API references
To fix https://github.com/google/adk-docs/issues/131

PiperOrigin-RevId: 783080206
2025-07-14 16:16:57 -07:00
Frank Liu 94b5aaf0a1 fix: Correct EventAction merging logic and add corresponding tests 2025-07-14 14:54:36 -07:00
Xiang (Sean) Zhou 62a611956f fix: Add instruction in long running tool description to avoid being invoked again by model
PiperOrigin-RevId: 783039620
2025-07-14 14:18:49 -07:00
Holt Skinner a6716a5514 fix: Import A2A well known path from A2A sdk
PiperOrigin-RevId: 783036797
2025-07-14 14:11:01 -07:00
Che Liu a4baa35b84 feat: Add a sample for how to use plugins
PiperOrigin-RevId: 782616659
2025-07-13 10:47:11 -07:00
Ankur Sharma bab3be2cf3 feat: Add support for persisting eval run results
If the EvalRunResultsManager is provided to LocalEvalService, then we want to persist the eval run results using it.

PiperOrigin-RevId: 782196848
2025-07-11 19:29:32 -07:00
Ankur Sharma 33eec34577 feat: Adding implementation of evaluate method in LocalEvalService
Also, delete agent_creator.py file. We added this file by mistake.

PiperOrigin-RevId: 782193593
2025-07-11 19:15:40 -07:00
Liang Wu 48971d43d0 feat(config): support output_key and include_contents in LlmAgentConfig
PiperOrigin-RevId: 782170665
2025-07-11 17:47:42 -07:00
Liang Wu b2ef9a069e feat(config): support sub_agents in BaseAgentConfig
Currently only support path to YAML or code reference to agent instance.

PiperOrigin-RevId: 782157110
2025-07-11 16:57:53 -07:00
Xiang (Sean) Zhou 134ec0d71e fix: Fix the long running function response event merge logic
1) raise explicit error if the response event contains responses against multiple function call events
2) merge all function responses for the corresponding function call event

PiperOrigin-RevId: 782154577
2025-07-11 16:48:59 -07:00
Xiang (Sean) Zhou a8fcc1b8ab fix: Return final task result in task artifact instead of status message
According to a2a protocol task artifact is a different concept from adk artifact.
if a task is completed the final result should be in task artifact.

PiperOrigin-RevId: 782154265
2025-07-11 16:47:28 -07:00
Xiang (Sean) Zhou a57d629bb9 chore: Update the README of a2a sample agent
PiperOrigin-RevId: 782153753
2025-07-11 16:44:58 -07:00
Copybara-Service 202558fd52 Merge pull request #1829 from matino:expose_credential_service
PiperOrigin-RevId: 782150305
2025-07-11 16:34:58 -07:00
seanzhou1023 997b6a964d Merge branch 'main' into expose_credential_service 2025-07-11 15:38:27 -07:00
seanzhou1023 c6c6f34429 Update agent_tool.py 2025-07-11 15:34:07 -07:00
seanzhou1023 7de173ed90 Update test_readonly_context.py 2025-07-11 15:32:37 -07:00
seanzhou1023 080edded76 Update readonly_context.py 2025-07-11 15:31:13 -07:00
Copybara-Service 074b7bebdf Merge pull request #1855 from marcellomaugeri:main
PiperOrigin-RevId: 782121756
2025-07-11 15:04:53 -07:00
Sean Zhou 0a187b4a7d auto format the codes 2025-07-11 14:42:12 -07:00
seanzhou1023 b2a5afaa67 Merge branch 'main' into main 2025-07-11 14:40:31 -07:00
Copybara-Service b43a73bb3f Merge pull request #1602 from DavidSchmidt00:fix_graph
PiperOrigin-RevId: 782090252
2025-07-11 13:27:57 -07:00
Sean Zhou 8acfcfeb5d auto format codes 2025-07-11 13:14:33 -07:00
seanzhou1023 274e375e6f Merge branch 'main' into fix_graph 2025-07-11 12:55:24 -07:00
Copybara-Service 0a65b528e3 Merge pull request #1866 from hironow:hironow/type-fix-for-static
PiperOrigin-RevId: 782040553
2025-07-11 11:02:03 -07:00
Copybara-Service 0a9e67dbca Merge pull request #1875 from bih:patch-1
PiperOrigin-RevId: 781815489
2025-07-10 21:32:19 -07:00
Copybara-Service 4ba0f0a8de Merge pull request #120 from soundTricker:feature/80-sa-auth-for-google-tool
PiperOrigin-RevId: 781815460
2025-07-10 21:30:49 -07:00
Sean Zhou 4c5ef1e235 feat: Add configure service account auth for google_api_tool_set 2025-07-10 21:23:59 -07:00
Ankur Sharma 51be7a899c feat: Add implementation of BaseEvalService that runs evals locally
This change:
- Introduces the LocalEvalService Class.
- Implements only the "perform_inference" method. Evaluate method will be implemented in the next CL.
- Adds required test coverage.

PiperOrigin-RevId: 781781954
2025-07-10 19:25:24 -07:00
seanzhou1023 4845b7bacc Merge branch 'main' into patch-1 2025-07-10 17:53:08 -07:00
Che Liu 162228d208 feat: Integrating Plugin with ADK
This change integrates the plugin system with ADK. PluginManager is attached to the invocation context similar to session/artifact/memory.

It includes integrations with following ADK internal callbacks:

* App callbacks: Integrated in the BaseRunner class, in run_async and run_live
* On Message callbacks: Integrated in the BaseRunner class, triggers on run_async.
* Agent callbacks: Integrated in the BaseAgent class. Leveraging the existing *callback functions
* Model callbacks: Integrating in the base_llm_flow.
* Tool callbacks: Integrated in functions.py, wrapped around the code for agent tool_callbacks

Sample code to use plugins:

```python
# Add plugins to Runner

runner = Runner(
      app_name="my-app",
      agent=root_agent,
      artifact_service=artifact_service,
      session_service=session_service,
      memory_service=memory_service,
      plugins=[
        MySamplePlugin(),
        LoggingPlugin(),
      ],
  )

```

PiperOrigin-RevId: 781746586
2025-07-10 17:28:42 -07:00
Che Liu 16ba91cd01 feat: Implement PluginService for registering and executing plugins
PluginService takes the registration of plugins, and provide the wrapper utilities to execute all plugins.

PiperOrigin-RevId: 781745769
2025-07-10 17:26:05 -07:00
Che Liu 4dce9ef519 feat: ADK Plugin Base Class
Plugin is a collection of callbacks (lifecycle hooks), including callbacks for Agent, Tools, Model and App. It helps developers to add customizations and change behaviors of their agent application easily

This is the first change that introduces only the base interfaces based on the doc.

plugins folder is generated with `create_symlink.sh plugins`

PiperOrigin-RevId: 781745044
2025-07-10 17:25:54 -07:00
seanzhou1023 297f9e3ec9 Merge branch 'main' into patch-1 2025-07-10 17:19:59 -07:00
seanzhou1023 894ef31fc1 Update base_toolset.py 2025-07-10 17:19:42 -07:00
Yanfei Chen 5a721d99a7 test: add the unit test for EnterpriseWebSearchTool
PiperOrigin-RevId: 781743222
2025-07-10 17:16:13 -07:00
Google Team Member 2f655f0c67 fix: Add support for code execution result and skip inline data in anthropic llm
```
blaze run assistant/lamda/bard/scrape/vertex/tools:use_vertex_agent --    --prompt_collection_id=cab_v0p7@3     --agent_name=vertex_1p_agent     --kernel_id=vertex_agent:claude-sonnet-4@20250514 --dynamically_inject_tools=True --add_final_answer_tag=True --example_ids=plot-line-004_0.5_new_1
```
TODO: https://evalhub.corp.google.com/runs/Uf5en14gqmK-R/links
PiperOrigin-RevId: 781724544
2025-07-10 16:17:27 -07:00
Liang Wu 68f34135fe feat(config): add ParallelAgent and SequentialAgent to the loader
PiperOrigin-RevId: 781712004
2025-07-10 15:42:51 -07:00
Liang Wu a313c2c1af feat(config): add configs for ParallelAgent and SequentialAgent
PiperOrigin-RevId: 781704814
2025-07-10 15:24:22 -07:00
Liang Wu d83362725d feat(config): add disallow_transfer_to_parent and disallow_transfer_to_peers to LlmAgentConfig
PiperOrigin-RevId: 781690247
2025-07-10 14:45:01 -07:00
Liang Wu aef54f8eb7 feat(config): support loading from YAML config in CLI
The supported CLIs are: `adk web`, `adk run` and `adk api_server`.

PiperOrigin-RevId: 781666724
2025-07-10 13:47:58 -07:00
Liang Wu ca396a3ab1 feat(config): add APIs for building config agents
Including basic fields in configs, the from_config() methods and the JSON schema. Other fields will be added in following PRs.

PiperOrigin-RevId: 781660569
2025-07-10 13:32:28 -07:00
Xuan Yang fb2415395f chore: create an initial prototype agent to answer Github discussion questions
This agent will post a comment to answer questions or provide more information according to the knowledge base.

PiperOrigin-RevId: 781651937
2025-07-10 13:09:56 -07:00
Liang Wu 2034fbb8f1 chore: remove unused line for Client()
PiperOrigin-RevId: 781617898
2025-07-10 11:41:27 -07:00
Xuan Yang 149c5fe4ff chore: update triage agent to assign the issue to owner after labeling it
PiperOrigin-RevId: 781604031
2025-07-10 11:06:45 -07:00
Bilawal Hameed 0fe8dcabda docs: s/readony_context/readonly_context/g 2025-07-10 18:42:41 +01:00
Google Team Member 10197db0d7 feat: Make InMemoryMemoryService thread-safe
Even though InMemoryMemoryService is intended only for testing and local development, we eliminate a potential source of bugs during prototyping by providing a thread-safe InMemoryMemoryService.

PiperOrigin-RevId: 781554006
2025-07-10 08:55:18 -07:00
Xiang (Sean) Zhou 584c8c6d91 fix: improve partial event handling and streaming aggregation
- Allow run_async to break on partial events instead of raising ValueError
- Generate aggregated streaming content regardless of finish_reason
- Add error_code and error_message to final streaming responses if model response is interrupted.

PiperOrigin-RevId: 781377328
2025-07-09 23:03:33 -07:00
Xuan Yang 444eba58a3 chore: rename session_factory to sql_session for readability
PiperOrigin-RevId: 781359783
2025-07-09 21:59:58 -07:00
Hangfei Lin d792018c09 test: Adds test for streaming + function calls
This commit includes a number of new tests for live streaming with function calls. These tests cover various scenarios:
- Single function calls
- Multiple function calls
- Parallel function calls
- Function calls with errors
- Synchronous function calls
- Simple streaming tools
- Video streaming tools
- Stopping a streaming tool
- Multiple streaming tools simultaneously

The tests use mock models and custom runners to simulate the interaction between the agent, model, and tools. They verify that function calls are correctly generated, executed, and that the expected data is returned.

PiperOrigin-RevId: 781318483
2025-07-09 19:41:27 -07:00
hironow 843b791bb0 chore(fix typo): correct typo AUTHLIB_AVIALABLE → AUTHLIB_AVAILABLE 2025-07-10 03:08:31 +09:00
Marcello Maugeri 2ac8c44668 feat(lite_llm): add PDF support
Adds the ability to ingest and process PDF documents in the lite_llm pipeline.
2025-07-09 10:17:29 +02:00
Mateusz Czubak d3b500bd68 feat: expose credential service in readonly context 2025-07-08 11:32:50 +02:00
David Schmidt c592a10e94 Merge branch 'main' into fix_graph 2025-07-07 21:58:54 +02:00
David Schmidt 438a008cb2 Merge branch 'main' into fix_graph 2025-07-04 14:02:33 +02:00
Hangfei Lin 63fda1b7bf Merge branch 'main' into fix_graph 2025-06-25 16:38:20 -07:00
David Schmidt a794728a2f fix(agent_graph): Prevent duplicate edges in agent graph 2025-06-23 19:54:36 +02:00
144 changed files with 16356 additions and 5583 deletions
+17 -1
View File
@@ -10,6 +10,15 @@ The Agent Development Kit (ADK) is an open-source, code-first Python toolkit for
Please refer to [ADK Project Overview and Architecture](https://github.com/google/adk-python/blob/main/contributing/adk_project_overview_and_architecture.md) for details.
### ADK Live (Bidi-streaming)
- ADK live feature can be accessed from runner.run_live(...) and corresponding FAST api endpoint.
- ADK live feature is built on top of [Gemini Live API](https://cloud.google.com/vertex-ai/generative-ai/docs/live-api). We integrate Gemini Live API through [GenAI SDK](https://github.com/googleapis/python-genai).
- ADK live related configs are in [run_config.py](https://github.com/google/adk-python/blob/main/src/google/adk/agents/run_config.py).
- ADK live under multi-agent scenario: we convert the audio into text. This text will be passed to next agent as context.
- Most logics are in [base_llm_flow.py](https://github.com/google/adk-python/blob/main/src/google/adk/flows/llm_flows/base_llm_flow.py) and [gemini_llm_connection.py](https://github.com/google/adk-python/blob/main/src/google/adk/models/gemini_llm_connection.py).
- Tests are in [tests/unittests/streaming](https://github.com/google/adk-python/tree/main/tests/unittests/streaming).
## ADK: Style Guides
### Python Style Guide
@@ -200,4 +209,11 @@ The following changes are considered breaking and necessitate a MAJOR version
(e.g., the switch from snake_case to camelCase for all JSON payloads).
- Dependency Removal: Removing support for a previously integrated third-party
library or tool type.
library or tool type.
## Commit Message Format
- Please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
format.
- If it's not a breaking change, please add #non-breaking tag. If it's a
breaking change, please add #breaking.
+27
View File
@@ -1,5 +1,32 @@
# Changelog
## 1.7.0 (2025-07-16)
### Features
* Add ability to send state change with message [3f9f773](https://github.com/google/adk-python/commit/3f9f773d9b5fcca343e32f76f6d5677b7cf4c327)
* [Eval] Support for persisting eval run results [bab3be2](https://github.com/google/adk-python/commit/bab3be2cf31dc9afd00bcce70103bdaa5460f1a3)
* Introduce [Plugin]: Plugin is simply a class that packages these individual callback functions together for a broader purpose[162228d](https://github.com/google/adk-python/commit/162228d208dca39550a75221030edf9876bf8e3a)
### Bug Fixes
* Create correct object for image and video content in litellm [bf7745f](https://github.com/google/adk-python/commit/bf7745f42811de3c9c80ec0998001ae50960dafc)
* Support project-based gemini model path for BuiltInCodeExecutor and all built-in tools [a5d6f1e](https://github.com/google/adk-python/commit/a5d6f1e52ee36d84f94693086f74e4ca2d0bed65)
* Add instruction in long running tool description to avoid being invoked again by model [62a6119](https://github.com/google/adk-python/commit/62a611956f8907e0580955adb23dfb6d7799bf4f)
* [A2A] Import A2A well known path from A2A sdk [a6716a5](https://github.com/google/adk-python/commit/a6716a55140f63834ae4e3507b38786da9fdbee2)
* Fix the long running function response event merge logic [134ec0d](https://github.com/google/adk-python/commit/134ec0d71e8de4cf9bcbe370c7e739e7ada123f3)
* [A2A] Return final task result in task artifact instead of status message [a8fcc1b](https://github.com/google/adk-python/commit/a8fcc1b8ab0d47eccf6612a6eb8be021bff5ed3a)
* Make InMemoryMemoryService thread-safe [10197db](https://github.com/google/adk-python/commit/10197db0d752defc5976d1f276c7b5405a94c75b)
### Improvements
* Improve partial event handling and streaming aggregation [584c8c6](https://github.com/google/adk-python/commit/584c8c6d91308e62285c94629f020f2746e88f6f)
### Documentation
* Update agent transfer related doc string and comments [b1fa383](https://github.com/google/adk-python/commit/b1fa383e739d923399b3a23ca10435c0fba3460b)
* Update doc string for GcsArtifactService [498ce90](https://github.com/google/adk-python/commit/498ce906dd9b323b6277bc8118e1bcc68c38c1b5)
## [1.6.1](https://github.com/google/adk-python/compare/v1.5.0...v1.6.1) (2025-07-09)
### Features
+2 -1
View File
@@ -163,7 +163,8 @@ You can extend this sample by:
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- Verify the agent.json URL matches the running A2A server
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**OAuth Issues:**
- Verify OAuth client ID and secret are correctly set in .env file
+2 -1
View File
@@ -14,6 +14,7 @@
from google.adk.agents import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.tools.langchain_tool import LangchainTool
from langchain_community.tools import YouTubeSearchTool
@@ -41,7 +42,7 @@ bigquery_agent = RemoteA2aAgent(
name="bigquery_agent",
description="Help customer to manage notion workspace.",
agent_card=(
"http://localhost:8001/a2a/bigquery_agent/.well-known/agent.json"
f"http://localhost:8001/a2a/bigquery_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
)
+2 -1
View File
@@ -113,7 +113,8 @@ You can extend this sample by:
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- Verify the agent.json URL matches the running A2A server
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**Agent Not Responding:**
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
+2 -1
View File
@@ -15,6 +15,7 @@
import random
from google.adk.agents import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.tools.example_tool import ExampleTool
from google.genai import types
@@ -87,7 +88,7 @@ prime_agent = RemoteA2aAgent(
name="prime_agent",
description="Agent that handles checking if numbers are prime.",
agent_card=(
"http://localhost:8001/a2a/check_prime_agent/.well-known/agent.json"
f"http://localhost:8001/a2a/check_prime_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
)
@@ -122,7 +122,7 @@ You can extend this sample by:
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- Verify the agent.json URL matches the running A2A server
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**Agent Not Responding:**
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
@@ -14,6 +14,7 @@
from google.adk import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.genai import types
@@ -28,7 +29,9 @@ def reimburse(purpose: str, amount: float) -> str:
approval_agent = RemoteA2aAgent(
name='approval_agent',
description='Help approve the reimburse if the amount is greater than 100.',
agent_card='http://localhost:8001/a2a/human_in_loop/.well-known/agent.json',
agent_card=(
f'http://localhost:8001/a2a/human_in_loop{AGENT_CARD_WELL_KNOWN_PATH}'
),
)
@@ -0,0 +1,83 @@
# ADK Answering Agent
The ADK Answering Agent is a Python-based agent designed to help answer questions in GitHub discussions for the `google/adk-python` repository. It uses a large language model to analyze open discussions, retrieve information from document store, generate response, and post a comment in the github discussion.
This agent can be operated in three distinct modes: an interactive mode for local use, a batch script mode for oncall use, or as a fully automated GitHub Actions workflow (TBD).
---
## Interactive Mode
This mode allows you to run the agent locally to review its recommendations in real-time before any changes are made to your repository's issues.
### Features
* **Web Interface**: The agent's interactive mode can be rendered in a web browser using the ADK's `adk web` command.
* **User Approval**: In interactive mode, the agent is instructed to ask for your confirmation before posting a comment to a GitHub issue.
* **Question & Answer**: You can ask ADK related questions, and the agent will provide answers based on its knowledge on ADK.
### Running in Interactive Mode
To run the agent in interactive mode, first set the required environment variables. Then, execute the following command in your terminal:
```bash
adk web
```
This will start a local server and provide a URL to access the agent's web interface in your browser.
---
## Batch Script Mode
The `answer_discussions.py` is created for ADK oncall team to batch process discussions.
### Features
* **Batch Process**: Taken either a number as the count of the recent discussions or a list of discussion numbers, the script will invoke the agent to answer all the specified discussions in one single run.
### Running in Interactive Mode
To run the agent in batch script mode, first set the required environment variables. Then, execute the following command in your terminal:
```bash
export PYTHONPATH=contributing/samples
python -m adk_answering_agent.answer_discussions --numbers 27 36 # Answer specified discussions
```
Or `python -m adk_answering_agent.answer_discussions --recent 10` to answer the 10 most recent updated discussions.
---
## GitHub Workflow Mode
The `main.py` is reserved for the Github Workflow. The detailed setup for the automatic workflow is TBD.
---
## Setup and Configuration
Whether running in interactive or workflow mode, the agent requires the following setup.
### Dependencies
The agent requires the following Python libraries.
```bash
pip install --upgrade pip
pip install google-adk requests
```
The agent also requires gcloud login:
```bash
gcloud auth application-default login
```
### Environment Variables
The following environment variables are required for the agent to connect to the necessary services.
* `GITHUB_TOKEN=YOUR_GITHUB_TOKEN`: **(Required)** A GitHub Personal Access Token with `issues:write` permissions. Needed for both interactive and workflow modes.
* `GOOGLE_GENAI_USE_VERTEXAI=TRUE`: **(Required)** Use Google Vertex AI for the authentication.
* `GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID`: **(Required)** The Google Cloud project ID.
* `GOOGLE_CLOUD_LOCATION=LOCATION`: **(Required)** The Google Cloud region.
* `VERTEXAI_DATASTORE_ID=YOUR_DATASTORE_ID`: **(Required)** The Vertex AI datastore ID for the document store (i.e. knowledge base).
* `OWNER`: The GitHub organization or username that owns the repository (e.g., `google`). Needed for both modes.
* `REPO`: The name of the GitHub repository (e.g., `adk-python`). Needed for both modes.
* `INTERACTIVE`: Controls the agent's interaction mode. For the automated workflow, this is set to `0`. For interactive mode, it should be set to `1` or left unset.
For local execution in interactive mode, you can place these variables in a `.env` file in the project's root directory. For the GitHub workflow, they should be configured as repository secrets.
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,275 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from adk_answering_agent.settings import BOT_RESPONSE_LABEL
from adk_answering_agent.settings import IS_INTERACTIVE
from adk_answering_agent.settings import OWNER
from adk_answering_agent.settings import REPO
from adk_answering_agent.settings import VERTEXAI_DATASTORE_ID
from adk_answering_agent.utils import error_response
from adk_answering_agent.utils import run_graphql_query
from google.adk.agents import Agent
from google.adk.tools import VertexAiSearchTool
import requests
if IS_INTERACTIVE:
APPROVAL_INSTRUCTION = (
"Ask for user approval or confirmation for adding the comment."
)
else:
APPROVAL_INSTRUCTION = (
"**Do not** wait or ask for user approval or confirmation for adding the"
" comment."
)
def get_discussion_and_comments(discussion_number: int) -> dict[str, Any]:
"""Fetches a discussion and its comments using the GitHub GraphQL API.
Args:
discussion_number: The number of the GitHub discussion.
Returns:
A dictionary with the request status and the discussion details.
"""
print(f"Attempting to get discussion #{discussion_number} and its comments")
query = """
query($owner: String!, $repo: String!, $discussionNumber: Int!) {
repository(owner: $owner, name: $repo) {
discussion(number: $discussionNumber) {
id
title
body
createdAt
closed
author {
login
}
# For each discussion, fetch the latest 20 labels.
labels(last: 20) {
nodes {
id
name
}
}
# For each discussion, fetch the latest 100 comments.
comments(last: 100) {
nodes {
id
body
createdAt
author {
login
}
# For each discussion, fetch the latest 50 replies
replies(last: 50) {
nodes {
id
body
createdAt
author {
login
}
}
}
}
}
}
}
}
"""
variables = {
"owner": OWNER,
"repo": REPO,
"discussionNumber": discussion_number,
}
try:
response = run_graphql_query(query, variables)
if "errors" in response:
return error_response(str(response["errors"]))
discussion_data = (
response.get("data", {}).get("repository", {}).get("discussion")
)
if not discussion_data:
return error_response(f"Discussion #{discussion_number} not found.")
return {"status": "success", "discussion": discussion_data}
except requests.exceptions.RequestException as e:
return error_response(str(e))
def add_comment_to_discussion(
discussion_id: str, comment_body: str
) -> dict[str, Any]:
"""Adds a comment to a specific discussion.
Args:
discussion_id: The GraphQL node ID of the discussion.
comment_body: The content of the comment in Markdown.
Returns:
The status of the request and the new comment's details.
"""
print(f"Adding comment to discussion {discussion_id}")
query = """
mutation($discussionId: ID!, $body: String!) {
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
comment {
id
body
createdAt
author {
login
}
}
}
}
"""
variables = {"discussionId": discussion_id, "body": comment_body}
try:
response = run_graphql_query(query, variables)
if "errors" in response:
return error_response(str(response["errors"]))
new_comment = (
response.get("data", {}).get("addDiscussionComment", {}).get("comment")
)
return {"status": "success", "comment": new_comment}
except requests.exceptions.RequestException as e:
return error_response(str(e))
def get_label_id(label_name: str) -> str | None:
"""Helper function to find the GraphQL node ID for a given label name."""
print(f"Finding ID for label '{label_name}'...")
query = """
query($owner: String!, $repo: String!, $labelName: String!) {
repository(owner: $owner, name: $repo) {
label(name: $labelName) {
id
}
}
}
"""
variables = {"owner": OWNER, "repo": REPO, "labelName": label_name}
try:
response = run_graphql_query(query, variables)
if "errors" in response:
print(
f"[Warning] Error from GitHub API response for label '{label_name}':"
f" {response['errors']}"
)
return None
label_info = response["data"].get("repository", {}).get("label")
if label_info:
return label_info.get("id")
print(f"[Warning] Label information for '{label_name}' not found.")
return None
except requests.exceptions.RequestException as e:
print(f"[Warning] Error from GitHub API: {e}")
return None
def add_label_to_discussion(
discussion_id: str, label_name: str
) -> dict[str, Any]:
"""Adds a label to a specific discussion.
Args:
discussion_id: The GraphQL node ID of the discussion.
label_name: The name of the label to add (e.g., "bug").
Returns:
The status of the request and the label details.
"""
print(
f"Attempting to add label '{label_name}' to discussion {discussion_id}..."
)
# First, get the GraphQL ID of the label by its name
label_id = get_label_id(label_name)
if not label_id:
return error_response(f"Label '{label_name}' not found.")
# Then, perform the mutation to add the label to the discussion
mutation = """
mutation AddLabel($discussionId: ID!, $labelId: ID!) {
addLabelsToLabelable(input: {labelableId: $discussionId, labelIds: [$labelId]}) {
clientMutationId
}
}
"""
variables = {"discussionId": discussion_id, "labelId": label_id}
try:
response = run_graphql_query(mutation, variables)
if "errors" in response:
return error_response(str(response["errors"]))
return {"status": "success", "label_id": label_id, "label_name": label_name}
except requests.exceptions.RequestException as e:
return error_response(str(e))
root_agent = Agent(
model="gemini-2.5-pro",
name="adk_answering_agent",
description="Answer questions about ADK repo.",
instruction=f"""
You are a helpful assistant that responds to questions from the GitHub repository `{OWNER}/{REPO}`
based on information about Google ADK found in the document store. You can access the document store
using the `VertexAiSearchTool`.
When user specifies a discussion number, here are the steps:
1. Use the `get_discussion_and_comments` tool to get the details of the discussion including the comments.
2. Focus on the latest comment but reference all comments if needed to understand the context.
* If there is no comment at all, just focus on the discussion title and body.
3. If all the following conditions are met, try to add a comment to the discussion, otherwise, do not respond:
* The discussion is not closed.
* The latest comment is not from you or other agents (marked as "Response from XXX Agent").
* The latest comment is asking a question or requesting information.
4. Use the `VertexAiSearchTool` to find relevant information before answering.
5. If you can find relevant information, use the `add_comment_to_discussion` tool to add a comment to the discussion.
6. If you post a commment and the discussion does not have a label named {BOT_RESPONSE_LABEL},
add the label {BOT_RESPONSE_LABEL} to the discussion using the `add_label_to_discussion` tool.
IMPORTANT:
* {APPROVAL_INSTRUCTION}
* Your response should be based on the information you found in the document store. Do not invent
information that is not in the document store. Do not invent citations which are not in the document store.
* If you can't find the answer or information in the document store, **do not** respond.
* Include a bolded note (e.g. "Response from ADK Answering Agent") in your comment
to indicate this comment was added by an ADK Answering Agent.
* Have an empty line between the note and the rest of your response.
* Inlclude a short summary of your response in the comment as a TLDR, e.g. "**TLDR**: <your summary>".
* Have a divider line between the TLDR and your detail response.
* Do not respond to any other discussion except the one specified by the user.
* Please include your justification for your decision in your output
to the user who is telling with you.
* If you uses citation from the document store, please provide a footnote
referencing the source document format it as: "[1] URL of the document".
* Replace the "gs://prefix/" part, e.g. "gs://adk-qa-bucket/", to be "https://github.com/google/"
* Add "blob/main/" after the repo name, e.g. "adk-python", "adk-docs", for example:
* If the original URL is "gs://adk-qa-bucket/adk-python/src/google/adk/version.py",
then the citation URL is "https://github.com/google/adk-python/blob/main/src/google/adk/version.py",
* If the original URL is "gs://adk-qa-bucket/adk-docs/docs/index.md",
then the citation URL is "https://github.com/google/adk-docs/blob/main/docs/index.md"
* If the file is a html file, replace the ".html" to be ".md"
""",
tools=[
VertexAiSearchTool(data_store_id=VERTEXAI_DATASTORE_ID),
get_discussion_and_comments,
add_comment_to_discussion,
add_label_to_discussion,
],
)
@@ -0,0 +1,172 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import asyncio
import sys
import time
from adk_answering_agent import agent
from adk_answering_agent.settings import OWNER
from adk_answering_agent.settings import REPO
from adk_answering_agent.utils import call_agent_async
from adk_answering_agent.utils import run_graphql_query
from google.adk.runners import InMemoryRunner
import requests
APP_NAME = "adk_discussion_answering_app"
USER_ID = "adk_discussion_answering_assistant"
async def list_most_recent_discussions(count: int = 1) -> list[int] | None:
"""Fetches a specified number of the most recently updated discussions.
Args:
count: The number of discussions to retrieve. Defaults to 1.
Returns:
A list of discussion numbers.
"""
print(
f"Attempting to fetch the {count} most recently updated discussions from"
f" {OWNER}/{REPO}..."
)
query = """
query($owner: String!, $repo: String!, $count: Int!) {
repository(owner: $owner, name: $repo) {
discussions(
first: $count
orderBy: {field: UPDATED_AT, direction: DESC}
) {
nodes {
title
number
updatedAt
author {
login
}
}
}
}
}
"""
variables = {"owner": OWNER, "repo": REPO, "count": count}
try:
response = run_graphql_query(query, variables)
if "errors" in response:
print(f"Error from GitHub API: {response['errors']}", file=sys.stderr)
return None
discussions = (
response.get("data", {})
.get("repository", {})
.get("discussions", {})
.get("nodes", [])
)
return [d["number"] for d in discussions]
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}", file=sys.stderr)
return None
def process_arguments():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(
description="A script that answer questions for Github discussions.",
epilog=(
"Example usage: \n"
"\tpython -m adk_answering_agent.answer_discussions --recent 10\n"
"\tpython -m adk_answering_agent.answer_discussions --numbers 21 31\n"
),
formatter_class=argparse.RawTextHelpFormatter,
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--recent",
type=int,
metavar="COUNT",
help="Answer the N most recently updated discussion numbers.",
)
group.add_argument(
"--numbers",
type=int,
nargs="+",
metavar="NUM",
help="Answer a specific list of discussion numbers.",
)
if len(sys.argv) == 1:
parser.print_help(sys.stderr)
sys.exit(1)
return parser.parse_args()
async def main():
args = process_arguments()
discussion_numbers = []
if args.recent:
discussion_numbers = await list_most_recent_discussions(count=args.recent)
elif args.numbers:
discussion_numbers = args.numbers
if not discussion_numbers:
print("No discussions specified. Exiting...", file=sys.stderr)
sys.exit(1)
print(f"Will try to answer discussions: {discussion_numbers}...")
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
for discussion_number in discussion_numbers:
print("#" * 80)
print(f"Starting to process discussion #{discussion_number}...")
# Create a new session for each discussion to avoid interference.
session = await runner.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID
)
prompt = (
f"Please check discussion #{discussion_number} see if you can help"
" answer the question or provide some information!"
)
response = await call_agent_async(runner, USER_ID, session.id, prompt)
print(f"<<<< Agent Final Output: {response}\n")
if __name__ == "__main__":
start_time = time.time()
print(
f"Start answering discussions for {OWNER}/{REPO} at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}"
)
print("-" * 80)
asyncio.run(main())
print("-" * 80)
end_time = time.time()
print(
"Discussion answering finished at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(end_time))}",
)
print("Total script execution time:", f"{end_time - start_time:.2f} seconds")
@@ -0,0 +1,66 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
import time
from adk_answering_agent import agent
from adk_answering_agent.settings import DISCUSSION_NUMBER
from adk_answering_agent.settings import OWNER
from adk_answering_agent.settings import REPO
from adk_answering_agent.utils import call_agent_async
from adk_answering_agent.utils import parse_number_string
from google.adk.runners import InMemoryRunner
APP_NAME = "adk_answering_app"
USER_ID = "adk_answering_user"
async def main():
runner = InMemoryRunner(
agent=agent.root_agent,
app_name=APP_NAME,
)
session = await runner.session_service.create_session(
app_name=APP_NAME, user_id=USER_ID
)
discussion_number = parse_number_string(DISCUSSION_NUMBER)
if not discussion_number:
print(f"Error: Invalid discussion number received: {DISCUSSION_NUMBER}.")
return
prompt = (
f"Please check discussion #{discussion_number} see if you can help answer"
" the question or provide some information!"
)
response = await call_agent_async(runner, USER_ID, session.id, prompt)
print(f"<<<< Agent Final Output: {response}\n")
if __name__ == "__main__":
start_time = time.time()
print(
f"Start Q&A checking on {OWNER}/{REPO} discussion #{DISCUSSION_NUMBER} at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(start_time))}"
)
print("-" * 80)
asyncio.run(main())
print("-" * 80)
end_time = time.time()
print(
"Q&A checking finished at"
f" {time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(end_time))}",
)
print("Total script execution time:", f"{end_time - start_time:.2f} seconds")
@@ -0,0 +1,37 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from dotenv import load_dotenv
load_dotenv(override=True)
GITHUB_BASE_URL = "https://api.github.com"
GITHUB_GRAPHQL_URL = GITHUB_BASE_URL + "/graphql"
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
if not GITHUB_TOKEN:
raise ValueError("GITHUB_TOKEN environment variable not set")
VERTEXAI_DATASTORE_ID = os.getenv("VERTEXAI_DATASTORE_ID")
if not VERTEXAI_DATASTORE_ID:
raise ValueError("VERTEXAI_DATASTORE_ID environment variable not set")
OWNER = os.getenv("OWNER", "google")
REPO = os.getenv("REPO", "adk-python")
BOT_RESPONSE_LABEL = os.getenv("BOT_RESPONSE_LABEL", "bot_responded")
DISCUSSION_NUMBER = os.getenv("DISCUSSION_NUMBER")
IS_INTERACTIVE = os.getenv("INTERACTIVE", "1").lower() in ["true", "1"]
@@ -0,0 +1,81 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import sys
from typing import Any
from adk_answering_agent.settings import GITHUB_GRAPHQL_URL
from adk_answering_agent.settings import GITHUB_TOKEN
from google.adk.agents.run_config import RunConfig
from google.adk.runners import Runner
from google.genai import types
import requests
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
}
def error_response(error_message: str) -> dict[str, Any]:
return {"status": "error", "error_message": error_message}
def run_graphql_query(query: str, variables: dict[str, Any]) -> dict[str, Any]:
"""Executes a GraphQL query."""
payload = {"query": query, "variables": variables}
response = requests.post(
GITHUB_GRAPHQL_URL, headers=headers, json=payload, timeout=60
)
response.raise_for_status()
return response.json()
def parse_number_string(number_str: str | None, default_value: int = 0) -> int:
"""Parse a number from the given string."""
if not number_str:
return default_value
try:
return int(number_str)
except ValueError:
print(
f"Warning: Invalid number string: {number_str}. Defaulting to"
f" {default_value}.",
file=sys.stderr,
)
return default_value
async def call_agent_async(
runner: Runner, user_id: str, session_id: str, prompt: str
) -> str:
"""Call the agent asynchronously with the user's prompt."""
content = types.Content(
role="user", parts=[types.Part.from_text(text=prompt)]
)
final_response_text = ""
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=content,
run_config=RunConfig(save_input_blobs_as_artifacts=False),
):
if event.content and event.content.parts:
if text := "".join(part.text or "" for part in event.content.parts):
if event.author != "user":
final_response_text += text
return final_response_text
@@ -20,6 +20,7 @@ import requests
headers = {
"Authorization": f"token {GITHUB_TOKEN}",
"Accept": "application/vnd.github.v3+json",
"X-GitHub-Api-Version": "2022-11-28",
}
@@ -25,18 +25,18 @@ from adk_triaging_agent.utils import post_request
from google.adk import Agent
import requests
ALLOWED_LABELS = [
"documentation",
"services",
"question",
"tools",
"eval",
"live",
"models",
"tracing",
"core",
"web",
]
LABEL_TO_OWNER = {
"documentation": "polong",
"services": "DeanChensj",
"question": "",
"tools": "seanzhou1023",
"eval": "ankursharmas",
"live": "hangfei",
"models": "selcukgun",
"tracing": "Jacksunwei",
"core": "Jacksunwei",
"web": "wyf7107",
}
APPROVAL_INSTRUCTION = (
"Do not ask for user approval for labeling! If you can't find appropriate"
@@ -78,33 +78,61 @@ def list_unlabeled_issues(issue_count: int) -> dict[str, Any]:
return {"status": "success", "issues": unlabeled_issues}
def add_label_to_issue(issue_number: int, label: str) -> dict[str, Any]:
"""Add the specified label to the given issue number.
def add_label_and_owner_to_issue(
issue_number: int, label: str
) -> dict[str, Any]:
"""Add the specified label and owner to the given issue number.
Args:
issue_number: issue number of the Github issue.
label: label to assign
Returns:
The the status of this request, with the applied label when successful.
The the status of this request, with the applied label and assigned owner
when successful.
"""
print(f"Attempting to add label '{label}' to issue #{issue_number}")
if label not in ALLOWED_LABELS:
if label not in LABEL_TO_OWNER:
return error_response(
f"Error: Label '{label}' is not an allowed label. Will not apply."
)
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
payload = [label, BOT_LABEL]
label_url = (
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
)
label_payload = [label, BOT_LABEL]
try:
response = post_request(url, payload)
response = post_request(label_url, label_payload)
except requests.exceptions.RequestException as e:
return error_response(f"Error: {e}")
owner = LABEL_TO_OWNER.get(label, None)
if not owner:
return {
"status": "warning",
"message": (
f"{response}\n\nLabel '{label}' does not have an owner. Will not"
" assign."
),
"applied_label": label,
}
assignee_url = (
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/assignees"
)
assignee_payload = {"assignees": [owner]}
try:
response = post_request(assignee_url, assignee_payload)
except requests.exceptions.RequestException as e:
return error_response(f"Error: {e}")
return {
"status": "success",
"message": response,
"applied_label": label,
"assigned_owner": owner,
}
@@ -128,9 +156,12 @@ root_agent = Agent(
- If it's agent orchestration, agent definition, label it with "core".
- If you can't find a appropriate labels for the issue, follow the previous instruction that starts with "IMPORTANT:".
Call the `add_label_and_owner_to_issue` tool to label the issue, which will also assign the issue to the owner of the label.
Present the followings in an easy to read format highlighting issue number and your label.
- the issue summary in a few sentence
- your label recommendation and justification
- the owner of the label if you assign the issue to an owner
""",
tools=[list_unlabeled_issues, add_label_to_issue],
tools=[list_unlabeled_issues, add_label_and_owner_to_issue],
)
@@ -14,10 +14,6 @@
from google.adk import Agent
from google.adk.tools import google_search
from google.genai import Client
# Only Vertex AI supports image generation for now.
client = Client()
root_agent = Agent(
model='gemini-2.0-flash-001',
@@ -0,0 +1,15 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,269 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import datetime
import random
import time
from typing import Any
from typing import Dict
from typing import Optional
from google.adk import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
def get_weather(location: str, tool_context: ToolContext) -> Dict[str, Any]:
"""Get weather information for a location.
Args:
location: The city or location to get weather for.
Returns:
A dictionary containing weather information.
"""
# Simulate weather data
temperatures = [-10, -5, 0, 5, 10, 15, 20, 25, 30, 35]
conditions = ["sunny", "cloudy", "rainy", "snowy", "windy"]
return {
"location": location,
"temperature": random.choice(temperatures),
"condition": random.choice(conditions),
"humidity": random.randint(30, 90),
"timestamp": datetime.now().isoformat(),
}
async def calculate_async(operation: str, x: float, y: float) -> Dict[str, Any]:
"""Perform async mathematical calculations.
Args:
operation: The operation to perform (add, subtract, multiply, divide).
x: First number.
y: Second number.
Returns:
A dictionary containing the calculation result.
"""
# Simulate some async work
await asyncio.sleep(0.1)
operations = {
"add": x + y,
"subtract": x - y,
"multiply": x * y,
"divide": x / y if y != 0 else float("inf"),
}
result = operations.get(operation.lower(), "Unknown operation")
return {
"operation": operation,
"x": x,
"y": y,
"result": result,
"timestamp": datetime.now().isoformat(),
}
def log_activity(message: str, tool_context: ToolContext) -> Dict[str, str]:
"""Log an activity message with timestamp.
Args:
message: The message to log.
Returns:
A dictionary confirming the log entry.
"""
if "activity_log" not in tool_context.state:
tool_context.state["activity_log"] = []
log_entry = {"timestamp": datetime.now().isoformat(), "message": message}
tool_context.state["activity_log"].append(log_entry)
return {
"status": "logged",
"entry": log_entry,
"total_entries": len(tool_context.state["activity_log"]),
}
# Before tool callbacks
def before_tool_audit_callback(
tool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict[str, Any]]:
"""Audit callback that logs all tool calls before execution."""
print(f"🔍 AUDIT: About to call tool '{tool.name}' with args: {args}")
# Add audit info to tool context state
if "audit_log" not in tool_context.state:
tool_context.state["audit_log"] = []
tool_context.state["audit_log"].append({
"type": "before_call",
"tool_name": tool.name,
"args": args,
"timestamp": datetime.now().isoformat(),
})
# Return None to allow normal tool execution
return None
def before_tool_security_callback(
tool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict[str, Any]]:
"""Security callback that can block certain tool calls."""
# Example: Block weather requests for restricted locations
if tool.name == "get_weather" and args.get("location", "").lower() in [
"classified",
"secret",
]:
print(
"đźš« SECURITY: Blocked weather request for restricted location:"
f" {args.get('location')}"
)
return {
"error": "Access denied",
"reason": "Location access is restricted",
"requested_location": args.get("location"),
}
# Allow other calls to proceed
return None
async def before_tool_async_callback(
tool, args: Dict[str, Any], tool_context: ToolContext
) -> Optional[Dict[str, Any]]:
"""Async before callback that can add preprocessing."""
print(f"⚡ ASYNC BEFORE: Processing tool '{tool.name}' asynchronously")
# Simulate some async preprocessing
await asyncio.sleep(0.05)
# For calculation tool, we could add validation
if (
tool.name == "calculate_async"
and args.get("operation") == "divide"
and args.get("y") == 0
):
print("đźš« VALIDATION: Prevented division by zero")
return {
"error": "Division by zero",
"operation": args.get("operation"),
"x": args.get("x"),
"y": args.get("y"),
}
return None
# After tool callbacks
def after_tool_enhancement_callback(
tool,
args: Dict[str, Any],
tool_context: ToolContext,
tool_response: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
"""Enhance tool responses with additional metadata."""
print(f"✨ ENHANCE: Adding metadata to response from '{tool.name}'")
# Add enhancement metadata
enhanced_response = tool_response.copy()
enhanced_response.update({
"enhanced": True,
"enhancement_timestamp": datetime.now().isoformat(),
"tool_name": tool.name,
"execution_context": "live_streaming",
})
return enhanced_response
async def after_tool_async_callback(
tool,
args: Dict[str, Any],
tool_context: ToolContext,
tool_response: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
"""Async after callback for post-processing."""
print(
f"🔄 ASYNC AFTER: Post-processing response from '{tool.name}'"
" asynchronously"
)
# Simulate async post-processing
await asyncio.sleep(0.05)
# Add async processing metadata
processed_response = tool_response.copy()
processed_response.update({
"async_processed": True,
"processing_time": "0.05s",
"processor": "async_after_callback",
})
return processed_response
import asyncio
# Create the agent with tool callbacks
root_agent = Agent(
model="gemini-2.0-flash-live-preview-04-09", # for Vertex project
# model="gemini-2.0-flash-live-001", # for AI studio key
name="tool_callbacks_agent",
description=(
"Live streaming agent that demonstrates tool callbacks functionality. "
"It can get weather, perform calculations, and log activities while "
"showing how before and after tool callbacks work in live mode."
),
instruction="""
You are a helpful assistant that can:
1. Get weather information for any location using the get_weather tool
2. Perform mathematical calculations using the calculate_async tool
3. Log activities using the log_activity tool
Important behavioral notes:
- You have several callbacks that will be triggered before and after tool calls
- Before callbacks can audit, validate, or even block tool calls
- After callbacks can enhance or modify tool responses
- Some locations like "classified" or "secret" are restricted for weather requests
- Division by zero will be prevented by validation callbacks
- All your tool responses will be enhanced with additional metadata
When users ask you to test callbacks, explain what's happening with the callback system.
Be conversational and explain the callback behavior you observe.
""",
tools=[
get_weather,
calculate_async,
log_activity,
],
# Multiple before tool callbacks (will be processed in order until one returns a response)
before_tool_callback=[
before_tool_audit_callback,
before_tool_security_callback,
before_tool_async_callback,
],
# Multiple after tool callbacks (will be processed in order until one returns a response)
after_tool_callback=[
after_tool_enhancement_callback,
after_tool_async_callback,
],
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting(
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)

Some files were not shown because too many files have changed in this diff Show More