You've already forked adk-python
mirror of
https://github.com/encounter/adk-python.git
synced 2026-03-30 10:57:20 -07:00
Merge branch 'main' into main
This commit is contained in:
@@ -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())
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user