From bbe1c9dc6644973f2775351d7c8aac53bd0da6ea Mon Sep 17 00:00:00 2001 From: Xuan Yang Date: Thu, 17 Jul 2025 15:34:31 -0700 Subject: [PATCH 1/6] fix: specify version into the uv installation action to bypass the setup-uv bug https://github.com/astral-sh/setup-uv/issues/489 PiperOrigin-RevId: 784339143 --- .github/workflows/python-unit-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/python-unit-tests.yml b/.github/workflows/python-unit-tests.yml index 52e61b8a..62f22433 100644 --- a/.github/workflows/python-unit-tests.yml +++ b/.github/workflows/python-unit-tests.yml @@ -38,6 +38,8 @@ jobs: - name: Install the latest version of uv uses: astral-sh/setup-uv@v6 + with: + version: "latest" - name: Install dependencies run: | From 35de210d4e5fe6605cd119b62e71a9914910e1ea Mon Sep 17 00:00:00 2001 From: Xuan Yang Date: Thu, 17 Jul 2025 15:48:42 -0700 Subject: [PATCH 2/6] chore: add a script to upload ADK docs for the ADK Answering Agent PiperOrigin-RevId: 784343501 --- .../samples/adk_answering_agent/README.md | 31 ++- .../samples/adk_answering_agent/settings.py | 5 + .../upload_docs_to_vertex_ai_search.py | 222 ++++++++++++++++++ 3 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 contributing/samples/adk_answering_agent/upload_docs_to_vertex_ai_search.py diff --git a/contributing/samples/adk_answering_agent/README.md b/contributing/samples/adk_answering_agent/README.md index e2af591d..158c825a 100644 --- a/contributing/samples/adk_answering_agent/README.md +++ b/contributing/samples/adk_answering_agent/README.md @@ -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. \ No newline at end of file diff --git a/contributing/samples/adk_answering_agent/settings.py b/contributing/samples/adk_answering_agent/settings.py index 9f152678..c8bd146b 100644 --- a/contributing/samples/adk_answering_agent/settings.py +++ b/contributing/samples/adk_answering_agent/settings.py @@ -29,6 +29,11 @@ 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") diff --git a/contributing/samples/adk_answering_agent/upload_docs_to_vertex_ai_search.py b/contributing/samples/adk_answering_agent/upload_docs_to_vertex_ai_search.py new file mode 100644 index 00000000..9dd7ca6a --- /dev/null +++ b/contributing/samples/adk_answering_agent/upload_docs_to_vertex_ai_search.py @@ -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()) From 36e45cdab3bbfb653eee3f9ed875b59bcd525ea1 Mon Sep 17 00:00:00 2001 From: Ankur Sharma Date: Thu, 17 Jul 2025 15:59:16 -0700 Subject: [PATCH 3/6] feat: Enable FinalResponseMatchV2 metric as an experiment PiperOrigin-RevId: 784346859 --- src/google/adk/cli/cli_eval.py | 21 ++++++++++++++---- src/google/adk/evaluation/eval_metrics.py | 4 ++++ .../adk/evaluation/final_response_match_v2.py | 4 ++-- .../evaluation/metric_evaluator_registry.py | 22 ++++++++++++++----- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/google/adk/cli/cli_eval.py b/src/google/adk/cli/cli_eval.py index d122c215..b9630103 100644 --- a/src/google/adk/cli/cli_eval.py +++ b/src/google/adk/cli/cli_eval.py @@ -15,6 +15,7 @@ from __future__ import annotations import importlib.util +import inspect import json import logging import os @@ -31,6 +32,7 @@ from ..evaluation.eval_case import EvalCase from ..evaluation.eval_metrics import EvalMetric from ..evaluation.eval_metrics import EvalMetricResult from ..evaluation.eval_metrics import EvalMetricResultPerInvocation +from ..evaluation.eval_metrics import JudgeModelOptions from ..evaluation.eval_result import EvalCaseResult from ..evaluation.evaluator import EvalStatus from ..evaluation.evaluator import Evaluator @@ -42,6 +44,7 @@ logger = logging.getLogger("google_adk." + __name__) TOOL_TRAJECTORY_SCORE_KEY = "tool_trajectory_avg_score" RESPONSE_MATCH_SCORE_KEY = "response_match_score" SAFETY_V1_KEY = "safety_v1" +FINAL_RESPONSE_MATCH_V2 = "final_response_match_v2" # This evaluation is not very stable. # This is always optional unless explicitly specified. RESPONSE_EVALUATION_SCORE_KEY = "response_evaluation_score" @@ -191,10 +194,16 @@ async def run_evals( for eval_metric in eval_metrics: metric_evaluator = _get_evaluator(eval_metric) - evaluation_result = metric_evaluator.evaluate_invocations( - actual_invocations=inference_result, - expected_invocations=eval_case.conversation, - ) + if inspect.iscoroutinefunction(metric_evaluator.evaluate_invocations): + evaluation_result = await metric_evaluator.evaluate_invocations( + actual_invocations=inference_result, + expected_invocations=eval_case.conversation, + ) + else: + evaluation_result = metric_evaluator.evaluate_invocations( + actual_invocations=inference_result, + expected_invocations=eval_case.conversation, + ) overall_eval_metric_results.append( EvalMetricResult( @@ -260,6 +269,7 @@ async def run_evals( def _get_evaluator(eval_metric: EvalMetric) -> Evaluator: try: + from ..evaluation.final_response_match_v2 import FinalResponseMatchV2Evaluator from ..evaluation.response_evaluator import ResponseEvaluator from ..evaluation.safety_evaluator import SafetyEvaluatorV1 from ..evaluation.trajectory_evaluator import TrajectoryEvaluator @@ -276,5 +286,8 @@ def _get_evaluator(eval_metric: EvalMetric) -> Evaluator: ) elif eval_metric.metric_name == SAFETY_V1_KEY: return SafetyEvaluatorV1(eval_metric) + elif eval_metric.metric_name == FINAL_RESPONSE_MATCH_V2: + eval_metric.judge_model_options = JudgeModelOptions() + return FinalResponseMatchV2Evaluator(eval_metric) raise ValueError(f"Unsupported eval metric: {eval_metric}") diff --git a/src/google/adk/evaluation/eval_metrics.py b/src/google/adk/evaluation/eval_metrics.py index 8cf23542..1f6acf26 100644 --- a/src/google/adk/evaluation/eval_metrics.py +++ b/src/google/adk/evaluation/eval_metrics.py @@ -36,6 +36,10 @@ class PrebuiltMetrics(Enum): RESPONSE_MATCH_SCORE = "response_match_score" + SAFETY_V1 = "safety_v1" + + FINAL_RESPONSE_MATCH_V2 = "final_response_match_v2" + MetricName: TypeAlias = Union[str, PrebuiltMetrics] diff --git a/src/google/adk/evaluation/final_response_match_v2.py b/src/google/adk/evaluation/final_response_match_v2.py index ad43448d..cd13a073 100644 --- a/src/google/adk/evaluation/final_response_match_v2.py +++ b/src/google/adk/evaluation/final_response_match_v2.py @@ -21,7 +21,7 @@ from typing import Optional from typing_extensions import override from ..models.llm_response import LlmResponse -from ..utils.feature_decorator import working_in_progress +from ..utils.feature_decorator import experimental from .eval_case import Invocation from .eval_metrics import EvalMetric from .evaluator import EvalStatus @@ -125,7 +125,7 @@ def _parse_critique(response: str) -> Label: return label -@working_in_progress +@experimental class FinalResponseMatchV2Evaluator(LlmAsJudge): """V2 final response match evaluator which uses an LLM to judge responses. diff --git a/src/google/adk/evaluation/metric_evaluator_registry.py b/src/google/adk/evaluation/metric_evaluator_registry.py index 99a70089..c3af0656 100644 --- a/src/google/adk/evaluation/metric_evaluator_registry.py +++ b/src/google/adk/evaluation/metric_evaluator_registry.py @@ -21,7 +21,9 @@ from .eval_metrics import EvalMetric from .eval_metrics import MetricName from .eval_metrics import PrebuiltMetrics from .evaluator import Evaluator +from .final_response_match_v2 import FinalResponseMatchV2Evaluator from .response_evaluator import ResponseEvaluator +from .safety_evaluator import SafetyEvaluatorV1 from .trajectory_evaluator import TrajectoryEvaluator logger = logging.getLogger("google_adk." + __name__) @@ -71,16 +73,24 @@ def _get_default_metric_evaluator_registry() -> MetricEvaluatorRegistry: metric_evaluator_registry = MetricEvaluatorRegistry() metric_evaluator_registry.register_evaluator( - metric_name=PrebuiltMetrics.TOOL_TRAJECTORY_AVG_SCORE, - evaluator=type(TrajectoryEvaluator), + metric_name=PrebuiltMetrics.TOOL_TRAJECTORY_AVG_SCORE.value, + evaluator=TrajectoryEvaluator, ) metric_evaluator_registry.register_evaluator( - metric_name=PrebuiltMetrics.RESPONSE_EVALUATION_SCORE, - evaluator=type(ResponseEvaluator), + metric_name=PrebuiltMetrics.RESPONSE_EVALUATION_SCORE.value, + evaluator=ResponseEvaluator, ) metric_evaluator_registry.register_evaluator( - metric_name=PrebuiltMetrics.RESPONSE_MATCH_SCORE, - evaluator=type(ResponseEvaluator), + metric_name=PrebuiltMetrics.RESPONSE_MATCH_SCORE.value, + evaluator=ResponseEvaluator, + ) + metric_evaluator_registry.register_evaluator( + metric_name=PrebuiltMetrics.SAFETY_V1.value, + evaluator=SafetyEvaluatorV1, + ) + metric_evaluator_registry.register_evaluator( + metric_name=PrebuiltMetrics.FINAL_RESPONSE_MATCH_V2.value, + evaluator=FinalResponseMatchV2Evaluator, ) return metric_evaluator_registry From 2e778049d0a675e458f4e35fe4104ca1298dbfcf Mon Sep 17 00:00:00 2001 From: Shangjie Chen Date: Thu, 17 Jul 2025 16:19:16 -0700 Subject: [PATCH 4/6] feat: Support passing fully qualified agent engine resource name when constructing session service and memory service Resolves https://github.com/google/adk-python/issues/1760 PiperOrigin-RevId: 784353411 --- src/google/adk/cli/cli_tools_click.py | 14 +++++--- src/google/adk/cli/fast_api.py | 50 ++++++++++++++++++++------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 95e61778..078925d1 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -461,7 +461,10 @@ def adk_services_options(): "--session_service_uri", help=( """Optional. The URI of the session service. - - Use 'agentengine://' to connect to Agent Engine sessions. + - Use 'agentengine://' to connect to Agent Engine + sessions. can either be the full qualified resource + name 'projects/abc/locations/us-central1/reasoningEngines/123' or + the resource id '123'. - Use 'sqlite://' to connect to a SQLite DB. - See https://docs.sqlalchemy.org/en/20/core/engines.html#backend-specific-urls for more details on supported database URIs.""" ), @@ -487,11 +490,12 @@ def adk_services_options(): @click.option( "--memory_service_uri", type=str, - help=( - """Optional. The URI of the memory service. + help=("""Optional. The URI of the memory service. - Use 'rag://' to connect to Vertex AI Rag Memory Service. - - Use 'agentengine://' to connect to Vertex AI Memory Bank Service. e.g. agentengine://12345""" - ), + - Use 'agentengine://' to connect to Agent Engine + sessions. can either be the full qualified resource + name 'projects/abc/locations/us-central1/reasoningEngines/123' or + the resource id '123'."""), default=None, ) @functools.wraps(func) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index face2495..2b43a864 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -297,6 +297,31 @@ def get_fast_api_app( eval_sets_manager = LocalEvalSetsManager(agents_dir=agents_dir) eval_set_results_manager = LocalEvalSetResultsManager(agents_dir=agents_dir) + def _parse_agent_engine_resource_name(agent_engine_id_or_resource_name): + if not agent_engine_id_or_resource_name: + raise click.ClickException( + "Agent engine resource name or resource id can not be empty." + ) + + # "projects/my-project/locations/us-central1/reasoningEngines/1234567890", + if "/" in agent_engine_id_or_resource_name: + # Validate resource name. + if len(agent_engine_id_or_resource_name.split("/")) != 6: + raise click.ClickException( + "Agent engine resource name is mal-formatted. It should be of" + " format :" + " projects/{project_id}/locations/{location}/reasoningEngines/{resource_id}" + ) + project = agent_engine_id_or_resource_name.split("/")[1] + location = agent_engine_id_or_resource_name.split("/")[3] + agent_engine_id = agent_engine_id_or_resource_name.split("/")[-1] + else: + envs.load_dotenv_for_agent("", agents_dir) + project = os.environ["GOOGLE_CLOUD_PROJECT"] + location = os.environ["GOOGLE_CLOUD_LOCATION"] + agent_engine_id = agent_engine_id_or_resource_name + return project, location, agent_engine_id + # Build the Memory service if memory_service_uri: if memory_service_uri.startswith("rag://"): @@ -308,13 +333,13 @@ def get_fast_api_app( rag_corpus=f'projects/{os.environ["GOOGLE_CLOUD_PROJECT"]}/locations/{os.environ["GOOGLE_CLOUD_LOCATION"]}/ragCorpora/{rag_corpus}' ) elif memory_service_uri.startswith("agentengine://"): - agent_engine_id = memory_service_uri.split("://")[1] - if not agent_engine_id: - raise click.ClickException("Agent engine id can not be empty.") - envs.load_dotenv_for_agent("", agents_dir) + agent_engine_id_or_resource_name = memory_service_uri.split("://")[1] + project, location, agent_engine_id = _parse_agent_engine_resource_name( + agent_engine_id_or_resource_name + ) memory_service = VertexAiMemoryBankService( - project=os.environ["GOOGLE_CLOUD_PROJECT"], - location=os.environ["GOOGLE_CLOUD_LOCATION"], + project=project, + location=location, agent_engine_id=agent_engine_id, ) else: @@ -327,14 +352,13 @@ def get_fast_api_app( # Build the Session service if session_service_uri: if session_service_uri.startswith("agentengine://"): - # Create vertex session service - agent_engine_id = session_service_uri.split("://")[1] - if not agent_engine_id: - raise click.ClickException("Agent engine id can not be empty.") - envs.load_dotenv_for_agent("", agents_dir) + agent_engine_id_or_resource_name = session_service_uri.split("://")[1] + project, location, agent_engine_id = _parse_agent_engine_resource_name( + agent_engine_id_or_resource_name + ) session_service = VertexAiSessionService( - project=os.environ["GOOGLE_CLOUD_PROJECT"], - location=os.environ["GOOGLE_CLOUD_LOCATION"], + project=project, + location=location, agent_engine_id=agent_engine_id, ) else: From b1f4aebb25093d7f91a92b966c14be6a182f90f6 Mon Sep 17 00:00:00 2001 From: "Wei Sun (Jack)" Date: Thu, 17 Jul 2025 16:19:43 -0700 Subject: [PATCH 5/6] chore: lint fixings in vertex_ai_memory_bank.py * Adds type hints; * Switches to lazy evaluation in logs. PiperOrigin-RevId: 784353566 --- .../memory/vertex_ai_memory_bank_service.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/google/adk/memory/vertex_ai_memory_bank_service.py b/src/google/adk/memory/vertex_ai_memory_bank_service.py index 083b48e8..f8da6acd 100644 --- a/src/google/adk/memory/vertex_ai_memory_bank_service.py +++ b/src/google/adk/memory/vertex_ai_memory_bank_service.py @@ -16,13 +16,15 @@ from __future__ import annotations import json import logging +from typing import Any +from typing import Dict from typing import Optional from typing import TYPE_CHECKING +from google.genai import Client +from google.genai import types from typing_extensions import override -from google import genai - from .base_memory_service import BaseMemoryService from .base_memory_service import SearchMemoryResponse from .memory_entry import MemoryEntry @@ -84,7 +86,7 @@ class VertexAiMemoryBankService(BaseMemoryService): path=f'reasoningEngines/{self._agent_engine_id}/memories:generate', request_dict=request_dict, ) - logger.info(f'Generate memory response: {api_response}') + logger.info('Generate memory response: %s', api_response) else: logger.info('No events to add to memory.') @@ -106,7 +108,7 @@ class VertexAiMemoryBankService(BaseMemoryService): }, ) api_response = _convert_api_response(api_response) - logger.info(f'Search memory response: {api_response}') + logger.info('Search memory response: %s', api_response) if not api_response or not api_response.get('retrievedMemories', None): return SearchMemoryResponse() @@ -117,10 +119,8 @@ class VertexAiMemoryBankService(BaseMemoryService): memory_events.append( MemoryEntry( author='user', - content=genai.types.Content( - parts=[ - genai.types.Part(text=memory.get('memory').get('fact')) - ], + content=types.Content( + parts=[types.Part(text=memory.get('memory').get('fact'))], role='user', ), timestamp=memory.get('updateTime'), @@ -137,13 +137,13 @@ class VertexAiMemoryBankService(BaseMemoryService): Returns: An API client for the given project and location. """ - client = genai.Client( + client = Client( vertexai=True, project=self._project, location=self._location ) return client._api_client -def _convert_api_response(api_response): +def _convert_api_response(api_response) -> Dict[str, Any]: """Converts the API response to a JSON object based on the type.""" if hasattr(api_response, 'body'): return json.loads(api_response.body) From bb4ff2cc3d40452cb9e0b47b83cb9b2b20f3eb45 Mon Sep 17 00:00:00 2001 From: "Wei Sun (Jack)" Date: Thu, 17 Jul 2025 17:20:41 -0700 Subject: [PATCH 6/6] chore: fixes formatting in adk_project_overview_and_architecture.md PiperOrigin-RevId: 784371131 --- .../adk_project_overview_and_architecture.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/contributing/adk_project_overview_and_architecture.md b/contributing/adk_project_overview_and_architecture.md index 106ecb03..5b150c5c 100644 --- a/contributing/adk_project_overview_and_architecture.md +++ b/contributing/adk_project_overview_and_architecture.md @@ -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). -