Merge branch 'main' into main

This commit is contained in:
seanzhou1023
2025-07-17 18:23:32 -07:00
committed by GitHub
12 changed files with 362 additions and 52 deletions
+2
View File
@@ -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: |
@@ -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).
@@ -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,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")
@@ -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())
+17 -4
View File
@@ -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}")
+9 -5
View File
@@ -461,7 +461,10 @@ def adk_services_options():
"--session_service_uri",
help=(
"""Optional. The URI of the session service.
- Use 'agentengine://<agent_engine_resource_id>' to connect to Agent Engine sessions.
- Use 'agentengine://<agent_engine>' to connect to Agent Engine
sessions. <agent_engine> can either be the full qualified resource
name 'projects/abc/locations/us-central1/reasoningEngines/123' or
the resource id '123'.
- Use 'sqlite://<path_to_sqlite_file>' 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://<rag_corpus_id>' to connect to Vertex AI Rag Memory Service.
- Use 'agentengine://<agent_engine_resource_id>' to connect to Vertex AI Memory Bank Service. e.g. agentengine://12345"""
),
- Use 'agentengine://<agent_engine>' to connect to Agent Engine
sessions. <agent_engine> 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)
+37 -13
View File
@@ -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:
@@ -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]
@@ -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.
@@ -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
@@ -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)