Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
642 changes: 562 additions & 80 deletions contributing/samples/adk_documentation/adk_release_analyzer/agent.py

Large diffs are not rendered by default.

111 changes: 111 additions & 0 deletions contributing/samples/adk_documentation/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,3 +548,114 @@ def _git_grep(
check=False, # Don't raise error on non-zero exit code (1 means no match)
)
return grep_process


def get_file_diff_for_release(
repo_owner: str,
repo_name: str,
start_tag: str,
end_tag: str,
file_path: str,
) -> Dict[str, Any]:
"""Gets the diff/patch for a specific file between two release tags.

This is useful for incremental processing where you want to analyze
one file at a time instead of loading all changes at once.

Args:
repo_owner: The name of the repository owner.
repo_name: The name of the repository.
start_tag: The older tag (base) for the comparison.
end_tag: The newer tag (head) for the comparison.
file_path: The relative path of the file to get the diff for.

Returns:
A dictionary containing the status and the file diff details.
"""
url = f"{GITHUB_BASE_URL}/repos/{repo_owner}/{repo_name}/compare/{start_tag}...{end_tag}"

try:
comparison_data = get_request(url)
changed_files = comparison_data.get("files", [])

for file_data in changed_files:
if file_data.get("filename") == file_path:
return {
"status": "success",
"file": {
"relative_path": file_data.get("filename"),
"status": file_data.get("status"),
"additions": file_data.get("additions"),
"deletions": file_data.get("deletions"),
"changes": file_data.get("changes"),
"patch": file_data.get("patch", "No patch available."),
},
}

return error_response(f"File {file_path} not found in the comparison.")
except requests.exceptions.HTTPError as e:
return error_response(f"HTTP Error: {e}")
except requests.exceptions.RequestException as e:
return error_response(f"Request Error: {e}")


def get_changed_files_summary(
repo_owner: str, repo_name: str, start_tag: str, end_tag: str
) -> Dict[str, Any]:
"""Gets a summary of changed files between two releases without patches.

This is a lighter-weight version of get_changed_files_between_releases
that only returns file paths and metadata, without the actual diff content.
Use this for planning which files to analyze.

Args:
repo_owner: The name of the repository owner.
repo_name: The name of the repository.
start_tag: The older tag (base) for the comparison.
end_tag: The newer tag (head) for the comparison.

Returns:
A dictionary containing the status and a summary of changed files.
"""
url = f"{GITHUB_BASE_URL}/repos/{repo_owner}/{repo_name}/compare/{start_tag}...{end_tag}"

try:
comparison_data = get_request(url)
changed_files = comparison_data.get("files", [])

# Group files by directory for easier processing
files_by_dir: Dict[str, List[Dict[str, Any]]] = {}
formatted_files = []

for file_data in changed_files:
file_info = {
"relative_path": file_data.get("filename"),
"status": file_data.get("status"),
"additions": file_data.get("additions"),
"deletions": file_data.get("deletions"),
"changes": file_data.get("changes"),
}
formatted_files.append(file_info)

# Group by top-level directory
path = file_data.get("filename", "")
parts = path.split("/")
top_dir = parts[0] if parts else "root"
if top_dir not in files_by_dir:
files_by_dir[top_dir] = []
files_by_dir[top_dir].append(file_info)

return {
"status": "success",
"total_files": len(formatted_files),
"files": formatted_files,
"files_by_directory": files_by_dir,
"compare_url": (
f"https://github.com/{repo_owner}/{repo_name}"
f"/compare/{start_tag}...{end_tag}"
),
}
except requests.exceptions.HTTPError as e:
return error_response(f"HTTP Error: {e}")
except requests.exceptions.RequestException as e:
return error_response(f"Request Error: {e}")
10 changes: 5 additions & 5 deletions contributing/samples/bigquery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ distributed via the `google.adk.tools.bigquery` module. These tools include:
5. `get_job_info`
Fetches metadata about a BigQuery job.

5. `execute_sql`
6. `execute_sql`

Runs or dry-runs a SQL query in BigQuery.

6. `ask_data_insights`
7. `ask_data_insights`

Natural language-in, natural language-out tool that answers questions
about structured data in BigQuery. Provides a one-stop solution for generating
Expand All @@ -38,18 +38,18 @@ distributed via the `google.adk.tools.bigquery` module. These tools include:
the official [Conversational Analytics API documentation](https://cloud.google.com/gemini/docs/conversational-analytics-api/overview)
for instructions.

7. `forecast`
8. `forecast`

Perform time series forecasting using BigQuery's `AI.FORECAST` function,
leveraging the TimesFM 2.0 model.

8. `analyze_contribution`
9. `analyze_contribution`

Perform contribution analysis in BigQuery by creating a temporary
`CONTRIBUTION_ANALYSIS` model and then querying it with
`ML.GET_INSIGHTS` to find top contributors for a given metric.

9. `detect_anomalies`
10. `detect_anomalies`

Perform time series anomaly detection in BigQuery by creating a temporary
`ARIMA_PLUS` model and then querying it with
Expand Down
55 changes: 55 additions & 0 deletions contributing/samples/bigquery_mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# BigQuery MCP Toolset Sample

## Introduction

This sample agent demonstrates using ADK's `McpToolset` to interact with
BigQuery's official MCP endpoint, allowing an agent to access and execute
toole by leveraging the Model Context Protocol (MCP). These tools include:


1. `list_dataset_ids`

Fetches BigQuery dataset ids present in a GCP project.

2. `get_dataset_info`

Fetches metadata about a BigQuery dataset.

3. `list_table_ids`

Fetches table ids present in a BigQuery dataset.

4. `get_table_info`

Fetches metadata about a BigQuery table.

5. `execute_sql`

Runs or dry-runs a SQL query in BigQuery.

## How to use

Set up your project and local authentication by following the guide
[Use the BigQuery remote MCP server](https://docs.cloud.google.com/bigquery/docs/use-bigquery-mcp).
This agent uses Application Default Credentials (ADC) to authenticate with the
BigQuery MCP endpoint.

Set up environment variables in your `.env` file for using
[Google AI Studio](https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-ai-studio)
or
[Google Cloud Vertex AI](https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai)
for the LLM service for your agent. For example, for using Google AI Studio you
would set:

* GOOGLE_GENAI_USE_VERTEXAI=FALSE
* GOOGLE_API_KEY={your api key}

Then run the agent using `adk run .` or `adk web .` in this directory.

## Sample prompts

* which weather datasets exist in bigquery public data?
* tell me more about noaa_lightning
* which tables exist in the ml_datasets dataset?
* show more details about the penguins table
* compute penguins population per island.
15 changes: 15 additions & 0 deletions contributing/samples/bigquery_mcp/__init__.py
Original file line number Diff line number Diff line change
@@ -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
51 changes: 51 additions & 0 deletions contributing/samples/bigquery_mcp/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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 google.adk.agents.llm_agent import LlmAgent
from google.adk.tools.mcp_tool.mcp_session_manager import StreamableHTTPConnectionParams
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
import google.auth

BIGQUERY_AGENT_NAME = "adk_sample_bigquery_mcp_agent"
BIGQUERY_MCP_ENDPOINT = "https://bigquery.googleapis.com/mcp"
BIGQUERY_SCOPE = "https://www.googleapis.com/auth/bigquery"

# Initialize the tools to use the application default credentials.
# https://cloud.google.com/docs/authentication/provide-credentials-adc
credentials, project_id = google.auth.default(scopes=[BIGQUERY_SCOPE])
credentials.refresh(google.auth.transport.requests.Request())
oauth_token = credentials.token

bigquery_mcp_toolset = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url=BIGQUERY_MCP_ENDPOINT,
headers={"Authorization": f"Bearer {oauth_token}"},
)
)

# The variable name `root_agent` determines what your root agent is for the
# debug CLI
root_agent = LlmAgent(
model="gemini-2.5-flash",
name=BIGQUERY_AGENT_NAME,
description=(
"Agent to answer questions about BigQuery data and models and execute"
" SQL queries using MCP."
),
instruction="""\
You are a data science agent with access to several BigQuery tools provided via MCP.
Make use of those tools to answer the user's questions.
""",
tools=[bigquery_mcp_toolset],
)
28 changes: 27 additions & 1 deletion src/google/adk/a2a/converters/part_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = 'function_response'
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = 'code_execution_result'
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = 'executable_code'
A2A_DATA_PART_TEXT_MIME_TYPE = 'text/plain'
A2A_DATA_PART_START_TAG = b'<a2a_datapart_json>'
A2A_DATA_PART_END_TAG = b'</a2a_datapart_json>'


A2APartToGenAIPartConverter = Callable[
Expand Down Expand Up @@ -130,7 +133,16 @@ def convert_a2a_part_to_genai_part(
part.data, by_alias=True
)
)
return genai_types.Part(text=json.dumps(part.data))
return genai_types.Part(
inline_data=genai_types.Blob(
data=A2A_DATA_PART_START_TAG
+ part.model_dump_json(by_alias=True, exclude_none=True).encode(
'utf-8'
)
+ A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
)
)

logger.warning(
'Cannot convert unsupported part type: %s for A2A part: %s',
Expand Down Expand Up @@ -163,6 +175,20 @@ def convert_genai_part_to_a2a_part(
)

if part.inline_data:
if (
part.inline_data.mime_type == A2A_DATA_PART_TEXT_MIME_TYPE
and part.inline_data.data is not None
and part.inline_data.data.startswith(A2A_DATA_PART_START_TAG)
and part.inline_data.data.endswith(A2A_DATA_PART_END_TAG)
):
return a2a_types.Part(
root=a2a_types.DataPart.model_validate_json(
part.inline_data.data[
len(A2A_DATA_PART_START_TAG) : -len(A2A_DATA_PART_END_TAG)
]
)
)
# The default case for inline_data is to convert it to FileWithBytes.
a2a_part = a2a_types.FilePart(
file=a2a_types.FileWithBytes(
bytes=base64.b64encode(part.inline_data.data).decode('utf-8'),
Expand Down
2 changes: 0 additions & 2 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,6 @@ async def _lifespan(app: FastAPI):
host=host,
port=port,
reload=reload,
log_level=log_level.lower(),
)

server = uvicorn.Server(config)
Expand Down Expand Up @@ -1368,7 +1367,6 @@ def cli_api_server(
host=host,
port=port,
reload=reload,
log_level=log_level.lower(),
)
server = uvicorn.Server(config)
server.run()
Expand Down
16 changes: 12 additions & 4 deletions src/google/adk/models/gemini_llm_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

from google.genai import types

from ..utils.content_utils import filter_audio_parts
from ..utils.context_utils import Aclosing
from ..utils.variant_utils import GoogleLLMVariant
from .base_llm_connection import BaseLlmConnection
Expand Down Expand Up @@ -63,15 +64,22 @@ async def send_history(self, history: list[types.Content]):
# TODO: Remove this filter and translate unary contents to streaming
# contents properly.

# We ignore any audio from user during the agent transfer phase
# Filter out audio parts from history because:
# 1. audio has already been transcribed.
# 2. sending audio via connection.send or connection.send_live_content is
# not supported by LIVE API (session will be corrupted).
# This method is called when:
# 1. Agent transfer to a new agent
# 2. Establishing a new live connection with previous ADK session history

contents = [
content
filtered
for content in history
if content.parts and content.parts[0].text
if (filtered := filter_audio_parts(content)) is not None
]
logger.debug('Sending history to live connection: %s', contents)

if contents:
logger.debug('Sending history to live connection: %s', contents)
await self._gemini_session.send(
input=types.LiveClientContent(
turns=contents,
Expand Down
Loading
Loading