You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ae4c69c32 | |||
| 32df937ebc | |||
| 702e9bf556 | |||
| ff31f57dc9 | |||
| d4f01afc15 | |||
| f46396fa98 | |||
| 13ff009d34 | |||
| 6cc3d9ddd1 | |||
| 083dcb4465 | |||
| b2c2f1bd33 | |||
| a77d68964a | |||
| d1f182e8e6 | |||
| 0e173d7363 | |||
| 18f5bea411 | |||
| f2caf2eeca | |||
| dfee06ac06 | |||
| 2aab1cf98e | |||
| 96a0d4b02c | |||
| 65cb6d6bf3 | |||
| fc85348f91 | |||
| 9467720919 | |||
| ee3918e34e | |||
| 81e0d4083f | |||
| 2486349268 | |||
| 3643b4ae19 | |||
| cec400ada3 | |||
| d2461ecccb | |||
| ffe2bdbe4c | |||
| 67284fc466 | |||
| 0ec69d05a4 | |||
| f1e0bc0b18 | |||
| 5edc493da9 | |||
| 3568c9291d | |||
| d0a330cd15 | |||
| 637fa410d8 | |||
| 6f016609e8 | |||
| bb1b1c695f | |||
| bb4ff2cc3d | |||
| b1f4aebb25 | |||
| 2e778049d0 | |||
| 36e45cdab3 | |||
| 35de210d4e | |||
| bbe1c9dc66 | |||
| 9ad4350c88 | |||
| 2f8bb91e6b | |||
| 3f01c8c999 | |||
| b5850e0757 | |||
| 53df35ee58 | |||
| 6e68c2d7f3 | |||
| 8ada46a18e | |||
| 1c4c887bec | |||
| 78697aa6af | |||
| b17d8b6e36 | |||
| 377b5a9b78 | |||
| 33ac8380ad | |||
| 31fa5d91bd | |||
| b705b35977 | |||
| eccb484985 | |||
| aabfde56e4 | |||
| f2b9e72f82 | |||
| 3263a97999 | |||
| 36866935a3 | |||
| de238eeab1 | |||
| 84579cab56 | |||
| 34b001292a |
@@ -1,62 +0,0 @@
|
||||
# .github/workflows/pr-commit-check.yml
|
||||
# This GitHub Action workflow checks if a pull request has more than one commit.
|
||||
# If it does, it fails the check and instructs the user to squash their commits.
|
||||
|
||||
name: 'PR Commit Check'
|
||||
|
||||
# This workflow runs on pull request events.
|
||||
# It's configured to run on any pull request that is opened or synchronized (new commits pushed).
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
# Defines the jobs that will run as part of the workflow.
|
||||
jobs:
|
||||
check-commit-count:
|
||||
# The type of runner that the job will run on. 'ubuntu-latest' is a good default.
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# The steps that will be executed as part of the job.
|
||||
steps:
|
||||
# Step 1: Check out the code
|
||||
# This action checks out your repository under $GITHUB_WORKSPACE, so your workflow can access it.
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# We need to fetch all commits to accurately count them.
|
||||
# '0' means fetch all history for all branches and tags.
|
||||
fetch-depth: 0
|
||||
|
||||
# Step 2: Count the commits in the pull request
|
||||
# This step runs a script to get the number of commits in the PR.
|
||||
- name: Count Commits
|
||||
id: count_commits
|
||||
# We use `git rev-list --count` to count the commits.
|
||||
# ${{ github.event.pull_request.base.sha }} is the commit SHA of the base branch.
|
||||
# ${{ github.event.pull_request.head.sha }} is the commit SHA of the head branch (the PR branch).
|
||||
# The '..' syntax gives us the list of commits in the head branch that are not in the base branch.
|
||||
# The output of the command (the count) is stored in a step output variable named 'count'.
|
||||
run: |
|
||||
count=$(git rev-list --count ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
|
||||
echo "commit_count=$count" >> $GITHUB_OUTPUT
|
||||
|
||||
# Step 3: Check if the commit count is greater than 1
|
||||
# This step uses the output from the previous step to decide whether to pass or fail.
|
||||
- name: Check Commit Count
|
||||
# This step only runs if the 'commit_count' output from the 'count_commits' step is greater than 1.
|
||||
if: steps.count_commits.outputs.commit_count > 1
|
||||
# If the condition is met, the workflow will exit with a failure status.
|
||||
run: |
|
||||
echo "This pull request has ${{ steps.count_commits.outputs.commit_count }} commits."
|
||||
echo "Please squash them into a single commit before merging."
|
||||
echo "You can use git rebase -i HEAD~N"
|
||||
echo "...where N is the number of commits you want to squash together. The PR check conveniently tells you this number! For example, if the check says you have 3 commits, you would run: git rebase -i HEAD~3."
|
||||
echo "Because you have rewritten the commit history, you must use the --force flag to update the pull request: git push --force"
|
||||
exit 1
|
||||
|
||||
# Step 4: Success message
|
||||
# This step runs if the commit count is not greater than 1 (i.e., it's 1).
|
||||
- name: Success
|
||||
if: steps.count_commits.outputs.commit_count <= 1
|
||||
run: |
|
||||
echo "This pull request has a single commit. Great job!"
|
||||
@@ -28,20 +28,22 @@ Google Agent Development Kit (ADK) for Python
|
||||
|
||||
Adhere to this structure for compatibility with ADK tooling.
|
||||
|
||||
my_adk_project/ \
|
||||
└── src/ \
|
||||
└── my_app/ \
|
||||
├── agents/ \
|
||||
│ ├── my_agent/ \
|
||||
```
|
||||
my_adk_project/
|
||||
└── src/
|
||||
└── my_app/
|
||||
├── agents/
|
||||
│ ├── my_agent/
|
||||
│ │ ├── __init__.py # Must contain: from. import agent \
|
||||
│ │ └── agent.py # Must contain: root_agent = Agent(...) \
|
||||
│ └── another_agent/ \
|
||||
│ ├── __init__.py \
|
||||
│ └── another_agent/
|
||||
│ ├── __init__.py
|
||||
│ └── agent.py\
|
||||
```
|
||||
|
||||
agent.py: Must define the agent and assign it to a variable named root_agent. This is how ADK's tools find it.
|
||||
|
||||
__init__.py: In each agent directory, it must contain from. import agent to make the agent discoverable.
|
||||
`__init__.py`: In each agent directory, it must contain from. import agent to make the agent discoverable.
|
||||
|
||||
## Local Development & Debugging
|
||||
|
||||
@@ -108,4 +110,3 @@ Test Cases: Create JSON files with input and a reference (expected tool calls an
|
||||
Metrics: tool_trajectory_avg_score (does it use tools correctly?) and response_match_score (is the final answer good?).
|
||||
|
||||
Run via: adk web (UI), pytest (for CI/CD), or adk eval (CLI).
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# A2A Root Sample Agent
|
||||
|
||||
This sample demonstrates how to use a **remote Agent-to-Agent (A2A) agent as the root agent** in the Agent Development Kit (ADK). This is a simplified approach where the main agent is actually a remote A2A service, also showcasing how to run remote agents using uvicorn command.
|
||||
|
||||
## Overview
|
||||
|
||||
The A2A Root sample consists of:
|
||||
|
||||
- **Root Agent** (`agent.py`): A remote A2A agent proxy as root agent that talks to a remote a2a agent running on a separate server
|
||||
- **Remote Hello World Agent** (`remote_a2a/hello_world/agent.py`): The actual agent implementation that handles dice rolling and prime number checking running on remote server
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌────────────────────┐
|
||||
│ Root Agent │───▶│ Remote Hello │
|
||||
│ (RemoteA2aAgent)│ │ World Agent │
|
||||
│ (localhost:8000)│ │ (localhost:8001) │
|
||||
└─────────────────┘ └────────────────────┘
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Remote A2A as Root Agent**
|
||||
- The `root_agent` is a `RemoteA2aAgent` that connects to a remote A2A service
|
||||
- Demonstrates how to use remote agents as the primary agent instead of local agents
|
||||
- Shows the flexibility of the A2A architecture for distributed agent deployment
|
||||
|
||||
### 2. **Uvicorn Server Deployment**
|
||||
- The remote agent is served using uvicorn, a lightweight ASGI server
|
||||
- Demonstrates a simple way to deploy A2A agents without using the ADK CLI
|
||||
- Shows how to expose A2A agents as standalone web services
|
||||
|
||||
### 3. **Agent Functionality**
|
||||
- **Dice Rolling**: Can roll dice with configurable number of sides
|
||||
- **Prime Number Checking**: Can check if numbers are prime
|
||||
- **State Management**: Maintains roll history in tool context
|
||||
- **Parallel Tool Execution**: Can use multiple tools in parallel
|
||||
|
||||
### 4. **Simple Deployment Pattern**
|
||||
- Uses the `to_a2a()` utility to convert a standard ADK agent to an A2A service
|
||||
- Minimal configuration required for remote agent deployment
|
||||
|
||||
## Setup and Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **Start the Remote A2A Agent server**:
|
||||
```bash
|
||||
# Start the remote agent using uvicorn
|
||||
uvicorn contributing.samples.a2a_root.remote_a2a.hello_world.agent:a2a_app --host localhost --port 8001
|
||||
```
|
||||
|
||||
2. **Run the Main Agent**:
|
||||
```bash
|
||||
# In a separate terminal, run the adk web server
|
||||
adk web contributing/samples/
|
||||
```
|
||||
|
||||
### Example Interactions
|
||||
|
||||
Once both services are running, you can interact with the root agent:
|
||||
|
||||
**Simple Dice Rolling:**
|
||||
```
|
||||
User: Roll a 6-sided die
|
||||
Bot: I rolled a 4 for you.
|
||||
```
|
||||
|
||||
**Prime Number Checking:**
|
||||
```
|
||||
User: Is 7 a prime number?
|
||||
Bot: Yes, 7 is a prime number.
|
||||
```
|
||||
|
||||
**Combined Operations:**
|
||||
```
|
||||
User: Roll a 10-sided die and check if it's prime
|
||||
Bot: I rolled an 8 for you.
|
||||
Bot: 8 is not a prime number.
|
||||
```
|
||||
|
||||
**Multiple Rolls with Prime Checking:**
|
||||
```
|
||||
User: Roll a die 3 times and check which results are prime
|
||||
Bot: I rolled a 3 for you.
|
||||
Bot: I rolled a 7 for you.
|
||||
Bot: I rolled a 4 for you.
|
||||
Bot: 3, 7 are prime numbers.
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Root Agent (`agent.py`)
|
||||
|
||||
- **`root_agent`**: A `RemoteA2aAgent` that connects to the remote A2A service
|
||||
- **Agent Card URL**: Points to the well-known agent card endpoint on the remote server
|
||||
|
||||
### Remote Hello World Agent (`remote_a2a/hello_world/agent.py`)
|
||||
|
||||
- **`roll_die(sides: int)`**: Function tool for rolling dice with state management
|
||||
- **`check_prime(nums: list[int])`**: Async function for prime number checking
|
||||
- **`root_agent`**: The main agent with comprehensive instructions
|
||||
- **`a2a_app`**: The A2A application created using `to_a2a()` utility
|
||||
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Connection Issues:**
|
||||
- Ensure the uvicorn server is running on port 8001
|
||||
- Check that no firewall is blocking localhost connections
|
||||
- Verify the agent card URL in the root agent configuration
|
||||
- Check uvicorn logs for any startup errors
|
||||
|
||||
**Agent Not Responding:**
|
||||
- Check the uvicorn server logs for errors
|
||||
- Verify the agent instructions are clear and unambiguous
|
||||
- Ensure the A2A app is properly configured with the correct port
|
||||
|
||||
**Uvicorn Issues:**
|
||||
- Make sure the module path is correct: `contributing.samples.a2a_root.remote_a2a.hello_world.agent:a2a_app`
|
||||
- Check that all dependencies are installed
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
# 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 a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
|
||||
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
|
||||
|
||||
root_agent = RemoteA2aAgent(
|
||||
name="hello_world_agent",
|
||||
description=(
|
||||
"Helpful assistant that can roll dice and check if numbers are prime."
|
||||
),
|
||||
agent_card=f"http://localhost:8001/{AGENT_CARD_WELL_KNOWN_PATH}",
|
||||
)
|
||||
@@ -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,111 @@
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.a2a.utils.agent_to_a2a import to_a2a
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def roll_die(sides: int, tool_context: ToolContext) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
tool_context: the tool context
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
result = random.randint(1, sides)
|
||||
if not 'rolls' in tool_context.state:
|
||||
tool_context.state['rolls'] = []
|
||||
|
||||
tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
|
||||
return result
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
'No prime numbers found.'
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.0-flash',
|
||||
name='hello_world_agent',
|
||||
description=(
|
||||
'hello world agent that can roll a dice of 8 sides and check prime'
|
||||
' numbers.'
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[
|
||||
roll_die,
|
||||
check_prime,
|
||||
],
|
||||
# planner=BuiltInPlanner(
|
||||
# thinking_config=types.ThinkingConfig(
|
||||
# include_thoughts=True,
|
||||
# ),
|
||||
# ),
|
||||
generate_content_config=types.GenerateContentConfig(
|
||||
safety_settings=[
|
||||
types.SafetySetting( # avoid false alarm about rolling dice.
|
||||
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=types.HarmBlockThreshold.OFF,
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
a2a_app = to_a2a(root_agent, port=8001)
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
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).
|
||||
This agent can be operated in three distinct modes:
|
||||
|
||||
- An interactive mode for local use.
|
||||
- A batch script mode for oncall use.
|
||||
- A fully automated GitHub Actions workflow (TBD).
|
||||
|
||||
---
|
||||
|
||||
@@ -50,6 +54,15 @@ The `main.py` is reserved for the Github Workflow. The detailed setup for the au
|
||||
|
||||
---
|
||||
|
||||
## Update the Knowledge Base
|
||||
|
||||
The `upload_docs_to_vertex_ai_search.py` is a script to upload ADK related docs to Vertex AI Search datastore to update the knowledge base. It can be executed with the following command in your terminal:
|
||||
|
||||
```bash
|
||||
export PYTHONPATH=contributing/samples # If not already exported
|
||||
python -m adk_answering_agent.upload_docs_to_vertex_ai_search
|
||||
```
|
||||
|
||||
## Setup and Configuration
|
||||
|
||||
Whether running in interactive or workflow mode, the agent requires the following setup.
|
||||
@@ -59,7 +72,7 @@ The agent requires the following Python libraries.
|
||||
|
||||
```bash
|
||||
pip install --upgrade pip
|
||||
pip install google-adk requests
|
||||
pip install google-adk
|
||||
```
|
||||
|
||||
The agent also requires gcloud login:
|
||||
@@ -68,6 +81,12 @@ The agent also requires gcloud login:
|
||||
gcloud auth application-default login
|
||||
```
|
||||
|
||||
The upload script requires the following additional Python libraries.
|
||||
|
||||
```bash
|
||||
pip install google-cloud-storage google-cloud-discoveryengine
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
The following environment variables are required for the agent to connect to the necessary services.
|
||||
|
||||
@@ -75,9 +94,15 @@ The following environment variables are required for the agent to connect to the
|
||||
* `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).
|
||||
* `VERTEXAI_DATASTORE_ID=YOUR_DATASTORE_ID`: **(Required)** The full Vertex AI datastore ID for the document store (i.e. knowledge base), with the format of `projects/{project_number}/locations/{location}/collections/{collection}/dataStores/{datastore_id}`.
|
||||
* `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.
|
||||
|
||||
The following environment variables are required to upload the docs to update the knowledge base.
|
||||
|
||||
* `GCS_BUCKET_NAME=YOUR_GCS_BUCKET_NAME`: **(Required)** The name of the GCS bucket to store the documents.
|
||||
* `ADK_DOCS_ROOT_PATH=YOUR_ADK_DOCS_ROOT_PATH`: **(Required)** Path to the root of the downloaded adk-docs repo.
|
||||
* `ADK_PYTHON_ROOT_PATH=YOUR_ADK_PYTHON_ROOT_PATH`: **(Required)** Path to the root of the downloaded adk-python repo.
|
||||
|
||||
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.
|
||||
@@ -29,9 +29,14 @@ VERTEXAI_DATASTORE_ID = os.getenv("VERTEXAI_DATASTORE_ID")
|
||||
if not VERTEXAI_DATASTORE_ID:
|
||||
raise ValueError("VERTEXAI_DATASTORE_ID environment variable not set")
|
||||
|
||||
GOOGLE_CLOUD_PROJECT = os.getenv("GOOGLE_CLOUD_PROJECT")
|
||||
GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME")
|
||||
ADK_DOCS_ROOT_PATH = os.getenv("ADK_DOCS_ROOT_PATH")
|
||||
ADK_PYTHON_ROOT_PATH = os.getenv("ADK_PYTHON_ROOT_PATH")
|
||||
|
||||
OWNER = os.getenv("OWNER", "google")
|
||||
REPO = os.getenv("REPO", "adk-python")
|
||||
BOT_RESPONSE_LABEL = os.getenv("BOT_RESPONSE_LABEL", "bot_responded")
|
||||
BOT_RESPONSE_LABEL = os.getenv("BOT_RESPONSE_LABEL", "bot responded")
|
||||
DISCUSSION_NUMBER = os.getenv("DISCUSSION_NUMBER")
|
||||
|
||||
IS_INTERACTIVE = os.getenv("INTERACTIVE", "1").lower() in ["true", "1"]
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
# 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
|
||||
import sys
|
||||
|
||||
from adk_answering_agent.settings import ADK_DOCS_ROOT_PATH
|
||||
from adk_answering_agent.settings import ADK_PYTHON_ROOT_PATH
|
||||
from adk_answering_agent.settings import GCS_BUCKET_NAME
|
||||
from adk_answering_agent.settings import GOOGLE_CLOUD_PROJECT
|
||||
from adk_answering_agent.settings import VERTEXAI_DATASTORE_ID
|
||||
from google.api_core.exceptions import GoogleAPICallError
|
||||
from google.cloud import discoveryengine_v1beta as discoveryengine
|
||||
from google.cloud import storage
|
||||
import markdown
|
||||
|
||||
GCS_PREFIX_TO_ROOT_PATH = {
|
||||
"adk-docs": ADK_DOCS_ROOT_PATH,
|
||||
"adk-python": ADK_PYTHON_ROOT_PATH,
|
||||
}
|
||||
|
||||
|
||||
def cleanup_gcs_prefix(project_id: str, bucket_name: str, prefix: str) -> bool:
|
||||
"""Delete all the objects with the given prefix in the bucket."""
|
||||
print(f"Start cleaning up GCS: gs://{bucket_name}/{prefix}...")
|
||||
try:
|
||||
storage_client = storage.Client(project=project_id)
|
||||
bucket = storage_client.bucket(bucket_name)
|
||||
blobs = list(bucket.list_blobs(prefix=prefix))
|
||||
|
||||
if not blobs:
|
||||
print("GCS target location is already empty, no need to clean up.")
|
||||
return True
|
||||
|
||||
bucket.delete_blobs(blobs)
|
||||
print(f"Successfully deleted {len(blobs)} objects.")
|
||||
return True
|
||||
except GoogleAPICallError as e:
|
||||
print(f"[ERROR] Failed to clean up GCS: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def upload_directory_to_gcs(
|
||||
source_directory: str, project_id: str, bucket_name: str, prefix: str
|
||||
) -> bool:
|
||||
"""Upload the whole directory into GCS."""
|
||||
print(
|
||||
f"Start uploading directory {source_directory} to GCS:"
|
||||
f" gs://{bucket_name}/{prefix}..."
|
||||
)
|
||||
|
||||
if not os.path.isdir(source_directory):
|
||||
print(f"[Error] {source_directory} is not a directory or does not exist.")
|
||||
return False
|
||||
|
||||
storage_client = storage.Client(project=project_id)
|
||||
bucket = storage_client.bucket(bucket_name)
|
||||
file_count = 0
|
||||
for root, dirs, files in os.walk(source_directory):
|
||||
# Modify the 'dirs' list in-place to prevent os.walk from descending
|
||||
# into hidden directories.
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
|
||||
# Keep only .md and .py files.
|
||||
files = [f for f in files if f.endswith(".md") or f.endswith(".py")]
|
||||
|
||||
for filename in files:
|
||||
local_path = os.path.join(root, filename)
|
||||
|
||||
relative_path = os.path.relpath(local_path, source_directory)
|
||||
gcs_path = os.path.join(prefix, relative_path)
|
||||
|
||||
try:
|
||||
content_type = None
|
||||
if filename.lower().endswith(".md"):
|
||||
# Vertex AI search doesn't recognize text/markdown,
|
||||
# convert it to html and use text/html instead
|
||||
content_type = "text/html"
|
||||
with open(local_path, "r", encoding="utf-8") as f:
|
||||
md_content = f.read()
|
||||
html_content = markdown.markdown(
|
||||
md_content, output_format="html5", encoding="utf-8"
|
||||
)
|
||||
if not html_content:
|
||||
print(" - Skipped empty file: " + local_path)
|
||||
continue
|
||||
gcs_path = gcs_path.removesuffix(".md") + ".html"
|
||||
bucket.blob(gcs_path).upload_from_string(
|
||||
html_content, content_type=content_type
|
||||
)
|
||||
else: # Python files
|
||||
bucket.blob(gcs_path).upload_from_filename(
|
||||
local_path, content_type=content_type
|
||||
)
|
||||
type_msg = (
|
||||
f"(type {content_type})" if content_type else "(type auto-detect)"
|
||||
)
|
||||
print(
|
||||
f" - Uploaded {type_msg}: {local_path} ->"
|
||||
f" gs://{bucket_name}/{gcs_path}"
|
||||
)
|
||||
file_count += 1
|
||||
except GoogleAPICallError as e:
|
||||
print(
|
||||
f"[ERROR] Error uploading file {local_path}: {e}", file=sys.stderr
|
||||
)
|
||||
return False
|
||||
|
||||
print(f"Sucessfully uploaded {file_count} files to GCS.")
|
||||
return True
|
||||
|
||||
|
||||
def import_from_gcs_to_vertex_ai(
|
||||
full_datastore_id: str,
|
||||
gcs_bucket: str,
|
||||
) -> bool:
|
||||
"""Triggers a bulk import task from a GCS folder to Vertex AI Search."""
|
||||
print(f"Triggering FULL SYNC import from gs://{gcs_bucket}/**...")
|
||||
|
||||
try:
|
||||
client = discoveryengine.DocumentServiceClient()
|
||||
gcs_uri = f"gs://{gcs_bucket}/**"
|
||||
request = discoveryengine.ImportDocumentsRequest(
|
||||
# parent has the format of
|
||||
# "projects/{project_number}/locations/{location}/collections/{collection}/dataStores/{datastore_id}/branches/default_branch"
|
||||
parent=full_datastore_id + "/branches/default_branch",
|
||||
# Specify the GCS source and use "content" for unstructed data.
|
||||
gcs_source=discoveryengine.GcsSource(
|
||||
input_uris=[gcs_uri], data_schema="content"
|
||||
),
|
||||
reconciliation_mode=discoveryengine.ImportDocumentsRequest.ReconciliationMode.FULL,
|
||||
)
|
||||
operation = client.import_documents(request=request)
|
||||
print(
|
||||
"Successfully started full sync import operation."
|
||||
f"Operation Name: {operation.operation.name}"
|
||||
)
|
||||
return True
|
||||
|
||||
except GoogleAPICallError as e:
|
||||
print(f"[ERROR] Error triggering import: {e}", file=sys.stderr)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
# Check required environment variables.
|
||||
if not GOOGLE_CLOUD_PROJECT:
|
||||
print(
|
||||
"[ERROR] GOOGLE_CLOUD_PROJECT environment variable not set. Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not GCS_BUCKET_NAME:
|
||||
print(
|
||||
"[ERROR] GCS_BUCKET_NAME environment variable not set. Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not VERTEXAI_DATASTORE_ID:
|
||||
print(
|
||||
"[ERROR] VERTEXAI_DATASTORE_ID environment variable not set."
|
||||
" Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not ADK_DOCS_ROOT_PATH:
|
||||
print(
|
||||
"[ERROR] ADK_DOCS_ROOT_PATH environment variable not set. Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not ADK_PYTHON_ROOT_PATH:
|
||||
print(
|
||||
"[ERROR] ADK_PYTHON_ROOT_PATH environment variable not set. Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
for gcs_prefix in GCS_PREFIX_TO_ROOT_PATH:
|
||||
# 1. Cleanup the GSC for a clean start.
|
||||
if not cleanup_gcs_prefix(
|
||||
GOOGLE_CLOUD_PROJECT, GCS_BUCKET_NAME, gcs_prefix
|
||||
):
|
||||
print("[ERROR] Failed to clean up GCS. Exiting...", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 2. Upload the docs to GCS.
|
||||
if not upload_directory_to_gcs(
|
||||
GCS_PREFIX_TO_ROOT_PATH[gcs_prefix],
|
||||
GOOGLE_CLOUD_PROJECT,
|
||||
GCS_BUCKET_NAME,
|
||||
gcs_prefix,
|
||||
):
|
||||
print("[ERROR] Failed to upload docs to GCS. Exiting...", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 3. Import the docs from GCS to Vertex AI Search.
|
||||
if not import_from_gcs_to_vertex_ai(VERTEXAI_DATASTORE_ID, GCS_BUCKET_NAME):
|
||||
print(
|
||||
"[ERROR] Failed to import docs from GCS to Vertex AI Search."
|
||||
" Exiting...",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
print("--- Sync task has been successfully initiated ---")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -21,12 +21,14 @@ from adk_triaging_agent.settings import OWNER
|
||||
from adk_triaging_agent.settings import REPO
|
||||
from adk_triaging_agent.utils import error_response
|
||||
from adk_triaging_agent.utils import get_request
|
||||
from adk_triaging_agent.utils import patch_request
|
||||
from adk_triaging_agent.utils import post_request
|
||||
from google.adk import Agent
|
||||
import requests
|
||||
|
||||
LABEL_TO_OWNER = {
|
||||
"documentation": "polong",
|
||||
"agent engine": "yeesian",
|
||||
"documentation": "polong-lin",
|
||||
"services": "DeanChensj",
|
||||
"question": "",
|
||||
"tools": "seanzhou1023",
|
||||
@@ -136,6 +138,30 @@ def add_label_and_owner_to_issue(
|
||||
}
|
||||
|
||||
|
||||
def change_issue_type(issue_number: int, issue_type: str) -> dict[str, Any]:
|
||||
"""Change the issue type of the given issue number.
|
||||
|
||||
Args:
|
||||
issue_number: issue number of the Github issue, in string foramt.
|
||||
issue_type: issue type to assign
|
||||
|
||||
Returns:
|
||||
The the status of this request, with the applied issue type when successful.
|
||||
"""
|
||||
print(
|
||||
f"Attempting to change issue type '{issue_type}' to issue #{issue_number}"
|
||||
)
|
||||
url = f"{GITHUB_BASE_URL}/repos/{OWNER}/{REPO}/issues/{issue_number}"
|
||||
payload = {"type": issue_type}
|
||||
|
||||
try:
|
||||
response = patch_request(url, payload)
|
||||
except requests.exceptions.RequestException as e:
|
||||
return error_response(f"Error: {e}")
|
||||
|
||||
return {"status": "success", "message": response, "issue_type": issue_type}
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model="gemini-2.5-pro",
|
||||
name="adk_triaging_assistant",
|
||||
@@ -143,6 +169,7 @@ root_agent = Agent(
|
||||
instruction=f"""
|
||||
You are a triaging bot for the Github {REPO} repo with the owner {OWNER}. You will help get issues, and recommend a label.
|
||||
IMPORTANT: {APPROVAL_INSTRUCTION}
|
||||
|
||||
Here are the rules for labeling:
|
||||
- If the user is asking about documentation-related questions, label it with "documentation".
|
||||
- If it's about session, memory services, label it with "services"
|
||||
@@ -154,14 +181,24 @@ root_agent = Agent(
|
||||
- If it's about model support(non-Gemini, like Litellm, Ollama, OpenAI models), label it with "models".
|
||||
- If it's about tracing, label it with "tracing".
|
||||
- If it's agent orchestration, agent definition, label it with "core".
|
||||
- If it's about agent engine, label it with "agent engine".
|
||||
- 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.
|
||||
|
||||
After you label the issue, call the `change_issue_type` tool to change the issue type:
|
||||
- If the issue is a bug report, change the issue type to "Bug".
|
||||
- If the issue is a feature request, change the issue type to "Feature".
|
||||
- Otherwise, **do not change the issue type**.
|
||||
|
||||
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_and_owner_to_issue],
|
||||
tools=[
|
||||
list_unlabeled_issues,
|
||||
add_label_and_owner_to_issue,
|
||||
change_issue_type,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ if not GITHUB_TOKEN:
|
||||
|
||||
OWNER = os.getenv("OWNER", "google")
|
||||
REPO = os.getenv("REPO", "adk-python")
|
||||
BOT_LABEL = os.getenv("BOT_LABEL", "bot_triaged")
|
||||
BOT_LABEL = os.getenv("BOT_LABEL", "bot triaged")
|
||||
EVENT_NAME = os.getenv("EVENT_NAME")
|
||||
ISSUE_NUMBER = os.getenv("ISSUE_NUMBER")
|
||||
ISSUE_TITLE = os.getenv("ISSUE_TITLE")
|
||||
|
||||
@@ -39,6 +39,12 @@ def post_request(url: str, payload: Any) -> dict[str, Any]:
|
||||
return response.json()
|
||||
|
||||
|
||||
def patch_request(url: str, payload: Any) -> dict[str, Any]:
|
||||
response = requests.patch(url, headers=headers, json=payload, timeout=60)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def error_response(error_message: str) -> dict[str, Any]:
|
||||
return {"status": "error", "message": error_message}
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
import random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.planners import BuiltInPlanner
|
||||
from google.adk.planners import PlanReActPlanner
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
from google.genai import types
|
||||
|
||||
|
||||
+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
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
# 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 random
|
||||
|
||||
from google.adk import Agent
|
||||
from google.adk.agents.callback_context import CallbackContext
|
||||
from google.adk.models import LlmRequest
|
||||
from google.adk.tools.tool_context import ToolContext
|
||||
|
||||
|
||||
def roll_die(sides: int, tool_context: ToolContext) -> int:
|
||||
"""Roll a die and return the rolled result.
|
||||
|
||||
Args:
|
||||
sides: The integer number of sides the die has.
|
||||
|
||||
Returns:
|
||||
An integer of the result of rolling the die.
|
||||
"""
|
||||
result = random.randint(1, sides)
|
||||
if not 'rolls' in tool_context.state:
|
||||
tool_context.state['rolls'] = []
|
||||
|
||||
tool_context.state['rolls'] = tool_context.state['rolls'] + [result]
|
||||
return result
|
||||
|
||||
|
||||
async def check_prime(nums: list[int]) -> str:
|
||||
"""Check if a given list of numbers are prime.
|
||||
|
||||
Args:
|
||||
nums: The list of numbers to check.
|
||||
|
||||
Returns:
|
||||
A str indicating which number is prime.
|
||||
"""
|
||||
primes = set()
|
||||
for number in nums:
|
||||
number = int(number)
|
||||
if number <= 1:
|
||||
continue
|
||||
is_prime = True
|
||||
for i in range(2, int(number**0.5) + 1):
|
||||
if number % i == 0:
|
||||
is_prime = False
|
||||
break
|
||||
if is_prime:
|
||||
primes.add(number)
|
||||
return (
|
||||
'No prime numbers found.'
|
||||
if not primes
|
||||
else f"{', '.join(str(num) for num in primes)} are prime numbers."
|
||||
)
|
||||
|
||||
|
||||
def create_slice_history_callback(n_recent_turns):
|
||||
async def before_model_callback(
|
||||
callback_context: CallbackContext, llm_request: LlmRequest
|
||||
):
|
||||
if n_recent_turns < 1:
|
||||
return
|
||||
|
||||
user_indexes = [
|
||||
i
|
||||
for i, content in enumerate(llm_request.contents)
|
||||
if content.role == 'user'
|
||||
]
|
||||
|
||||
if n_recent_turns > len(user_indexes):
|
||||
return
|
||||
|
||||
suffix_idx = user_indexes[-n_recent_turns]
|
||||
llm_request.contents = llm_request.contents[suffix_idx:]
|
||||
|
||||
return before_model_callback
|
||||
|
||||
|
||||
root_agent = Agent(
|
||||
model='gemini-2.0-flash',
|
||||
name='short_history_agent',
|
||||
description=(
|
||||
'an agent that maintains only the last turn in its context window.'
|
||||
' numbers.'
|
||||
),
|
||||
instruction="""
|
||||
You roll dice and answer questions about the outcome of the dice rolls.
|
||||
You can roll dice of different sizes.
|
||||
You can use multiple tools in parallel by calling functions in parallel(in one request and in one round).
|
||||
It is ok to discuss previous dice roles, and comment on the dice rolls.
|
||||
When you are asked to roll a die, you must call the roll_die tool with the number of sides. Be sure to pass in an integer. Do not pass in a string.
|
||||
You should never roll a die on your own.
|
||||
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
|
||||
You should not check prime numbers before calling the tool.
|
||||
When you are asked to roll a die and check prime numbers, you should always make the following two function calls:
|
||||
1. You should first call the roll_die tool to get a roll. Wait for the function response before calling the check_prime tool.
|
||||
2. After you get the function response from roll_die tool, you should call the check_prime tool with the roll_die result.
|
||||
2.1 If user asks you to check primes based on previous rolls, make sure you include the previous rolls in the list.
|
||||
3. When you respond, you must include the roll_die result from step 1.
|
||||
You should always perform the previous 3 steps when asking for a roll and checking prime numbers.
|
||||
You should not rely on the previous history on prime results.
|
||||
""",
|
||||
tools=[roll_die, check_prime],
|
||||
before_model_callback=create_slice_history_callback(n_recent_turns=2),
|
||||
)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# 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
|
||||
import warnings
|
||||
|
||||
import agent
|
||||
from dotenv import load_dotenv
|
||||
from google.adk import Runner
|
||||
from google.adk.artifacts import InMemoryArtifactService
|
||||
from google.adk.cli.utils import logs
|
||||
from google.adk.sessions import InMemorySessionService
|
||||
from google.adk.sessions import Session
|
||||
from google.genai import types
|
||||
|
||||
load_dotenv(override=True)
|
||||
warnings.filterwarnings('ignore', category=UserWarning)
|
||||
logs.log_to_tmp_folder()
|
||||
|
||||
|
||||
async def main():
|
||||
app_name = 'my_app'
|
||||
user_id_1 = 'user1'
|
||||
session_service = InMemorySessionService()
|
||||
artifact_service = InMemoryArtifactService()
|
||||
runner = Runner(
|
||||
app_name=app_name,
|
||||
agent=agent.root_agent,
|
||||
artifact_service=artifact_service,
|
||||
session_service=session_service,
|
||||
)
|
||||
session_11 = await session_service.create_session(
|
||||
app_name=app_name, user_id=user_id_1
|
||||
)
|
||||
|
||||
async def run_prompt(session: Session, new_message: str):
|
||||
content = types.Content(
|
||||
role='user', parts=[types.Part.from_text(text=new_message)]
|
||||
)
|
||||
print('** User says:', content.model_dump(exclude_none=True))
|
||||
async for event in runner.run_async(
|
||||
user_id=user_id_1,
|
||||
session_id=session.id,
|
||||
new_message=content,
|
||||
):
|
||||
if event.content.parts and event.content.parts[0].text:
|
||||
print(f'** {event.author}: {event.content.parts[0].text}')
|
||||
|
||||
start_time = time.time()
|
||||
print('Start time:', start_time)
|
||||
print('------------------------------------')
|
||||
await run_prompt(session_11, 'Hi')
|
||||
await run_prompt(session_11, 'Roll a die with 100 sides')
|
||||
await run_prompt(session_11, 'Roll a die again with 100 sides.')
|
||||
await run_prompt(session_11, 'What numbers did I got?')
|
||||
print(
|
||||
await artifact_service.list_artifact_keys(
|
||||
app_name=app_name, user_id=user_id_1, session_id=session_11.id
|
||||
)
|
||||
)
|
||||
end_time = time.time()
|
||||
print('------------------------------------')
|
||||
print('End time:', end_time)
|
||||
print('Total time:', end_time - start_time)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
@@ -17,20 +17,31 @@ This agent aims to test the Langchain tool with Langchain's StructuredTool
|
||||
"""
|
||||
from google.adk.agents import Agent
|
||||
from google.adk.tools.langchain_tool import LangchainTool
|
||||
from langchain.tools import tool
|
||||
from langchain_core.tools.structured import StructuredTool
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
def add(x, y) -> int:
|
||||
async def add(x, y) -> int:
|
||||
return x + y
|
||||
|
||||
|
||||
@tool
|
||||
def minus(x, y) -> int:
|
||||
return x - y
|
||||
|
||||
|
||||
class AddSchema(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
test_langchain_tool = StructuredTool.from_function(
|
||||
class MinusSchema(BaseModel):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
|
||||
test_langchain_add_tool = StructuredTool.from_function(
|
||||
add,
|
||||
name="add",
|
||||
description="Adds two numbers",
|
||||
@@ -45,5 +56,8 @@ root_agent = Agent(
|
||||
"You are a helpful assistant for user questions, you have access to a"
|
||||
" tool that adds two numbers."
|
||||
),
|
||||
tools=[LangchainTool(tool=test_langchain_tool)],
|
||||
tools=[
|
||||
LangchainTool(tool=test_langchain_add_tool),
|
||||
LangchainTool(tool=minus),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
# Toolbox Agent
|
||||
|
||||
This agent is utilizing [mcp toolbox for database](https://googleapis.github.io/genai-toolbox/getting-started/introduction/) to assist end user based on the informaton stored in database.
|
||||
Follow below steps to run this agent
|
||||
This agent utilizes [MCP toolbox for database](https://googleapis.github.io/genai-toolbox/getting-started/introduction/) to assist end users based on information stored in a database.
|
||||
|
||||
# Install toolbox
|
||||
Follow the steps below to run this agent.
|
||||
|
||||
* Run below command:
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure you have Python installed on your system.
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Toolbox
|
||||
|
||||
Run the following command to download and install the toolbox:
|
||||
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
@@ -13,20 +20,29 @@ curl -O https://storage.googleapis.com/genai-toolbox/v0.5.0/$OS/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
|
||||
# install SQLite
|
||||
### 2. Install SQLite
|
||||
|
||||
* install sqlite from https://sqlite.org/
|
||||
Install SQLite from [https://sqlite.org/](https://sqlite.org/)
|
||||
|
||||
### 3. Install Required Python Dependencies
|
||||
|
||||
# Create DB (optional. The db instance is already attached in the folder)
|
||||
**Important**: The ADK's `ToolboxToolset` class requires the `toolbox-core` package, which is not automatically installed with the ADK. Install it using:
|
||||
|
||||
* Run below command:
|
||||
```bash
|
||||
pip install toolbox-core
|
||||
```
|
||||
|
||||
### 4. Create Database (Optional)
|
||||
|
||||
*Note: A database instance is already included in the project folder. Skip this step if you want to use the existing database.*
|
||||
|
||||
To create a new database:
|
||||
|
||||
```bash
|
||||
sqlite3 tool_box.db
|
||||
```
|
||||
|
||||
* Run below SQL:
|
||||
Run the following SQL commands to set up the hotels table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE hotels(
|
||||
@@ -39,7 +55,6 @@ CREATE TABLE hotels(
|
||||
booked BIT NOT NULL
|
||||
);
|
||||
|
||||
|
||||
INSERT INTO hotels(id, name, location, price_tier, checkin_date, checkout_date, booked)
|
||||
VALUES
|
||||
(1, 'Hilton Basel', 'Basel', 'Luxury', '2024-04-22', '2024-04-20', 0),
|
||||
@@ -54,21 +69,27 @@ VALUES
|
||||
(10, 'Comfort Inn Bern', 'Bern', 'Midscale', '2024-04-04', '2024-04-16', 0);
|
||||
```
|
||||
|
||||
# create tools configurations
|
||||
### 5. Create Tools Configuration
|
||||
|
||||
* Create a yaml file named "tools.yaml", see its contents in the agent folder.
|
||||
Create a YAML file named `tools.yaml`. See the contents in the agent folder for reference.
|
||||
|
||||
# start toolbox server
|
||||
### 6. Start Toolbox Server
|
||||
|
||||
* Run below commands in the agent folder
|
||||
Run the following command in the agent folder:
|
||||
|
||||
```bash
|
||||
toolbox --tools-file "tools.yaml"
|
||||
```
|
||||
|
||||
# start ADK web UI
|
||||
The server will start at `http://127.0.0.1:5000` by default.
|
||||
|
||||
# send user query
|
||||
### 7. Start ADK Web UI
|
||||
|
||||
* query 1: what can you do for me ?
|
||||
* query 2: could you let know the information about "Hilton Basel" hotel ?
|
||||
Follow the ADK documentation to start the web user interface.
|
||||
|
||||
## Testing the Agent
|
||||
|
||||
Once everything is set up, you can test the agent with these sample queries:
|
||||
|
||||
- **Query 1**: "What can you do for me?"
|
||||
- **Query 2**: "Could you let me know the information about 'Hilton Basel' hotel?"
|
||||
|
||||
@@ -105,6 +105,7 @@ test = [
|
||||
"pytest-mock>=3.14.0",
|
||||
"pytest-xdist>=3.6.1",
|
||||
"pytest>=8.3.4",
|
||||
"python-multipart>=0.0.9",
|
||||
# go/keep-sorted end
|
||||
]
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@ try:
|
||||
except ImportError as e:
|
||||
if sys.version_info < (3, 10):
|
||||
raise ImportError(
|
||||
'A2A Tool requires Python 3.10 or above. Please upgrade your Python'
|
||||
' version.'
|
||||
'A2A requires Python 3.10 or above. Please upgrade your Python version.'
|
||||
) from e
|
||||
else:
|
||||
raise e
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user