You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f6ab08580 | |||
| 7415f2c094 | |||
| 6071b34650 | |||
| b89aac9eb3 | |||
| 3f9f773d9b | |||
| 7e8762896e | |||
| a4cbbf3e81 | |||
| b977d12ea8 | |||
| b1fa383e73 | |||
| 1a75848391 | |||
| bf7745f428 | |||
| c2058f3779 | |||
| 94dc03761e | |||
| 41f1888116 | |||
| 3c6e18fd1c | |||
| ce037ec888 | |||
| 391cf3f320 | |||
| a5d6f1e52e | |||
| 30d7b37069 | |||
| 57043d3e7d | |||
| 4a2589a090 | |||
| c25911c912 | |||
| a50419961c | |||
| 498ce906dd | |||
| f52df2bdb6 | |||
| 5124252c94 | |||
| dea1ee14ab | |||
| 94b5aaf0a1 | |||
| 62a611956f | |||
| a6716a5514 | |||
| a4baa35b84 | |||
| bab3be2cf3 | |||
| 33eec34577 | |||
| 48971d43d0 | |||
| b2ef9a069e | |||
| 134ec0d71e | |||
| a8fcc1b8ab | |||
| a57d629bb9 | |||
| 202558fd52 | |||
| 997b6a964d | |||
| c6c6f34429 | |||
| 7de173ed90 | |||
| 080edded76 | |||
| 074b7bebdf | |||
| 0a187b4a7d | |||
| b2a5afaa67 | |||
| b43a73bb3f | |||
| 8acfcfeb5d | |||
| 274e375e6f | |||
| 0a65b528e3 | |||
| 0a9e67dbca | |||
| 4ba0f0a8de | |||
| 4c5ef1e235 | |||
| 51be7a899c | |||
| 4845b7bacc | |||
| 162228d208 | |||
| 16ba91cd01 | |||
| 4dce9ef519 | |||
| 297f9e3ec9 | |||
| 894ef31fc1 | |||
| 5a721d99a7 | |||
| 2f655f0c67 | |||
| 68f34135fe | |||
| a313c2c1af | |||
| d83362725d | |||
| aef54f8eb7 | |||
| ca396a3ab1 | |||
| fb2415395f | |||
| 2034fbb8f1 | |||
| 149c5fe4ff | |||
| 0fe8dcabda | |||
| 10197db0d7 | |||
| 584c8c6d91 | |||
| 444eba58a3 | |||
| d792018c09 | |||
| 843b791bb0 | |||
| 2ac8c44668 | |||
| d3b500bd68 | |||
| c592a10e94 | |||
| 438a008cb2 | |||
| 63fda1b7bf | |||
| a794728a2f |
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user