You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-03-30 10:57:20 -07:00
Merge branch 'main' into expose_credential_service
This commit is contained in:
@@ -1,5 +1,74 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.1](https://github.com/google/adk-python/compare/v1.5.0...v1.6.1) (2025-07-09)
|
||||
|
||||
### Features
|
||||
|
||||
* Add A2A support as experimental features [f0183a9](https://github.com/google/adk-python/commit/f0183a9b98b0bcf8aab4f948f467cef204ddc9d6)
|
||||
* Install google-adk with a2a extra: pip install google-adk[a2a]
|
||||
* Users can serve agents as A2A agent with `--a2a` option for `adk web` and
|
||||
`adk api_server`
|
||||
* Users can run a remote A2A agent with `RemoteA2AAgent` class
|
||||
* Three A2A agent samples are added:
|
||||
* contributing/samples/a2a_basic
|
||||
* contributing/samples/a2a_auth
|
||||
* contributing/samples/a2a_human_in_loop
|
||||
|
||||
* Support agent hot reload.[e545e5a](https://github.com/google/adk-python/commit/e545e5a570c1331d2ed8fda31c7244b5e0f71584)
|
||||
Users can add `--reload_agents` flag to `adk web` and `adk api_server` command
|
||||
to reload agents automatically when new changes are detected.
|
||||
|
||||
* Eval features
|
||||
* Implement auto rater-based evaluator for responses [75699fb](https://github.com/google/adk-python/commit/75699fbeca06f99c6f2415938da73bb423ec9b9b)
|
||||
* Add Safety evaluator metric [0bd05df](https://github.com/google/adk-python/commit/0bd05df471a440159a44b5864be4740b0f1565f9)
|
||||
* Add BaseEvalService declaration and surrounding data models [b0d88bf](https://github.com/google/adk-python/commit/b0d88bf17242e738bcd409b3d106deed8ce4d407)
|
||||
|
||||
* Minor features
|
||||
* Add `custom_metadata` to VertexAiSessionService when adding events [a021222](https://github.com/google/adk-python/commit/a02122207734cabb26f7c23e84d2336c4b8b0375)
|
||||
* Support protected write in BigQuery `execute_sql` tool [dc43d51](https://github.com/google/adk-python/commit/dc43d518c90b44932b3fdedd33fca9e6c87704e2)
|
||||
* Added clone() method to BaseAgent to allow users to create copies of an agent [d263afd] (https://github.com/google/adk-python/commit/d263afd91ba4a3444e5321c0e1801c499dec4c68)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Support project-based gemini model path to use enterprise_web_search_tool [e33161b](https://github.com/google/adk-python/commit/e33161b4f8650e8bcb36c650c4e2d1fe79ae2526)
|
||||
* Use inspect.signature() instead of typing.get_type_hints for examining function signatures[4ca77bc](https://github.com/google/adk-python/commit/4ca77bc056daa575621a80d3c8d5014b78209233)
|
||||
* Replace Event ID generation with UUID4 to prevent SQLite integrity constraint failures [e437c7a](https://github.com/google/adk-python/commit/e437c7aac650ac6a53fcfa71bd740e3e5ec0f230)
|
||||
* Remove duplicate options from `adk deploy` [3fa2ea7](https://github.com/google/adk-python/commit/3fa2ea7cb923c9f8606d98b45a23bd58a7027436)
|
||||
* Fix scenario where a user can access another users events given the same session id [362fb3f](https://github.com/google/adk-python/commit/362fb3f2b7ac4ad15852d00ce4f3935249d097f6)
|
||||
* Handle unexpected 'parameters' argument in FunctionTool.run_async [0959b06](https://github.com/google/adk-python/commit/0959b06dbdf3037fe4121f12b6d25edca8fb9afc)
|
||||
* Make sure each partial event has different timestamp [17d6042](https://github.com/google/adk-python/commit/17d604299505c448fcb55268f0cbaeb6c4fa314a)
|
||||
* Avoid pydantic.ValidationError when the model stream returns empty final chunk [9b75e24](https://github.com/google/adk-python/commit/9b75e24d8c01878c153fec26ccfea4490417d23b)
|
||||
* Fix google_search_tool.py to support updated Gemini LIVE model naming [77b869f](https://github.com/google/adk-python/commit/77b869f5e35a66682cba35563824fd23a9028d7c)
|
||||
* Adding detailed information on each metric evaluation [04de3e1](https://github.com/google/adk-python/commit/04de3e197d7a57935488eb7bfa647c7ab62cd9d9)
|
||||
* Converts litellm generate config err [3901fad](https://github.com/google/adk-python/commit/3901fade71486a1e9677fe74a120c3f08efe9d9e)
|
||||
* Save output in state via output_key only when the event is authored by current agent [20279d9](https://github.com/google/adk-python/commit/20279d9a50ac051359d791dea77865c17c0bbf9e)
|
||||
* Treat SQLite database update time as UTC for session's last update time [3f621ae](https://github.com/google/adk-python/commit/3f621ae6f2a5fac7f992d3d833a5311b4d4e7091)
|
||||
* Raise ValueError when sessionId and userId are incorrect combination(#1653) [4e765ae](https://github.com/google/adk-python/commit/4e765ae2f3821318e581c26a52e11d392aaf72a4)
|
||||
* Support API-Key for MCP Tool authentication [045aea9](https://github.com/google/adk-python/commit/045aea9b15ad0190a960f064d6e1e1fc7f964c69)
|
||||
* Lock LangGraph version to <= 0.4.10 [9029b8a](https://github.com/google/adk-python/commit/9029b8a66e9d5e0d29d9a6df0e5590cc7c0e9038)
|
||||
* Update the retry logic of create session polling [3d2f13c](https://github.com/google/adk-python/commit/3d2f13cecd3fef5adfa1c98bf23d7b68ff355f4d)
|
||||
|
||||
### Chores
|
||||
|
||||
* Extract mcp client creation logic to a separate method [45d60a1](https://github.com/google/adk-python/commit/45d60a1906bfe7c43df376a829377e2112ea3d17)
|
||||
* Add tests for live streaming configs [bf39c00](https://github.com/google/adk-python/commit/bf39c006102ef3f01e762e7bb744596a4589f171)
|
||||
* Update ResponseEvaluator to use newer version of Eval SDK [62c4a85](https://github.com/google/adk-python/commit/62c4a8591780a9a3fdb03a0de11092d84118a1b9)
|
||||
* Add util to build our llms.txt and llms-full.txt files [a903c54](https://github.com/google/adk-python/commit/a903c54bacfcb150dc315bec9c67bf7ce9551c07)
|
||||
* Create an example for multi agent live streaming [a58cc3d](https://github.com/google/adk-python/commit/a58cc3d882e59358553e8ea16d166b1ab6d3aa71)
|
||||
* Refactor the ADK Triaging Agent to make the code easier to read [b6c7b5b](https://github.com/google/adk-python/commit/b6c7b5b64fcd2e83ed43f7b96ea43791733955d8)
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* Update the a2a exmaple link in README.md [d0fdfb8](https://github.com/google/adk-python/commit/d0fdfb8c8e2e32801999c81de8d8ed0be3f88e76)
|
||||
* Adds AGENTS.md to provide relevant project context for the Gemini CLI [37108be](https://github.com/google/adk-python/commit/37108be8557e011f321de76683835448213f8515)
|
||||
* Update CONTRIBUTING.md [ffa9b36](https://github.com/google/adk-python/commit/ffa9b361db615ae365ba62c09a8f4226fb761551)
|
||||
* Add adk project overview and architecture [28d0ea8](https://github.com/google/adk-python/commit/28d0ea876f2f8de952f1eccbc788e98e39f50cf5)
|
||||
* Add docstring to clarify that inmemory service are not suitable for production [dc414cb](https://github.com/google/adk-python/commit/dc414cb5078326b8c582b3b9072cbda748766286)
|
||||
* Update agents.md to include versioning strategy [6a39c85](https://github.com/google/adk-python/commit/6a39c854e032bda3bc15f0e4fe159b41cf2f474b)
|
||||
* Add tenacity into project.toml [df141db](https://github.com/google/adk-python/commit/df141db60c1137a6bcddd6d46aad3dc506868543)
|
||||
* Updating CONTRIBUTING.md with missing extra [e153d07](https://github.com/google/adk-python/commit/e153d075939fb628a7dc42b12e1b3461842db541)
|
||||
|
||||
## [1.5.0](https://github.com/google/adk-python/compare/v1.4.2...v1.5.0) (2025-06-25)
|
||||
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ prime_agent = RemoteA2aAgent(
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-1.5-flash",
|
||||
model="gemini-2.0-flash",
|
||||
name="root_agent",
|
||||
instruction="""
|
||||
You are a helpful assistant that can roll dice and check if numbers are prime.
|
||||
|
||||
@@ -33,7 +33,7 @@ approval_agent = RemoteA2aAgent(
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-1.5-flash',
|
||||
model='gemini-2.0-flash',
|
||||
name='reimbursement_agent',
|
||||
instruction="""
|
||||
You are an agent whose job is to handle the reimbursement process for
|
||||
|
||||
@@ -39,7 +39,7 @@ def ask_for_approval(
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-1.5-flash',
|
||||
model='gemini-2.0-flash',
|
||||
name='reimbursement_agent',
|
||||
instruction="""
|
||||
You are an agent whose job is to handle the reimbursement process for
|
||||
|
||||
@@ -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,192 @@
|
||||
# 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 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 comment, fetch the latest 100 comments.
|
||||
comments(last: 100) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
createdAt
|
||||
author {
|
||||
login
|
||||
}
|
||||
# For each comment, 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))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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,
|
||||
],
|
||||
)
|
||||
@@ -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,36 @@
|
||||
# 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")
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
+15
@@ -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,150 @@
|
||||
# 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.
|
||||
|
||||
# pylint: disable=g-importing-member
|
||||
|
||||
import os
|
||||
|
||||
from google.adk import Agent
|
||||
import requests
|
||||
|
||||
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", "")
|
||||
if not GITHUB_TOKEN:
|
||||
raise ValueError("GITHUB_TOKEN environment variable not set")
|
||||
|
||||
OWNER = os.getenv("OWNER", "google")
|
||||
REPO = os.getenv("REPO", "adk-python")
|
||||
|
||||
|
||||
def get_github_pr_info_http(pr_number: int) -> str | None:
|
||||
"""Fetches information for a GitHub Pull Request by sending direct HTTP requests.
|
||||
|
||||
Args:
|
||||
pr_number (int): The number of the Pull Request.
|
||||
|
||||
Returns:
|
||||
pr_message: A string.
|
||||
"""
|
||||
base_url = "https://api.github.com"
|
||||
|
||||
headers = {
|
||||
"Accept": "application/vnd.github+json",
|
||||
"Authorization": f"Bearer {GITHUB_TOKEN}",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
}
|
||||
|
||||
pr_message = ""
|
||||
|
||||
# --- 1. Get main PR details ---
|
||||
pr_url = f"{base_url}/repos/{OWNER}/{REPO}/pulls/{pr_number}"
|
||||
print(f"Fetching PR details from: {pr_url}")
|
||||
try:
|
||||
response = requests.get(pr_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
pr_data = response.json()
|
||||
pr_message += f"The PR title is: {pr_data.get('title')}\n"
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(
|
||||
f"HTTP Error fetching PR details: {e.response.status_code} - "
|
||||
f" {e.response.text}"
|
||||
)
|
||||
return None
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Network or request error fetching PR details: {e}")
|
||||
return None
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
return None
|
||||
|
||||
# --- 2. Fetching associated commits (paginated) ---
|
||||
commits_url = pr_data.get(
|
||||
"commits_url"
|
||||
) # This URL is provided in the initial PR response
|
||||
if commits_url:
|
||||
print("\n--- Associated Commits in this PR: ---")
|
||||
page = 1
|
||||
while True:
|
||||
# GitHub API often uses 'per_page' and 'page' for pagination
|
||||
params = {
|
||||
"per_page": 100,
|
||||
"page": page,
|
||||
} # Fetch up to 100 commits per page
|
||||
try:
|
||||
response = requests.get(commits_url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
commits_data = response.json()
|
||||
|
||||
if not commits_data: # No more commits
|
||||
break
|
||||
|
||||
pr_message += "The associated commits are:\n"
|
||||
for commit in commits_data:
|
||||
message = commit.get("commit", {}).get("message", "").splitlines()[0]
|
||||
if message:
|
||||
pr_message += message + "\n"
|
||||
|
||||
# Check for 'Link' header to determine if more pages exist
|
||||
# This is how GitHub's API indicates pagination
|
||||
if "Link" in response.headers:
|
||||
link_header = response.headers["Link"]
|
||||
if 'rel="next"' in link_header:
|
||||
page += 1 # Move to the next page
|
||||
else:
|
||||
break # No more pages
|
||||
else:
|
||||
break # No Link header, so probably only one page
|
||||
|
||||
except requests.exceptions.HTTPError as e:
|
||||
print(
|
||||
f"HTTP Error fetching PR commits (page {page}):"
|
||||
f" {e.response.status_code} - {e.response.text}"
|
||||
)
|
||||
break
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(
|
||||
f"Network or request error fetching PR commits (page {page}): {e}"
|
||||
)
|
||||
break
|
||||
else:
|
||||
print("Commits URL not found in PR data.")
|
||||
|
||||
return pr_message
|
||||
|
||||
|
||||
system_prompt = """
|
||||
You are a helpful assistant to generate reasonable descriptions for pull requests for software engineers.
|
||||
|
||||
The descritions should not be too short (e.g.: less than 3 words), or too long (e.g.: more than 30 words).
|
||||
|
||||
The generated description should start with `chore`, `docs`, `feat`, `fix`, `test`, or `refactor`.
|
||||
`feat` stands for a new feature.
|
||||
`fix` stands for a bug fix.
|
||||
`chore`, `docs`, `test`, and `refactor` stand for improvements.
|
||||
|
||||
Some good descriptions are:
|
||||
1. feat: Added implementation for `get_eval_case`, `update_eval_case` and `delete_eval_case` for the local eval sets manager.
|
||||
2. feat: Provide inject_session_state as public util method.
|
||||
|
||||
Some bad descriptions are:
|
||||
1. fix: This fixes bugs.
|
||||
2. feat: This is a new feature.
|
||||
|
||||
"""
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.0-flash",
|
||||
name="github_pr_agent",
|
||||
description="Generate pull request descriptions for ADK.",
|
||||
instruction=system_prompt,
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
# pylint: disable=g-importing-member
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
import agent
|
||||
from google.adk.agents.run_config import RunConfig
|
||||
from google.adk.runners import InMemoryRunner
|
||||
from google.adk.sessions import Session
|
||||
from google.genai import types
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = "adk_pr_app"
|
||||
user_id_1 = "adk_pr_user"
|
||||
runner = InMemoryRunner(
|
||||
agent=agent.root_agent,
|
||||
app_name=app_name,
|
||||
)
|
||||
session_11 = await runner.session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_agent_prompt(session: Session, prompt_text: str):
|
||||
content = types.Content(
|
||||
role="user", parts=[types.Part.from_text(text=prompt_text)]
|
||||
)
|
||||
final_agent_response_parts = []
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
run_config=RunConfig(save_input_blobs_as_artifacts=False),
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
if event.author == agent.root_agent.name:
|
||||
final_agent_response_parts.append(event.content.parts[0].text)
|
||||
print(f"<<<< Agent Final Output: {''.join(final_agent_response_parts)}\n")
|
||||
|
||||
pr_message = agent.get_github_pr_info_http(pr_number=1422)
|
||||
query = "Generate pull request description for " + pr_message
|
||||
await run_agent_prompt(session_11, query)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_time = time.time()
|
||||
print(
|
||||
"Script start time:",
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(start_time)),
|
||||
)
|
||||
print("------------------------------------")
|
||||
asyncio.run(main())
|
||||
end_time = time.time()
|
||||
print("------------------------------------")
|
||||
print(
|
||||
"Script end time:",
|
||||
time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(end_time)),
|
||||
)
|
||||
print("Total script execution time:", f"{end_time - start_time:.2f} seconds")
|
||||
@@ -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',
|
||||
|
||||
@@ -51,6 +51,7 @@ dependencies = [
|
||||
"typing-extensions>=4.5, <5",
|
||||
"tzlocal>=5.3", # Time zone utilities
|
||||
"uvicorn>=0.34.0", # ASGI server for FastAPI
|
||||
"watchdog>=6.0.0", # For file change detection and hot reload
|
||||
"websockets>=15.0.1", # For BaseLlmFlow
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
|
||||
from pydantic import RootModel
|
||||
|
||||
from ..utils.feature_decorator import working_in_progress
|
||||
from .llm_agent import LlmAgentConfig
|
||||
from .loop_agent import LoopAgentConfig
|
||||
from .parallel_agent import ParallelAgentConfig
|
||||
from .sequential_agent import SequentialAgentConfig
|
||||
|
||||
# A discriminated union of all possible agent configurations.
|
||||
ConfigsUnion = Union[
|
||||
LlmAgentConfig,
|
||||
LoopAgentConfig,
|
||||
ParallelAgentConfig,
|
||||
SequentialAgentConfig,
|
||||
]
|
||||
|
||||
|
||||
# Use a RootModel to represent the agent directly at the top level.
|
||||
# The `discriminator` is applied to the union within the RootModel.
|
||||
@working_in_progress("AgentConfig is not ready for use.")
|
||||
class AgentConfig(RootModel[ConfigsUnion]):
|
||||
"""The config for the YAML schema to create an agent."""
|
||||
|
||||
class Config:
|
||||
# Pydantic v2 requires this for discriminated unions on RootModel
|
||||
# This tells the model to look at the 'agent_class' field of the input
|
||||
# data to decide which model from the `ConfigsUnion` to use.
|
||||
discriminator = "agent_class"
|
||||
@@ -19,9 +19,14 @@ from typing import Any
|
||||
from typing import AsyncGenerator
|
||||
from typing import Awaitable
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
from google.genai import types
|
||||
@@ -34,6 +39,7 @@ from typing_extensions import override
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from ..events.event import Event
|
||||
from ..utils.feature_decorator import working_in_progress
|
||||
from .callback_context import CallbackContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -56,6 +62,8 @@ AfterAgentCallback: TypeAlias = Union[
|
||||
list[_SingleAgentCallback],
|
||||
]
|
||||
|
||||
SelfAgent = TypeVar('SelfAgent', bound='BaseAgent')
|
||||
|
||||
|
||||
class BaseAgent(BaseModel):
|
||||
"""Base class for all agents in Agent Development Kit."""
|
||||
@@ -121,6 +129,56 @@ class BaseAgent(BaseModel):
|
||||
response and appended to event history as agent response.
|
||||
"""
|
||||
|
||||
def clone(
|
||||
self: SelfAgent, update: Mapping[str, Any] | None = None
|
||||
) -> SelfAgent:
|
||||
"""Creates a copy of this agent instance.
|
||||
|
||||
Args:
|
||||
update: Optional mapping of new values for the fields of the cloned agent.
|
||||
The keys of the mapping are the names of the fields to be updated, and
|
||||
the values are the new values for those fields.
|
||||
For example: {"name": "cloned_agent"}
|
||||
|
||||
Returns:
|
||||
A new agent instance with identical configuration as the original
|
||||
agent except for the fields specified in the update.
|
||||
"""
|
||||
if update is not None and 'parent_agent' in update:
|
||||
raise ValueError(
|
||||
'Cannot update `parent_agent` field in clone. Parent agent is set'
|
||||
' only when the parent agent is instantiated with the sub-agents.'
|
||||
)
|
||||
|
||||
# Only allow updating fields that are defined in the agent class.
|
||||
allowed_fields = set(self.__class__.model_fields)
|
||||
if update is not None:
|
||||
invalid_fields = set(update) - allowed_fields
|
||||
if invalid_fields:
|
||||
raise ValueError(
|
||||
f'Cannot update non-existent fields in {self.__class__.__name__}:'
|
||||
f' {invalid_fields}'
|
||||
)
|
||||
|
||||
cloned_agent = self.model_copy(update=update)
|
||||
|
||||
if update is None or 'sub_agents' not in update:
|
||||
# If `sub_agents` is not provided in the update, need to recursively clone
|
||||
# the sub-agents to avoid sharing the sub-agents with the original agent.
|
||||
cloned_agent.sub_agents = []
|
||||
for sub_agent in self.sub_agents:
|
||||
cloned_sub_agent = sub_agent.clone()
|
||||
cloned_sub_agent.parent_agent = cloned_agent
|
||||
cloned_agent.sub_agents.append(cloned_sub_agent)
|
||||
else:
|
||||
for sub_agent in cloned_agent.sub_agents:
|
||||
sub_agent.parent_agent = cloned_agent
|
||||
|
||||
# Remove the parent agent from the cloned agent to avoid sharing the parent
|
||||
# agent with the cloned agent.
|
||||
cloned_agent.parent_agent = None
|
||||
return cloned_agent
|
||||
|
||||
@final
|
||||
async def run_async(
|
||||
self,
|
||||
@@ -169,11 +227,18 @@ class BaseAgent(BaseModel):
|
||||
"""
|
||||
with tracer.start_as_current_span(f'agent_run [{self.name}]'):
|
||||
ctx = self._create_invocation_context(parent_context)
|
||||
# TODO(hangfei): support before/after_agent_callback
|
||||
|
||||
if event := await self.__handle_before_agent_callback(ctx):
|
||||
yield event
|
||||
if ctx.end_invocation:
|
||||
return
|
||||
|
||||
async for event in self._run_live_impl(ctx):
|
||||
yield event
|
||||
|
||||
if event := await self.__handle_after_agent_callback(ctx):
|
||||
yield event
|
||||
|
||||
async def _run_async_impl(
|
||||
self, ctx: InvocationContext
|
||||
) -> AsyncGenerator[Event, None]:
|
||||
@@ -277,73 +342,99 @@ class BaseAgent(BaseModel):
|
||||
) -> Optional[Event]:
|
||||
"""Runs the before_agent_callback if it exists.
|
||||
|
||||
Args:
|
||||
ctx: InvocationContext, the invocation context for this agent.
|
||||
|
||||
Returns:
|
||||
Optional[Event]: an event if callback provides content or changed state.
|
||||
"""
|
||||
ret_event = None
|
||||
|
||||
if not self.canonical_before_agent_callbacks:
|
||||
return ret_event
|
||||
|
||||
callback_context = CallbackContext(ctx)
|
||||
|
||||
for callback in self.canonical_before_agent_callbacks:
|
||||
before_agent_callback_content = callback(
|
||||
callback_context=callback_context
|
||||
)
|
||||
if inspect.isawaitable(before_agent_callback_content):
|
||||
before_agent_callback_content = await before_agent_callback_content
|
||||
if before_agent_callback_content:
|
||||
ret_event = Event(
|
||||
invocation_id=ctx.invocation_id,
|
||||
author=self.name,
|
||||
branch=ctx.branch,
|
||||
content=before_agent_callback_content,
|
||||
actions=callback_context._event_actions,
|
||||
# Run callbacks from the plugins.
|
||||
before_agent_callback_content = (
|
||||
await ctx.plugin_manager.run_before_agent_callback(
|
||||
agent=self, callback_context=callback_context
|
||||
)
|
||||
ctx.end_invocation = True
|
||||
return ret_event
|
||||
)
|
||||
|
||||
# If no overrides are provided from the plugins, further run the canonical
|
||||
# callbacks.
|
||||
if (
|
||||
not before_agent_callback_content
|
||||
and self.canonical_before_agent_callbacks
|
||||
):
|
||||
for callback in self.canonical_before_agent_callbacks:
|
||||
before_agent_callback_content = callback(
|
||||
callback_context=callback_context
|
||||
)
|
||||
if inspect.isawaitable(before_agent_callback_content):
|
||||
before_agent_callback_content = await before_agent_callback_content
|
||||
if before_agent_callback_content:
|
||||
break
|
||||
|
||||
# Process the override content if exists, and further process the state
|
||||
# change if exists.
|
||||
if before_agent_callback_content:
|
||||
ret_event = Event(
|
||||
invocation_id=ctx.invocation_id,
|
||||
author=self.name,
|
||||
branch=ctx.branch,
|
||||
content=before_agent_callback_content,
|
||||
actions=callback_context._event_actions,
|
||||
)
|
||||
ctx.end_invocation = True
|
||||
return ret_event
|
||||
|
||||
if callback_context.state.has_delta():
|
||||
ret_event = Event(
|
||||
return Event(
|
||||
invocation_id=ctx.invocation_id,
|
||||
author=self.name,
|
||||
branch=ctx.branch,
|
||||
actions=callback_context._event_actions,
|
||||
)
|
||||
|
||||
return ret_event
|
||||
return None
|
||||
|
||||
async def __handle_after_agent_callback(
|
||||
self, invocation_context: InvocationContext
|
||||
) -> Optional[Event]:
|
||||
"""Runs the after_agent_callback if it exists.
|
||||
|
||||
Args:
|
||||
invocation_context: InvocationContext, the invocation context for this
|
||||
agent.
|
||||
|
||||
Returns:
|
||||
Optional[Event]: an event if callback provides content or changed state.
|
||||
"""
|
||||
ret_event = None
|
||||
|
||||
if not self.canonical_after_agent_callbacks:
|
||||
return ret_event
|
||||
|
||||
callback_context = CallbackContext(invocation_context)
|
||||
|
||||
for callback in self.canonical_after_agent_callbacks:
|
||||
after_agent_callback_content = callback(callback_context=callback_context)
|
||||
if inspect.isawaitable(after_agent_callback_content):
|
||||
after_agent_callback_content = await after_agent_callback_content
|
||||
if after_agent_callback_content:
|
||||
ret_event = Event(
|
||||
invocation_id=invocation_context.invocation_id,
|
||||
author=self.name,
|
||||
branch=invocation_context.branch,
|
||||
content=after_agent_callback_content,
|
||||
actions=callback_context._event_actions,
|
||||
# Run callbacks from the plugins.
|
||||
after_agent_callback_content = (
|
||||
await invocation_context.plugin_manager.run_after_agent_callback(
|
||||
agent=self, callback_context=callback_context
|
||||
)
|
||||
return ret_event
|
||||
)
|
||||
|
||||
if callback_context.state.has_delta():
|
||||
# If no overrides are provided from the plugins, further run the canonical
|
||||
# callbacks.
|
||||
if (
|
||||
not after_agent_callback_content
|
||||
and self.canonical_after_agent_callbacks
|
||||
):
|
||||
for callback in self.canonical_after_agent_callbacks:
|
||||
after_agent_callback_content = callback(
|
||||
callback_context=callback_context
|
||||
)
|
||||
if inspect.isawaitable(after_agent_callback_content):
|
||||
after_agent_callback_content = await after_agent_callback_content
|
||||
if after_agent_callback_content:
|
||||
break
|
||||
|
||||
# Process the override content if exists, and further process the state
|
||||
# change if exists.
|
||||
if after_agent_callback_content:
|
||||
ret_event = Event(
|
||||
invocation_id=invocation_context.invocation_id,
|
||||
author=self.name,
|
||||
@@ -351,8 +442,17 @@ class BaseAgent(BaseModel):
|
||||
content=after_agent_callback_content,
|
||||
actions=callback_context._event_actions,
|
||||
)
|
||||
return ret_event
|
||||
|
||||
return ret_event
|
||||
if callback_context.state.has_delta():
|
||||
return Event(
|
||||
invocation_id=invocation_context.invocation_id,
|
||||
author=self.name,
|
||||
branch=invocation_context.branch,
|
||||
content=after_agent_callback_content,
|
||||
actions=callback_context._event_actions,
|
||||
)
|
||||
return None
|
||||
|
||||
@override
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
@@ -385,3 +485,49 @@ class BaseAgent(BaseModel):
|
||||
)
|
||||
sub_agent.parent_agent = self
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
@working_in_progress('BaseAgent.from_config is not ready for use.')
|
||||
def from_config(
|
||||
cls: Type[SelfAgent],
|
||||
config: BaseAgentConfig,
|
||||
) -> SelfAgent:
|
||||
"""Creates an agent from a config.
|
||||
|
||||
This method converts fields in a config to the corresponding
|
||||
fields in an agent.
|
||||
|
||||
Child classes should re-implement this method to support loading from their
|
||||
custom config types.
|
||||
|
||||
Args:
|
||||
config: The config to create the agent from.
|
||||
|
||||
Returns:
|
||||
The created agent.
|
||||
"""
|
||||
kwargs: Dict[str, Any] = {
|
||||
'name': config.name,
|
||||
'description': config.description,
|
||||
}
|
||||
return cls(**kwargs)
|
||||
|
||||
|
||||
@working_in_progress('BaseAgentConfig is not ready for use.')
|
||||
class BaseAgentConfig(BaseModel):
|
||||
"""The config for the YAML schema of a BaseAgent.
|
||||
|
||||
Do not use this class directly. It's the base class for all agent configs.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra='forbid')
|
||||
|
||||
agent_class: Literal['BaseAgent'] = 'BaseAgent'
|
||||
"""Required. The class of the agent. The value is used to differentiate
|
||||
among different agent classes."""
|
||||
|
||||
name: str
|
||||
"""Required. The name of the agent."""
|
||||
|
||||
description: str = ''
|
||||
"""Optional. The description of the agent."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user