mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Merge branch 'main' into main
This commit is contained in:
@@ -1,5 +1,74 @@
|
|||||||
# Changelog
|
# 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)
|
## [1.5.0](https://github.com/google/adk-python/compare/v1.4.2...v1.5.0) (2025-06-25)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
headers = {
|
||||||
"Authorization": f"token {GITHUB_TOKEN}",
|
"Authorization": f"token {GITHUB_TOKEN}",
|
||||||
"Accept": "application/vnd.github.v3+json",
|
"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
|
from google.adk import Agent
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
ALLOWED_LABELS = [
|
LABEL_TO_OWNER = {
|
||||||
"documentation",
|
"documentation": "polong",
|
||||||
"services",
|
"services": "DeanChensj",
|
||||||
"question",
|
"question": "",
|
||||||
"tools",
|
"tools": "seanzhou1023",
|
||||||
"eval",
|
"eval": "ankursharmas",
|
||||||
"live",
|
"live": "hangfei",
|
||||||
"models",
|
"models": "selcukgun",
|
||||||
"tracing",
|
"tracing": "Jacksunwei",
|
||||||
"core",
|
"core": "Jacksunwei",
|
||||||
"web",
|
"web": "wyf7107",
|
||||||
]
|
}
|
||||||
|
|
||||||
APPROVAL_INSTRUCTION = (
|
APPROVAL_INSTRUCTION = (
|
||||||
"Do not ask for user approval for labeling! If you can't find appropriate"
|
"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}
|
return {"status": "success", "issues": unlabeled_issues}
|
||||||
|
|
||||||
|
|
||||||
def add_label_to_issue(issue_number: int, label: str) -> dict[str, Any]:
|
def add_label_and_owner_to_issue(
|
||||||
"""Add the specified label to the given issue number.
|
issue_number: int, label: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Add the specified label and owner to the given issue number.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
issue_number: issue number of the Github issue.
|
issue_number: issue number of the Github issue.
|
||||||
label: label to assign
|
label: label to assign
|
||||||
|
|
||||||
Returns:
|
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}")
|
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(
|
return error_response(
|
||||||
f"Error: Label '{label}' is not an allowed label. Will not apply."
|
f"Error: Label '{label}' is not an allowed label. Will not apply."
|
||||||
)
|
)
|
||||||
|
|
||||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
|
label_url = (
|
||||||
payload = [label, BOT_LABEL]
|
f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}/labels"
|
||||||
|
)
|
||||||
|
label_payload = [label, BOT_LABEL]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = post_request(url, payload)
|
response = post_request(label_url, label_payload)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
return error_response(f"Error: {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 {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": response,
|
"message": response,
|
||||||
"applied_label": label,
|
"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 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:".
|
- 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.
|
Present the followings in an easy to read format highlighting issue number and your label.
|
||||||
- the issue summary in a few sentence
|
- the issue summary in a few sentence
|
||||||
- your label recommendation and justification
|
- 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 import Agent
|
||||||
from google.adk.tools import google_search
|
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(
|
root_agent = Agent(
|
||||||
model='gemini-2.0-flash-001',
|
model='gemini-2.0-flash-001',
|
||||||
|
|||||||
@@ -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 AsyncGenerator
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
from typing import final
|
from typing import final
|
||||||
|
from typing import Literal
|
||||||
|
from typing import Mapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
@@ -34,6 +39,7 @@ from typing_extensions import override
|
|||||||
from typing_extensions import TypeAlias
|
from typing_extensions import TypeAlias
|
||||||
|
|
||||||
from ..events.event import Event
|
from ..events.event import Event
|
||||||
|
from ..utils.feature_decorator import working_in_progress
|
||||||
from .callback_context import CallbackContext
|
from .callback_context import CallbackContext
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -56,6 +62,8 @@ AfterAgentCallback: TypeAlias = Union[
|
|||||||
list[_SingleAgentCallback],
|
list[_SingleAgentCallback],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SelfAgent = TypeVar('SelfAgent', bound='BaseAgent')
|
||||||
|
|
||||||
|
|
||||||
class BaseAgent(BaseModel):
|
class BaseAgent(BaseModel):
|
||||||
"""Base class for all agents in Agent Development Kit."""
|
"""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.
|
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
|
@final
|
||||||
async def run_async(
|
async def run_async(
|
||||||
self,
|
self,
|
||||||
@@ -169,11 +227,18 @@ class BaseAgent(BaseModel):
|
|||||||
"""
|
"""
|
||||||
with tracer.start_as_current_span(f'agent_run [{self.name}]'):
|
with tracer.start_as_current_span(f'agent_run [{self.name}]'):
|
||||||
ctx = self._create_invocation_context(parent_context)
|
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):
|
async for event in self._run_live_impl(ctx):
|
||||||
yield event
|
yield event
|
||||||
|
|
||||||
|
if event := await self.__handle_after_agent_callback(ctx):
|
||||||
|
yield event
|
||||||
|
|
||||||
async def _run_async_impl(
|
async def _run_async_impl(
|
||||||
self, ctx: InvocationContext
|
self, ctx: InvocationContext
|
||||||
) -> AsyncGenerator[Event, None]:
|
) -> AsyncGenerator[Event, None]:
|
||||||
@@ -277,73 +342,99 @@ class BaseAgent(BaseModel):
|
|||||||
) -> Optional[Event]:
|
) -> Optional[Event]:
|
||||||
"""Runs the before_agent_callback if it exists.
|
"""Runs the before_agent_callback if it exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ctx: InvocationContext, the invocation context for this agent.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[Event]: an event if callback provides content or changed state.
|
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)
|
callback_context = CallbackContext(ctx)
|
||||||
|
|
||||||
for callback in self.canonical_before_agent_callbacks:
|
# Run callbacks from the plugins.
|
||||||
before_agent_callback_content = callback(
|
before_agent_callback_content = (
|
||||||
callback_context=callback_context
|
await ctx.plugin_manager.run_before_agent_callback(
|
||||||
)
|
agent=self, 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,
|
|
||||||
)
|
)
|
||||||
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():
|
if callback_context.state.has_delta():
|
||||||
ret_event = Event(
|
return Event(
|
||||||
invocation_id=ctx.invocation_id,
|
invocation_id=ctx.invocation_id,
|
||||||
author=self.name,
|
author=self.name,
|
||||||
branch=ctx.branch,
|
branch=ctx.branch,
|
||||||
actions=callback_context._event_actions,
|
actions=callback_context._event_actions,
|
||||||
)
|
)
|
||||||
|
|
||||||
return ret_event
|
return None
|
||||||
|
|
||||||
async def __handle_after_agent_callback(
|
async def __handle_after_agent_callback(
|
||||||
self, invocation_context: InvocationContext
|
self, invocation_context: InvocationContext
|
||||||
) -> Optional[Event]:
|
) -> Optional[Event]:
|
||||||
"""Runs the after_agent_callback if it exists.
|
"""Runs the after_agent_callback if it exists.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
invocation_context: InvocationContext, the invocation context for this
|
||||||
|
agent.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[Event]: an event if callback provides content or changed state.
|
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)
|
callback_context = CallbackContext(invocation_context)
|
||||||
|
|
||||||
for callback in self.canonical_after_agent_callbacks:
|
# Run callbacks from the plugins.
|
||||||
after_agent_callback_content = callback(callback_context=callback_context)
|
after_agent_callback_content = (
|
||||||
if inspect.isawaitable(after_agent_callback_content):
|
await invocation_context.plugin_manager.run_after_agent_callback(
|
||||||
after_agent_callback_content = await after_agent_callback_content
|
agent=self, callback_context=callback_context
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
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(
|
ret_event = Event(
|
||||||
invocation_id=invocation_context.invocation_id,
|
invocation_id=invocation_context.invocation_id,
|
||||||
author=self.name,
|
author=self.name,
|
||||||
@@ -351,8 +442,17 @@ class BaseAgent(BaseModel):
|
|||||||
content=after_agent_callback_content,
|
content=after_agent_callback_content,
|
||||||
actions=callback_context._event_actions,
|
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
|
@override
|
||||||
def model_post_init(self, __context: Any) -> None:
|
def model_post_init(self, __context: Any) -> None:
|
||||||
@@ -385,3 +485,49 @@ class BaseAgent(BaseModel):
|
|||||||
)
|
)
|
||||||
sub_agent.parent_agent = self
|
sub_agent.parent_agent = self
|
||||||
return 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."""
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from ..utils.feature_decorator import working_in_progress
|
||||||
|
from .agent_config import AgentConfig
|
||||||
|
from .base_agent import BaseAgent
|
||||||
|
from .llm_agent import LlmAgent
|
||||||
|
from .llm_agent import LlmAgentConfig
|
||||||
|
from .loop_agent import LoopAgent
|
||||||
|
from .loop_agent import LoopAgentConfig
|
||||||
|
from .parallel_agent import ParallelAgent
|
||||||
|
from .parallel_agent import ParallelAgentConfig
|
||||||
|
from .sequential_agent import SequentialAgent
|
||||||
|
from .sequential_agent import SequentialAgentConfig
|
||||||
|
|
||||||
|
|
||||||
|
@working_in_progress("from_config is not ready for use.")
|
||||||
|
def from_config(config_path: str) -> BaseAgent:
|
||||||
|
"""Build agent from a configfile path.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: the path to a YAML config file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created agent instance.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If config file doesn't exist.
|
||||||
|
ValidationError: If config file's content is invalid YAML.
|
||||||
|
ValueError: If agent type is unsupported.
|
||||||
|
"""
|
||||||
|
abs_path = os.path.abspath(config_path)
|
||||||
|
config = _load_config_from_path(abs_path)
|
||||||
|
|
||||||
|
if isinstance(config.root, LlmAgentConfig):
|
||||||
|
return LlmAgent.from_config(config.root)
|
||||||
|
elif isinstance(config.root, LoopAgentConfig):
|
||||||
|
return LoopAgent.from_config(config.root)
|
||||||
|
elif isinstance(config.root, ParallelAgentConfig):
|
||||||
|
return ParallelAgent.from_config(config.root)
|
||||||
|
elif isinstance(config.root, SequentialAgentConfig):
|
||||||
|
return SequentialAgent.from_config(config.root)
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported config type")
|
||||||
|
|
||||||
|
|
||||||
|
@working_in_progress("_load_config_from_path is not ready for use.")
|
||||||
|
def _load_config_from_path(config_path: str) -> AgentConfig:
|
||||||
|
"""Load an agent's configuration from a YAML file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_path: Path to the YAML config file. Both relative and absolute
|
||||||
|
paths are accepted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The loaded and validated AgentConfig object.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If config file doesn't exist.
|
||||||
|
ValidationError: If config file's content is invalid YAML.
|
||||||
|
"""
|
||||||
|
config_path = Path(config_path)
|
||||||
|
|
||||||
|
if not config_path.exists():
|
||||||
|
raise FileNotFoundError(f"Config file not found: {config_path}")
|
||||||
|
|
||||||
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
|
config_data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
return AgentConfig.model_validate(config_data)
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
{
|
||||||
|
"$defs": {
|
||||||
|
"LlmAgentConfig": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "The config for the YAML schema of a LlmAgent.",
|
||||||
|
"properties": {
|
||||||
|
"agent_class": {
|
||||||
|
"default": "LlmAgent",
|
||||||
|
"enum": [
|
||||||
|
"LlmAgent",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"title": "Agent Class",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Model"
|
||||||
|
},
|
||||||
|
"instruction": {
|
||||||
|
"title": "Instruction",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disallow_transfer_to_parent": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Disallow Transfer To Parent"
|
||||||
|
},
|
||||||
|
"disallow_transfer_to_peers": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Disallow Transfer To Peers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"instruction"
|
||||||
|
],
|
||||||
|
"title": "LlmAgentConfig",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"LoopAgentConfig": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "The config for the YAML schema of a LoopAgent.",
|
||||||
|
"properties": {
|
||||||
|
"agent_class": {
|
||||||
|
"const": "LoopAgent",
|
||||||
|
"default": "LoopAgent",
|
||||||
|
"title": "Agent Class",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Description",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"max_iterations": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"default": null,
|
||||||
|
"title": "Max Iterations"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"title": "LoopAgentConfig",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ParallelAgentConfig": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "The config for the YAML schema of a ParallelAgent.",
|
||||||
|
"properties": {
|
||||||
|
"agent_class": {
|
||||||
|
"const": "ParallelAgent",
|
||||||
|
"default": "ParallelAgent",
|
||||||
|
"title": "Agent Class",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Description",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"title": "ParallelAgentConfig",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SequentialAgentConfig": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"description": "The config for the YAML schema of a SequentialAgent.",
|
||||||
|
"properties": {
|
||||||
|
"agent_class": {
|
||||||
|
"const": "SequentialAgent",
|
||||||
|
"default": "SequentialAgent",
|
||||||
|
"title": "Agent Class",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"title": "Name",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"default": "",
|
||||||
|
"title": "Description",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"title": "SequentialAgentConfig",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/LlmAgentConfig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/LoopAgentConfig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/ParallelAgentConfig"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SequentialAgentConfig"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The config for the YAML schema to create an agent.",
|
||||||
|
"title": "AgentConfig"
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ from pydantic import ConfigDict
|
|||||||
from ..artifacts.base_artifact_service import BaseArtifactService
|
from ..artifacts.base_artifact_service import BaseArtifactService
|
||||||
from ..auth.credential_service.base_credential_service import BaseCredentialService
|
from ..auth.credential_service.base_credential_service import BaseCredentialService
|
||||||
from ..memory.base_memory_service import BaseMemoryService
|
from ..memory.base_memory_service import BaseMemoryService
|
||||||
|
from ..plugins.plugin_manager import PluginManager
|
||||||
from ..sessions.base_session_service import BaseSessionService
|
from ..sessions.base_session_service import BaseSessionService
|
||||||
from ..sessions.session import Session
|
from ..sessions.session import Session
|
||||||
from .active_streaming_tool import ActiveStreamingTool
|
from .active_streaming_tool import ActiveStreamingTool
|
||||||
@@ -153,6 +154,9 @@ class InvocationContext(BaseModel):
|
|||||||
run_config: Optional[RunConfig] = None
|
run_config: Optional[RunConfig] = None
|
||||||
"""Configurations for live agents under this invocation."""
|
"""Configurations for live agents under this invocation."""
|
||||||
|
|
||||||
|
plugin_manager: PluginManager = PluginManager()
|
||||||
|
"""The manager for keeping track of plugins in this invocation."""
|
||||||
|
|
||||||
_invocation_cost_manager: _InvocationCostManager = _InvocationCostManager()
|
_invocation_cost_manager: _InvocationCostManager = _InvocationCostManager()
|
||||||
"""A container to keep track of different kinds of costs incurred as a part
|
"""A container to keep track of different kinds of costs incurred as a part
|
||||||
of this invocation.
|
of this invocation.
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ from typing import Any
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from typing import Awaitable
|
from typing import Awaitable
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Dict
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Type
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from google.genai import types
|
from google.genai import types
|
||||||
@@ -48,7 +50,9 @@ from ..tools.base_tool import BaseTool
|
|||||||
from ..tools.base_toolset import BaseToolset
|
from ..tools.base_toolset import BaseToolset
|
||||||
from ..tools.function_tool import FunctionTool
|
from ..tools.function_tool import FunctionTool
|
||||||
from ..tools.tool_context import ToolContext
|
from ..tools.tool_context import ToolContext
|
||||||
|
from ..utils.feature_decorator import working_in_progress
|
||||||
from .base_agent import BaseAgent
|
from .base_agent import BaseAgent
|
||||||
|
from .base_agent import BaseAgentConfig
|
||||||
from .callback_context import CallbackContext
|
from .callback_context import CallbackContext
|
||||||
from .invocation_context import InvocationContext
|
from .invocation_context import InvocationContext
|
||||||
from .readonly_context import ReadonlyContext
|
from .readonly_context import ReadonlyContext
|
||||||
@@ -516,5 +520,44 @@ class LlmAgent(BaseAgent):
|
|||||||
)
|
)
|
||||||
return generate_content_config
|
return generate_content_config
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@override
|
||||||
|
@working_in_progress('LlmAgent.from_config is not ready for use.')
|
||||||
|
def from_config(
|
||||||
|
cls: Type[LlmAgent],
|
||||||
|
config: LlmAgentConfig,
|
||||||
|
) -> LlmAgent:
|
||||||
|
agent = super().from_config(config)
|
||||||
|
if config.model:
|
||||||
|
agent.model = config.model
|
||||||
|
if config.instruction:
|
||||||
|
agent.instruction = config.instruction
|
||||||
|
if config.disallow_transfer_to_parent:
|
||||||
|
agent.disallow_transfer_to_parent = config.disallow_transfer_to_parent
|
||||||
|
if config.disallow_transfer_to_peers:
|
||||||
|
agent.disallow_transfer_to_peers = config.disallow_transfer_to_peers
|
||||||
|
return agent
|
||||||
|
|
||||||
|
|
||||||
Agent: TypeAlias = LlmAgent
|
Agent: TypeAlias = LlmAgent
|
||||||
|
|
||||||
|
|
||||||
|
class LlmAgentConfig(BaseAgentConfig):
|
||||||
|
"""The config for the YAML schema of a LlmAgent."""
|
||||||
|
|
||||||
|
agent_class: Literal['LlmAgent', ''] = 'LlmAgent'
|
||||||
|
"""The value is used to uniquely identify the LlmAgent class. If it is
|
||||||
|
empty, it is by default an LlmAgent."""
|
||||||
|
|
||||||
|
model: Optional[str] = None
|
||||||
|
"""Optional. LlmAgent.model. If not set, the model will be inherited from
|
||||||
|
the ancestor."""
|
||||||
|
|
||||||
|
instruction: str
|
||||||
|
"""Required. LlmAgent.instruction."""
|
||||||
|
|
||||||
|
disallow_transfer_to_parent: Optional[bool] = None
|
||||||
|
"""Optional. LlmAgent.disallow_transfer_to_parent."""
|
||||||
|
|
||||||
|
disallow_transfer_to_peers: Optional[bool] = None
|
||||||
|
"""Optional. LlmAgent.disallow_transfer_to_peers."""
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user