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
1 change: 1 addition & 0 deletions .github/workflows/stale-bot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ on:

jobs:
audit-stale-issues:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
timeout-minutes: 60

Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ jobs:
# - New issues (need component labeling)
# - Issues labeled with "planned" (need owner assignment)
if: >-
github.event_name == 'schedule' ||
github.event.action == 'opened' ||
github.event.label.name == 'planned'
github.repository == 'google/adk-python' && (
github.event_name == 'schedule' ||
github.event.action == 'opened' ||
github.event.label.name == 'planned'
)
permissions:
issues: write
contents: read
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload-adk-docs-to-vertex-ai-search.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

jobs:
upload-adk-docs-to-vertex-ai-search:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest

steps:
Expand Down
11 changes: 10 additions & 1 deletion src/google/adk/agents/remote_a2a_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ def _create_a2a_request_for_user_function_response(

return a2a_message

def _is_remote_response(self, event: Event) -> bool:
return (
event.author == self.name
and event.custom_metadata
and event.custom_metadata.get(A2A_METADATA_PREFIX + "response", False)
)

def _construct_message_parts_from_session(
self, ctx: InvocationContext
) -> tuple[list[A2APart], Optional[str]]:
Expand All @@ -365,7 +372,7 @@ def _construct_message_parts_from_session(

events_to_process = []
for event in reversed(ctx.session.events):
if event.author == self.name:
if self._is_remote_response(event):
# stop on content generated by current a2a agent given it should already
# be in remote session
if event.custom_metadata:
Expand Down Expand Up @@ -496,6 +503,8 @@ async def _handle_a2a_response(
invocation_id=ctx.invocation_id,
branch=ctx.branch,
)
event.custom_metadata = event.custom_metadata or {}
event.custom_metadata[A2A_METADATA_PREFIX + "response"] = True
return event
except A2AClientError as e:
logger.error("Failed to handle A2A response: %s", e)
Expand Down
13 changes: 11 additions & 2 deletions src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1558,8 +1558,17 @@ async def event_generator():
yield f"data: {sse_event}\n\n"
except Exception as e:
logger.exception("Error in event_generator: %s", e)
# You might want to yield an error event here
yield f'data: {{"error": "{str(e)}"}}\n\n'
# Yield a proper Event object for the error
error_event = Event(
author="system",
content=types.Content(
role="model", parts=[types.Part(text=f"Error: {e}")]
),
)
yield (
"data:"
f" {error_event.model_dump_json(by_alias=True, exclude_none=True)}\n\n"
)

# Returns a streaming response with the proper media type for SSE
return StreamingResponse(
Expand Down
91 changes: 69 additions & 22 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,44 @@
import subprocess
from typing import Final
from typing import Optional
import warnings

import click
from packaging.version import parse

_IS_WINDOWS = os.name == 'nt'
_GCLOUD_CMD = 'gcloud.cmd' if _IS_WINDOWS else 'gcloud'
_LOCAL_STORAGE_FLAG_MIN_VERSION: Final[str] = '1.21.0'
_AGENT_ENGINE_REQUIREMENT: Final[str] = (
'google-cloud-aiplatform[adk,agent_engines]'
)


def _ensure_agent_engine_dependency(requirements_txt_path: str) -> None:
"""Ensures staged requirements include Agent Engine dependencies."""
if not os.path.exists(requirements_txt_path):
raise FileNotFoundError(
f'requirements.txt not found at: {requirements_txt_path}'
)

requirements = ''
with open(requirements_txt_path, 'r', encoding='utf-8') as f:
requirements = f.read()

for line in requirements.splitlines():
stripped = line.strip()
if (
stripped
and not stripped.startswith('#')
and stripped.startswith('google-cloud-aiplatform')
):
return

with open(requirements_txt_path, 'a', encoding='utf-8') as f:
if requirements and not requirements.endswith('\n'):
f.write('\n')
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')


_DOCKERFILE_TEMPLATE: Final[str] = """
FROM python:3.11-slim
Expand Down Expand Up @@ -74,12 +105,7 @@

if {is_config_agent}:
from google.adk.agents import config_agent_utils
try:
# This path is for local loading.
root_agent = config_agent_utils.from_config("{agent_folder}/root_agent.yaml")
except FileNotFoundError:
# This path is used to support the file structure in Agent Engine.
root_agent = config_agent_utils.from_config("./{temp_folder}/{app_name}/root_agent.yaml")
root_agent = config_agent_utils.from_config("{agent_folder}/root_agent.yaml")
else:
from .agent import {adk_app_object}

Expand Down Expand Up @@ -661,7 +687,7 @@ def to_agent_engine(
agent_folder: str,
temp_folder: Optional[str] = None,
adk_app: str,
staging_bucket: str,
staging_bucket: Optional[str] = None,
trace_to_cloud: Optional[bool] = None,
api_key: Optional[str] = None,
adk_app_object: Optional[str] = None,
Expand Down Expand Up @@ -704,7 +730,8 @@ def to_agent_engine(
files. It will be replaced with the generated files if it already exists.
adk_app (str): The name of the file (without .py) containing the AdkApp
instance.
staging_bucket (str): The GCS bucket for staging the deployment artifacts.
staging_bucket (str): Deprecated. This argument is no longer required or
used.
trace_to_cloud (bool): Whether to enable Cloud Trace.
api_key (str): Optional. The API key to use for Express Mode.
If not provided, the API key from the GOOGLE_API_KEY environment variable
Expand Down Expand Up @@ -734,26 +761,41 @@ def to_agent_engine(
app_name = os.path.basename(agent_folder)
display_name = display_name or app_name
parent_folder = os.path.dirname(agent_folder)
if parent_folder != os.getcwd():
click.echo(f'Please deploy from the project dir: {parent_folder}')
return
tmp_app_name = app_name + '_tmp' + datetime.now().strftime('%Y%m%d_%H%M%S')
temp_folder = temp_folder or tmp_app_name
agent_src_path = os.path.join(parent_folder, temp_folder)
click.echo(f'Staging all files in: {agent_src_path}')
adk_app_object = adk_app_object or 'root_agent'
if adk_app_object not in ['root_agent', 'app']:
click.echo(
f'Invalid adk_app_object: {adk_app_object}. Please use "root_agent"'
' or "app".'
)
return
if staging_bucket:
warnings.warn(
'WARNING: `staging_bucket` is deprecated and will be removed in a'
' future release. Please drop it from the list of arguments.',
DeprecationWarning,
stacklevel=2,
)

original_cwd = os.getcwd()
did_change_cwd = False
if parent_folder != original_cwd:
click.echo(
'Agent Engine deployment uses relative paths; temporarily switching '
f'working directory to: {parent_folder}'
)
os.chdir(parent_folder)
did_change_cwd = True
tmp_app_name = app_name + '_tmp' + datetime.now().strftime('%Y%m%d_%H%M%S')
temp_folder = temp_folder or tmp_app_name
agent_src_path = os.path.join(parent_folder, temp_folder)
click.echo(f'Staging all files in: {agent_src_path}')
# remove agent_src_path if it exists
if os.path.exists(agent_src_path):
click.echo('Removing existing files')
shutil.rmtree(agent_src_path)

try:
click.echo(f'Staging all files in: {agent_src_path}')
ignore_patterns = None
ae_ignore_path = os.path.join(agent_folder, '.ae_ignore')
if os.path.exists(ae_ignore_path):
Expand All @@ -762,15 +804,18 @@ def to_agent_engine(
patterns = [pattern.strip() for pattern in f.readlines()]
ignore_patterns = shutil.ignore_patterns(*patterns)
click.echo('Copying agent source code...')
shutil.copytree(agent_folder, agent_src_path, ignore=ignore_patterns)
shutil.copytree(
agent_folder,
agent_src_path,
ignore=ignore_patterns,
dirs_exist_ok=True,
)
click.echo('Copying agent source code complete.')

project = _resolve_project(project)

click.echo('Resolving files and dependencies...')
agent_config = {}
if staging_bucket:
agent_config['staging_bucket'] = staging_bucket
if not agent_engine_config_file:
# Attempt to read the agent engine config from .agent_engine_config.json in the dir (if any).
agent_engine_config_file = os.path.join(
Expand Down Expand Up @@ -813,8 +858,9 @@ def to_agent_engine(
if not os.path.exists(requirements_txt_path):
click.echo(f'Creating {requirements_txt_path}...')
with open(requirements_txt_path, 'w', encoding='utf-8') as f:
f.write('google-cloud-aiplatform[adk,agent_engines]')
f.write(_AGENT_ENGINE_REQUIREMENT + '\n')
click.echo(f'Created {requirements_txt_path}')
_ensure_agent_engine_dependency(requirements_txt_path)
agent_config['requirements_file'] = f'{temp_folder}/requirements.txt'

env_vars = {}
Expand Down Expand Up @@ -912,8 +958,7 @@ def to_agent_engine(
app_name=app_name,
trace_to_cloud_option=trace_to_cloud,
is_config_agent=is_config_agent,
temp_folder=temp_folder,
agent_folder=agent_folder,
agent_folder=f'./{temp_folder}',
adk_app_object=adk_app_object,
adk_app_type=adk_app_type,
express_mode=api_key is not None,
Expand Down Expand Up @@ -946,7 +991,9 @@ def to_agent_engine(
click.secho(f'✅ Updated agent engine: {agent_engine_id}', fg='green')
finally:
click.echo(f'Cleaning up the temp folder: {temp_folder}')
shutil.rmtree(temp_folder)
shutil.rmtree(agent_src_path)
if did_change_cwd:
os.chdir(original_cwd)


def to_gke(
Expand Down
23 changes: 16 additions & 7 deletions src/google/adk/cli/cli_tools_click.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,19 @@ def wrapper(*args, **kwargs):
return decorator


def _deprecate_staging_bucket(ctx, param, value):
if value:
click.echo(
click.style(
f"WARNING: --{param} is deprecated and will be removed. Please"
" leave it unspecified.",
fg="yellow",
),
err=True,
)
return value


def deprecated_adk_services_options():
"""Deprecated ADK services options."""

Expand Down Expand Up @@ -1689,10 +1702,8 @@ def cli_migrate_session(
"--staging_bucket",
type=str,
default=None,
help=(
"Optional. GCS bucket for staging the deployment artifacts. It will be"
" ignored if api_key is set."
),
help="Deprecated. This argument is no longer required or used.",
callback=_deprecate_staging_bucket,
)
@click.option(
"--agent_engine_id",
Expand Down Expand Up @@ -1827,16 +1838,14 @@ def cli_deploy_agent_engine(

# With Google Cloud Project and Region
adk deploy agent_engine --project=[project] --region=[region]
--staging_bucket=[staging_bucket] --display_name=[app_name]
my_agent
--display_name=[app_name] my_agent
"""
logging.getLogger("vertexai_genai.agentengines").setLevel(logging.INFO)
try:
cli_deploy.to_agent_engine(
agent_folder=agent,
project=project,
region=region,
staging_bucket=staging_bucket,
agent_engine_id=agent_engine_id,
trace_to_cloud=trace_to_cloud,
api_key=api_key,
Expand Down
16 changes: 16 additions & 0 deletions src/google/adk/tools/_function_tool_declarations.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@

from __future__ import annotations

import collections.abc
import inspect
import logging
from typing import Any
from typing import Callable
from typing import get_args
from typing import get_origin
from typing import get_type_hints
from typing import Optional
from typing import Type
Expand Down Expand Up @@ -145,6 +148,19 @@ def _build_response_json_schema(
except TypeError:
pass

# Handle AsyncGenerator and Generator return types (streaming tools)
# AsyncGenerator[YieldType, SendType] -> use YieldType as response schema
# Generator[YieldType, SendType, ReturnType] -> use YieldType as response schema
origin = get_origin(return_annotation)
if origin is not None and (
origin is collections.abc.AsyncGenerator
or origin is collections.abc.Generator
):
type_args = get_args(return_annotation)
if type_args:
# First type argument is the yield type
return_annotation = type_args[0]

try:
adapter = pydantic.TypeAdapter(
return_annotation,
Expand Down
Loading
Loading