mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-13 16:45:01 -05:00
Compare commits
39 Commits
feat/add-g
...
adk-js-pro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f74bae8ff6 | ||
|
|
a0df7dfa59 | ||
|
|
9554ee0277 | ||
|
|
ff1ff4a534 | ||
|
|
49b53c83ce | ||
|
|
c7e41262a0 | ||
|
|
5e3b8d3672 | ||
|
|
4ff4f258aa | ||
|
|
5fe9984661 | ||
|
|
a212aedd19 | ||
|
|
9210e5555c | ||
|
|
b43af71793 | ||
|
|
da1f463dd1 | ||
|
|
3265f7e3a6 | ||
|
|
336743f747 | ||
|
|
911069ae8d | ||
|
|
cee59d52c3 | ||
|
|
9517daba09 | ||
|
|
3c61ee0597 | ||
|
|
19271eb9ee | ||
|
|
3a150c77ca | ||
|
|
ca6f31a192 | ||
|
|
d7faf7700f | ||
|
|
37a60ea2a6 | ||
|
|
8de16976ae | ||
|
|
49cb2f39f7 | ||
|
|
f169874e53 | ||
|
|
db8c3a3c77 | ||
|
|
8b33b0c67f | ||
|
|
35fa73516b | ||
|
|
66df3bfd21 | ||
|
|
73e0edc3cd | ||
|
|
3f32a9aab6 | ||
|
|
28006fc9b2 | ||
|
|
56c69131b4 | ||
|
|
ad4a509340 | ||
|
|
d39acac96c | ||
|
|
6df2ad28a9 | ||
|
|
8416378613 |
@@ -1,125 +0,0 @@
|
||||
# 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.
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
TABLE_NAME="hotels_go"
|
||||
QUICKSTART_GO_DIR="docs/en/getting-started/quickstart/go"
|
||||
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||
|
||||
PROXY_PID=""
|
||||
TOOLBOX_PID=""
|
||||
|
||||
install_system_packages() {
|
||||
apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
wget \
|
||||
gettext-base \
|
||||
netcat-openbsd
|
||||
}
|
||||
|
||||
start_cloud_sql_proxy() {
|
||||
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||
PROXY_PID=$!
|
||||
|
||||
for i in {1..30}; do
|
||||
if nc -z 127.0.0.1 5432; then
|
||||
echo "Cloud SQL Proxy is up and running."
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||
exit 1
|
||||
}
|
||||
|
||||
setup_toolbox() {
|
||||
TOOLBOX_YAML="/tools.yaml"
|
||||
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||
chmod +x "/toolbox"
|
||||
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||
TOOLBOX_PID=$!
|
||||
sleep 2
|
||||
}
|
||||
|
||||
setup_orch_table() {
|
||||
export TABLE_NAME
|
||||
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||
}
|
||||
|
||||
run_orch_test() {
|
||||
local orch_dir="$1"
|
||||
local orch_name
|
||||
orch_name=$(basename "$orch_dir")
|
||||
|
||||
if [ "$orch_name" == "openAI" ]; then
|
||||
echo -e "\nSkipping framework '${orch_name}': Temporarily excluded."
|
||||
return
|
||||
fi
|
||||
|
||||
(
|
||||
set -e
|
||||
setup_orch_table
|
||||
|
||||
echo "--- Preparing module for $orch_name ---"
|
||||
cd "$orch_dir"
|
||||
|
||||
if [ -f "go.mod" ]; then
|
||||
go mod tidy
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
export ORCH_NAME="$orch_name"
|
||||
|
||||
echo "--- Running tests for $orch_name ---"
|
||||
go test -v ./...
|
||||
)
|
||||
}
|
||||
|
||||
cleanup_all() {
|
||||
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||
if [ -n "$TOOLBOX_PID" ]; then
|
||||
kill $TOOLBOX_PID || true
|
||||
fi
|
||||
if [ -n "$PROXY_PID" ]; then
|
||||
kill $PROXY_PID || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_all EXIT
|
||||
|
||||
# Main script execution
|
||||
install_system_packages
|
||||
start_cloud_sql_proxy
|
||||
|
||||
export PGHOST=127.0.0.1
|
||||
export PGPORT=5432
|
||||
export PGPASSWORD="$DB_PASSWORD"
|
||||
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||
|
||||
setup_toolbox
|
||||
|
||||
for ORCH_DIR in "$QUICKSTART_GO_DIR"/*/; do
|
||||
if [ ! -d "$ORCH_DIR" ]; then
|
||||
continue
|
||||
fi
|
||||
run_orch_test "$ORCH_DIR"
|
||||
done
|
||||
@@ -1,125 +0,0 @@
|
||||
# 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.
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
TABLE_NAME="hotels_js"
|
||||
QUICKSTART_JS_DIR="docs/en/getting-started/quickstart/js"
|
||||
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||
|
||||
# Initialize process IDs to empty at the top of the script
|
||||
PROXY_PID=""
|
||||
TOOLBOX_PID=""
|
||||
|
||||
install_system_packages() {
|
||||
apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
wget \
|
||||
gettext-base \
|
||||
netcat-openbsd
|
||||
}
|
||||
|
||||
start_cloud_sql_proxy() {
|
||||
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||
PROXY_PID=$!
|
||||
|
||||
for i in {1..30}; do
|
||||
if nc -z 127.0.0.1 5432; then
|
||||
echo "Cloud SQL Proxy is up and running."
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||
exit 1
|
||||
}
|
||||
|
||||
setup_toolbox() {
|
||||
TOOLBOX_YAML="/tools.yaml"
|
||||
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||
chmod +x "/toolbox"
|
||||
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||
TOOLBOX_PID=$!
|
||||
sleep 2
|
||||
}
|
||||
|
||||
setup_orch_table() {
|
||||
export TABLE_NAME
|
||||
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||
}
|
||||
|
||||
run_orch_test() {
|
||||
local orch_dir="$1"
|
||||
local orch_name
|
||||
orch_name=$(basename "$orch_dir")
|
||||
|
||||
(
|
||||
set -e
|
||||
echo "--- Preparing environment for $orch_name ---"
|
||||
setup_orch_table
|
||||
|
||||
cd "$orch_dir"
|
||||
echo "Installing dependencies for $orch_name..."
|
||||
if [ -f "package-lock.json" ]; then
|
||||
npm ci
|
||||
else
|
||||
npm install
|
||||
fi
|
||||
|
||||
cd ..
|
||||
|
||||
echo "--- Running tests for $orch_name ---"
|
||||
export ORCH_NAME="$orch_name"
|
||||
node --test quickstart.test.js
|
||||
|
||||
echo "--- Cleaning environment for $orch_name ---"
|
||||
rm -rf "${orch_name}/node_modules"
|
||||
)
|
||||
}
|
||||
|
||||
cleanup_all() {
|
||||
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||
if [ -n "$TOOLBOX_PID" ]; then
|
||||
kill $TOOLBOX_PID || true
|
||||
fi
|
||||
if [ -n "$PROXY_PID" ]; then
|
||||
kill $PROXY_PID || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_all EXIT
|
||||
|
||||
# Main script execution
|
||||
install_system_packages
|
||||
start_cloud_sql_proxy
|
||||
|
||||
export PGHOST=127.0.0.1
|
||||
export PGPORT=5432
|
||||
export PGPASSWORD="$DB_PASSWORD"
|
||||
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||
|
||||
setup_toolbox
|
||||
|
||||
for ORCH_DIR in "$QUICKSTART_JS_DIR"/*/; do
|
||||
if [ ! -d "$ORCH_DIR" ]; then
|
||||
continue
|
||||
fi
|
||||
run_orch_test "$ORCH_DIR"
|
||||
done
|
||||
@@ -1,115 +0,0 @@
|
||||
# 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.
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
TABLE_NAME="hotels_python"
|
||||
QUICKSTART_PYTHON_DIR="docs/en/getting-started/quickstart/python"
|
||||
SQL_FILE=".ci/quickstart_test/setup_hotels_sample.sql"
|
||||
|
||||
PROXY_PID=""
|
||||
TOOLBOX_PID=""
|
||||
|
||||
install_system_packages() {
|
||||
apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
python3-venv \
|
||||
wget \
|
||||
gettext-base \
|
||||
netcat-openbsd
|
||||
}
|
||||
|
||||
start_cloud_sql_proxy() {
|
||||
wget "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" &
|
||||
PROXY_PID=$!
|
||||
|
||||
for i in {1..30}; do
|
||||
if nc -z 127.0.0.1 5432; then
|
||||
echo "Cloud SQL Proxy is up and running."
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Cloud SQL Proxy failed to start within the timeout period."
|
||||
exit 1
|
||||
}
|
||||
|
||||
setup_toolbox() {
|
||||
TOOLBOX_YAML="/tools.yaml"
|
||||
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||
if [ ! -f "$TOOLBOX_YAML" ]; then echo "Failed to create tools.yaml"; exit 1; fi
|
||||
wget "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||
chmod +x "/toolbox"
|
||||
/toolbox --tools-file "$TOOLBOX_YAML" &
|
||||
TOOLBOX_PID=$!
|
||||
sleep 2
|
||||
}
|
||||
|
||||
setup_orch_table() {
|
||||
export TABLE_NAME
|
||||
envsubst < "$SQL_FILE" | psql -h "$PGHOST" -p "$PGPORT" -U "$DB_USER" -d "$DATABASE_NAME"
|
||||
}
|
||||
|
||||
run_orch_test() {
|
||||
local orch_dir="$1"
|
||||
local orch_name
|
||||
orch_name=$(basename "$orch_dir")
|
||||
(
|
||||
set -e
|
||||
setup_orch_table
|
||||
cd "$orch_dir"
|
||||
local VENV_DIR=".venv"
|
||||
python3 -m venv "$VENV_DIR"
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install -r requirements.txt
|
||||
echo "--- Running tests for $orch_name ---"
|
||||
cd ..
|
||||
ORCH_NAME="$orch_name" pytest
|
||||
rm -rf "$VENV_DIR"
|
||||
)
|
||||
}
|
||||
|
||||
cleanup_all() {
|
||||
echo "--- Final cleanup: Shutting down processes and dropping table ---"
|
||||
if [ -n "$TOOLBOX_PID" ]; then
|
||||
kill $TOOLBOX_PID || true
|
||||
fi
|
||||
if [ -n "$PROXY_PID" ]; then
|
||||
kill $PROXY_PID || true
|
||||
fi
|
||||
}
|
||||
trap cleanup_all EXIT
|
||||
|
||||
# Main script execution
|
||||
install_system_packages
|
||||
start_cloud_sql_proxy
|
||||
|
||||
export PGHOST=127.0.0.1
|
||||
export PGPORT=5432
|
||||
export PGPASSWORD="$DB_PASSWORD"
|
||||
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||
|
||||
setup_toolbox
|
||||
|
||||
for ORCH_DIR in "$QUICKSTART_PYTHON_DIR"/*/; do
|
||||
if [ ! -d "$ORCH_DIR" ]; then
|
||||
continue
|
||||
fi
|
||||
run_orch_test "$ORCH_DIR"
|
||||
done
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
steps:
|
||||
- name: "${_IMAGE}"
|
||||
id: "js-pre-post-processing-test"
|
||||
entrypoint: "bash"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
set -ex
|
||||
chmod +x .ci/sample_tests/run_tests.sh
|
||||
.ci/sample_tests/run_tests.sh
|
||||
env:
|
||||
- "CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}"
|
||||
- "GCP_PROJECT=${_GCP_PROJECT}"
|
||||
- "DATABASE_NAME=${_DATABASE_NAME}"
|
||||
- "DB_USER=${_DB_USER}"
|
||||
- "TARGET_ROOT=${_TARGET_ROOT}"
|
||||
- "TARGET_LANG=${_TARGET_LANG}"
|
||||
- "TABLE_NAME=${_TABLE_NAME}"
|
||||
- "SQL_FILE=${_SQL_FILE}"
|
||||
- "AGENT_FILE_PATTERN=${_AGENT_FILE_PATTERN}"
|
||||
secretEnv: ["TOOLS_YAML_CONTENT", "GOOGLE_API_KEY", "DB_PASSWORD"]
|
||||
|
||||
availableSecrets:
|
||||
secretManager:
|
||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/5
|
||||
env: "TOOLS_YAML_CONTENT"
|
||||
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||
env: "GOOGLE_API_KEY"
|
||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||
env: "DB_PASSWORD"
|
||||
|
||||
timeout: 1200s
|
||||
|
||||
substitutions:
|
||||
_TARGET_LANG: "js"
|
||||
_IMAGE: "node:22"
|
||||
_TARGET_ROOT: "docs/en/samples/pre_post_processing/js"
|
||||
_TABLE_NAME: "hotels_js_pre_post_processing"
|
||||
_SQL_FILE: ".ci/sample_tests/setup_hotels.sql"
|
||||
_AGENT_FILE_PATTERN: "agent.js"
|
||||
|
||||
options:
|
||||
logging: CLOUD_LOGGING_ONLY
|
||||
@@ -0,0 +1,57 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
steps:
|
||||
- name: "${_IMAGE}"
|
||||
id: "py-pre-post-processing-test"
|
||||
entrypoint: "bash"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
set -ex
|
||||
chmod +x .ci/sample_tests/run_tests.sh
|
||||
.ci/sample_tests/run_tests.sh
|
||||
env:
|
||||
- "CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}"
|
||||
- "GCP_PROJECT=${_GCP_PROJECT}"
|
||||
- "DATABASE_NAME=${_DATABASE_NAME}"
|
||||
- "DB_USER=${_DB_USER}"
|
||||
- "TARGET_ROOT=${_TARGET_ROOT}"
|
||||
- "TARGET_LANG=${_TARGET_LANG}"
|
||||
- "TABLE_NAME=${_TABLE_NAME}"
|
||||
- "SQL_FILE=${_SQL_FILE}"
|
||||
- "AGENT_FILE_PATTERN=${_AGENT_FILE_PATTERN}"
|
||||
secretEnv: ["TOOLS_YAML_CONTENT", "GOOGLE_API_KEY", "DB_PASSWORD"]
|
||||
|
||||
availableSecrets:
|
||||
secretManager:
|
||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/5
|
||||
env: "TOOLS_YAML_CONTENT"
|
||||
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||
env: "GOOGLE_API_KEY"
|
||||
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||
env: "DB_PASSWORD"
|
||||
|
||||
timeout: 1200s
|
||||
|
||||
substitutions:
|
||||
_TARGET_LANG: "python"
|
||||
_IMAGE: "gcr.io/google.com/cloudsdktool/cloud-sdk:537.0.0"
|
||||
_TARGET_ROOT: "docs/en/samples/pre_post_processing/python"
|
||||
_TABLE_NAME: "hotels_py_pre_post_processing"
|
||||
_SQL_FILE: ".ci/sample_tests/setup_hotels.sql"
|
||||
_AGENT_FILE_PATTERN: "agent.py"
|
||||
|
||||
options:
|
||||
logging: CLOUD_LOGGING_ONLY
|
||||
@@ -23,13 +23,18 @@ steps:
|
||||
- |
|
||||
set -ex
|
||||
export VERSION=$(cat ./cmd/version.txt)
|
||||
chmod +x .ci/quickstart_test/run_go_tests.sh
|
||||
.ci/quickstart_test/run_go_tests.sh
|
||||
chmod +x .ci/sample_tests/run_tests.sh
|
||||
.ci/sample_tests/run_tests.sh
|
||||
env:
|
||||
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||
- 'DB_USER=${_DB_USER}'
|
||||
- 'TARGET_ROOT=docs/en/getting-started/quickstart/go'
|
||||
- 'TARGET_LANG=go'
|
||||
- 'TABLE_NAME=hotels_go'
|
||||
- 'SQL_FILE=.ci/sample_tests/setup_hotels.sql'
|
||||
- 'AGENT_FILE_PATTERN=quickstart.go'
|
||||
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||
|
||||
availableSecrets:
|
||||
@@ -23,13 +23,18 @@ steps:
|
||||
- |
|
||||
set -ex
|
||||
export VERSION=$(cat ./cmd/version.txt)
|
||||
chmod +x .ci/quickstart_test/run_js_tests.sh
|
||||
.ci/quickstart_test/run_js_tests.sh
|
||||
chmod +x .ci/sample_tests/run_tests.sh
|
||||
.ci/sample_tests/run_tests.sh
|
||||
env:
|
||||
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||
- 'DB_USER=${_DB_USER}'
|
||||
- 'TARGET_ROOT=docs/en/getting-started/quickstart/js'
|
||||
- 'TARGET_LANG=js'
|
||||
- 'TABLE_NAME=hotels_js'
|
||||
- 'SQL_FILE=.ci/sample_tests/setup_hotels.sql'
|
||||
- 'AGENT_FILE_PATTERN=quickstart.js'
|
||||
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||
|
||||
availableSecrets:
|
||||
@@ -23,13 +23,18 @@ steps:
|
||||
- |
|
||||
set -ex
|
||||
export VERSION=$(cat ./cmd/version.txt)
|
||||
chmod +x .ci/quickstart_test/run_py_tests.sh
|
||||
.ci/quickstart_test/run_py_tests.sh
|
||||
chmod +x .ci/sample_tests/run_py_tests.sh
|
||||
.ci/sample_tests/run_py_tests.sh
|
||||
env:
|
||||
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||
- 'DATABASE_NAME=${_DATABASE_NAME}'
|
||||
- 'DB_USER=${_DB_USER}'
|
||||
- 'TARGET_ROOT=docs/en/getting-started/quickstart/python'
|
||||
- 'TARGET_LANG=python'
|
||||
- 'TABLE_NAME=hotels_python'
|
||||
- 'SQL_FILE=.ci/sample_tests/setup_hotels.sql'
|
||||
- 'AGENT_FILE_PATTERN=quickstart.py'
|
||||
secretEnv: ['TOOLS_YAML_CONTENT', 'GOOGLE_API_KEY', 'DB_PASSWORD']
|
||||
|
||||
availableSecrets:
|
||||
202
.ci/sample_tests/run_tests.sh
Normal file
202
.ci/sample_tests/run_tests.sh
Normal file
@@ -0,0 +1,202 @@
|
||||
# Copyright 2026 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.
|
||||
|
||||
set -e
|
||||
|
||||
# --- Configuration (from Environment Variables) ---
|
||||
# TARGET_ROOT: The directory to search for tests (e.g., docs/en/getting-started/quickstart/js)
|
||||
# TARGET_LANG: python, js, go
|
||||
# TABLE_NAME: Database table name to use
|
||||
# SQL_FILE: Path to the SQL setup file
|
||||
# AGENT_FILE_PATTERN: Filename to look for (e.g., quickstart.js or agent.py)
|
||||
|
||||
VERSION=$(cat ./cmd/version.txt)
|
||||
|
||||
# Process IDs & Logs
|
||||
PROXY_PID=""
|
||||
TOOLBOX_PID=""
|
||||
PROXY_LOG="cloud_sql_proxy.log"
|
||||
TOOLBOX_LOG="toolbox_server.log"
|
||||
|
||||
install_system_packages() {
|
||||
echo "Installing system packages..."
|
||||
apt-get update && apt-get install -y \
|
||||
postgresql-client \
|
||||
wget \
|
||||
gettext-base \
|
||||
netcat-openbsd
|
||||
|
||||
if [[ "$TARGET_LANG" == "python" ]]; then
|
||||
apt-get install -y python3-venv
|
||||
fi
|
||||
}
|
||||
|
||||
start_cloud_sql_proxy() {
|
||||
echo "Starting Cloud SQL Proxy..."
|
||||
wget -q "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.10.0/cloud-sql-proxy.linux.amd64" -O /usr/local/bin/cloud-sql-proxy
|
||||
chmod +x /usr/local/bin/cloud-sql-proxy
|
||||
cloud-sql-proxy "${CLOUD_SQL_INSTANCE}" > "$PROXY_LOG" 2>&1 &
|
||||
PROXY_PID=$!
|
||||
|
||||
# Health Check
|
||||
for i in {1..30}; do
|
||||
if nc -z 127.0.0.1 5432; then
|
||||
echo "Cloud SQL Proxy is up and running."
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "ERROR: Cloud SQL Proxy failed to start. Logs:"
|
||||
cat "$PROXY_LOG"
|
||||
exit 1
|
||||
}
|
||||
|
||||
setup_toolbox() {
|
||||
echo "Setting up Toolbox server..."
|
||||
TOOLBOX_YAML="/tools.yaml"
|
||||
echo "${TOOLS_YAML_CONTENT}" > "$TOOLBOX_YAML"
|
||||
wget -q "https://storage.googleapis.com/genai-toolbox/v${VERSION}/linux/amd64/toolbox" -O "/toolbox"
|
||||
chmod +x "/toolbox"
|
||||
/toolbox --tools-file "$TOOLBOX_YAML" > "$TOOLBOX_LOG" 2>&1 &
|
||||
TOOLBOX_PID=$!
|
||||
|
||||
# Health Check
|
||||
for i in {1..15}; do
|
||||
if nc -z 127.0.0.1 5000; then
|
||||
echo "Toolbox server is up and running."
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "ERROR: Toolbox server failed to start. Logs:"
|
||||
cat "$TOOLBOX_LOG"
|
||||
exit 1
|
||||
}
|
||||
|
||||
setup_db_table() {
|
||||
echo "Setting up database table $TABLE_NAME using $SQL_FILE..."
|
||||
export TABLE_NAME
|
||||
envsubst < "$SQL_FILE" | psql -h 127.0.0.1 -p 5432 -U "$DB_USER" -d "$DATABASE_NAME"
|
||||
}
|
||||
|
||||
run_python_test() {
|
||||
local dir=$1
|
||||
local name=$(basename "$dir")
|
||||
echo "--- Running Python Test: $name ---"
|
||||
(
|
||||
cd "$dir"
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -q -r requirements.txt pytest
|
||||
|
||||
cd ..
|
||||
local test_file=$(find . -maxdepth 1 -name "*test.py" | head -n 1)
|
||||
if [ -n "$test_file" ]; then
|
||||
echo "Found native test: $test_file. Running pytest..."
|
||||
export ORCH_NAME="$name"
|
||||
export PYTHONPATH="../"
|
||||
pytest "$test_file"
|
||||
else
|
||||
echo "No native test found. running agent directly..."
|
||||
export PYTHONPATH="../"
|
||||
python3 "${name}/${AGENT_FILE_PATTERN}"
|
||||
fi
|
||||
rm -rf "${name}/.venv"
|
||||
)
|
||||
}
|
||||
|
||||
run_js_test() {
|
||||
local dir=$1
|
||||
local name=$(basename "$dir")
|
||||
echo "--- Running JS Test: $name ---"
|
||||
(
|
||||
cd "$dir"
|
||||
if [ -f "package-lock.json" ]; then npm ci -q; else npm install -q; fi
|
||||
|
||||
cd ..
|
||||
# Looking for a JS test file in the parent directory
|
||||
local test_file=$(find . -maxdepth 1 -name "*test.js" | head -n 1)
|
||||
if [ -n "$test_file" ]; then
|
||||
echo "Found native test: $test_file. Running node --test..."
|
||||
export ORCH_NAME="$name"
|
||||
node --test "$test_file"
|
||||
else
|
||||
echo "No native test found. running agent directly..."
|
||||
node "${name}/${AGENT_FILE_PATTERN}"
|
||||
fi
|
||||
rm -rf "${name}/node_modules"
|
||||
)
|
||||
}
|
||||
|
||||
run_go_test() {
|
||||
local dir=$1
|
||||
local name=$(basename "$dir")
|
||||
|
||||
if [ "$name" == "openAI" ]; then
|
||||
echo -e "\nSkipping framework '${name}': Temporarily excluded."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "--- Running Go Test: $name ---"
|
||||
(
|
||||
cd "$dir"
|
||||
if [ -f "go.mod" ]; then
|
||||
go mod tidy
|
||||
fi
|
||||
|
||||
cd ..
|
||||
local test_file=$(find . -maxdepth 1 -name "*test.go" | head -n 1)
|
||||
if [ -n "$test_file" ]; then
|
||||
echo "Found native test: $test_file. Running go test..."
|
||||
export ORCH_NAME="$name"
|
||||
go test -v ./...
|
||||
else
|
||||
echo "No native test found. running agent directly..."
|
||||
cd "$name"
|
||||
go run "."
|
||||
fi
|
||||
)
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
echo "Cleaning up background processes..."
|
||||
[ -n "$TOOLBOX_PID" ] && kill "$TOOLBOX_PID" || true
|
||||
[ -n "$PROXY_PID" ] && kill "$PROXY_PID" || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# --- Execution ---
|
||||
install_system_packages
|
||||
start_cloud_sql_proxy
|
||||
|
||||
export PGHOST=127.0.0.1
|
||||
export PGPORT=5432
|
||||
export PGPASSWORD="$DB_PASSWORD"
|
||||
export GOOGLE_API_KEY="$GOOGLE_API_KEY"
|
||||
|
||||
setup_toolbox
|
||||
setup_db_table
|
||||
|
||||
echo "Scanning $TARGET_ROOT for tests with pattern $AGENT_FILE_PATTERN..."
|
||||
|
||||
find "$TARGET_ROOT" -name "$AGENT_FILE_PATTERN" | while read -r agent_file; do
|
||||
sample_dir=$(dirname "$agent_file")
|
||||
if [[ "$TARGET_LANG" == "python" ]]; then
|
||||
run_python_test "$sample_dir"
|
||||
elif [[ "$TARGET_LANG" == "js" ]]; then
|
||||
run_js_test "$sample_dir"
|
||||
elif [[ "$TARGET_LANG" == "go" ]]; then
|
||||
run_go_test "$sample_dir"
|
||||
fi
|
||||
done
|
||||
47
docs/en/samples/pre_post_processing/_index.md
Normal file
47
docs/en/samples/pre_post_processing/_index.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "Pre and Post processing"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Pre and Post processing in GenAI applications.
|
||||
---
|
||||
|
||||
Pre and post processing allow developers to intercept and modify interactions between the agent and its tools or the user.
|
||||
|
||||
> **Note**: These capabilities are typically features of **orchestration frameworks** (like LangChain, LangGraph, or Agent Builder) rather than the Toolbox SDK itself. However, Toolbox tools are designed to fully leverage these framework capabilities to support robust, secure, and compliant agent architectures.
|
||||
|
||||
## Types of Processing
|
||||
|
||||
### Pre-processing
|
||||
|
||||
Pre-processing occurs before a tool is executed or an agent processes a message. Key types include:
|
||||
|
||||
- **Input Sanitization & Redaction**: Detecting and masking sensitive information (like PII) in user queries or tool arguments to prevent it from being logged or sent to unauthorized systems.
|
||||
- **Business Logic Validation**: Verifying that the proposed action complies with business rules (e.g., ensuring a requested hotel stay does not exceed 14 days, or checking if a user has sufficient permission).
|
||||
- **Security Guardrails**: Analyzing inputs for potential prompt injection attacks or malicious payloads.
|
||||
|
||||
### Post-processing
|
||||
|
||||
Post-processing occurs after a tool has executed or the model has generated a response. Key types include:
|
||||
|
||||
- **Response Enrichment**: Injecting additional data into the tool output that wasn't part of the raw API response (e.g., calculating loyalty points earned based on the booking value).
|
||||
- **Output Formatting**: Transforming raw data (like JSON or XML) into a more human-readable or model-friendly format to improve the agent's understanding.
|
||||
- **Compliance Auditing**: Logging the final outcome of transactions, including the original request and the result, to a secure audit trail.
|
||||
|
||||
## Processing Scopes
|
||||
|
||||
While processing logic can be applied at various levels (Agent, Model, Tool), this guide primarily focuses on **Tool Level** processing, which is most relevant for granular control over tool execution.
|
||||
|
||||
### Tool Level (Primary Focus)
|
||||
|
||||
Wraps individual tool executions. This is best for logic specific to a single tool or a set of tools.
|
||||
|
||||
- **Scope**: Intercepts the raw inputs (arguments) to a tool and its outputs.
|
||||
- **Use Cases**: Argument validation, output formatting, specific privacy rules for sensitive tools.
|
||||
|
||||
### Comparison with Other Levels
|
||||
|
||||
It is helpful to understand how tool-level processing differs from other scopes:
|
||||
|
||||
- **Model Level**: Intercepts individual calls to the LLM (prompts and responses). Unlike tool-level, this applies globally to all text sent/received, making it better for global PII redaction or token tracking.
|
||||
- **Agent Level**: Wraps the high-level execution loop (e.g., a "turn" in the conversation). Unlike tool-level, this envelopes the entire turn (user input to final response), making it suitable for session management or end-to-end auditing.
|
||||
4
docs/en/samples/pre_post_processing/golden.txt
Normal file
4
docs/en/samples/pre_post_processing/golden.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Final Client Response:
|
||||
AI:
|
||||
Loyalty Points
|
||||
POLICY CHECK: Intercepting 'update-hotel'
|
||||
39
docs/en/samples/pre_post_processing/js.md
Normal file
39
docs/en/samples/pre_post_processing/js.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
title: "(JS) Pre and post processing"
|
||||
type: docs
|
||||
weight: 5
|
||||
description: >
|
||||
How to add pre and post processing to your JS toolbox applications.
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This tutorial assumes that you have set up a basic toolbox application as described in the [local quickstart](../../getting-started/local_quickstart_js).
|
||||
|
||||
This guide demonstrates how to implement these patterns in your Toolbox applications.
|
||||
|
||||
## Implementation
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{% tab header="ADK" text=true %}}
|
||||
The following example demonstrates how to use the `beforeToolCallback` and `afterToolCallback` hooks in the ADK `LlmAgent` to implement pre and post processing logic.
|
||||
|
||||
```js
|
||||
{{< include "js/adk/agent.js" >}}
|
||||
```
|
||||
|
||||
You can also add model-level (`beforeModelCallback`, `afterModelCallback`) and agent-level (`beforeAgentCallback`, `afterAgentCallback`) hooks to intercept messages at different stages of the execution loop.
|
||||
|
||||
For more information, see the [ADK Callbacks documentation](https://google.github.io/adk-docs/callbacks/types-of-callbacks/).
|
||||
{{% /tab %}}
|
||||
{{% tab header="Langchain" text=true %}}
|
||||
The following example demonstrates how to use `ToolboxClient` with LangChain's middleware to implement pre and post processing for tool calls.
|
||||
|
||||
```js
|
||||
{{< include "js/langchain/agent.js" >}}
|
||||
```
|
||||
|
||||
For more information, see the [LangChain Middleware documentation](https://js.langchain.com/docs/introduction/middleware).
|
||||
You can also add model-level (`wrap_model`) and agent-level (`before_agent`, `after_agent`) hooks to intercept messages at different stages of the execution loop. See the [LangChain Middleware documentation](https://js.langchain.com/docs/introduction/middleware) for details on these additional hook types.
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
97
docs/en/samples/pre_post_processing/js/adk/agent.js
Normal file
97
docs/en/samples/pre_post_processing/js/adk/agent.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { InMemoryRunner, LlmAgent, LogLevel } from '@google/adk';
|
||||
import { ToolboxClient } from '@toolbox-sdk/adk';
|
||||
|
||||
process.env.GOOGLE_GENAI_API_KEY = process.env.GOOGLE_API_KEY || 'your-api-key'; // Replace it with your API key
|
||||
|
||||
const systemPrompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
// Pre-Processing
|
||||
function preProcess({tool, args}) {
|
||||
const name = tool.name;
|
||||
console.log(`POLICY CHECK: Intercepting '${name}'`);
|
||||
|
||||
if (name === "update-hotel" && args.checkin_date && args.checkout_date) {
|
||||
const start = new Date(args.checkin_date);
|
||||
const end = new Date(args.checkout_date);
|
||||
const duration = (end - start) / (1000 * 60 * 60 * 24); // days
|
||||
|
||||
if (duration > 14) {
|
||||
console.log("BLOCKED: Stay too long");
|
||||
return "Error: Maximum stay duration is 14 days.";
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Post-Processing
|
||||
function postProcess({tool, response}) {
|
||||
const name = tool.name;
|
||||
console.log(`ENRICHING RESPONSE: Intercepting '${name}'`);
|
||||
let content = response;
|
||||
if (typeof response === 'object' && response !== null) {
|
||||
if (response.result) content = response.result;
|
||||
else if (response.content) content = response.content;
|
||||
}
|
||||
if (name === "book-hotel") {
|
||||
const loyaltyBonus = 500;
|
||||
const enrichedContent = `Booking Confirmed!\n You earned ${loyaltyBonus} Loyalty Points with this stay.\n\nSystem Details: ${content}`;
|
||||
if (typeof response === 'object' && response !== null) {
|
||||
return { ...response, result: enrichedContent, content: enrichedContent };
|
||||
}
|
||||
return enrichedContent;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function runTurn(runner, userId, sessionId, prompt) {
|
||||
console.log(`\nUSER: '${prompt}'`);
|
||||
const content = { role: 'user', parts: [{ text: prompt }] };
|
||||
const stream = runner.runAsync({ userId, sessionId, newMessage: content });
|
||||
|
||||
let fullText = "";
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.content && chunk.content.parts) {
|
||||
fullText += chunk.content.parts.map(p => p.text || "").join("");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("-".repeat(50));
|
||||
console.log(`AI: ${fullText}`);
|
||||
}
|
||||
|
||||
export async function main() {
|
||||
const userId = 'test_user';
|
||||
const client = new ToolboxClient('http://127.0.0.1:5000');
|
||||
const tools = await client.loadToolset("my-toolset");
|
||||
|
||||
const rootAgent = new LlmAgent({
|
||||
name: 'hotel_agent',
|
||||
model: 'gemini-2.5-flash',
|
||||
description: 'Agent for hotel bookings and administration.',
|
||||
instruction: systemPrompt,
|
||||
tools: tools,
|
||||
// Add any pre- and post- processing callbacks
|
||||
beforeToolCallback: preProcess,
|
||||
afterToolCallback: postProcess
|
||||
});
|
||||
|
||||
const appName = rootAgent.name;
|
||||
const runner = new InMemoryRunner({ agent: rootAgent, appName, logLevel: LogLevel.ERROR });
|
||||
const session = await runner.sessionService.createSession({ appName, userId });
|
||||
|
||||
// Turn 1: Booking
|
||||
await runTurn(runner, userId, session.id, "Book hotel with id 3.");
|
||||
|
||||
// Turn 2: Policy Violation
|
||||
await runTurn(runner, userId, session.id, "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-10");
|
||||
}
|
||||
|
||||
main();
|
||||
3728
docs/en/samples/pre_post_processing/js/adk/package-lock.json
generated
Normal file
3728
docs/en/samples/pre_post_processing/js/adk/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
docs/en/samples/pre_post_processing/js/adk/package.json
Normal file
16
docs/en/samples/pre_post_processing/js/adk/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "adk",
|
||||
"version": "1.0.0",
|
||||
"description": "ADK.js sample for pre/post processing",
|
||||
"type": "module",
|
||||
"main": "agent.js",
|
||||
"scripts": {
|
||||
"start": "node agent.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@google/adk": "^0.2.0",
|
||||
"@toolbox-sdk/adk": "^0.2.1"
|
||||
}
|
||||
}
|
||||
89
docs/en/samples/pre_post_processing/js/agent.test.js
Normal file
89
docs/en/samples/pre_post_processing/js/agent.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2026 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 { describe, test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const ORCH_NAME = process.env.ORCH_NAME;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const orchDir = path.join(__dirname, ORCH_NAME);
|
||||
const agentPath = path.join(orchDir, "agent.js");
|
||||
|
||||
const { main: runAgent } = await import(agentPath);
|
||||
|
||||
const GOLDEN_FILE_PATH = path.resolve(__dirname, "../golden.txt");
|
||||
|
||||
describe(`${ORCH_NAME} Pre/Post Processing Agent`, () => {
|
||||
let capturedOutput = [];
|
||||
let capturedErrors = [];
|
||||
let originalLog;
|
||||
let originalError;
|
||||
|
||||
before(() => {
|
||||
originalLog = console.log;
|
||||
originalError = console.error;
|
||||
|
||||
console.log = (...args) => {
|
||||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||||
capturedOutput.push(msg);
|
||||
};
|
||||
|
||||
console.error = (...args) => {
|
||||
const msg = args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' ');
|
||||
capturedErrors.push(msg);
|
||||
};
|
||||
});
|
||||
|
||||
after(() => {
|
||||
console.log = originalLog;
|
||||
console.error = originalError;
|
||||
});
|
||||
|
||||
test("runs without errors and outputContainsRequiredKeywords", async () => {
|
||||
capturedOutput = [];
|
||||
capturedErrors = [];
|
||||
|
||||
await runAgent();
|
||||
assert.equal(
|
||||
capturedErrors.length,
|
||||
0,
|
||||
`Script produced stderr: ${capturedErrors.join("\n")}`
|
||||
);
|
||||
|
||||
const actualOutput = capturedOutput.join("\n");
|
||||
|
||||
assert.ok(
|
||||
actualOutput.length > 0,
|
||||
"Assertion Failed: Script ran successfully but produced no output."
|
||||
);
|
||||
|
||||
const goldenFile = fs.readFileSync(GOLDEN_FILE_PATH, "utf8");
|
||||
const keywords = goldenFile.split("\n").filter((kw) => kw.trim() !== "");
|
||||
const missingKeywords = [];
|
||||
|
||||
for (const keyword of keywords) {
|
||||
if (!actualOutput.includes(keyword)) {
|
||||
missingKeywords.push(keyword);
|
||||
}
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
missingKeywords.length === 0,
|
||||
`Assertion Failed: The following keywords were missing from the output: [${missingKeywords.join(", ")}]`
|
||||
);
|
||||
});
|
||||
});
|
||||
150
docs/en/samples/pre_post_processing/js/langchain/agent.js
Normal file
150
docs/en/samples/pre_post_processing/js/langchain/agent.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { ToolboxClient } from "@toolbox-sdk/core";
|
||||
import { ChatVertexAI } from "@langchain/google-vertexai";
|
||||
import { AgentExecutor, createToolCallingAgent } from "langchain/agents";
|
||||
import { ChatPromptTemplate } from "@langchain/core/prompts";
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import { fileURLToPath } from "url";
|
||||
import process from "process";
|
||||
|
||||
const systemPrompt = `
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
`;
|
||||
|
||||
/**
|
||||
* Pre-processing: Enforce Business Rules
|
||||
* @param {string} name
|
||||
* @param {object} args
|
||||
* @returns {string|null} Error message if blocked, null otherwise
|
||||
*/
|
||||
function checkBusinessRules(name, args) {
|
||||
console.log(`POLICY CHECK: Intercepting '${name}'`);
|
||||
|
||||
if (name === "update-hotel" && args.checkin_date && args.checkout_date) {
|
||||
try {
|
||||
const start = new Date(args.checkin_date);
|
||||
const end = new Date(args.checkout_date);
|
||||
const duration = (end - start) / (1000 * 60 * 60 * 24); // days
|
||||
|
||||
if (duration > 14) {
|
||||
console.log("BLOCKED: Stay too long");
|
||||
return "Error: Maximum stay duration is 14 days.";
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid dates
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-processing: Enrich Response
|
||||
* @param {string} name
|
||||
* @param {any} result
|
||||
* @returns {any} Enriched result
|
||||
*/
|
||||
function enrichResponse(name, result) {
|
||||
let content = result;
|
||||
if (typeof result === 'object' && result !== null && result.content) {
|
||||
content = result.content;
|
||||
}
|
||||
|
||||
if (name === "book-hotel" && typeof content === 'string' && !content.includes("Error")) {
|
||||
const loyaltyBonus = 500;
|
||||
const enrichedContent = `Booking Confirmed!\n You earned ${loyaltyBonus} Loyalty Points with this stay.\n\nSystem Details: ${content}`;
|
||||
|
||||
if (typeof result === 'object' && result !== null) {
|
||||
result.content = enrichedContent;
|
||||
return result;
|
||||
}
|
||||
return enrichedContent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a tool to add pre- and post-processing logic.
|
||||
* @param {import("@langchain/core/tools").StructuredTool} toolInstance
|
||||
*/
|
||||
function wrapToolWithBusinessLogic(toolInstance) {
|
||||
const originalInvoke = toolInstance.invoke.bind(toolInstance);
|
||||
|
||||
toolInstance.invoke = async (input, config) => {
|
||||
// 1. Pre-processing
|
||||
const validationError = checkBusinessRules(toolInstance.name, input);
|
||||
if (validationError) {
|
||||
return validationError;
|
||||
}
|
||||
|
||||
// 2. Execution
|
||||
const result = await originalInvoke(input, config);
|
||||
|
||||
// 3. Post-processing
|
||||
return enrichResponse(toolInstance.name, result);
|
||||
};
|
||||
|
||||
return toolInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to run a single turn.
|
||||
*/
|
||||
async function runTurn(agentExecutor, input) {
|
||||
console.log(`\nUSER: '${input}'`);
|
||||
const result = await agentExecutor.invoke({ input });
|
||||
console.log("-".repeat(50));
|
||||
console.log("Final Client Response:");
|
||||
console.log(`AI: ${result.output}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const client = new ToolboxClient("http://127.0.0.1:5000");
|
||||
|
||||
try {
|
||||
const rawTools = await client.loadToolset("my-toolset");
|
||||
|
||||
// Convert & Wrap tools
|
||||
const tools = rawTools
|
||||
.map(t => tool(t, {
|
||||
name: t.getName(),
|
||||
description: t.getDescription(),
|
||||
schema: t.getParamSchema()
|
||||
}))
|
||||
.map(wrapToolWithBusinessLogic);
|
||||
|
||||
const model = new ChatVertexAI({
|
||||
model: "gemini-2.5-flash",
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const prompt = ChatPromptTemplate.fromMessages([
|
||||
["system", systemPrompt],
|
||||
["placeholder", "{chat_history}"],
|
||||
["human", "{input}"],
|
||||
["placeholder", "{agent_scratchpad}"],
|
||||
]);
|
||||
|
||||
const agent = createToolCallingAgent({ llm: model, tools, prompt });
|
||||
const agentExecutor = new AgentExecutor({ agent, tools, verbose: false });
|
||||
|
||||
// Turn 1: Booking
|
||||
await runTurn(agentExecutor, "Book hotel with id 3.");
|
||||
|
||||
// Turn 2: Policy Violation
|
||||
await runTurn(agentExecutor, "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-10");
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error running agent:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
main();
|
||||
}
|
||||
|
||||
export { main };
|
||||
1851
docs/en/samples/pre_post_processing/js/langchain/package-lock.json
generated
Normal file
1851
docs/en/samples/pre_post_processing/js/langchain/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "langchain",
|
||||
"version": "1.0.0",
|
||||
"description": "LangChain.js sample for pre/post processing",
|
||||
"type": "module",
|
||||
"main": "agent.js",
|
||||
"scripts": {
|
||||
"start": "node agent.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@langchain/core": "^0.3.0",
|
||||
"@langchain/google-vertexai": "^0.1.0",
|
||||
"@toolbox-sdk/core": "^0.2.1",
|
||||
"langchain": "^0.3.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
31
docs/en/samples/pre_post_processing/python.md
Normal file
31
docs/en/samples/pre_post_processing/python.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: "(Python) Pre and post processing"
|
||||
type: docs
|
||||
weight: 4
|
||||
description: >
|
||||
How to add pre and post processing to your Python toolbox applications.
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This tutorial assumes that you have set up a basic toolbox application as described in the [local quickstart](../../getting-started/local_quickstart).
|
||||
|
||||
This guide demonstrates how to implement these patterns in your Toolbox applications.
|
||||
|
||||
## Python
|
||||
|
||||
{{< tabpane persist=header >}}
|
||||
{{% tab header="ADK" text=true %}}
|
||||
Coming soon.
|
||||
{{% /tab %}}
|
||||
{{% tab header="Langchain" text=true %}}
|
||||
The following example demonstrates how to use `ToolboxClient` with LangChain's middleware to implement pre and post processing for tool calls.
|
||||
|
||||
```py
|
||||
{{< include "python/langchain/agent.py" >}}
|
||||
```
|
||||
|
||||
For more information, see the [LangChain Middleware documentation](https://docs.langchain.com/oss/python/langchain/middleware/custom#wrap-style-hooks).
|
||||
You can also add model-level (`wrap_model`) and agent-level (`before_agent`, `after_agent`) hooks to intercept messages at different stages of the execution loop. See the [LangChain Middleware documentation](https://docs.langchain.com/oss/python/langchain/middleware/custom#wrap-style-hooks) for details on these additional hook types.
|
||||
{{% /tab %}}
|
||||
{{< /tabpane >}}
|
||||
4
docs/en/samples/pre_post_processing/python/__init__.py
Normal file
4
docs/en/samples/pre_post_processing/python/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# This file makes the 'pre_post_processing/python' directory a Python package.
|
||||
|
||||
# You can include any package-level initialization logic here if needed.
|
||||
# For now, this file is empty.
|
||||
59
docs/en/samples/pre_post_processing/python/agent_test.py
Normal file
59
docs/en/samples/pre_post_processing/python/agent_test.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# Copyright 2026 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 asyncio
|
||||
import importlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ORCH_NAME = os.environ.get("ORCH_NAME")
|
||||
module_path = f"python.{ORCH_NAME}.agent"
|
||||
agent = importlib.import_module(module_path)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def golden_keywords():
|
||||
"""Loads expected keywords from the golden.txt file."""
|
||||
golden_file_path = Path(__file__).resolve().parent.parent / "golden.txt"
|
||||
if not golden_file_path.exists():
|
||||
pytest.fail(f"Golden file not found: {golden_file_path}")
|
||||
try:
|
||||
with open(golden_file_path, "r") as f:
|
||||
return [line.strip() for line in f.readlines() if line.strip()]
|
||||
except Exception as e:
|
||||
pytest.fail(f"Could not read golden.txt: {e}")
|
||||
|
||||
|
||||
# --- Execution Tests ---
|
||||
class TestExecution:
|
||||
"""Test framework execution and output validation."""
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def script_output(self, capsys):
|
||||
"""Run the agent function and return its output."""
|
||||
asyncio.run(agent.main())
|
||||
return capsys.readouterr()
|
||||
|
||||
def test_script_runs_without_errors(self, script_output):
|
||||
"""Test that the script runs and produces no stderr."""
|
||||
assert script_output.err == "", f"Script produced stderr: {script_output.err}"
|
||||
|
||||
def test_keywords_in_output(self, script_output, golden_keywords):
|
||||
"""Test that expected keywords are present in the script's output."""
|
||||
output = script_output.out
|
||||
print(f"\nAgent Output:\n{output}\n")
|
||||
missing_keywords = [kw for kw in golden_keywords if kw not in output]
|
||||
assert not missing_keywords, f"Missing keywords in output: {missing_keywords}"
|
||||
@@ -0,0 +1 @@
|
||||
# Empty init for package resolution
|
||||
120
docs/en/samples/pre_post_processing/python/langchain/agent.py
Normal file
120
docs/en/samples/pre_post_processing/python/langchain/agent.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Copyright 2026 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 asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langchain.agents.middleware import wrap_tool_call
|
||||
from langchain_core.messages import ToolMessage
|
||||
from langchain_google_vertexai import ChatVertexAI
|
||||
from toolbox_langchain import ToolboxClient
|
||||
|
||||
system_prompt = """
|
||||
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||
location and price tier. Always mention hotel ids while performing any
|
||||
searches. This is very important for any operations. For any bookings or
|
||||
cancellations, please provide the appropriate confirmation. Be sure to
|
||||
update checkin or checkout dates if mentioned by the user.
|
||||
Don't ask for confirmations from the user.
|
||||
"""
|
||||
|
||||
|
||||
# Pre processing
|
||||
@wrap_tool_call
|
||||
async def enforce_business_rules(request, handler):
|
||||
"""
|
||||
Business Logic Validation:
|
||||
Enforces max stay duration (e.g., max 14 days).
|
||||
"""
|
||||
tool_call = request.tool_call
|
||||
name = tool_call["name"]
|
||||
args = tool_call["args"]
|
||||
|
||||
print(f"POLICY CHECK: Intercepting '{name}'")
|
||||
|
||||
if name == "update-hotel":
|
||||
if "checkin_date" in args and "checkout_date" in args:
|
||||
try:
|
||||
start = datetime.fromisoformat(args["checkin_date"])
|
||||
end = datetime.fromisoformat(args["checkout_date"])
|
||||
duration = (end - start).days
|
||||
|
||||
if duration > 14:
|
||||
print("BLOCKED: Stay too long")
|
||||
return ToolMessage(
|
||||
content="Error: Maximum stay duration is 14 days.",
|
||||
tool_call_id=tool_call["id"],
|
||||
)
|
||||
except ValueError:
|
||||
pass # Ignore invalid date formats
|
||||
|
||||
return await handler(request)
|
||||
|
||||
|
||||
# Post processing
|
||||
@wrap_tool_call
|
||||
async def enrich_response(request, handler):
|
||||
"""
|
||||
Post-Processing & Enrichment:
|
||||
Adds loyalty points information to successful bookings.
|
||||
Standardizes output format.
|
||||
"""
|
||||
result = await handler(request)
|
||||
|
||||
if isinstance(result, ToolMessage):
|
||||
content = str(result.content)
|
||||
tool_name = request.tool_call["name"]
|
||||
|
||||
if tool_name == "book-hotel" and "Error" not in content:
|
||||
loyalty_bonus = 500
|
||||
result.content = f"Booking Confirmed!\n You earned {loyalty_bonus} Loyalty Points with this stay.\n\nSystem Details: {content}"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def main():
|
||||
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||
tools = await client.aload_toolset("my-toolset")
|
||||
model = ChatVertexAI(model="gemini-2.5-flash")
|
||||
agent = create_agent(
|
||||
system_prompt=system_prompt,
|
||||
model=model,
|
||||
tools=tools,
|
||||
middleware=[enforce_business_rules, enrich_response],
|
||||
)
|
||||
|
||||
user_input = "Book hotel with id 3."
|
||||
response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": user_input}]}
|
||||
)
|
||||
|
||||
print("-" * 50)
|
||||
print("Final Client Response:")
|
||||
last_ai_msg = response["messages"][-1].content
|
||||
print(f"AI: {last_ai_msg}")
|
||||
|
||||
# Test Pre-processing
|
||||
print("-" * 50)
|
||||
user_input = "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-01-20"
|
||||
response = await agent.ainvoke(
|
||||
{"messages": [{"role": "user", "content": user_input}]}
|
||||
)
|
||||
last_ai_msg = response["messages"][-1].content
|
||||
print(f"AI: {last_ai_msg}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,3 @@
|
||||
langchain==1.2.6
|
||||
langchain-google-vertexai==3.2.2
|
||||
toolbox-langchain==0.5.8
|
||||
Reference in New Issue
Block a user