mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-18 11:02:26 -05:00
Compare commits
30 Commits
lsc-177139
...
v0.20.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5156db2621 | ||
|
|
7c67bcc810 | ||
|
|
f6804420b9 | ||
|
|
927881ffb9 | ||
|
|
42c8dd7ddd | ||
|
|
ae0c29254a | ||
|
|
46b072c3f4 | ||
|
|
4aabb4aaca | ||
|
|
897c63dcea | ||
|
|
22b5aca395 | ||
|
|
57f6220b9e | ||
|
|
c451015509 | ||
|
|
ef63860559 | ||
|
|
a89191d8bb | ||
|
|
13a682f407 | ||
|
|
dc7c62c951 | ||
|
|
aec8897805 | ||
|
|
a4c9287aec | ||
|
|
2c228ef4f2 | ||
|
|
1e9c4762a5 | ||
|
|
7e6e88a21f | ||
|
|
b2ea4b7b8f | ||
|
|
cfd4b18dee | ||
|
|
d2576cbc38 | ||
|
|
cd56ea44fb | ||
|
|
12bdd95459 | ||
|
|
61739300be | ||
|
|
3b140f5006 | ||
|
|
84e826a93e | ||
|
|
edd739c490 |
@@ -34,9 +34,47 @@ steps:
|
||||
path: "/gopath"
|
||||
script: |
|
||||
go test -c -race -cover \
|
||||
-coverpkg=./internal/sources/...,./internal/tools/... ./tests/...
|
||||
-coverpkg=./internal/sources/...,./internal/tools/... \
|
||||
$(go list ./tests/... | grep -v '/tests/prompts')
|
||||
chmod +x .ci/test_with_coverage.sh
|
||||
|
||||
- id: "compile-prompt-test-binary"
|
||||
name: golang:1
|
||||
waitFor: ["install-dependencies"]
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
script: |
|
||||
for dir in ./tests/prompts/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
PROMPT_TYPE=$(basename "$dir")
|
||||
echo "--- Compiling prompt test for ${PROMPT_TYPE} with targeted coverage ---"
|
||||
|
||||
go test -c -race -cover \
|
||||
-coverpkg=./internal/prompts/... \
|
||||
-o "prompt.${PROMPT_TYPE}.test" \
|
||||
"${dir}"
|
||||
fi
|
||||
done
|
||||
|
||||
chmod +x .ci/test_prompts_with_coverage.sh
|
||||
|
||||
- id: "prompts-custom"
|
||||
name: golang:1
|
||||
waitFor: ["compile-prompt-test-binary"]
|
||||
entrypoint: /bin/bash
|
||||
env:
|
||||
- "GOPATH=/gopath"
|
||||
volumes:
|
||||
- name: "go"
|
||||
path: "/gopath"
|
||||
args:
|
||||
- -c
|
||||
- |
|
||||
.ci/test_prompts_with_coverage.sh "custom"
|
||||
|
||||
- id: "cloud-sql-pg"
|
||||
name: golang:1
|
||||
waitFor: ["compile-test-binary"]
|
||||
|
||||
75
.ci/test_prompts_with_coverage.sh
Normal file
75
.ci/test_prompts_with_coverage.sh
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
# .ci/test_prompts_with_coverage.sh
|
||||
#
|
||||
# This script runs a specific prompt integration test, calculates its
|
||||
# code coverage, and checks if it meets a minimum threshold.
|
||||
#
|
||||
# It is called with one argument: the type of the prompt.
|
||||
# Example usage: .ci/test_prompts_with_coverage.sh "custom"
|
||||
|
||||
# Exit immediately if a command fails.
|
||||
set -e
|
||||
|
||||
# --- 1. Define Variables ---
|
||||
|
||||
# The first argument is the prompt type (e.g., "custom").
|
||||
PROMPT_TYPE=$1
|
||||
COVERAGE_THRESHOLD=80 # Minimum coverage percentage required.
|
||||
|
||||
if [ -z "$PROMPT_TYPE" ]; then
|
||||
echo "Error: No prompt type provided. Please call this script with an argument."
|
||||
echo "Usage: .ci/test_prompts_with_coverage.sh <prompt_type>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct names based on the prompt type.
|
||||
TEST_BINARY="./prompt.${PROMPT_TYPE}.test"
|
||||
TEST_NAME="$(tr '[:lower:]' '[:upper:]' <<< ${PROMPT_TYPE:0:1})${PROMPT_TYPE:1} Prompts"
|
||||
COVERAGE_FILE="coverage.prompts-${PROMPT_TYPE}.out"
|
||||
|
||||
|
||||
# --- 2. Run Integration Tests ---
|
||||
|
||||
echo "--- Running integration tests for ${TEST_NAME} ---"
|
||||
|
||||
# Safety check for the binary's existence.
|
||||
if [ ! -f "$TEST_BINARY" ]; then
|
||||
echo "Error: Test binary not found at ${TEST_BINARY}. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Execute the test binary and generate the coverage file.
|
||||
# If the tests fail, the 'set -e' command will cause the script to exit here.
|
||||
if ! ./"${TEST_BINARY}" -test.v -test.coverprofile="${COVERAGE_FILE}"; then
|
||||
echo "Error: Tests for ${TEST_NAME} failed. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- Tests for ${TEST_NAME} passed successfully ---"
|
||||
|
||||
|
||||
# --- 3. Calculate and Check Coverage ---
|
||||
|
||||
echo "Calculating coverage for ${TEST_NAME}..."
|
||||
|
||||
# Calculate the total coverage percentage from the generated file.
|
||||
# The '2>/dev/null' suppresses warnings if the coverage file is empty.
|
||||
total_coverage=$(go tool cover -func="${COVERAGE_FILE}" 2>/dev/null | grep "total:" | awk '{print $3}')
|
||||
|
||||
if [ -z "$total_coverage" ]; then
|
||||
echo "Warning: Could not calculate coverage for ${TEST_NAME}. The coverage report might be empty."
|
||||
total_coverage="0%"
|
||||
fi
|
||||
|
||||
echo "${TEST_NAME} total coverage: $total_coverage"
|
||||
|
||||
# Remove the '%' sign for numerical comparison.
|
||||
coverage_numeric=$(echo "$total_coverage" | sed 's/%//')
|
||||
|
||||
# Check if the coverage is below the defined threshold.
|
||||
if awk -v coverage="$coverage_numeric" -v threshold="$COVERAGE_THRESHOLD" 'BEGIN {exit !(coverage < threshold)}'; then
|
||||
echo "Coverage failure: ${TEST_NAME} total coverage (${total_coverage}) is below the ${COVERAGE_THRESHOLD}% threshold."
|
||||
exit 1
|
||||
else
|
||||
echo "Coverage for ${TEST_NAME} is sufficient."
|
||||
fi
|
||||
11
.github/CODEOWNERS
vendored
11
.github/CODEOWNERS
vendored
@@ -4,3 +4,14 @@
|
||||
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax
|
||||
|
||||
* @googleapis/senseai-eco
|
||||
# Code & Tests
|
||||
**/alloydb*/ @googleapis/toolbox-alloydb
|
||||
**/bigquery/ @googleapis/toolbox-bigquery
|
||||
**/bigtable/ @googleapis/toolbox-bigtable
|
||||
**/cloudsqlmssql/ @googleapis/toolbox-cloud-sql-mssql
|
||||
**/cloudsqlmysql/ @googleapis/toolbox-cloud-sql-mysql
|
||||
**/cloudsqlpg/ @googleapis/toolbox-cloud-sql-postgres
|
||||
**/dataplex/ @googleapis/toolbox-dataplex
|
||||
**/firestore/ @googleapis/toolbox-firestore
|
||||
**/looker/ @googleapis/toolbox-looker
|
||||
**/spanner/ @googleapis/toolbox-spanner
|
||||
|
||||
84
.github/blunderbuss.yml
vendored
84
.github/blunderbuss.yml
vendored
@@ -19,17 +19,89 @@ assign_issues:
|
||||
- anubhav756
|
||||
- twishabansal
|
||||
assign_issues_by:
|
||||
- labels:
|
||||
- 'product: alloydb'
|
||||
to:
|
||||
- 'googleapis/toolbox-alloydb'
|
||||
- labels:
|
||||
- 'product: bigquery'
|
||||
to:
|
||||
- Genesis929
|
||||
- shobsi
|
||||
- jiaxunwu
|
||||
- 'googleapis/toolbox-bigquery'
|
||||
- labels:
|
||||
- 'product: bigtable'
|
||||
to:
|
||||
- 'googleapis/toolbox-bigtable'
|
||||
- labels:
|
||||
- 'product: mssql'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-mssql'
|
||||
- labels:
|
||||
- 'product: mysql'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-mysql'
|
||||
- labels:
|
||||
- 'product: postgres'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-postgres'
|
||||
- labels:
|
||||
- 'product: dataplex'
|
||||
to:
|
||||
- 'googleapis/toolbox-dataplex'
|
||||
- labels:
|
||||
- 'product: firestore'
|
||||
to:
|
||||
- 'googleapis/toolbox-firestore'
|
||||
- labels:
|
||||
- 'product: looker'
|
||||
to:
|
||||
- drstrangelooker
|
||||
- 'googleapis/toolbox-looker'
|
||||
- labels:
|
||||
- 'product: spanner'
|
||||
to:
|
||||
- 'googleapis/toolbox-spanner'
|
||||
assign_prs:
|
||||
- Yuan325
|
||||
- duwenxin99
|
||||
- averikitsch
|
||||
- duwenxin99
|
||||
- averikitsch
|
||||
assign_prs_by:
|
||||
- labels:
|
||||
- 'product: alloydb'
|
||||
to:
|
||||
- 'googleapis/toolbox-alloydb'
|
||||
- labels:
|
||||
- 'product: bigquery'
|
||||
to:
|
||||
- 'googleapis/toolbox-bigquery'
|
||||
- labels:
|
||||
- 'product: bigtable'
|
||||
to:
|
||||
- 'googleapis/toolbox-bigtable'
|
||||
- labels:
|
||||
- 'product: mssql'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-mssql'
|
||||
- labels:
|
||||
- 'product: mysql'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-mysql'
|
||||
- labels:
|
||||
- 'product: postgres'
|
||||
to:
|
||||
- 'googleapis/toolbox-cloud-sql-postgres'
|
||||
- labels:
|
||||
- 'product: dataplex'
|
||||
to:
|
||||
- 'googleapis/toolbox-dataplex'
|
||||
- labels:
|
||||
- 'product: firestore'
|
||||
to:
|
||||
- 'googleapis/toolbox-firestore'
|
||||
- labels:
|
||||
- 'product: looker'
|
||||
to:
|
||||
- 'googleapis/toolbox-looker'
|
||||
- labels:
|
||||
- 'product: spanner'
|
||||
to:
|
||||
- 'googleapis/toolbox-spanner'
|
||||
|
||||
|
||||
86
.github/labels.yaml
vendored
86
.github/labels.yaml
vendored
@@ -93,10 +93,90 @@
|
||||
description: 'Use label to signal PR should be included in the next release.'
|
||||
|
||||
# Product Labels
|
||||
- name: 'product: alloydb'
|
||||
color: 5065c7
|
||||
description: 'AlloyDB'
|
||||
- name: 'product: bigquery'
|
||||
color: 5065c7
|
||||
description: 'Product: Assigned to the BigQuery team.'
|
||||
# Product Labels
|
||||
description: 'BigQuery'
|
||||
- name: 'product: bigtable'
|
||||
color: 5065c7
|
||||
description: 'Bigtable'
|
||||
- name: 'product: cassandra'
|
||||
color: 5065c7
|
||||
description: 'Cassandra'
|
||||
- name: 'product: clickhouse'
|
||||
color: 5065c7
|
||||
description: 'ClickHouse'
|
||||
- name: 'product: mssql'
|
||||
color: 5065c7
|
||||
description: 'SQL Server'
|
||||
- name: 'product: mysql'
|
||||
color: 5065c7
|
||||
description: 'MySQL'
|
||||
- name: 'product: postgres'
|
||||
color: 5065c7
|
||||
description: 'PostgreSQL'
|
||||
- name: 'product: couchbase'
|
||||
color: 5065c7
|
||||
description: 'Couchbase'
|
||||
- name: 'product: dataplex'
|
||||
color: 5065c7
|
||||
description: 'Dataplex'
|
||||
- name: 'product: dgraph'
|
||||
color: 5065c7
|
||||
description: 'Dgraph'
|
||||
- name: 'product: elasticsearch'
|
||||
color: 5065c7
|
||||
description: 'Elasticsearch'
|
||||
- name: 'product: firebird'
|
||||
color: 5065c7
|
||||
description: 'Firebird'
|
||||
- name: 'product: firestore'
|
||||
color: 5065c7
|
||||
description: 'Firestore'
|
||||
- name: 'product: looker'
|
||||
color: 5065c7
|
||||
description: 'Product: Assigned to the Looker team.'
|
||||
description: 'Looker'
|
||||
- name: 'product: mindsdb'
|
||||
color: 5065c7
|
||||
description: 'MindsDB'
|
||||
- name: 'product: mongodb'
|
||||
color: 5065c7
|
||||
description: 'MongoDB'
|
||||
- name: 'product: neo4j'
|
||||
color: 5065c7
|
||||
description: 'Neo4j'
|
||||
- name: 'product: oceanbase'
|
||||
color: 5065c7
|
||||
description: 'OceanBase'
|
||||
- name: 'product: oracle'
|
||||
color: 5065c7
|
||||
description: 'Oracle'
|
||||
- name: 'product: redis'
|
||||
color: 5065c7
|
||||
description: 'Redis'
|
||||
- name: 'product: serverlessspark'
|
||||
color: 5065c7
|
||||
description: 'Serverless Spark'
|
||||
- name: 'product: singlestore'
|
||||
color: 5065c7
|
||||
description: 'SingleStore'
|
||||
- name: 'product: spanner'
|
||||
color: 5065c7
|
||||
description: 'Spanner'
|
||||
- name: 'product: sqlite'
|
||||
color: 5065c7
|
||||
description: 'SQLite'
|
||||
- name: 'product: tidb'
|
||||
color: 5065c7
|
||||
description: 'TiDB'
|
||||
- name: 'product: trino'
|
||||
color: 5065c7
|
||||
description: 'Trino'
|
||||
- name: 'product: valkey'
|
||||
color: 5065c7
|
||||
description: 'Valkey'
|
||||
- name: 'product: yugabytedb'
|
||||
color: 5065c7
|
||||
description: 'YugabyteDB'
|
||||
|
||||
12
.github/release-please.yml
vendored
12
.github/release-please.yml
vendored
@@ -38,4 +38,14 @@ extraFiles: [
|
||||
"docs/en/how-to/connect-ide/neo4j_mcp.md",
|
||||
"docs/en/how-to/connect-ide/sqlite_mcp.md",
|
||||
"gemini-extension.json",
|
||||
]
|
||||
{
|
||||
"type": "json",
|
||||
"path": ".registry/server.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": ".registry/server.json",
|
||||
"jsonpath": "$.packages[0].identifier"
|
||||
},
|
||||
]
|
||||
|
||||
2
.github/workflows/lint.yaml
vendored
2
.github/workflows/lint.yaml
vendored
@@ -69,4 +69,4 @@ jobs:
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||
with:
|
||||
version: latest
|
||||
args: --timeout 4m
|
||||
args: --timeout 10m
|
||||
|
||||
73
.github/workflows/publish-mcp.yml
vendored
Normal file
73
.github/workflows/publish-mcp.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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.
|
||||
|
||||
name: Publish to MCP Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"] # Triggers on version tags like v1.0.0
|
||||
# allow manual triggering with no inputs required
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC authentication
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Wait for image in Artifact Registry
|
||||
shell: bash
|
||||
run: |
|
||||
MAX_ATTEMPTS=10
|
||||
VERSION=$(jq -r '.version' .registry/server.json)
|
||||
REGISTRY_URL="https://us-central1-docker.pkg.dev/v2/database-toolbox/toolbox/toolbox/manifests/${VERSION}"
|
||||
|
||||
# initially sleep time to wait for the version release
|
||||
sleep 3m
|
||||
|
||||
for i in $(seq 1 ${MAX_ATTEMPTS}); do
|
||||
echo "Attempt $i: Checking for image ${REGISTRY_URL}..."
|
||||
# Use curl to check the manifest header
|
||||
# Using -I to fetch headers only, -s silent, -f fail fast on errors.
|
||||
curl -Isf "${REGISTRY_URL}" > /dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Image found! Continuing to next steps."
|
||||
exit 0
|
||||
else
|
||||
echo "❌ Image not found (likely 404 error) on attempt $i."
|
||||
if [ $i -lt ${MAX_ATTEMPTS} ]; then
|
||||
echo "Sleeping for 5 minutes before next attempt..."
|
||||
sleep 2m
|
||||
else
|
||||
echo "Maximum attempts reached. Image not found."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Install MCP Publisher
|
||||
run: |
|
||||
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
||||
|
||||
- name: Login to MCP Registry
|
||||
run: ./mcp-publisher login github-oidc
|
||||
|
||||
- name: Publish to MCP Registry
|
||||
run: ./mcp-publisher publish --file=.registry/server.json
|
||||
5
.github/workflows/tests.yaml
vendored
5
.github/workflows/tests.yaml
vendored
@@ -81,11 +81,12 @@ jobs:
|
||||
run: |
|
||||
source_dir="./internal/sources/*"
|
||||
tool_dir="./internal/tools/*"
|
||||
prompt_dir="./internal/prompts/*"
|
||||
auth_dir="./internal/auth/*"
|
||||
int_test_dir="./tests/*"
|
||||
included_packages=$(go list ./... | grep -v -e "$source_dir" -e "$tool_dir" -e "$auth_dir" -e "$int_test_dir")
|
||||
included_packages=$(go list ./... | grep -v -e "$source_dir" -e "$tool_dir" -e "$prompt_dir" -e "$auth_dir" -e "$int_test_dir")
|
||||
go test -race -cover -coverprofile=coverage.out -v $included_packages
|
||||
go test -race -v ./internal/sources/... ./internal/tools/... ./internal/auth/...
|
||||
go test -race -v ./internal/sources/... ./internal/tools/... ./internal/prompts/... ./internal/auth/...
|
||||
|
||||
- name: Run tests without coverage
|
||||
if: ${{ runner.os != 'Linux' }}
|
||||
|
||||
@@ -51,6 +51,10 @@ ignoreFiles = ["quickstart/shared", "quickstart/python", "quickstart/js", "quick
|
||||
# Add a new version block here before every release
|
||||
# The order of versions in this file is mirrored into the dropdown
|
||||
|
||||
[[params.versions]]
|
||||
version = "v0.20.0"
|
||||
url = "https://googleapis.github.io/genai-toolbox/v0.20.0/"
|
||||
|
||||
[[params.versions]]
|
||||
version = "v0.19.1"
|
||||
url = "https://googleapis.github.io/genai-toolbox/v0.19.1/"
|
||||
|
||||
76
.registry/server.json
Normal file
76
.registry/server.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||
"name": "io.github.googleapis/genai-toolbox",
|
||||
"description": "MCP Toolbox for Databases enables your agent to connect to your database.",
|
||||
"title": "MCP Toolbox",
|
||||
"websiteUrl": "https://github.com/googleapis/genai-toolbox",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://github.com/googleapis/genai-toolbox/blob/main/.hugo/assets/icons/logo.svg",
|
||||
"mimeType": "image/svg+xml"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"url": "https://github.com/googleapis/genai-toolbox",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "0.20.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "oci",
|
||||
"registryBaseUrl": "https://artifactregistry.googleapis.com",
|
||||
"identifier": "us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:0.20.0",
|
||||
"transport": {
|
||||
"type": "streamable-http",
|
||||
"url": "http://{host}:{port}/mcp"
|
||||
},
|
||||
"runtimeArguments": [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--tools-file",
|
||||
"description": "File path specifying the tool configuration.",
|
||||
"default": "tools.yaml",
|
||||
"isRequired": false
|
||||
},
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--address",
|
||||
"description": "Address of the interface the server will listen on.",
|
||||
"value": "{host}",
|
||||
"variables": {
|
||||
"host": {
|
||||
"description": "ip address",
|
||||
"isRequired": true,
|
||||
"default": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--port",
|
||||
"description": "Port the server will listen on.",
|
||||
"value": "{port}",
|
||||
"variables": {
|
||||
"port": {
|
||||
"description": "port",
|
||||
"isRequired": true,
|
||||
"default": "5000"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--log-level",
|
||||
"description": "Specify the minimum level logged.",
|
||||
"default": "info",
|
||||
"choices": [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [0.20.0](https://github.com/googleapis/genai-toolbox/compare/v0.19.1...v0.20.0) (2025-11-14)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Added prompt support for toolbox ([#1798](https://github.com/googleapis/genai-toolbox/issues/1798)) ([cd56ea4](https://github.com/googleapis/genai-toolbox/commit/cd56ea44fbdd149fcb92324e70ee36ac747635db))
|
||||
* **source/alloydb, source/cloud-sql-postgres,source/cloud-sql-mysql,source/cloud-sql-mssql:** Use project from env for alloydb and cloud sql control plane tools ([#1588](https://github.com/googleapis/genai-toolbox/issues/1588)) ([12bdd95](https://github.com/googleapis/genai-toolbox/commit/12bdd954597e49d3ec6b247cc104584c5a4d1943))
|
||||
* **source/mysql:** Set default host and port for MySQL source ([#1922](https://github.com/googleapis/genai-toolbox/issues/1922)) ([2c228ef](https://github.com/googleapis/genai-toolbox/commit/2c228ef4f2d4cb8dfc41e845466bfe3566d141a1))
|
||||
* **source/Postgresql:** Set default host and port for Postgresql source ([#1927](https://github.com/googleapis/genai-toolbox/issues/1927)) ([7e6e88a](https://github.com/googleapis/genai-toolbox/commit/7e6e88a21f2b9b60e0d645cdde33a95892d31a04))
|
||||
* **tool/looker-generate-embed-url:** Adding generate embed url tool ([#1877](https://github.com/googleapis/genai-toolbox/issues/1877)) ([ef63860](https://github.com/googleapis/genai-toolbox/commit/ef63860559798fbad54c1051d9f53bce42d66464))
|
||||
* **tools/postgres:** Add `list_triggers`, `database_overview` tools for postgres ([#1912](https://github.com/googleapis/genai-toolbox/issues/1912)) ([a4c9287](https://github.com/googleapis/genai-toolbox/commit/a4c9287aecf848faa98d973a9ce5b13fa309a58e))
|
||||
* **tools/postgres:** Add list_indexes, list_sequences tools for postgres ([#1765](https://github.com/googleapis/genai-toolbox/issues/1765)) ([897c63d](https://github.com/googleapis/genai-toolbox/commit/897c63dcea43226262d2062088c59f2d1068fca7))
|
||||
|
||||
## [0.19.1](https://github.com/googleapis/genai-toolbox/compare/v0.18.0...v0.19.1) (2025-11-07)
|
||||
|
||||
|
||||
|
||||
@@ -337,7 +337,7 @@ for instructions on developing Toolbox SDKs.
|
||||
|
||||
Team `@googleapis/senseai-eco` has been set as
|
||||
[CODEOWNERS](.github/CODEOWNERS). The GitHub TeamSync tool is used to create
|
||||
this team from MDB Group, `senseai-eco`.
|
||||
this team from MDB Group, `senseai-eco`. Additionally, database-specific GitHub teams (e.g., `@googleapis/toolbox-alloydb`) have been created from MDB groups to manage code ownership and review for individual database products.
|
||||
|
||||
Team `@googleapis/toolbox-contributors` has write access to this repo. They
|
||||
can create branches and approve test runs. But they do not have the ability
|
||||
@@ -441,7 +441,7 @@ Trigger pull request tests for external contributors by:
|
||||
|
||||
## Repo Setup & Automation
|
||||
|
||||
* .github/blunderbuss.yml - Auto-assign issues and PRs from GitHub teams
|
||||
* .github/blunderbuss.yml - Auto-assign issues and PRs from GitHub teams. Use a product label to assign to a product-specific team member.
|
||||
* .github/renovate.json5 - Tooling for dependency updates. Dependabot is built
|
||||
into the GitHub repo for GitHub security warnings
|
||||
* go/github-issue-mirror - GitHub issues are automatically mirrored into buganizer
|
||||
|
||||
32
README.md
32
README.md
@@ -39,6 +39,7 @@ documentation](https://googleapis.github.io/genai-toolbox/).
|
||||
- [Sources](#sources)
|
||||
- [Tools](#tools)
|
||||
- [Toolsets](#toolsets)
|
||||
- [Prompts](#prompts)
|
||||
- [Versioning](#versioning)
|
||||
- [Pre-1.0.0 Versioning](#pre-100-versioning)
|
||||
- [Post-1.0.0 Versioning](#post-100-versioning)
|
||||
@@ -124,7 +125,7 @@ To install Toolbox as a binary:
|
||||
>
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.19.1
|
||||
> export VERSION=0.20.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
@@ -137,7 +138,7 @@ To install Toolbox as a binary:
|
||||
>
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.19.1
|
||||
> export VERSION=0.20.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
@@ -150,7 +151,7 @@ To install Toolbox as a binary:
|
||||
>
|
||||
> ```sh
|
||||
> # see releases page for other versions
|
||||
> export VERSION=0.19.1
|
||||
> export VERSION=0.20.0
|
||||
> curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
|
||||
> chmod +x toolbox
|
||||
> ```
|
||||
@@ -163,7 +164,7 @@ To install Toolbox as a binary:
|
||||
>
|
||||
> ```powershell
|
||||
> # see releases page for other versions
|
||||
> $VERSION = "0.19.1"
|
||||
> $VERSION = "0.20.0"
|
||||
> Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
|
||||
> ```
|
||||
>
|
||||
@@ -176,7 +177,7 @@ You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.19.1
|
||||
export VERSION=0.20.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -200,7 +201,7 @@ To install from source, ensure you have the latest version of
|
||||
[Go installed](https://go.dev/doc/install), and then run the following command:
|
||||
|
||||
```sh
|
||||
go install github.com/googleapis/genai-toolbox@v0.19.1
|
||||
go install github.com/googleapis/genai-toolbox@v0.20.0
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -932,6 +933,25 @@ all_tools = client.load_toolset()
|
||||
my_second_toolset = client.load_toolset("my_second_toolset")
|
||||
```
|
||||
|
||||
### Prompts
|
||||
|
||||
The `prompts` section of a `tools.yaml` defines prompts that can be used for
|
||||
interactions with LLMs.
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
code_review:
|
||||
description: "Asks the LLM to analyze code quality and suggest improvements."
|
||||
messages:
|
||||
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
|
||||
arguments:
|
||||
- name: "code"
|
||||
description: "The code to review"
|
||||
```
|
||||
|
||||
For more details on configuring prompts, see the
|
||||
[Prompts](https://googleapis.github.io/genai-toolbox/resources/prompts).
|
||||
|
||||
## Versioning
|
||||
|
||||
This project uses [semantic versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`).
|
||||
|
||||
42
cmd/root.go
42
cmd/root.go
@@ -35,12 +35,16 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/telemetry"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
|
||||
// Import prompt packages for side effect of registration
|
||||
_ "github.com/googleapis/genai-toolbox/internal/prompts/custom"
|
||||
|
||||
// Import tool packages for side effect of registration
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreatecluster"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/alloydb/alloydbcreateinstance"
|
||||
@@ -118,6 +122,7 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookercreateprojectfile"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdeleteprojectfile"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookerdevmode"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergenerateembedurl"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectiondatabases"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnections"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/looker/lookergetconnectionschemas"
|
||||
@@ -172,12 +177,16 @@ import (
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/oceanbase/oceanbasesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oracleexecutesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/oracle/oraclesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresdatabaseoverview"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgresexecutesql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistactivequeries"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistavailableextensions"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistindexes"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistinstalledextensions"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistschemas"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistsequences"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttables"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslisttriggers"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgreslistviews"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/tools/redis"
|
||||
@@ -360,12 +369,13 @@ type ToolsFile struct {
|
||||
AuthServices server.AuthServiceConfigs `yaml:"authServices"`
|
||||
Tools server.ToolConfigs `yaml:"tools"`
|
||||
Toolsets server.ToolsetConfigs `yaml:"toolsets"`
|
||||
Prompts server.PromptConfigs `yaml:"prompts"`
|
||||
}
|
||||
|
||||
// parseEnv replaces environment variables ${ENV_NAME} with their values.
|
||||
// also support ${ENV_NAME:default_value}.
|
||||
func parseEnv(input string) (string, error) {
|
||||
re := regexp.MustCompile(`\$\{(\w+)(:(\w*))?\}`)
|
||||
re := regexp.MustCompile(`\$\{(\w+)(:([^}]*))?\}`)
|
||||
|
||||
var err error
|
||||
output := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||
@@ -376,7 +386,7 @@ func parseEnv(input string) (string, error) {
|
||||
if value, found := os.LookupEnv(variableName); found {
|
||||
return value
|
||||
}
|
||||
if parts[2] != "" {
|
||||
if len(parts) >= 4 && parts[2] != "" {
|
||||
return parts[3]
|
||||
}
|
||||
err = fmt.Errorf("environment variable not found: %q", variableName)
|
||||
@@ -412,6 +422,7 @@ func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
|
||||
AuthServices: make(server.AuthServiceConfigs),
|
||||
Tools: make(server.ToolConfigs),
|
||||
Toolsets: make(server.ToolsetConfigs),
|
||||
Prompts: make(server.PromptConfigs),
|
||||
}
|
||||
|
||||
var conflicts []string
|
||||
@@ -461,11 +472,20 @@ func mergeToolsFiles(files ...ToolsFile) (ToolsFile, error) {
|
||||
merged.Toolsets[name] = toolset
|
||||
}
|
||||
}
|
||||
|
||||
// Check for conflicts and merge prompts
|
||||
for name, prompt := range file.Prompts {
|
||||
if _, exists := merged.Prompts[name]; exists {
|
||||
conflicts = append(conflicts, fmt.Sprintf("prompt '%s' (file #%d)", name, fileIndex+1))
|
||||
} else {
|
||||
merged.Prompts[name] = prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If conflicts were detected, return an error
|
||||
if len(conflicts) > 0 {
|
||||
return ToolsFile{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, and toolset has a unique name across all files", strings.Join(conflicts, "\n - "))
|
||||
return ToolsFile{}, fmt.Errorf("resource conflicts detected:\n - %s\n\nPlease ensure each source, authService, tool, toolset and prompt has a unique name across all files", strings.Join(conflicts, "\n - "))
|
||||
}
|
||||
|
||||
return merged, nil
|
||||
@@ -539,14 +559,14 @@ func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Ser
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := validateReloadEdits(ctx, toolsFile)
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := validateReloadEdits(ctx, toolsFile)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("unable to validate reloaded edits: %w", err)
|
||||
logger.WarnContext(ctx, errMsg.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
|
||||
s.ResourceMgr.SetResources(sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -554,7 +574,7 @@ func handleDynamicReload(ctx context.Context, toolsFile ToolsFile, s *server.Ser
|
||||
// validateReloadEdits checks that the reloaded tools file configs can initialized without failing
|
||||
func validateReloadEdits(
|
||||
ctx context.Context, toolsFile ToolsFile,
|
||||
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, error,
|
||||
) (map[string]sources.Source, map[string]auth.AuthService, map[string]tools.Tool, map[string]tools.Toolset, map[string]prompts.Prompt, map[string]prompts.Promptset, error,
|
||||
) {
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
@@ -577,16 +597,17 @@ func validateReloadEdits(
|
||||
AuthServiceConfigs: toolsFile.AuthServices,
|
||||
ToolConfigs: toolsFile.Tools,
|
||||
ToolsetConfigs: toolsFile.Toolsets,
|
||||
PromptConfigs: toolsFile.Prompts,
|
||||
}
|
||||
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := server.InitializeConfigs(ctx, reloadedConfig)
|
||||
if err != nil {
|
||||
errMsg := fmt.Errorf("unable to initialize reloaded configs: %w", err)
|
||||
logger.WarnContext(ctx, errMsg.Error())
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
|
||||
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil
|
||||
}
|
||||
|
||||
// watchChanges checks for changes in the provided yaml tools file(s) or folder.
|
||||
@@ -877,7 +898,8 @@ func run(cmd *Command) error {
|
||||
}
|
||||
}
|
||||
|
||||
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets
|
||||
cmd.cfg.SourceConfigs, cmd.cfg.AuthServiceConfigs, cmd.cfg.ToolConfigs, cmd.cfg.ToolsetConfigs, cmd.cfg.PromptConfigs = toolsFile.Sources, toolsFile.AuthServices, toolsFile.Tools, toolsFile.Toolsets, toolsFile.Prompts
|
||||
|
||||
authSourceConfigs := toolsFile.AuthSources
|
||||
if authSourceConfigs != nil {
|
||||
cmd.logger.WarnContext(ctx, "`authSources` is deprecated, use `authServices` instead")
|
||||
|
||||
210
cmd/root_test.go
210
cmd/root_test.go
@@ -34,6 +34,8 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/auth/google"
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts/custom"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
cloudsqlpgsrc "github.com/googleapis/genai-toolbox/internal/sources/cloudsqlpg"
|
||||
httpsrc "github.com/googleapis/genai-toolbox/internal/sources/http"
|
||||
@@ -43,6 +45,7 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/http"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools/postgres/postgressql"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -513,8 +516,8 @@ func TestParseToolFile(t *testing.T) {
|
||||
Source: "my-pg-instance",
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("country", "some description"),
|
||||
Parameters: []parameters.Parameter{
|
||||
parameters.NewStringParameter("country", "some description"),
|
||||
},
|
||||
AuthRequired: []string{},
|
||||
},
|
||||
@@ -525,6 +528,38 @@ func TestParseToolFile(t *testing.T) {
|
||||
ToolNames: []string{"example_tool"},
|
||||
},
|
||||
},
|
||||
Prompts: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "with prompts example",
|
||||
in: `
|
||||
prompts:
|
||||
my-prompt:
|
||||
description: A prompt template for data analysis.
|
||||
arguments:
|
||||
- name: country
|
||||
description: The country to analyze.
|
||||
messages:
|
||||
- content: Analyze the data for {{.country}}.
|
||||
`,
|
||||
wantToolsFile: ToolsFile{
|
||||
Sources: nil,
|
||||
AuthServices: nil,
|
||||
Tools: nil,
|
||||
Toolsets: nil,
|
||||
Prompts: server.PromptConfigs{
|
||||
"my-prompt": &custom.Config{
|
||||
Name: "my-prompt",
|
||||
Description: "A prompt template for data analysis.",
|
||||
Arguments: prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("country", "The country to analyze.")},
|
||||
},
|
||||
Messages: []prompts.Message{
|
||||
{Role: "user", Content: "Analyze the data for {{.country}}."},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -544,7 +579,10 @@ func TestParseToolFile(t *testing.T) {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
t.Fatalf("incorrect toolsets parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
|
||||
t.Fatalf("incorrect prompts parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -645,10 +683,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
AuthRequired: []string{},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("country", "some description"),
|
||||
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
Parameters: []parameters.Parameter{
|
||||
parameters.NewStringParameter("country", "some description"),
|
||||
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -658,6 +696,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
ToolNames: []string{"example_tool"},
|
||||
},
|
||||
},
|
||||
Prompts: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -744,10 +783,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
AuthRequired: []string{},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("country", "some description"),
|
||||
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
Parameters: []parameters.Parameter{
|
||||
parameters.NewStringParameter("country", "some description"),
|
||||
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -757,6 +796,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
ToolNames: []string{"example_tool"},
|
||||
},
|
||||
},
|
||||
Prompts: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -845,10 +885,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
Description: "some description",
|
||||
Statement: "SELECT * FROM SQL_STATEMENT;\n",
|
||||
AuthRequired: []string{"my-google-service"},
|
||||
Parameters: []tools.Parameter{
|
||||
tools.NewStringParameter("country", "some description"),
|
||||
tools.NewIntParameterWithAuth("id", "user id", []tools.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
tools.NewStringParameterWithAuth("email", "user email", []tools.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
Parameters: []parameters.Parameter{
|
||||
parameters.NewStringParameter("country", "some description"),
|
||||
parameters.NewIntParameterWithAuth("id", "user id", []parameters.ParamAuthService{{Name: "my-google-service", Field: "user_id"}}),
|
||||
parameters.NewStringParameterWithAuth("email", "user email", []parameters.ParamAuthService{{Name: "my-google-service", Field: "email"}, {Name: "other-google-service", Field: "other_email"}}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -858,6 +898,7 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
ToolNames: []string{"example_tool"},
|
||||
},
|
||||
},
|
||||
Prompts: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -877,7 +918,10 @@ func TestParseToolFileWithAuth(t *testing.T) {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
t.Fatalf("incorrect toolsets parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
|
||||
t.Fatalf("incorrect prompts parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -894,6 +938,8 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
t.Setenv("cat_string", "cat")
|
||||
t.Setenv("food_string", "food")
|
||||
t.Setenv("TestHeader", "ACTUAL_HEADER")
|
||||
t.Setenv("prompt_name", "ACTUAL_PROMPT_NAME")
|
||||
t.Setenv("prompt_content", "ACTUAL_CONTENT")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
@@ -967,6 +1013,14 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
toolsets:
|
||||
${toolset_name}:
|
||||
- example_tool
|
||||
|
||||
|
||||
prompts:
|
||||
${prompt_name}:
|
||||
description: A test prompt for {{.name}}.
|
||||
messages:
|
||||
- role: user
|
||||
content: ${prompt_content}
|
||||
`,
|
||||
wantToolsFile: ToolsFile{
|
||||
Sources: server.SourceConfigs{
|
||||
@@ -1000,9 +1054,9 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
Path: "search?name=alice&pet=cat",
|
||||
Description: "some description",
|
||||
AuthRequired: []string{"my-google-auth-service", "other-auth-service"},
|
||||
QueryParams: []tools.Parameter{
|
||||
tools.NewStringParameterWithAuth("country", "some description",
|
||||
[]tools.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"},
|
||||
QueryParams: []parameters.Parameter{
|
||||
parameters.NewStringParameterWithAuth("country", "some description",
|
||||
[]parameters.ParamAuthService{{Name: "my-google-auth-service", Field: "user_id"},
|
||||
{Name: "other-auth-service", Field: "user_id"}}),
|
||||
},
|
||||
RequestBody: `{
|
||||
@@ -1012,9 +1066,9 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
"other": "$OTHER"
|
||||
}
|
||||
`,
|
||||
BodyParams: []tools.Parameter{tools.NewIntParameter("age", "age num"), tools.NewStringParameter("city", "city string")},
|
||||
BodyParams: []parameters.Parameter{parameters.NewIntParameter("age", "age num"), parameters.NewStringParameter("city", "city string")},
|
||||
Headers: map[string]string{"Authorization": "API_KEY", "Content-Type": "application/json"},
|
||||
HeaderParams: []tools.Parameter{tools.NewStringParameter("Language", "language string")},
|
||||
HeaderParams: []parameters.Parameter{parameters.NewStringParameter("Language", "language string")},
|
||||
},
|
||||
},
|
||||
Toolsets: server.ToolsetConfigs{
|
||||
@@ -1023,6 +1077,19 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
ToolNames: []string{"example_tool"},
|
||||
},
|
||||
},
|
||||
Prompts: server.PromptConfigs{
|
||||
"ACTUAL_PROMPT_NAME": &custom.Config{
|
||||
Name: "ACTUAL_PROMPT_NAME",
|
||||
Description: "A test prompt for {{.name}}.",
|
||||
Messages: []prompts.Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: "ACTUAL_CONTENT",
|
||||
},
|
||||
},
|
||||
Arguments: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1042,11 +1109,13 @@ func TestEnvVarReplacement(t *testing.T) {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Toolsets, toolsFile.Toolsets); diff != "" {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
t.Fatalf("incorrect toolsets parse: diff %v", diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.wantToolsFile.Prompts, toolsFile.Prompts); diff != "" {
|
||||
t.Fatalf("incorrect prompts parse: diff %v", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// normalizeFilepaths is a helper function to allow same filepath formats for Mac and Windows.
|
||||
@@ -1409,7 +1478,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"alloydb_postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "alloydb_postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1439,7 +1508,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"cloud_sql_postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "cloud_sql_postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1539,7 +1608,7 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
wantToolset: server.ToolsetConfigs{
|
||||
"postgres_database_tools": tools.ToolsetConfig{
|
||||
Name: "postgres_database_tools",
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas"},
|
||||
ToolNames: []string{"execute_sql", "list_tables", "list_active_queries", "list_available_extensions", "list_installed_extensions", "list_autovacuum_configurations", "list_memory_configurations", "list_top_bloated_tables", "list_replication_slots", "list_invalid_indexes", "get_query_plan", "list_views", "list_schemas", "database_overview", "list_triggers", "list_indexes", "list_sequences"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1662,6 +1731,10 @@ func TestPrebuiltTools(t *testing.T) {
|
||||
if diff := cmp.Diff(tc.wantToolset, toolsFile.Toolsets); diff != "" {
|
||||
t.Fatalf("incorrect tools parse: diff %v", diff)
|
||||
}
|
||||
// Prebuilt configs do not have prompts, so assert empty maps.
|
||||
if len(toolsFile.Prompts) != 0 {
|
||||
t.Fatalf("expected empty prompts map for prebuilt config, got: %v", toolsFile.Prompts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1734,3 +1807,88 @@ func TestFileLoadingErrors(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMergeToolsFiles(t *testing.T) {
|
||||
file1 := ToolsFile{
|
||||
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
|
||||
Tools: server.ToolConfigs{"tool1": http.Config{Name: "tool1"}},
|
||||
Toolsets: server.ToolsetConfigs{"set1": tools.ToolsetConfig{Name: "set1"}},
|
||||
}
|
||||
file2 := ToolsFile{
|
||||
AuthServices: server.AuthServiceConfigs{"auth1": google.Config{Name: "auth1"}},
|
||||
Tools: server.ToolConfigs{"tool2": http.Config{Name: "tool2"}},
|
||||
Toolsets: server.ToolsetConfigs{"set2": tools.ToolsetConfig{Name: "set2"}},
|
||||
}
|
||||
fileWithConflicts := ToolsFile{
|
||||
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
|
||||
Tools: server.ToolConfigs{"tool2": http.Config{Name: "tool2"}},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
files []ToolsFile
|
||||
want ToolsFile
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "merge two distinct files",
|
||||
files: []ToolsFile{file1, file2},
|
||||
want: ToolsFile{
|
||||
Sources: server.SourceConfigs{"source1": httpsrc.Config{Name: "source1"}},
|
||||
AuthServices: server.AuthServiceConfigs{"auth1": google.Config{Name: "auth1"}},
|
||||
Tools: server.ToolConfigs{"tool1": http.Config{Name: "tool1"}, "tool2": http.Config{Name: "tool2"}},
|
||||
Toolsets: server.ToolsetConfigs{"set1": tools.ToolsetConfig{Name: "set1"}, "set2": tools.ToolsetConfig{Name: "set2"}},
|
||||
Prompts: server.PromptConfigs{},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "merge with conflicts",
|
||||
files: []ToolsFile{file1, file2, fileWithConflicts},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "merge single file",
|
||||
files: []ToolsFile{file1},
|
||||
want: ToolsFile{
|
||||
Sources: file1.Sources,
|
||||
AuthServices: make(server.AuthServiceConfigs),
|
||||
Tools: file1.Tools,
|
||||
Toolsets: file1.Toolsets,
|
||||
Prompts: server.PromptConfigs{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge empty list",
|
||||
files: []ToolsFile{},
|
||||
want: ToolsFile{
|
||||
Sources: make(server.SourceConfigs),
|
||||
AuthServices: make(server.AuthServiceConfigs),
|
||||
Tools: make(server.ToolConfigs),
|
||||
Toolsets: make(server.ToolsetConfigs),
|
||||
Prompts: server.PromptConfigs{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := mergeToolsFiles(tc.files...)
|
||||
if (err != nil) != tc.wantErr {
|
||||
t.Fatalf("mergeToolsFiles() error = %v, wantErr %v", err, tc.wantErr)
|
||||
}
|
||||
if !tc.wantErr {
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("mergeToolsFiles() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for conflicting files but got none")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "resource conflicts detected") {
|
||||
t.Errorf("expected conflict error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.19.1
|
||||
0.20.0
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.19.1\" # x-release-please-version\n",
|
||||
"version = \"0.20.0\" # x-release-please-version\n",
|
||||
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
|
||||
"\n",
|
||||
"# Make the binary executable\n",
|
||||
|
||||
@@ -96,3 +96,21 @@ all_tools = client.load_toolset()
|
||||
# This will only load the tools listed in 'my_second_toolset'
|
||||
my_second_toolset = client.load_toolset("my_second_toolset")
|
||||
```
|
||||
|
||||
### Prompts
|
||||
|
||||
The `prompts` section of your `tools.yaml` defines the templates containing structured messages and instructions for interacting with language models.
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
code_review:
|
||||
description: "Asks the LLM to analyze code quality and suggest improvements."
|
||||
messages:
|
||||
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
|
||||
arguments:
|
||||
- name: "code"
|
||||
description: "The code to review"
|
||||
```
|
||||
|
||||
For more details on configuring different types of prompts, see the
|
||||
[Prompts](../resources/prompts/).
|
||||
|
||||
@@ -86,7 +86,7 @@ following instructions for your OS and CPU architecture.
|
||||
To install Toolbox as a binary on Linux (AMD64):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.19.1
|
||||
export VERSION=0.20.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/linux/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
@@ -95,7 +95,7 @@ chmod +x toolbox
|
||||
To install Toolbox as a binary on macOS (Apple Silicon):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.19.1
|
||||
export VERSION=0.20.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/arm64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
@@ -104,7 +104,7 @@ chmod +x toolbox
|
||||
To install Toolbox as a binary on macOS (Intel):
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.19.1
|
||||
export VERSION=0.20.0
|
||||
curl -L -o toolbox https://storage.googleapis.com/genai-toolbox/v$VERSION/darwin/amd64/toolbox
|
||||
chmod +x toolbox
|
||||
```
|
||||
@@ -113,7 +113,7 @@ chmod +x toolbox
|
||||
To install Toolbox as a binary on Windows (AMD64):
|
||||
```powershell
|
||||
# see releases page for other versions
|
||||
$VERSION = "0.19.1"
|
||||
$VERSION = "0.20.0"
|
||||
Invoke-WebRequest -Uri "https://storage.googleapis.com/genai-toolbox/v$VERSION/windows/amd64/toolbox.exe" -OutFile "toolbox.exe"
|
||||
```
|
||||
{{% /tab %}}
|
||||
@@ -124,7 +124,7 @@ You can also install Toolbox as a container:
|
||||
|
||||
```sh
|
||||
# see releases page for other versions
|
||||
export VERSION=0.19.1
|
||||
export VERSION=0.20.0
|
||||
docker pull us-central1-docker.pkg.dev/database-toolbox/toolbox/toolbox:$VERSION
|
||||
```
|
||||
|
||||
@@ -143,7 +143,7 @@ To install from source, ensure you have the latest version of
|
||||
[Go installed](https://go.dev/doc/install), and then run the following command:
|
||||
|
||||
```sh
|
||||
go install github.com/googleapis/genai-toolbox@v0.19.1
|
||||
go install github.com/googleapis/genai-toolbox@v0.20.0
|
||||
```
|
||||
|
||||
{{% /tab %}}
|
||||
|
||||
@@ -105,7 +105,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/googleapis/mcp-toolbox-sdk-go v0.4.0
|
||||
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6
|
||||
google.golang.org/genai v1.34.0
|
||||
google.golang.org/adk v0.1.0
|
||||
google.golang.org/genai v1.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -104,12 +104,12 @@ golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6 h1:LiCwUK/a39m3ZJYOBfJX0WAZLaHZjgU0DsJJsekPxLU=
|
||||
google.golang.org/adk v0.0.0-20251105212711-ccd61aa4a1b6/go.mod h1:NvtSLoNx7UzZIiUAI1KoJQLMmt9sG3oCgiCx1TLqKFw=
|
||||
google.golang.org/adk v0.1.0 h1:+w/fHuqRVolotOATlujRA+2DKUuDrFH2poRdEX2QjB8=
|
||||
google.golang.org/adk v0.1.0/go.mod h1:NvtSLoNx7UzZIiUAI1KoJQLMmt9sG3oCgiCx1TLqKFw=
|
||||
google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4=
|
||||
google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8=
|
||||
google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY=
|
||||
google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
|
||||
google.golang.org/genai v1.35.0 h1:Jo6g25CzVqFzGrX5mhWyBgQqXAUzxcx5jeK7U74zv9c=
|
||||
google.golang.org/genai v1.35.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f h1:vLd1CJuJOUgV6qijD7KT5Y2ZtC97ll4dxjTUappMnbo=
|
||||
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f/go.mod h1:PI3KrSadr00yqfv6UDvgZGFsmLqeRIwt8x4p5Oo7CdM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0=
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.24.6
|
||||
|
||||
require (
|
||||
github.com/googleapis/mcp-toolbox-sdk-go v0.4.0
|
||||
google.golang.org/genai v1.34.0
|
||||
google.golang.org/genai v1.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -102,8 +102,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4=
|
||||
google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8=
|
||||
google.golang.org/genai v1.34.0 h1:lPRJRO+HqRX1SwFo1Xb/22nZ5MBEPUbXDl61OoDxlbY=
|
||||
google.golang.org/genai v1.34.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
|
||||
google.golang.org/genai v1.35.0 h1:Jo6g25CzVqFzGrX5mhWyBgQqXAUzxcx5jeK7U74zv9c=
|
||||
google.golang.org/genai v1.35.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
|
||||
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f h1:vLd1CJuJOUgV6qijD7KT5Y2ZtC97ll4dxjTUappMnbo=
|
||||
google.golang.org/genproto v0.0.0-20251014184007-4626949a642f/go.mod h1:PI3KrSadr00yqfv6UDvgZGFsmLqeRIwt8x4p5Oo7CdM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0=
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
google-adk==1.18.0
|
||||
toolbox-core==0.5.2
|
||||
pytest==8.4.2
|
||||
pytest==9.0.1
|
||||
@@ -1,3 +1,3 @@
|
||||
google-genai==1.47.0
|
||||
toolbox-core==0.5.2
|
||||
pytest==8.4.2
|
||||
pytest==9.0.1
|
||||
|
||||
@@ -2,4 +2,4 @@ langchain==0.3.27
|
||||
langchain-google-vertexai==2.1.2
|
||||
langgraph==1.0.1
|
||||
toolbox-langchain==0.5.2
|
||||
pytest==8.4.2
|
||||
pytest==9.0.1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
llama-index==0.14.6
|
||||
llama-index-llms-google-genai==0.7.1
|
||||
llama-index-llms-google-genai==0.7.3
|
||||
toolbox-llamaindex==0.5.2
|
||||
pytest==8.4.2
|
||||
pytest==9.0.1
|
||||
|
||||
@@ -13,7 +13,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ description: >
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<link rel="canonical" href="https://cloud.google.com/alloydb/docs/quickstart/create-and-connect"/>
|
||||
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/alloydb/docs/quickstart/create-and-connect"/>
|
||||
<link rel="canonical" href="https://cloud.google.com/alloydb/docs/connect-ide-using-mcp-toolbox"/>
|
||||
<meta http-equiv="refresh" content="0;url=https://cloud.google.com/alloydb/docs/connect-ide-using-mcp-toolbox"/>
|
||||
</head>
|
||||
</html>
|
||||
|
||||
@@ -48,19 +48,19 @@ to expose your developer assistant tools to a Looker instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -45,19 +45,19 @@ instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -43,19 +43,19 @@ expose your developer assistant tools to a MySQL instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -44,19 +44,19 @@ expose your developer assistant tools to a Neo4j instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -56,19 +56,19 @@ Omni](https://cloud.google.com/alloydb/omni/current/docs/overview).
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -43,19 +43,19 @@ to expose your developer assistant tools to a SQLite instance:
|
||||
<!-- {x-release-please-start-version} -->
|
||||
{{< tabpane persist=header >}}
|
||||
{{< tab header="linux/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/linux/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/linux/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/arm64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/arm64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/arm64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="darwin/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/darwin/amd64/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/darwin/amd64/toolbox
|
||||
{{< /tab >}}
|
||||
|
||||
{{< tab header="windows/amd64" lang="bash" >}}
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/windows/amd64/toolbox.exe
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/windows/amd64/toolbox.exe
|
||||
{{< /tab >}}
|
||||
{{< /tabpane >}}
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -46,6 +46,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
* `list_schemas`: Lists schemas in the database.
|
||||
* `database_overview`: Fetches the current state of the PostgreSQL server.
|
||||
* `list_triggers`: Lists triggers in the database.
|
||||
* `list_indexes`: List available user indexes in a PostgreSQL database.
|
||||
* `list_sequences`: List sequences in a PostgreSQL database.
|
||||
|
||||
## AlloyDB Postgres Admin
|
||||
|
||||
@@ -216,6 +220,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
* `list_schemas`: Lists schemas in the database.
|
||||
* `database_overview`: Fetches the current state of the PostgreSQL server.
|
||||
* `list_triggers`: Lists triggers in the database.
|
||||
* `list_indexes`: List available user indexes in a PostgreSQL database.
|
||||
* `list_sequences`: List sequences in a PostgreSQL database.
|
||||
|
||||
## Cloud SQL for PostgreSQL Observability
|
||||
|
||||
@@ -489,8 +497,8 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
|
||||
* `--prebuilt` value: `postgres`
|
||||
* **Environment Variables:**
|
||||
* `POSTGRES_HOST`: The hostname or IP address of the PostgreSQL server.
|
||||
* `POSTGRES_PORT`: The port number for the PostgreSQL server.
|
||||
* `POSTGRES_HOST`: (Optional) The hostname or IP address of the PostgreSQL server.
|
||||
* `POSTGRES_PORT`: (Optional) The port number for the PostgreSQL server.
|
||||
* `POSTGRES_DATABASE`: The name of the database to connect to.
|
||||
* `POSTGRES_USER`: The database username.
|
||||
* `POSTGRES_PASSWORD`: The password for the database user.
|
||||
@@ -513,6 +521,10 @@ details on how to connect your AI tools (IDEs) to databases via Toolbox and MCP.
|
||||
* `list_views`: Lists views in the database from pg_views with a default
|
||||
limit of 50 rows. Returns schemaname, viewname and the ownername.
|
||||
* `list_schemas`: Lists schemas in the database.
|
||||
* `database_overview`: Fetches the current state of the PostgreSQL server.
|
||||
* `list_triggers`: Lists triggers in the database.
|
||||
* `list_indexes`: List available user indexes in a PostgreSQL database.
|
||||
* `list_sequences`: List sequences in a PostgreSQL database.
|
||||
|
||||
## Google Cloud Serverless for Apache Spark
|
||||
|
||||
|
||||
70
docs/en/resources/prompts/_index.md
Normal file
70
docs/en/resources/prompts/_index.md
Normal file
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: "Prompts"
|
||||
type: docs
|
||||
weight: 3
|
||||
description: >
|
||||
Prompts allow servers to provide structured messages and instructions for interacting with language models.
|
||||
---
|
||||
|
||||
A `prompt` represents a reusable prompt template that can be retrieved and used
|
||||
by MCP clients.
|
||||
|
||||
A Prompt is essentially a template for a message or a series of messages that can be sent to a Large Language Model (LLM). The Toolbox server implements the `prompts/list` and `prompts/get` methods from the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro) specification, allowing clients to discover and retrieve these prompts.
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
code_review:
|
||||
description: "Asks the LLM to analyze code quality and suggest improvements."
|
||||
messages:
|
||||
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
|
||||
arguments:
|
||||
- name: "code"
|
||||
description: "The code to review"
|
||||
```
|
||||
|
||||
## Prompt Schema
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| --- | --- | --- | --- |
|
||||
| description | string | No | A brief explanation of what the prompt does. |
|
||||
| kind | string | No | The kind of prompt. Defaults to `"custom"`. |
|
||||
| messages | [][Message](#message-schema) | Yes | A list of one or more message objects that make up the prompt's content. |
|
||||
| arguments | [][Argument](#argument-schema) | No | A list of arguments that can be interpolated into the prompt's content.|
|
||||
|
||||
## Message Schema
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| --- | --- | --- | --- |
|
||||
| role | string | No | The role of the sender. Can be `"user"` or `"assistant"`. Defaults to `"user"`. |
|
||||
| content | string | Yes | The text of the message. You can include placeholders for arguments using `{{.argument_name}}` syntax. |
|
||||
|
||||
## Argument Schema
|
||||
|
||||
An argument can be any [Parameter](../tools/_index.md#specifying-parameters)
|
||||
type. If the `type` field is not specified, it will default to `string`.
|
||||
|
||||
## Usage with Gemini CLI
|
||||
|
||||
Prompts defined in your `tools.yaml` can be seamlessly integrated with the Gemini CLI to create [custom slash commands](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#mcp-prompts-as-slash-commands). The workflow is as follows:
|
||||
|
||||
1. **Discovery:** When the Gemini CLI connects to your Toolbox server, it automatically calls `prompts/list` to discover all available prompts.
|
||||
|
||||
2. **Conversion:** Each discovered prompt is converted into a corresponding slash command. For example, a prompt named `code_review` becomes the `/code_review` command in the CLI.
|
||||
|
||||
3. **Execution:** You can execute the command as follows:
|
||||
|
||||
```bash
|
||||
/code_review --code="def hello():\n print('world')"
|
||||
```
|
||||
|
||||
4. **Interpolation:** Once all arguments are collected, the CLI calls prompts/get
|
||||
with your provided values to retrieve the final, interpolated prompt.
|
||||
Eg.
|
||||
|
||||
```bash
|
||||
Please review the following code for quality, correctness, and potential improvements: \ndef hello():\n print('world')
|
||||
```
|
||||
|
||||
5. **Response:** This completed prompt is then sent to the Gemini model, and the model's response is displayed back to you in the CLI.
|
||||
|
||||
## Kinds of prompts
|
||||
69
docs/en/resources/prompts/custom/_index.md
Normal file
69
docs/en/resources/prompts/custom/_index.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: "Custom"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
Custom prompts defined by the user.
|
||||
---
|
||||
|
||||
Custom prompts are defined by the user to be exposed through their MCP server.
|
||||
They are the default type for prompts.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Prompt
|
||||
|
||||
Here is an example of a simple prompt that takes a single argument, code, and
|
||||
asks an LLM to review it.
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
code_review:
|
||||
description: "Asks the LLM to analyze code quality and suggest improvements."
|
||||
messages:
|
||||
- content: "Please review the following code for quality, correctness, and potential improvements: \n\n{{.code}}"
|
||||
arguments:
|
||||
- name: "code"
|
||||
description: "The code to review"
|
||||
```
|
||||
|
||||
### Multi-message prompt
|
||||
|
||||
You can define prompts with multiple messages to set up more complex
|
||||
conversational contexts, like a role-playing scenario.
|
||||
|
||||
```yaml
|
||||
prompts:
|
||||
roleplay_scenario:
|
||||
description: "Sets up a roleplaying scenario with initial messages."
|
||||
arguments:
|
||||
- name: "character"
|
||||
description: "The character the AI should embody."
|
||||
- name: "situation"
|
||||
description: "The initial situation for the roleplay."
|
||||
messages:
|
||||
- role: "user"
|
||||
content: "Let's roleplay. You are {{.character}}. The situation is: {{.situation}}"
|
||||
- role: "assistant"
|
||||
content: "Okay, I understand. I am ready. What happens next?"
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Prompt Schema
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
| --- | --- | --- | --- |
|
||||
| kind | string | No | The kind of prompt. Must be `"custom"`. |
|
||||
| description | string | No | A brief explanation of what the prompt does. |
|
||||
| messages | [][Message](#message-schema) | Yes | A list of one or more message objects that make up the prompt's content. |
|
||||
| arguments | [][Argument](#argument-schema) | No | A list of arguments that can be interpolated into the prompt's content.|
|
||||
|
||||
### Message Schema
|
||||
|
||||
Refer to the default prompt [Message Schema](../_index.md#message-schema).
|
||||
|
||||
### Argument Schema
|
||||
|
||||
Refer to the default prompt [Argument Schema](../_index.md#argument-schema).
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
---
|
||||
title: "AlloyDB Admin"
|
||||
linkTitle: "AlloyDB Admin"
|
||||
title: AlloyDB Admin
|
||||
linkTitle: AlloyDB Admin
|
||||
type: docs
|
||||
weight: 2
|
||||
description: >
|
||||
The "alloydb-admin" source provides a client for the AlloyDB API.
|
||||
aliases:
|
||||
- /resources/sources/alloydb-admin
|
||||
weight: 1
|
||||
description: "The \"alloydb-admin\" source provides a client for the AlloyDB API.\n"
|
||||
aliases: [/resources/sources/alloydb-admin]
|
||||
---
|
||||
|
||||
## About
|
||||
@@ -17,6 +15,7 @@ tools to perform administrative tasks on AlloyDB resources, such as managing
|
||||
clusters, instances, and users.
|
||||
|
||||
Authentication can be handled in two ways:
|
||||
|
||||
1. **Application Default Credentials (ADC):** By default, the source uses ADC
|
||||
to authenticate with the API.
|
||||
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will
|
||||
@@ -36,7 +35,9 @@ sources:
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|----------------|:--------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| -------------- | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| kind | string | true | Must be "alloydb-admin". |
|
||||
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
|
||||
| defaultProject | string | false | The Google Cloud project ID to use for AlloyDB infrastructure tools. |
|
||||
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
|
||||
|
||||
@@ -51,6 +51,18 @@ cluster][alloydb-free-trial].
|
||||
- [`postgres-list-schemas`](../tools/postgres/postgres-list-schemas.md)
|
||||
List schemas in an AlloyDB for PostgreSQL database.
|
||||
|
||||
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
|
||||
Fetches the current state of the PostgreSQL server.
|
||||
|
||||
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
|
||||
List triggers in an AlloyDB for PostgreSQL database.
|
||||
|
||||
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
|
||||
List available user indexes in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
|
||||
List sequences in a PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [AlloyDB using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/alloydb_pg_mcp/)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
---
|
||||
title: "Cloud SQL Admin"
|
||||
title: Cloud SQL Admin
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
A "cloud-sql-admin" source provides a client for the Cloud SQL Admin API.
|
||||
aliases:
|
||||
- /resources/sources/cloud-sql-admin
|
||||
description: "A \"cloud-sql-admin\" source provides a client for the Cloud SQL Admin API.\n"
|
||||
aliases: [/resources/sources/cloud-sql-admin]
|
||||
---
|
||||
|
||||
## About
|
||||
@@ -16,6 +14,7 @@ allows tools to perform administrative tasks on Cloud SQL instances, such as
|
||||
creating users and databases.
|
||||
|
||||
Authentication can be handled in two ways:
|
||||
|
||||
1. **Application Default Credentials (ADC):** By default, the source uses ADC
|
||||
to authenticate with the API.
|
||||
2. **Client-side OAuth:** If `useClientOAuth` is set to `true`, the source will
|
||||
@@ -37,6 +36,7 @@ sources:
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|----------------|:--------:|:------------:|------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| -------------- | :------: | :----------: | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| kind | string | true | Must be "cloud-sql-admin". |
|
||||
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
|
||||
| defaultProject | string | false | The Google Cloud project ID to use for Cloud SQL infrastructure tools. |
|
||||
| useClientOAuth | boolean | false | If true, the source will use client-side OAuth for authorization. Otherwise, it will use Application Default Credentials. Defaults to `false`. |
|
||||
|
||||
@@ -47,6 +47,18 @@ to a database by following these instructions][csql-pg-quickstart].
|
||||
- [`postgres-list-schemas`](../tools/postgres/postgres-list-schemas.md)
|
||||
List schemas in a PostgreSQL database.
|
||||
|
||||
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
|
||||
Fetches the current state of the PostgreSQL server.
|
||||
|
||||
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
|
||||
List triggers in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
|
||||
List available user indexes in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
|
||||
List sequences in a PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [Cloud SQL for Postgres using
|
||||
|
||||
@@ -41,6 +41,18 @@ reputation for reliability, feature robustness, and performance.
|
||||
- [`postgres-list-schemas`](../tools/postgres/postgres-list-views.md)
|
||||
List schemas in a PostgreSQL database.
|
||||
|
||||
- [`postgres-database-overview`](../tools/postgres/postgres-database-overview.md)
|
||||
Fetches the current state of the PostgreSQL server.
|
||||
|
||||
- [`postgres-list-triggers`](../tools/postgres/postgres-list-triggers.md)
|
||||
List triggers in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-indexes`](../tools/postgres/postgres-list-indexes.md)
|
||||
List available user indexes in a PostgreSQL database.
|
||||
|
||||
- [`postgres-list-sequences`](../tools/postgres/postgres-list-sequences.md)
|
||||
List sequences in a PostgreSQL database.
|
||||
|
||||
### Pre-built Configurations
|
||||
|
||||
- [PostgreSQL using MCP](https://googleapis.github.io/genai-toolbox/how-to/connect-ide/postgres_mcp/)
|
||||
|
||||
47
docs/en/resources/tools/looker/looker-generate-embed-url.md
Normal file
47
docs/en/resources/tools/looker/looker-generate-embed-url.md
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: "looker-generate-embed-url"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
"looker-generate-embed-url" generates an embeddable URL for Looker content.
|
||||
aliases:
|
||||
- /resources/tools/looker-generate-embed-url
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `looker-generate-embed-url` tool generates an embeddable URL for a given piece of Looker content. The url generated is created for the user authenticated to the Looker source. When opened in the browser it will create a Looker Embed session.
|
||||
|
||||
It's compatible with the following sources:
|
||||
|
||||
- [looker](../../sources/looker.md)
|
||||
|
||||
`looker-generate-embed-url` takes two parameters:
|
||||
|
||||
1. the `type` of content (e.g., "dashboards", "looks", "query-visualization")
|
||||
2. the `id` of the content
|
||||
|
||||
It's recommended to use other tools from the Looker MCP toolbox with this tool to do things like fetch dashboard id's, generate a query, etc that can be supplied to this tool.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
generate_embed_url:
|
||||
kind: looker-generate-embed-url
|
||||
source: looker-source
|
||||
description: |
|
||||
generate_embed_url Tool
|
||||
|
||||
This tool generates an embeddable URL for Looker content.
|
||||
You need to provide the type of content (e.g., 'dashboards', 'looks', 'query-visualization')
|
||||
and the ID of the content.
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:------------:|----------------------------------------------------|
|
||||
| kind | string | true | Must be "looker-generate-embed-url" |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | true | Description of the tool that is passed to the LLM. |
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: "postgres-database-overview"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-database-overview" fetches the current state of the PostgreSQL server.
|
||||
aliases:
|
||||
- /resources/tools/postgres-database-overview
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-database-overview` fetches the current state of the PostgreSQL server. It's compatible with any of the following sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
`postgres-database-overview` fetches the current state of the PostgreSQL server This tool does not take any input parameters.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
database_overview:
|
||||
kind: postgres-database-overview
|
||||
source: cloudsql-pg-source
|
||||
description: |
|
||||
fetches the current state of the PostgreSQL server. It returns the postgres version, whether it's a replica, uptime duration, maximum connection limit, number of current connections, number of active connections and the percentage of connections in use.
|
||||
```
|
||||
|
||||
The response is a JSON object with the following elements:
|
||||
```json
|
||||
{
|
||||
"pg_version": "PostgreSQL server version string",
|
||||
"is_replica": "boolean indicating if the instance is in recovery mode",
|
||||
"uptime": "interval string representing the total server uptime",
|
||||
"max_connections": "integer maximum number of allowed connections",
|
||||
"current_connections": "integer number of current connections",
|
||||
"active_connections": "integer number of currently active connections",
|
||||
"pct_connections_used": "float percentage of max_connections currently in use"
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:-------------:|------------------------------------------------------|
|
||||
| kind | string | true | Must be "postgres-database-overview". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | false | Description of the tool that is passed to the agent. |
|
||||
67
docs/en/resources/tools/postgres/postgres-list-indexes.md
Normal file
67
docs/en/resources/tools/postgres/postgres-list-indexes.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: "postgres-list-indexes"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-list-indexes" tool lists indexes in a Postgres database.
|
||||
aliases:
|
||||
- /resources/tools/postgres-list-indexes
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-list-indexes` tool lists available user indexes in the database excluding those in `pg_catalog` and `information_schema`. It's compatible with any of the following sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
`postgres-list-indexes` lists detailed information as JSON for indexes. The tool takes the following input parameters:
|
||||
|
||||
- `table_name` (optional): A text to filter results by table name. The input is used within a LIKE clause. Default: `""`
|
||||
- `index_name` (optional): A text to filter results by index name. The input is used within a LIKE clause. Default: `""`
|
||||
- `schema_name` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
|
||||
- `limit` (optional): The maximum number of rows to return. Default: `50`.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_indexes:
|
||||
kind: postgres-list-indexes
|
||||
source: postgres-source
|
||||
description: |
|
||||
Lists available user indexes in the database, excluding system schemas (pg_catalog,
|
||||
information_schema). For each index, the following properties are returned:
|
||||
schema name, table name, index name, index type (access method), a boolean
|
||||
indicating if it's a unique index, a boolean indicating if it's for a primary key,
|
||||
the index definition, index size in bytes, the number of index scans, the number of
|
||||
index tuples read, the number of table tuples fetched via index scans, and a boolean
|
||||
indicating if the index has been used at least once.
|
||||
```
|
||||
|
||||
The response is a json array with the following elements:
|
||||
```json
|
||||
{
|
||||
"schema_name": "schema name",
|
||||
"table_name": "table name",
|
||||
"index_name": "index name",
|
||||
"index_type": "index access method (e.g btree, hash, gin)",
|
||||
"is_unique": "boolean indicating if the index is unique",
|
||||
"is_primary": "boolean indicating if the index is for a primary key",
|
||||
"index_definition": "index definition statement",
|
||||
"index_size_bytes": "index size in bytes",
|
||||
"index_scans": "Number of index scans initiated on this index",
|
||||
"tuples_read": "Number of index entries returned by scans on this index",
|
||||
"tuples_fetched": "Number of live table rows fetched by simple index scans using this index",
|
||||
"is_used": "boolean indicating if the index has been scanned at least once"
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:-------------:|------------------------------------------------------|
|
||||
| kind | string | true | Must be "postgres-list-indexes". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | false | Description of the tool that is passed to the agent. |
|
||||
61
docs/en/resources/tools/postgres/postgres-list-sequences.md
Normal file
61
docs/en/resources/tools/postgres/postgres-list-sequences.md
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: "postgres-list-sequences"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-list-sequences" tool lists sequences in a Postgres database.
|
||||
aliases:
|
||||
- /resources/tools/postgres-list-sequences
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-list-sequences` tool retrieves information about sequences in a Postgres database. It's compatible with any of the following sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
`postgres-list-sequences` lists detailed information as JSON for all sequences. The tool takes the following input parameters:
|
||||
|
||||
- `sequencename` (optional): A text to filter results by sequence name. The input is used within a LIKE clause. Default: `""`
|
||||
- `schemaname` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
|
||||
- `limit` (optional): The maximum number of rows to return. Default: `50`.
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
tools:
|
||||
list_indexes:
|
||||
kind: postgres-list-sequences
|
||||
source: postgres-source
|
||||
description: |
|
||||
Lists all the sequences in the database ordered by sequence name.
|
||||
Returns sequence name, schema name, sequence owner, data type of the
|
||||
sequence, starting value, minimum value, maximum value of the sequence,
|
||||
the value by which the sequence is incremented, and the last value
|
||||
generated by generated by the sequence in the current session.
|
||||
```
|
||||
|
||||
The response is a json array with the following elements:
|
||||
```json
|
||||
{
|
||||
"sequencename": "sequence name",
|
||||
"schemaname": "schema name",
|
||||
"sequenceowner": "owner of the sequence",
|
||||
"data_type": "data type of the sequence",
|
||||
"start_value": "starting value of the sequence",
|
||||
"min_value": "minimum value of the sequence",
|
||||
"max_value": "maximum value of the sequence",
|
||||
"increment_by": "increment value of the sequence",
|
||||
"last_value": "last value of the sequence"
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:-------------:|------------------------------------------------------|
|
||||
| kind | string | true | Must be "postgres-list-sequences". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | false | Description of the tool that is passed to the agent. |
|
||||
60
docs/en/resources/tools/postgres/postgres-list-triggers.md
Normal file
60
docs/en/resources/tools/postgres/postgres-list-triggers.md
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "postgres-list-triggers"
|
||||
type: docs
|
||||
weight: 1
|
||||
description: >
|
||||
The "postgres-list-triggers" tool lists triggers in a Postgres database.
|
||||
aliases:
|
||||
- /resources/tools/postgres-list-triggers
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
The `postgres-list-triggers` tool lists available non-internal triggers in the database. It's compatible with any of the following sources:
|
||||
|
||||
- [alloydb-postgres](../../sources/alloydb-pg.md)
|
||||
- [cloud-sql-postgres](../../sources/cloud-sql-pg.md)
|
||||
- [postgres](../../sources/postgres.md)
|
||||
|
||||
`postgres-list-triggers` lists detailed information as JSON for triggers. The tool takes the following input parameters:
|
||||
|
||||
- `trigger_name` (optional): A text to filter results by trigger name. The input is used within a LIKE clause. Default: `""`
|
||||
- `schema_name` (optional): A text to filter results by schema name. The input is used within a LIKE clause. Default: `""`
|
||||
- `table_name` (optional): A text to filter results by table name. The input is used within a LIKE clause. Default: `""`
|
||||
- `limit` (optional): The maximum number of triggers to return. Default: `50`
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```yaml
|
||||
```yaml
|
||||
tools:
|
||||
list_triggers:
|
||||
kind: postgres-list-triggers
|
||||
source: postgres-source
|
||||
description: |
|
||||
Lists all non-internal triggers in a database. Returns trigger name, schema name, table name, wether its enabled or disabled, timing (e.g BEFORE/AFTER of the event), the events that cause the trigger to fire such as INSERT, UPDATE, or DELETE, whether the trigger activates per ROW or per STATEMENT, the handler function executed by the trigger and full definition.
|
||||
```
|
||||
|
||||
The response is a json array with the following elements:
|
||||
```json
|
||||
{
|
||||
"trigger_name": "trigger name",
|
||||
"schema_name": "schema name",
|
||||
"table_name": "table name",
|
||||
"status": "Whether the trigger is currently active (ENABLED, DISABLED, REPLICA, ALWAYS).",
|
||||
"timing": "When it runs relative to the event (BEFORE, AFTER, INSTEAD OF).",
|
||||
"events": "The specific operations that fire it (INSERT, UPDATE, DELETE, TRUNCATE)",
|
||||
"activation_level": "Granularity of execution (ROW vs STATEMENT).",
|
||||
"function_name": "The function it executes",
|
||||
"definition": "Full SQL definition of the trigger"
|
||||
}
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
| **field** | **type** | **required** | **description** |
|
||||
|-------------|:--------:|:-------------:|------------------------------------------------------|
|
||||
| kind | string | true | Must be "postgres-list-triggers". |
|
||||
| source | string | true | Name of the source the SQL should execute on. |
|
||||
| description | string | false | Description of the tool that is passed to the agent. |
|
||||
@@ -771,7 +771,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.19.1\" # x-release-please-version\n",
|
||||
"version = \"0.20.0\" # x-release-please-version\n",
|
||||
"! curl -L -o /content/toolbox https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
|
||||
"\n",
|
||||
"# Make the binary executable\n",
|
||||
|
||||
@@ -123,7 +123,7 @@ In this section, we will download and install the Toolbox binary.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
export VERSION="0.19.1"
|
||||
export VERSION="0.20.0"
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v$VERSION/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"version = \"0.19.1\" # x-release-please-version\n",
|
||||
"version = \"0.20.0\" # x-release-please-version\n",
|
||||
"! curl -O https://storage.googleapis.com/genai-toolbox/v{version}/linux/amd64/toolbox\n",
|
||||
"\n",
|
||||
"# Make the binary executable\n",
|
||||
|
||||
@@ -179,7 +179,7 @@ to use BigQuery, and then run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ In this section, we will download Toolbox, configure our tools in a
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ In this section, we will download Toolbox and run the Toolbox server.
|
||||
<!-- {x-release-please-start-version} -->
|
||||
```bash
|
||||
export OS="linux/amd64" # one of linux/amd64, darwin/arm64, darwin/amd64, or windows/amd64
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.19.1/$OS/toolbox
|
||||
curl -O https://storage.googleapis.com/genai-toolbox/v0.20.0/$OS/toolbox
|
||||
```
|
||||
<!-- {x-release-please-end} -->
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcp-toolbox-for-databases",
|
||||
"version": "0.19.1",
|
||||
"version": "0.20.0",
|
||||
"description": "MCP Toolbox for Databases is an open-source MCP server for more than 30 different datasources.",
|
||||
"contextFileName": "MCP-TOOLBOX-EXTENSION.md"
|
||||
}
|
||||
6
go.mod
6
go.mod
@@ -44,8 +44,8 @@ require (
|
||||
github.com/sijms/go-ora/v2 v2.9.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/thlib/go-timezone-local v0.0.7
|
||||
github.com/trinodb/trino-go-client v0.329.0
|
||||
github.com/valkey-io/valkey-go v1.0.67
|
||||
github.com/trinodb/trino-go-client v0.330.0
|
||||
github.com/valkey-io/valkey-go v1.0.68
|
||||
github.com/yugabyte/pgx/v5 v5.5.3-yb-5
|
||||
go.mongodb.org/mongo-driver v1.17.4
|
||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
|
||||
@@ -56,7 +56,7 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.37.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
go.opentelemetry.io/otel/trace v1.38.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/oauth2 v0.33.0
|
||||
google.golang.org/api v0.251.0
|
||||
google.golang.org/genproto v0.0.0-20251022142026-3a174f9686a8
|
||||
google.golang.org/protobuf v1.36.10
|
||||
|
||||
12
go.sum
12
go.sum
@@ -1266,10 +1266,10 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
|
||||
github.com/thlib/go-timezone-local v0.0.7 h1:fX8zd3aJydqLlTs/TrROrIIdztzsdFV23OzOQx31jII=
|
||||
github.com/thlib/go-timezone-local v0.0.7/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/trinodb/trino-go-client v0.329.0 h1:tAQR5oXsW81C+lA0xiZsyoOcD7qYLv6Rtdw7SqH5Cy0=
|
||||
github.com/trinodb/trino-go-client v0.329.0/go.mod h1:BXj9QNy6pA4Gn8eIu9dVdRhetABCjFAOZ6xxsVsOZJE=
|
||||
github.com/valkey-io/valkey-go v1.0.67 h1:QPaRcuBmazhyoWTxk7I2XcSALhoL7UhAReR5o/rh1Po=
|
||||
github.com/valkey-io/valkey-go v1.0.67/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/trinodb/trino-go-client v0.330.0 h1:TBbHjFBuRjYbGtkNyRAJfzLOcwvz8ECihtMtxSzXqOc=
|
||||
github.com/trinodb/trino-go-client v0.330.0/go.mod h1:BXj9QNy6pA4Gn8eIu9dVdRhetABCjFAOZ6xxsVsOZJE=
|
||||
github.com/valkey-io/valkey-go v1.0.68 h1:bTbfonp49b41DqrF30q+y2JL3gcbjd2IiacFAtO4JBA=
|
||||
github.com/valkey-io/valkey-go v1.0.68/go.mod h1:bHmwjIEOrGq/ubOJfh5uMRs7Xj6mV3mQ/ZXUbmqpjqY=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
@@ -1544,8 +1544,8 @@ golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec
|
||||
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -30,4 +30,5 @@ type AuthService interface {
|
||||
AuthServiceKind() string
|
||||
GetName() string
|
||||
GetClaimsFromHeader(context.Context, http.Header) (map[string]any, error)
|
||||
ToConfig() AuthServiceConfig
|
||||
}
|
||||
|
||||
@@ -43,9 +43,7 @@ func (cfg Config) AuthServiceConfigKind() string {
|
||||
// Initialize a Google auth service
|
||||
func (cfg Config) Initialize() (auth.AuthService, error) {
|
||||
a := &AuthService{
|
||||
Name: cfg.Name,
|
||||
Kind: AuthServiceKind,
|
||||
ClientID: cfg.ClientID,
|
||||
Config: cfg,
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
@@ -54,9 +52,7 @@ var _ auth.AuthService = AuthService{}
|
||||
|
||||
// struct used to store auth service info
|
||||
type AuthService struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
ClientID string `yaml:"clientId"`
|
||||
Config
|
||||
}
|
||||
|
||||
// Returns the auth service kind
|
||||
@@ -64,6 +60,10 @@ func (a AuthService) AuthServiceKind() string {
|
||||
return AuthServiceKind
|
||||
}
|
||||
|
||||
func (a AuthService) ToConfig() auth.AuthServiceConfig {
|
||||
return a.Config
|
||||
}
|
||||
|
||||
// Returns the name of the auth service
|
||||
func (a AuthService) GetName() string {
|
||||
return a.Name
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
sources:
|
||||
alloydb-admin-source:
|
||||
kind: alloydb-admin
|
||||
defaultProject: ${ALLOYDB_POSTGRES_PROJECT:}
|
||||
tools:
|
||||
create_cluster:
|
||||
kind: alloydb-create-cluster
|
||||
@@ -30,8 +31,8 @@ tools:
|
||||
kind: alloydb-create-instance
|
||||
source: alloydb-admin-source
|
||||
list_clusters:
|
||||
kind: alloydb-list-clusters
|
||||
source: alloydb-admin-source
|
||||
kind: alloydb-list-clusters
|
||||
source: alloydb-admin-source
|
||||
list_instances:
|
||||
kind: alloydb-list-instances
|
||||
source: alloydb-admin-source
|
||||
|
||||
@@ -163,6 +163,22 @@ tools:
|
||||
list_schemas:
|
||||
kind: postgres-list-schemas
|
||||
source: alloydb-pg-source
|
||||
|
||||
list_indexes:
|
||||
kind: postgres-list-indexes
|
||||
source: alloydb-pg-source
|
||||
|
||||
list_sequences:
|
||||
kind: postgres-list-sequences
|
||||
source: alloydb-pg-source
|
||||
|
||||
database_overview:
|
||||
kind: postgres-database-overview
|
||||
source: alloydb-pg-source
|
||||
|
||||
list_triggers:
|
||||
kind: postgres-list-triggers
|
||||
source: alloydb-pg-source
|
||||
|
||||
toolsets:
|
||||
alloydb_postgres_database_tools:
|
||||
@@ -179,3 +195,7 @@ toolsets:
|
||||
- get_query_plan
|
||||
- list_views
|
||||
- list_schemas
|
||||
- database_overview
|
||||
- list_triggers
|
||||
- list_indexes
|
||||
- list_sequences
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
sources:
|
||||
cloud-sql-admin-source:
|
||||
kind: cloud-sql-admin
|
||||
defaultProject: ${CLOUD_SQL_MSSQL_PROJECT:}
|
||||
|
||||
tools:
|
||||
create_instance:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
sources:
|
||||
cloud-sql-admin-source:
|
||||
kind: cloud-sql-admin
|
||||
defaultProject: ${CLOUD_SQL_MYSQL_PROJECT:}
|
||||
|
||||
tools:
|
||||
create_instance:
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
sources:
|
||||
cloud-sql-admin-source:
|
||||
kind: cloud-sql-admin
|
||||
defaultProject: ${CLOUD_SQL_POSTGRES_PROJECT:}
|
||||
|
||||
tools:
|
||||
create_instance:
|
||||
|
||||
@@ -163,6 +163,22 @@ tools:
|
||||
kind: postgres-list-schemas
|
||||
source: cloudsql-pg-source
|
||||
|
||||
database_overview:
|
||||
kind: postgres-database-overview
|
||||
source: cloudsql-pg-source
|
||||
|
||||
list_triggers:
|
||||
kind: postgres-list-triggers
|
||||
source: cloudsql-pg-source
|
||||
|
||||
list_indexes:
|
||||
kind: postgres-list-indexes
|
||||
source: cloudsql-pg-source
|
||||
|
||||
list_sequences:
|
||||
kind: postgres-list-sequences
|
||||
source: cloudsql-pg-source
|
||||
|
||||
toolsets:
|
||||
cloud_sql_postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -178,3 +194,7 @@ toolsets:
|
||||
- get_query_plan
|
||||
- list_views
|
||||
- list_schemas
|
||||
- database_overview
|
||||
- list_triggers
|
||||
- list_indexes
|
||||
- list_sequences
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
sources:
|
||||
mysql-source:
|
||||
kind: mysql
|
||||
host: ${MYSQL_HOST}
|
||||
port: ${MYSQL_PORT}
|
||||
host: ${MYSQL_HOST:localhost}
|
||||
port: ${MYSQL_PORT:3306}
|
||||
database: ${MYSQL_DATABASE}
|
||||
user: ${MYSQL_USER}
|
||||
password: ${MYSQL_PASSWORD}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
sources:
|
||||
postgresql-source:
|
||||
kind: postgres
|
||||
host: ${POSTGRES_HOST}
|
||||
port: ${POSTGRES_PORT}
|
||||
host: ${POSTGRES_HOST:localhost}
|
||||
port: ${POSTGRES_PORT:5432}
|
||||
database: ${POSTGRES_DATABASE}
|
||||
user: ${POSTGRES_USER}
|
||||
password: ${POSTGRES_PASSWORD}
|
||||
@@ -162,6 +162,22 @@ tools:
|
||||
kind: postgres-list-schemas
|
||||
source: postgresql-source
|
||||
|
||||
database_overview:
|
||||
kind: postgres-database-overview
|
||||
source: postgresql-source
|
||||
|
||||
list_triggers:
|
||||
kind: postgres-list-triggers
|
||||
source: postgresql-source
|
||||
|
||||
list_indexes:
|
||||
kind: postgres-list-indexes
|
||||
source: postgresql-source
|
||||
|
||||
list_sequences:
|
||||
kind: postgres-list-sequences
|
||||
source: postgresql-source
|
||||
|
||||
toolsets:
|
||||
postgres_database_tools:
|
||||
- execute_sql
|
||||
@@ -177,3 +193,7 @@ toolsets:
|
||||
- get_query_plan
|
||||
- list_views
|
||||
- list_schemas
|
||||
- database_overview
|
||||
- list_triggers
|
||||
- list_indexes
|
||||
- list_sequences
|
||||
|
||||
89
internal/prompts/arguments.go
Normal file
89
internal/prompts/arguments.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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.
|
||||
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// ArgMcpManifest is the simplified manifest structure for an argument required for prompts.
|
||||
type ArgMcpManifest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// Argument is a wrapper around a parameters.Parameter that provides prompt-specific functionality.
|
||||
// If the 'type' field is not specified in a YAML definition, it defaults to 'string'.
|
||||
type Argument struct {
|
||||
parameters.Parameter
|
||||
}
|
||||
|
||||
// McpManifest returns the simplified manifest structure required for prompts.
|
||||
func (a Argument) McpManifest() ArgMcpManifest {
|
||||
return ArgMcpManifest{
|
||||
Name: a.GetName(),
|
||||
Description: a.Manifest().Description,
|
||||
Required: parameters.CheckParamRequired(a.GetRequired(), a.GetDefault()),
|
||||
}
|
||||
}
|
||||
|
||||
// Arguments is a slice of Argument.
|
||||
type Arguments []Argument
|
||||
|
||||
// UnmarshalYAML provides custom unmarshaling logic for Arguments.
|
||||
func (args *Arguments) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
|
||||
*args = make(Arguments, 0)
|
||||
var rawList []util.DelayedUnmarshaler
|
||||
if err := unmarshal(&rawList); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, u := range rawList {
|
||||
var p map[string]any
|
||||
if err := u.Unmarshal(&p); err != nil {
|
||||
return fmt.Errorf("error parsing argument: %w", err)
|
||||
}
|
||||
|
||||
// If 'type' is missing, default it to string.
|
||||
paramType, ok := p["type"]
|
||||
if !ok {
|
||||
p["type"] = parameters.TypeString
|
||||
paramType = parameters.TypeString
|
||||
}
|
||||
|
||||
// Call the clean, exported parser from the tools package. No more duplicated logic!
|
||||
param, err := parameters.ParseParameter(ctx, p, paramType.(string))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*args = append(*args, Argument{Parameter: param})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseArguments validates and processes the user-provided arguments against the prompt's requirements.
|
||||
func ParseArguments(arguments Arguments, args map[string]any, data map[string]map[string]any) (parameters.ParamValues, error) {
|
||||
var params parameters.Parameters
|
||||
for _, arg := range arguments {
|
||||
params = append(params, arg.Parameter)
|
||||
}
|
||||
return parameters.ParseParams(params, args, data)
|
||||
}
|
||||
249
internal/prompts/arguments_test.go
Normal file
249
internal/prompts/arguments_test.go
Normal file
@@ -0,0 +1,249 @@
|
||||
// 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.
|
||||
|
||||
package prompts_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// Test type aliases for convenience.
|
||||
type (
|
||||
Argument = prompts.Argument
|
||||
ArgMcpManifest = prompts.ArgMcpManifest
|
||||
Arguments = prompts.Arguments
|
||||
)
|
||||
|
||||
// Ptr is a helper function to create a pointer to a value.
|
||||
func Ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func makeArrayArg(name, desc string, items parameters.Parameter) Argument {
|
||||
return Argument{Parameter: parameters.NewArrayParameter(name, desc, items)}
|
||||
}
|
||||
|
||||
func TestArgMcpManifest(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
arg Argument
|
||||
expected ArgMcpManifest
|
||||
}{
|
||||
{
|
||||
name: "Required with no default",
|
||||
arg: Argument{Parameter: parameters.NewStringParameterWithRequired("name1", "desc1", true)},
|
||||
expected: ArgMcpManifest{
|
||||
Name: "name1", Description: "desc1", Required: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Not required with no default",
|
||||
arg: Argument{Parameter: parameters.NewStringParameterWithRequired("name2", "desc2", false)},
|
||||
expected: ArgMcpManifest{
|
||||
Name: "name2", Description: "desc2", Required: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Implicitly required with default",
|
||||
arg: Argument{Parameter: parameters.NewStringParameterWithDefault("name3", "defaultVal", "desc3")},
|
||||
expected: ArgMcpManifest{
|
||||
Name: "name3", Description: "desc3", Required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.arg.McpManifest()
|
||||
if diff := cmp.Diff(tc.expected, got); diff != "" {
|
||||
t.Errorf("McpManifest() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestArguments_UnmarshalYAML tests all unmarshaling logic for the Arguments type.
|
||||
func TestArgumentsUnmarshalYAML(t *testing.T) {
|
||||
t.Parallel()
|
||||
// paramComparer allows cmp.Diff to intelligently compare the parsed results.
|
||||
var transformFunc func(parameters.Parameter) any
|
||||
transformFunc = func(p parameters.Parameter) any {
|
||||
s := struct{ Name, Type, Desc string }{
|
||||
Name: p.GetName(),
|
||||
Type: p.GetType(),
|
||||
Desc: p.Manifest().Description,
|
||||
}
|
||||
if arr, ok := p.(*parameters.ArrayParameter); ok {
|
||||
s.Desc = fmt.Sprintf("%s items:%v", s.Desc, transformFunc(arr.GetItems()))
|
||||
}
|
||||
return s
|
||||
}
|
||||
paramComparer := cmp.Transformer("Parameter", transformFunc)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
yamlInput []map[string]any
|
||||
expectedArgs Arguments
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Defaults type to string when omitted",
|
||||
yamlInput: []map[string]any{
|
||||
{"name": "p1", "description": "d1"},
|
||||
},
|
||||
expectedArgs: Arguments{
|
||||
{Parameter: parameters.NewStringParameter("p1", "d1")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Respects type when present",
|
||||
yamlInput: []map[string]any{
|
||||
{"name": "p1", "description": "d1", "type": "integer"},
|
||||
},
|
||||
expectedArgs: Arguments{
|
||||
{Parameter: parameters.NewIntParameter("p1", "d1")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Parses complex types like arrays correctly",
|
||||
yamlInput: []map[string]any{
|
||||
{
|
||||
"name": "param_array",
|
||||
"description": "an array",
|
||||
"type": "array",
|
||||
"items": map[string]any{
|
||||
"name": "item_name",
|
||||
"type": "string",
|
||||
"description": "an item",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedArgs: Arguments{
|
||||
makeArrayArg("param_array", "an array", parameters.NewStringParameter("item_name", "an item")),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Propagates parsing error for unsupported type",
|
||||
yamlInput: []map[string]any{
|
||||
{"name": "p1", "description": "d1", "type": "unsupported"},
|
||||
},
|
||||
wantErr: `"unsupported" is not valid type for a parameter`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
yamlBytes, err := yaml.Marshal(tc.yamlInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Test setup failure: could not marshal test input to YAML: %v", err)
|
||||
}
|
||||
var got Arguments
|
||||
ctx, err := testutils.ContextWithNewLogger()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create logger using testutils: %v", err)
|
||||
}
|
||||
err = yaml.UnmarshalContext(ctx, yamlBytes, &got)
|
||||
|
||||
if tc.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("UnmarshalContext() expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("UnmarshalContext() error mismatch:\nwant to contain: %q\ngot: %q", tc.wantErr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("UnmarshalContext() returned unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expectedArgs, got, paramComparer); diff != "" {
|
||||
t.Errorf("UnmarshalContext() result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseArguments(t *testing.T) {
|
||||
t.Parallel()
|
||||
testArguments := prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("name", "A required name.")},
|
||||
{Parameter: parameters.NewIntParameterWithRequired("count", "An optional count.", false)},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
argsIn map[string]any
|
||||
want parameters.ParamValues
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Success with all parameters provided",
|
||||
argsIn: map[string]any{
|
||||
"name": "test-name",
|
||||
"count": 42,
|
||||
},
|
||||
want: parameters.ParamValues{
|
||||
{Name: "name", Value: "test-name"},
|
||||
{Name: "count", Value: 42},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Success with only required parameters",
|
||||
argsIn: map[string]any{
|
||||
"name": "another-name",
|
||||
},
|
||||
want: parameters.ParamValues{
|
||||
{Name: "name", Value: "another-name"},
|
||||
{Name: "count", Value: nil},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Failure with missing required parameter",
|
||||
argsIn: map[string]any{
|
||||
"count": 123,
|
||||
},
|
||||
wantErr: `parameter "name" is required`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := prompts.ParseArguments(testArguments, tc.argsIn, nil)
|
||||
if tc.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("ParseArguments() result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
81
internal/prompts/custom/custom.go
Normal file
81
internal/prompts/custom/custom.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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.
|
||||
|
||||
package custom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
type Message = prompts.Message
|
||||
|
||||
const kind = "custom"
|
||||
|
||||
// init registers this prompt kind with the prompt framework.
|
||||
func init() {
|
||||
if !prompts.Register(kind, newConfig) {
|
||||
panic(fmt.Sprintf("prompt kind %q already registered", kind))
|
||||
}
|
||||
}
|
||||
|
||||
// newConfig is the factory function for creating a custom prompt configuration.
|
||||
func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
|
||||
cfg := &Config{Name: name}
|
||||
if err := decoder.DecodeContext(ctx, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Config is the configuration for a custom prompt.
|
||||
// It implements both the prompts.PromptConfig and prompts.Prompt interfaces.
|
||||
type Config struct {
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description,omitempty"`
|
||||
Messages []Message `yaml:"messages"`
|
||||
Arguments prompts.Arguments `yaml:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
// Interface compliance checks.
|
||||
var _ prompts.PromptConfig = (*Config)(nil)
|
||||
var _ prompts.Prompt = (*Config)(nil)
|
||||
|
||||
func (c *Config) PromptConfigKind() string {
|
||||
return kind
|
||||
}
|
||||
|
||||
func (c *Config) Initialize() (prompts.Prompt, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Config) Manifest() prompts.Manifest {
|
||||
return prompts.GetManifest(c.Description, c.Arguments)
|
||||
}
|
||||
|
||||
func (c *Config) McpManifest() prompts.McpManifest {
|
||||
return prompts.GetMcpManifest(c.Name, c.Description, c.Arguments)
|
||||
}
|
||||
|
||||
func (c *Config) SubstituteParams(argValues parameters.ParamValues) (any, error) {
|
||||
return prompts.SubstituteMessages(c.Messages, c.Arguments, argValues)
|
||||
}
|
||||
|
||||
func (c *Config) ParseArgs(args map[string]any, data map[string]map[string]any) (parameters.ParamValues, error) {
|
||||
return prompts.ParseArguments(c.Arguments, args, data)
|
||||
}
|
||||
143
internal/prompts/custom/custom_test.go
Normal file
143
internal/prompts/custom/custom_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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.
|
||||
|
||||
package custom_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts/custom"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup a shared config for testing its methods
|
||||
testArgs := prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("name", "The name to use.")},
|
||||
{Parameter: parameters.NewStringParameterWithRequired("location", "The location.", false)},
|
||||
}
|
||||
|
||||
cfg := &custom.Config{
|
||||
Name: "TestConfig",
|
||||
Description: "A test config.",
|
||||
Messages: []custom.Message{
|
||||
{Role: "user", Content: "Hello, my name is {{.name}} and I am in {{.location}}."},
|
||||
},
|
||||
Arguments: testArgs,
|
||||
}
|
||||
|
||||
t.Run("Initialize and Kind", func(t *testing.T) {
|
||||
p, err := cfg.Initialize()
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() failed: %v", err)
|
||||
}
|
||||
if p == nil {
|
||||
t.Fatal("Initialize() returned a nil prompt")
|
||||
}
|
||||
if cfg.PromptConfigKind() != "custom" {
|
||||
t.Errorf("PromptConfigKind() = %q, want %q", cfg.PromptConfigKind(), "custom")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Manifest", func(t *testing.T) {
|
||||
want := prompts.Manifest{
|
||||
Description: "A test config.",
|
||||
Arguments: []parameters.ParameterManifest{
|
||||
{Name: "name", Type: "string", Required: true, Description: "The name to use.", AuthServices: []string{}},
|
||||
{Name: "location", Type: "string", Required: false, Description: "The location.", AuthServices: []string{}},
|
||||
},
|
||||
}
|
||||
got := cfg.Manifest()
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("Manifest() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("McpManifest", func(t *testing.T) {
|
||||
want := prompts.McpManifest{
|
||||
Name: "TestConfig",
|
||||
Description: "A test config.",
|
||||
Arguments: []prompts.ArgMcpManifest{
|
||||
{Name: "name", Description: "The name to use.", Required: true},
|
||||
{Name: "location", Description: "The location.", Required: false},
|
||||
},
|
||||
}
|
||||
got := cfg.McpManifest()
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("McpManifest() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SubstituteParams", func(t *testing.T) {
|
||||
argValues := parameters.ParamValues{
|
||||
{Name: "name", Value: "Alice"},
|
||||
{Name: "location", Value: "Wonderland"},
|
||||
}
|
||||
want := []prompts.Message{
|
||||
{Role: "user", Content: "Hello, my name is Alice and I am in Wonderland."},
|
||||
}
|
||||
|
||||
got, err := cfg.SubstituteParams(argValues)
|
||||
if err != nil {
|
||||
t.Fatalf("SubstituteParams() failed: %v", err)
|
||||
}
|
||||
|
||||
gotMessages, ok := got.([]prompts.Message)
|
||||
if !ok {
|
||||
t.Fatalf("expected result to be of type []prompts.Message, but got %T", got)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, gotMessages); diff != "" {
|
||||
t.Errorf("SubstituteParams() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ParseArgs", func(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
argsIn := map[string]any{
|
||||
"name": "Bob",
|
||||
"location": "the Builder",
|
||||
}
|
||||
want := parameters.ParamValues{
|
||||
{Name: "name", Value: "Bob"},
|
||||
{Name: "location", Value: "the Builder"},
|
||||
}
|
||||
got, err := cfg.ParseArgs(argsIn, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseArgs() failed: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("ParseArgs() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FailureMissingRequired", func(t *testing.T) {
|
||||
argsIn := map[string]any{
|
||||
"location": "missing name",
|
||||
}
|
||||
_, err := cfg.ParseArgs(argsIn, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for missing required arg, but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), `parameter "name" is required`) {
|
||||
t.Errorf("expected error to be about missing parameter, but got: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
77
internal/prompts/messages.go
Normal file
77
internal/prompts/messages.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// Message represents a single message in a prompt, with a role and content.
|
||||
type Message struct {
|
||||
Role string `yaml:"role,omitempty"`
|
||||
Content string `yaml:"content"`
|
||||
}
|
||||
|
||||
const (
|
||||
userRole = "user"
|
||||
assistantRole = "assistant"
|
||||
)
|
||||
|
||||
func (m *Message) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
// Use a type alias to prevent an infinite recursion loop. The alias
|
||||
// has the same fields but lacks the UnmarshalYAML method.
|
||||
type messageAlias Message
|
||||
var alias messageAlias
|
||||
if err := unmarshal(&alias); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*m = Message(alias)
|
||||
if m.Role == "" {
|
||||
m.Role = userRole
|
||||
}
|
||||
if m.Role != userRole && m.Role != assistantRole {
|
||||
return fmt.Errorf("invalid role %q: must be 'user' or 'assistant'", m.Role)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubstituteMessages takes a slice of Messages and a set of parameter values,
|
||||
// and returns a new slice with all template variables resolved.
|
||||
func SubstituteMessages(messages []Message, arguments Arguments, argValues parameters.ParamValues) ([]Message, error) {
|
||||
substitutedMessages := make([]Message, 0, len(messages))
|
||||
argsMap := argValues.AsMap()
|
||||
|
||||
var params parameters.Parameters
|
||||
for _, arg := range arguments {
|
||||
params = append(params, arg.Parameter)
|
||||
}
|
||||
|
||||
for _, msg := range messages {
|
||||
substitutedContent, err := parameters.ResolveTemplateParams(params, msg.Content, argsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error substituting params for message: %w", err)
|
||||
}
|
||||
|
||||
substitutedMessages = append(substitutedMessages, Message{
|
||||
Role: msg.Role,
|
||||
Content: substitutedContent,
|
||||
})
|
||||
}
|
||||
|
||||
return substitutedMessages, nil
|
||||
}
|
||||
133
internal/prompts/messages_test.go
Normal file
133
internal/prompts/messages_test.go
Normal file
@@ -0,0 +1,133 @@
|
||||
// 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.
|
||||
|
||||
package prompts_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
func TestMessageUnmarshalYAML(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
yamlInput map[string]any
|
||||
want prompts.Message
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Valid role: user",
|
||||
yamlInput: map[string]any{"role": "user", "content": "Hello"},
|
||||
want: prompts.Message{Role: "user", Content: "Hello"},
|
||||
},
|
||||
{
|
||||
name: "Valid role: assistant",
|
||||
yamlInput: map[string]any{"role": "assistant", "content": "Hi there"},
|
||||
want: prompts.Message{Role: "assistant", Content: "Hi there"},
|
||||
},
|
||||
{
|
||||
name: "Role is omitted, defaults to user",
|
||||
yamlInput: map[string]any{"content": "A message with no role"},
|
||||
want: prompts.Message{Role: "user", Content: "A message with no role"},
|
||||
},
|
||||
{
|
||||
name: "Invalid role: other",
|
||||
yamlInput: map[string]any{"role": "other", "content": "Some other role"},
|
||||
wantErr: `invalid role "other": must be 'user' or 'assistant'`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
yamlBytes, err := yaml.Marshal(tc.yamlInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Test setup failure: could not marshal test input: %v", err)
|
||||
}
|
||||
|
||||
var got prompts.Message
|
||||
err = yaml.Unmarshal(yamlBytes, &got)
|
||||
|
||||
if tc.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("expected an error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("unmarshal mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubstituteMessages(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
arguments := prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("name", "The name to use.")},
|
||||
{Parameter: parameters.NewStringParameterWithRequired("location", "The location.", false)},
|
||||
}
|
||||
messages := []prompts.Message{
|
||||
{Role: "user", Content: "Hello, my name is {{.name}} and I am in {{.location}}."},
|
||||
{Role: "assistant", Content: "Nice to meet you, {{.name}}!"},
|
||||
}
|
||||
argValues := parameters.ParamValues{
|
||||
{Name: "name", Value: "Alice"},
|
||||
{Name: "location", Value: "Wonderland"},
|
||||
}
|
||||
|
||||
want := []prompts.Message{
|
||||
{Role: "user", Content: "Hello, my name is Alice and I am in Wonderland."},
|
||||
{Role: "assistant", Content: "Nice to meet you, Alice!"},
|
||||
}
|
||||
|
||||
got, err := prompts.SubstituteMessages(messages, arguments, argValues)
|
||||
if err != nil {
|
||||
t.Fatalf("SubstituteMessages() failed: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Errorf("SubstituteMessages() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FailureInvalidTemplate", func(t *testing.T) {
|
||||
arguments := prompts.Arguments{}
|
||||
messages := []prompts.Message{
|
||||
{Content: "This has an {{.unclosed template"},
|
||||
}
|
||||
argValues := parameters.ParamValues{}
|
||||
|
||||
_, err := prompts.SubstituteMessages(messages, arguments, argValues)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for invalid template, but got nil")
|
||||
}
|
||||
wantErr := "unexpected <template> in operand"
|
||||
if !strings.Contains(err.Error(), wantErr) {
|
||||
t.Errorf("error mismatch:\n want to contain: %q\n got: %q", wantErr, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
111
internal/prompts/prompts.go
Normal file
111
internal/prompts/prompts.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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.
|
||||
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// PromptConfigFactory defines the signature for a function that creates and
|
||||
// decodes a specific prompt's configuration.
|
||||
type PromptConfigFactory func(ctx context.Context, name string, decoder *yaml.Decoder) (PromptConfig, error)
|
||||
|
||||
var promptRegistry = make(map[string]PromptConfigFactory)
|
||||
|
||||
// Register allows individual prompt packages to register their configuration
|
||||
// factory function. This is typically called from an init() function in the
|
||||
// prompt's package. It associates a 'kind' string with a function that can
|
||||
// produce the specific PromptConfig type. It returns true if the registration was
|
||||
// successful, and false if a prompt with the same kind was already registered.
|
||||
func Register(kind string, factory PromptConfigFactory) bool {
|
||||
if _, exists := promptRegistry[kind]; exists {
|
||||
// Prompt with this kind already exists, do not overwrite.
|
||||
return false
|
||||
}
|
||||
promptRegistry[kind] = factory
|
||||
return true
|
||||
}
|
||||
|
||||
// DecodeConfig looks up the registered factory for the given kind and uses it
|
||||
// to decode the prompt configuration.
|
||||
func DecodeConfig(ctx context.Context, kind, name string, decoder *yaml.Decoder) (PromptConfig, error) {
|
||||
factory, found := promptRegistry[kind]
|
||||
if !found && kind == "" {
|
||||
kind = "custom"
|
||||
factory, found = promptRegistry[kind]
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("unknown prompt kind: %q", kind)
|
||||
}
|
||||
|
||||
promptConfig, err := factory(ctx, name, decoder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse prompt %q as kind %q: %w", name, kind, err)
|
||||
}
|
||||
return promptConfig, nil
|
||||
}
|
||||
|
||||
type PromptConfig interface {
|
||||
PromptConfigKind() string
|
||||
Initialize() (Prompt, error)
|
||||
}
|
||||
|
||||
type Prompt interface {
|
||||
SubstituteParams(parameters.ParamValues) (any, error)
|
||||
ParseArgs(map[string]any, map[string]map[string]any) (parameters.ParamValues, error)
|
||||
Manifest() Manifest
|
||||
McpManifest() McpManifest
|
||||
}
|
||||
|
||||
// Manifest is the representation of prompts sent to Client SDKs.
|
||||
type Manifest struct {
|
||||
Description string `json:"description"`
|
||||
Arguments []parameters.ParameterManifest `json:"arguments"`
|
||||
}
|
||||
|
||||
// McpManifest is the definition for a prompt the MCP client can get.
|
||||
type McpManifest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Arguments []ArgMcpManifest `json:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
func GetMcpManifest(name, desc string, args Arguments) McpManifest {
|
||||
mcpArgs := make([]ArgMcpManifest, 0, len(args))
|
||||
for _, arg := range args {
|
||||
mcpArgs = append(mcpArgs, arg.McpManifest())
|
||||
}
|
||||
return McpManifest{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Arguments: mcpArgs,
|
||||
}
|
||||
}
|
||||
|
||||
func GetManifest(desc string, args Arguments) Manifest {
|
||||
paramManifests := make([]parameters.ParameterManifest, 0, len(args))
|
||||
for _, arg := range args {
|
||||
paramManifests = append(paramManifests, arg.Manifest())
|
||||
}
|
||||
return Manifest{
|
||||
Description: desc,
|
||||
Arguments: paramManifests,
|
||||
}
|
||||
}
|
||||
203
internal/prompts/prompts_test.go
Normal file
203
internal/prompts/prompts_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// 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.
|
||||
|
||||
package prompts_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
_ "github.com/googleapis/genai-toolbox/internal/prompts/custom"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
type mockPromptConfig struct {
|
||||
name string
|
||||
kind string
|
||||
}
|
||||
|
||||
func (m *mockPromptConfig) PromptConfigKind() string { return m.kind }
|
||||
func (m *mockPromptConfig) Initialize() (prompts.Prompt, error) { return nil, nil }
|
||||
|
||||
var errMockFactory = errors.New("mock factory error")
|
||||
|
||||
func mockFactory(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
|
||||
return &mockPromptConfig{name: name, kind: "mockKind"}, nil
|
||||
}
|
||||
|
||||
func mockErrorFactory(ctx context.Context, name string, decoder *yaml.Decoder) (prompts.PromptConfig, error) {
|
||||
return nil, errMockFactory
|
||||
}
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("RegisterAndDecodeSuccess", func(t *testing.T) {
|
||||
kind := "testKindSuccess"
|
||||
if !prompts.Register(kind, mockFactory) {
|
||||
t.Fatal("expected registration to succeed")
|
||||
}
|
||||
// This should fail because we are registering a duplicate
|
||||
if prompts.Register(kind, mockFactory) {
|
||||
t.Fatal("expected duplicate registration to fail")
|
||||
}
|
||||
|
||||
decoder := yaml.NewDecoder(strings.NewReader(""))
|
||||
config, err := prompts.DecodeConfig(ctx, kind, "testPrompt", decoder)
|
||||
if err != nil {
|
||||
t.Fatalf("expected DecodeConfig to succeed, but got error: %v", err)
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("expected a non-nil config")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DecodeUnknownKind", func(t *testing.T) {
|
||||
decoder := yaml.NewDecoder(strings.NewReader(""))
|
||||
_, err := prompts.DecodeConfig(ctx, "unregisteredKind", "testPrompt", decoder)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for unknown kind, but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown prompt kind") {
|
||||
t.Errorf("expected error to contain 'unknown prompt kind', but got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FactoryReturnsError", func(t *testing.T) {
|
||||
kind := "testKindError"
|
||||
if !prompts.Register(kind, mockErrorFactory) {
|
||||
t.Fatal("expected registration to succeed")
|
||||
}
|
||||
|
||||
decoder := yaml.NewDecoder(strings.NewReader(""))
|
||||
_, err := prompts.DecodeConfig(ctx, kind, "testPrompt", decoder)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error from the factory, but got nil")
|
||||
}
|
||||
if !errors.Is(err, errMockFactory) {
|
||||
t.Errorf("expected error to wrap mock factory error, but it didn't")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DecodeDefaultsToCustom", func(t *testing.T) {
|
||||
decoder := yaml.NewDecoder(strings.NewReader("description: A test prompt"))
|
||||
config, err := prompts.DecodeConfig(ctx, "", "testDefaultPrompt", decoder)
|
||||
if err != nil {
|
||||
t.Fatalf("expected DecodeConfig with empty kind to succeed, but got error: %v", err)
|
||||
}
|
||||
if config == nil {
|
||||
t.Fatal("expected a non-nil config for default kind")
|
||||
}
|
||||
if config.PromptConfigKind() != "custom" {
|
||||
t.Errorf("expected default kind to be 'custom', but got %q", config.PromptConfigKind())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMcpManifest(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
promptName string
|
||||
description string
|
||||
args prompts.Arguments
|
||||
want prompts.McpManifest
|
||||
}{
|
||||
{
|
||||
name: "No arguments",
|
||||
promptName: "test-prompt",
|
||||
description: "A test prompt.",
|
||||
args: prompts.Arguments{},
|
||||
want: prompts.McpManifest{
|
||||
Name: "test-prompt",
|
||||
Description: "A test prompt.",
|
||||
Arguments: []prompts.ArgMcpManifest{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With arguments",
|
||||
promptName: "arg-prompt",
|
||||
description: "Prompt with args.",
|
||||
args: prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("param1", "First param")},
|
||||
{Parameter: parameters.NewIntParameterWithRequired("param2", "Second param", false)},
|
||||
},
|
||||
want: prompts.McpManifest{
|
||||
Name: "arg-prompt",
|
||||
Description: "Prompt with args.",
|
||||
Arguments: []prompts.ArgMcpManifest{
|
||||
{Name: "param1", Description: "First param", Required: true},
|
||||
{Name: "param2", Description: "Second param", Required: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := prompts.GetMcpManifest(tc.promptName, tc.description, tc.args)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetMcpManifest() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetManifest(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := []struct {
|
||||
name string
|
||||
description string
|
||||
args prompts.Arguments
|
||||
want prompts.Manifest
|
||||
}{
|
||||
{
|
||||
name: "No arguments",
|
||||
description: "A simple prompt.",
|
||||
args: prompts.Arguments{},
|
||||
want: prompts.Manifest{
|
||||
Description: "A simple prompt.",
|
||||
Arguments: []parameters.ParameterManifest{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With arguments",
|
||||
description: "Prompt with arguments.",
|
||||
args: prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("param1", "First param")},
|
||||
{Parameter: parameters.NewBooleanParameterWithRequired("param2", "Second param", false)},
|
||||
},
|
||||
want: prompts.Manifest{
|
||||
Description: "Prompt with arguments.",
|
||||
Arguments: []parameters.ParameterManifest{
|
||||
{Name: "param1", Type: "string", Required: true, Description: "First param", AuthServices: []string{}},
|
||||
{Name: "param2", Type: "boolean", Required: false, Description: "Second param", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := prompts.GetManifest(tc.description, tc.args)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetManifest() mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
64
internal/prompts/promptsets.go
Normal file
64
internal/prompts/promptsets.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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.
|
||||
|
||||
package prompts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
type PromptsetConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
PromptNames []string `yaml:",inline"`
|
||||
}
|
||||
|
||||
type Promptset struct {
|
||||
Name string `yaml:"name"`
|
||||
Prompts []*Prompt `yaml:",inline"`
|
||||
Manifest PromptsetManifest `yaml:",inline"`
|
||||
McpManifest []McpManifest `yaml:",inline"`
|
||||
}
|
||||
|
||||
type PromptsetManifest struct {
|
||||
ServerVersion string `json:"serverVersion"`
|
||||
PromptsManifest map[string]Manifest `json:"prompts"`
|
||||
}
|
||||
|
||||
func (t PromptsetConfig) Initialize(serverVersion string, promptsMap map[string]Prompt) (Promptset, error) {
|
||||
// Check each declared prompt name exists
|
||||
var promptset Promptset
|
||||
promptset.Name = t.Name
|
||||
if !tools.IsValidName(promptset.Name) {
|
||||
return promptset, fmt.Errorf("invalid promptset name: %s", t)
|
||||
}
|
||||
promptset.Prompts = make([]*Prompt, 0, len(t.PromptNames))
|
||||
promptset.McpManifest = make([]McpManifest, 0, len(t.PromptNames))
|
||||
promptset.Manifest = PromptsetManifest{
|
||||
ServerVersion: serverVersion,
|
||||
PromptsManifest: make(map[string]Manifest, len(t.PromptNames)),
|
||||
}
|
||||
for _, promptName := range t.PromptNames {
|
||||
prompt, ok := promptsMap[promptName]
|
||||
if !ok {
|
||||
return promptset, fmt.Errorf("prompt does not exist: %s", t)
|
||||
}
|
||||
promptset.Prompts = append(promptset.Prompts, &prompt)
|
||||
promptset.Manifest.PromptsManifest[promptName] = prompt.Manifest()
|
||||
promptset.McpManifest = append(promptset.McpManifest, prompt.McpManifest())
|
||||
}
|
||||
|
||||
return promptset, nil
|
||||
}
|
||||
215
internal/prompts/promptsets_test.go
Normal file
215
internal/prompts/promptsets_test.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// 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.
|
||||
|
||||
package prompts_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// mockPrompt is a simple mock implementation of prompts.Prompt for testing.
|
||||
type mockPrompt struct {
|
||||
name string
|
||||
desc string
|
||||
args prompts.Arguments
|
||||
manifest prompts.Manifest
|
||||
mcpManifest prompts.McpManifest
|
||||
}
|
||||
|
||||
func (m *mockPrompt) SubstituteParams(parameters.ParamValues) (any, error) { return nil, nil }
|
||||
func (m *mockPrompt) ParseArgs(map[string]any, map[string]map[string]any) (parameters.ParamValues, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockPrompt) Manifest() prompts.Manifest { return m.manifest }
|
||||
func (m *mockPrompt) McpManifest() prompts.McpManifest { return m.mcpManifest }
|
||||
|
||||
// newMockPrompt creates a new mock prompt for testing.
|
||||
func newMockPrompt(name, desc string) prompts.Prompt {
|
||||
args := prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("arg1", "Test argument")},
|
||||
}
|
||||
return &mockPrompt{
|
||||
name: name,
|
||||
desc: desc,
|
||||
args: args,
|
||||
manifest: prompts.Manifest{
|
||||
Description: desc,
|
||||
Arguments: []parameters.ParameterManifest{
|
||||
{Name: "arg1", Type: "string", Required: true, Description: "Test argument", AuthServices: []string{}},
|
||||
},
|
||||
},
|
||||
mcpManifest: prompts.McpManifest{
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Arguments: []prompts.ArgMcpManifest{
|
||||
{Name: "arg1", Description: "Test argument", Required: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptsetConfig_Initialize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
promptsMap := map[string]prompts.Prompt{
|
||||
"prompt1": newMockPrompt("prompt1", "First test prompt"),
|
||||
"prompt2": newMockPrompt("prompt2", "Second test prompt"),
|
||||
}
|
||||
serverVersion := "v1.0.0"
|
||||
|
||||
p1 := promptsMap["prompt1"]
|
||||
p2 := promptsMap["prompt2"]
|
||||
prompt1Ptr := &p1
|
||||
prompt2Ptr := &p2
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
config prompts.PromptsetConfig
|
||||
want prompts.Promptset
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "Success case",
|
||||
config: prompts.PromptsetConfig{
|
||||
Name: "default",
|
||||
PromptNames: []string{"prompt1", "prompt2"},
|
||||
},
|
||||
want: prompts.Promptset{
|
||||
Name: "default",
|
||||
Prompts: []*prompts.Prompt{
|
||||
prompt1Ptr,
|
||||
prompt2Ptr,
|
||||
},
|
||||
Manifest: prompts.PromptsetManifest{
|
||||
ServerVersion: serverVersion,
|
||||
PromptsManifest: map[string]prompts.Manifest{
|
||||
"prompt1": promptsMap["prompt1"].Manifest(),
|
||||
"prompt2": promptsMap["prompt2"].Manifest(),
|
||||
},
|
||||
},
|
||||
McpManifest: []prompts.McpManifest{
|
||||
promptsMap["prompt1"].McpManifest(),
|
||||
promptsMap["prompt2"].McpManifest(),
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "Success case with one prompt",
|
||||
config: prompts.PromptsetConfig{
|
||||
Name: "single",
|
||||
PromptNames: []string{"prompt1"},
|
||||
},
|
||||
want: prompts.Promptset{
|
||||
Name: "single",
|
||||
Prompts: []*prompts.Prompt{
|
||||
prompt1Ptr,
|
||||
},
|
||||
Manifest: prompts.PromptsetManifest{
|
||||
ServerVersion: serverVersion,
|
||||
PromptsManifest: map[string]prompts.Manifest{
|
||||
"prompt1": promptsMap["prompt1"].Manifest(),
|
||||
},
|
||||
},
|
||||
McpManifest: []prompts.McpManifest{
|
||||
promptsMap["prompt1"].McpManifest(),
|
||||
},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "Failure case - invalid promptset name",
|
||||
config: prompts.PromptsetConfig{
|
||||
Name: "invalid name", // Contains a space
|
||||
PromptNames: []string{"prompt1"},
|
||||
},
|
||||
want: prompts.Promptset{Name: "invalid name"}, // Expect partial struct
|
||||
wantErr: "invalid promptset name",
|
||||
},
|
||||
{
|
||||
name: "Failure case - prompt not found",
|
||||
config: prompts.PromptsetConfig{
|
||||
Name: "missing_prompt",
|
||||
PromptNames: []string{"prompt1", "prompt_does_not_exist"},
|
||||
},
|
||||
// Expect partial struct with fields populated up to the error
|
||||
want: prompts.Promptset{
|
||||
Name: "missing_prompt",
|
||||
Prompts: []*prompts.Prompt{
|
||||
prompt1Ptr,
|
||||
},
|
||||
Manifest: prompts.PromptsetManifest{
|
||||
ServerVersion: serverVersion,
|
||||
PromptsManifest: map[string]prompts.Manifest{
|
||||
"prompt1": promptsMap["prompt1"].Manifest(),
|
||||
},
|
||||
},
|
||||
McpManifest: []prompts.McpManifest{
|
||||
promptsMap["prompt1"].McpManifest(),
|
||||
},
|
||||
},
|
||||
wantErr: "prompt does not exist",
|
||||
},
|
||||
{
|
||||
name: "Success case - empty prompt list",
|
||||
config: prompts.PromptsetConfig{
|
||||
Name: "empty",
|
||||
PromptNames: []string{},
|
||||
},
|
||||
want: prompts.Promptset{
|
||||
Name: "empty",
|
||||
Prompts: []*prompts.Prompt{},
|
||||
Manifest: prompts.PromptsetManifest{
|
||||
ServerVersion: serverVersion,
|
||||
PromptsManifest: map[string]prompts.Manifest{},
|
||||
},
|
||||
McpManifest: []prompts.McpManifest{},
|
||||
},
|
||||
wantErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := tc.config.Initialize(serverVersion, promptsMap)
|
||||
|
||||
if tc.wantErr != "" {
|
||||
if err == nil {
|
||||
t.Fatalf("Initialize() expected error but got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Errorf("Initialize() error mismatch:\n want to contain: %q\n got: %q", tc.wantErr, err.Error())
|
||||
}
|
||||
// Also check that the partially populated struct matches
|
||||
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(mockPrompt{})); diff != "" {
|
||||
t.Errorf("Initialize() partial result on error mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Initialize() returned unexpected error: %v", err)
|
||||
}
|
||||
// Using cmp.AllowUnexported because mockPrompt is unexported
|
||||
if diff := cmp.Diff(tc.want, got, cmp.AllowUnexported(mockPrompt{})); diff != "" {
|
||||
t.Errorf("Initialize() result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -227,7 +227,7 @@ func toolInvokeHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
params, err := tool.ParseParams(data, claimsFromAuth)
|
||||
if err != nil {
|
||||
// If auth error, return 401
|
||||
if errors.Is(err, tools.ErrUnauthorized) {
|
||||
if errors.Is(err, util.ErrUnauthorized) {
|
||||
s.logger.DebugContext(ctx, fmt.Sprintf("error parsing authenticated parameters from ID token: %s", err))
|
||||
_ = render.Render(w, r, newErrResponse(err, http.StatusUnauthorized))
|
||||
return
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
|
||||
func TestToolsetEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
|
||||
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -125,8 +125,8 @@ func TestToolsetEndpoint(t *testing.T) {
|
||||
|
||||
func TestToolGetEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
|
||||
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -213,8 +213,8 @@ func TestToolGetEndpoint(t *testing.T) {
|
||||
|
||||
func TestToolInvokeEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets)
|
||||
toolsMap, toolsets, _, _ := setUpResources(t, mockTools, nil)
|
||||
r, shutdown := setUpServer(t, "api", toolsMap, toolsets, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -25,37 +25,46 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/telemetry"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||
)
|
||||
|
||||
// fakeVersionString is used as a temporary version string in tests
|
||||
const fakeVersionString = "0.0.0"
|
||||
|
||||
var _ tools.Tool = &MockTool{}
|
||||
var (
|
||||
_ tools.Tool = &MockTool{}
|
||||
_ prompts.Prompt = &MockPrompt{}
|
||||
)
|
||||
|
||||
// MockTool is used to mock tools in tests
|
||||
type MockTool struct {
|
||||
Name string
|
||||
Description string
|
||||
Params []tools.Parameter
|
||||
Params []parameters.Parameter
|
||||
manifest tools.Manifest
|
||||
unauthorized bool
|
||||
requiresClientAuthrorization bool
|
||||
}
|
||||
|
||||
func (t MockTool) Invoke(context.Context, tools.ParamValues, tools.AccessToken) (any, error) {
|
||||
func (t MockTool) Invoke(context.Context, parameters.ParamValues, tools.AccessToken) (any, error) {
|
||||
mock := []any{t.Name}
|
||||
return mock, nil
|
||||
}
|
||||
|
||||
func (t MockTool) ToConfig() tools.ToolConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
// claims is a map of user info decoded from an auth token
|
||||
func (t MockTool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (tools.ParamValues, error) {
|
||||
return tools.ParseParams(t.Params, data, claimsMap)
|
||||
func (t MockTool) ParseParams(data map[string]any, claimsMap map[string]map[string]any) (parameters.ParamValues, error) {
|
||||
return parameters.ParseParams(t.Params, data, claimsMap)
|
||||
}
|
||||
|
||||
func (t MockTool) Manifest() tools.Manifest {
|
||||
pMs := make([]tools.ParameterManifest, 0, len(t.Params))
|
||||
pMs := make([]parameters.ParameterManifest, 0, len(t.Params))
|
||||
for _, p := range t.Params {
|
||||
pMs = append(pMs, p.Manifest())
|
||||
}
|
||||
@@ -73,7 +82,7 @@ func (t MockTool) RequiresClientAuthorization() bool {
|
||||
}
|
||||
|
||||
func (t MockTool) McpManifest() tools.McpManifest {
|
||||
properties := make(map[string]tools.ParameterMcpManifest)
|
||||
properties := make(map[string]parameters.ParameterMcpManifest)
|
||||
required := make([]string, 0)
|
||||
authParams := make(map[string][]string)
|
||||
|
||||
@@ -88,7 +97,7 @@ func (t MockTool) McpManifest() tools.McpManifest {
|
||||
}
|
||||
}
|
||||
|
||||
toolsSchema := tools.McpToolsSchema{
|
||||
toolsSchema := parameters.McpToolsSchema{
|
||||
Type: "object",
|
||||
Properties: properties,
|
||||
Required: required,
|
||||
@@ -109,41 +118,92 @@ func (t MockTool) McpManifest() tools.McpManifest {
|
||||
return mcpManifest
|
||||
}
|
||||
|
||||
// MockPrompt is used to mock prompts in tests
|
||||
type MockPrompt struct {
|
||||
Name string
|
||||
Description string
|
||||
Args prompts.Arguments
|
||||
}
|
||||
|
||||
func (p MockPrompt) SubstituteParams(vals parameters.ParamValues) (any, error) {
|
||||
return []prompts.Message{
|
||||
{
|
||||
Role: "user",
|
||||
Content: fmt.Sprintf("substituted %s", p.Name),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p MockPrompt) ParseArgs(data map[string]any, claimsMap map[string]map[string]any) (parameters.ParamValues, error) {
|
||||
var params parameters.Parameters
|
||||
for _, arg := range p.Args {
|
||||
params = append(params, arg.Parameter)
|
||||
}
|
||||
return parameters.ParseParams(params, data, claimsMap)
|
||||
}
|
||||
|
||||
func (p MockPrompt) Manifest() prompts.Manifest {
|
||||
var argManifests []parameters.ParameterManifest
|
||||
for _, arg := range p.Args {
|
||||
argManifests = append(argManifests, arg.Manifest())
|
||||
}
|
||||
return prompts.Manifest{
|
||||
Description: p.Description,
|
||||
Arguments: argManifests,
|
||||
}
|
||||
}
|
||||
|
||||
func (p MockPrompt) McpManifest() prompts.McpManifest {
|
||||
return prompts.GetMcpManifest(p.Name, p.Description, p.Args)
|
||||
}
|
||||
|
||||
var tool1 = MockTool{
|
||||
Name: "no_params",
|
||||
Params: []tools.Parameter{},
|
||||
Params: []parameters.Parameter{},
|
||||
}
|
||||
|
||||
var tool2 = MockTool{
|
||||
Name: "some_params",
|
||||
Params: tools.Parameters{
|
||||
tools.NewIntParameter("param1", "This is the first parameter."),
|
||||
tools.NewIntParameter("param2", "This is the second parameter."),
|
||||
Params: parameters.Parameters{
|
||||
parameters.NewIntParameter("param1", "This is the first parameter."),
|
||||
parameters.NewIntParameter("param2", "This is the second parameter."),
|
||||
},
|
||||
}
|
||||
|
||||
var tool3 = MockTool{
|
||||
Name: "array_param",
|
||||
Description: "some description",
|
||||
Params: tools.Parameters{
|
||||
tools.NewArrayParameter("my_array", "this param is an array of strings", tools.NewStringParameter("my_string", "string item")),
|
||||
Params: parameters.Parameters{
|
||||
parameters.NewArrayParameter("my_array", "this param is an array of strings", parameters.NewStringParameter("my_string", "string item")),
|
||||
},
|
||||
}
|
||||
|
||||
var tool4 = MockTool{
|
||||
Name: "unauthorized_tool",
|
||||
Params: []tools.Parameter{},
|
||||
Params: []parameters.Parameter{},
|
||||
unauthorized: true,
|
||||
}
|
||||
|
||||
var tool5 = MockTool{
|
||||
Name: "require_client_auth_tool",
|
||||
Params: []tools.Parameter{},
|
||||
Params: []parameters.Parameter{},
|
||||
requiresClientAuthrorization: true,
|
||||
}
|
||||
|
||||
var prompt1 = MockPrompt{
|
||||
Name: "prompt1",
|
||||
Args: prompts.Arguments{},
|
||||
}
|
||||
|
||||
var prompt2 = MockPrompt{
|
||||
Name: "prompt2",
|
||||
Args: prompts.Arguments{
|
||||
{Parameter: parameters.NewStringParameter("arg1", "This is the first argument.")},
|
||||
},
|
||||
}
|
||||
|
||||
// setUpResources setups resources to test against
|
||||
func setUpResources(t *testing.T, mockTools []MockTool) (map[string]tools.Tool, map[string]tools.Toolset) {
|
||||
func setUpResources(t *testing.T, mockTools []MockTool, mockPrompts []MockPrompt) (map[string]tools.Tool, map[string]tools.Toolset, map[string]prompts.Prompt, map[string]prompts.Promptset) {
|
||||
toolsMap := make(map[string]tools.Tool)
|
||||
var allTools []string
|
||||
for _, tool := range mockTools {
|
||||
@@ -165,11 +225,29 @@ func setUpResources(t *testing.T, mockTools []MockTool) (map[string]tools.Tool,
|
||||
}
|
||||
toolsets[name] = m
|
||||
}
|
||||
return toolsMap, toolsets
|
||||
|
||||
promptsMap := make(map[string]prompts.Prompt)
|
||||
var allPrompts []string
|
||||
for _, prompt := range mockPrompts {
|
||||
promptsMap[prompt.Name] = prompt
|
||||
allPrompts = append(allPrompts, prompt.Name)
|
||||
}
|
||||
|
||||
promptsets := make(map[string]prompts.Promptset)
|
||||
if len(allPrompts) > 0 {
|
||||
psc := prompts.PromptsetConfig{Name: "", PromptNames: allPrompts}
|
||||
ps, err := psc.Initialize(fakeVersionString, promptsMap)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to initialize default promptset: %s", err)
|
||||
}
|
||||
promptsets[""] = ps
|
||||
}
|
||||
|
||||
return toolsMap, toolsets, promptsMap, promptsets
|
||||
}
|
||||
|
||||
// setUpServer create a new server with tools and toolsets that are given
|
||||
func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, toolsets map[string]tools.Toolset) (chi.Router, func()) {
|
||||
// setUpServer create a new server with tools, toolsets, prompts, and promptsets.
|
||||
func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, toolsets map[string]tools.Toolset, prompts map[string]prompts.Prompt, promptsets map[string]prompts.Promptset) (chi.Router, func()) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
testLogger, err := log.NewStdLogger(os.Stdout, os.Stderr, "info")
|
||||
@@ -189,7 +267,7 @@ func setUpServer(t *testing.T, router string, tools map[string]tools.Tool, tools
|
||||
|
||||
sseManager := newSseManager(ctx)
|
||||
|
||||
resourceManager := NewResourceManager(nil, nil, tools, toolsets)
|
||||
resourceManager := NewResourceManager(nil, nil, tools, toolsets, prompts, promptsets)
|
||||
|
||||
server := Server{
|
||||
version: fakeVersionString,
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/auth/google"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
@@ -41,6 +42,10 @@ type ServerConfig struct {
|
||||
ToolConfigs ToolConfigs
|
||||
// ToolsetConfigs defines what tools are available.
|
||||
ToolsetConfigs ToolsetConfigs
|
||||
// PromptConfigs defines what prompts are available
|
||||
PromptConfigs PromptConfigs
|
||||
// PromptsetConfigs defines what prompts are available
|
||||
PromptsetConfigs PromptsetConfigs
|
||||
// LoggingFormat defines whether structured loggings are used.
|
||||
LoggingFormat logFormat
|
||||
// LogLevel defines the levels to log.
|
||||
@@ -251,7 +256,7 @@ func (c *ToolConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interfac
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToolConfigs is a type used to allow unmarshal of the toolset configs
|
||||
// ToolsetConfigs is a type used to allow unmarshal of the toolset configs
|
||||
type ToolsetConfigs map[string]tools.ToolsetConfig
|
||||
|
||||
// validate interface
|
||||
@@ -270,3 +275,69 @@ func (c *ToolsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(inter
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PromptConfigs is a type used to allow unmarshal of the prompt configs
|
||||
type PromptConfigs map[string]prompts.PromptConfig
|
||||
|
||||
// validate interface
|
||||
var _ yaml.InterfaceUnmarshalerContext = &PromptConfigs{}
|
||||
|
||||
func (c *PromptConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
|
||||
*c = make(PromptConfigs)
|
||||
var raw map[string]util.DelayedUnmarshaler
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, u := range raw {
|
||||
var v map[string]any
|
||||
if err := u.Unmarshal(&v); err != nil {
|
||||
return fmt.Errorf("unable to unmarshal prompt %q: %w", name, err)
|
||||
}
|
||||
|
||||
// Look for the 'kind' field. If it's not present, kindStr will be an
|
||||
// empty string, which prompts.DecodeConfig will correctly default to "custom".
|
||||
var kindStr string
|
||||
if kindVal, ok := v["kind"]; ok {
|
||||
var isString bool
|
||||
kindStr, isString = kindVal.(string)
|
||||
if !isString {
|
||||
return fmt.Errorf("invalid 'kind' field for prompt %q (must be a string)", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new, strict decoder for this specific prompt's data.
|
||||
yamlDecoder, err := util.NewStrictDecoder(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating YAML decoder for prompt %q: %w", name, err)
|
||||
}
|
||||
|
||||
// Use the central registry to decode the prompt based on its kind.
|
||||
promptCfg, err := prompts.DecodeConfig(ctx, kindStr, name, yamlDecoder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
(*c)[name] = promptCfg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PromptsetConfigs is a type used to allow unmarshal of the PromptsetConfigs configs
|
||||
type PromptsetConfigs map[string]prompts.PromptsetConfig
|
||||
|
||||
// validate interface
|
||||
var _ yaml.InterfaceUnmarshalerContext = &PromptsetConfigs{}
|
||||
|
||||
func (c *PromptsetConfigs) UnmarshalYAML(ctx context.Context, unmarshal func(interface{}) error) error {
|
||||
*c = make(PromptsetConfigs)
|
||||
|
||||
var raw map[string][]string
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, promptList := range raw {
|
||||
(*c)[name] = prompts.PromptsetConfig{Name: name, PromptNames: promptList}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
|
||||
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
|
||||
v20250326 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20250326"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
@@ -144,7 +143,7 @@ func (s *stdioSession) readInputStream(ctx context.Context) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", nil)
|
||||
v, res, err := processMcpMessage(ctx, []byte(line), s.server, s.protocol, "", "", nil)
|
||||
if err != nil {
|
||||
// errors during the processing of message will generate a valid MCP Error response.
|
||||
// server can continue to run.
|
||||
@@ -374,6 +373,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
toolsetName := chi.URLParam(r, "toolsetName")
|
||||
promptsetName := chi.URLParam(r, "promptsetName")
|
||||
s.logger.DebugContext(ctx, fmt.Sprintf("toolset name: %s", toolsetName))
|
||||
span.SetAttributes(attribute.String("toolset_name", toolsetName))
|
||||
|
||||
@@ -406,7 +406,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, r.Header)
|
||||
v, res, err := processMcpMessage(ctx, body, s, protocolVersion, toolsetName, promptsetName, r.Header)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, fmt.Errorf("error processing message: %w", err).Error())
|
||||
}
|
||||
@@ -444,7 +444,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
case jsonrpc.INVALID_REQUEST:
|
||||
errStr := err.Error()
|
||||
if errors.Is(err, tools.ErrUnauthorized) {
|
||||
if errors.Is(err, util.ErrUnauthorized) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
} else if strings.Contains(errStr, "Error 401") {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
@@ -459,7 +459,7 @@ func httpHandler(s *Server, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// processMcpMessage process the messages received from clients
|
||||
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, header http.Header) (string, any, error) {
|
||||
func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVersion string, toolsetName string, promptsetName string, header http.Header) (string, any, error) {
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return "", jsonrpc.NewError("", jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
@@ -514,7 +514,12 @@ func processMcpMessage(ctx context.Context, body []byte, s *Server, protocolVers
|
||||
err = fmt.Errorf("toolset does not exist")
|
||||
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
|
||||
promptset, ok := s.ResourceMgr.GetPromptset(promptsetName)
|
||||
if !ok {
|
||||
err = fmt.Errorf("promptset does not exist")
|
||||
return "", jsonrpc.NewError(baseMessage.Id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
res, err := mcp.ProcessMethod(ctx, protocolVersion, baseMessage.Id, baseMessage.Method, toolset, s.ResourceMgr.GetToolsMap(), promptset, s.ResourceMgr.GetPromptsMap(), s.ResourceMgr.GetAuthServiceMap(), body, header)
|
||||
return "", res, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
mcputil "github.com/googleapis/genai-toolbox/internal/server/mcp/util"
|
||||
v20241105 "github.com/googleapis/genai-toolbox/internal/server/mcp/v20241105"
|
||||
@@ -60,12 +61,16 @@ func InitializeResponse(ctx context.Context, id jsonrpc.RequestId, body []byte,
|
||||
}
|
||||
|
||||
toolsListChanged := false
|
||||
promptsListChanged := false
|
||||
result := mcputil.InitializeResult{
|
||||
ProtocolVersion: protocolVersion,
|
||||
Capabilities: mcputil.ServerCapabilities{
|
||||
Tools: &mcputil.ListChanged{
|
||||
ListChanged: &toolsListChanged,
|
||||
},
|
||||
Prompts: &mcputil.ListChanged{
|
||||
ListChanged: &promptsListChanged,
|
||||
},
|
||||
},
|
||||
ServerInfo: mcputil.Implementation{
|
||||
BaseMetadata: mcputil.BaseMetadata{
|
||||
@@ -95,14 +100,14 @@ func NotificationHandler(ctx context.Context, body []byte) error {
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
// This is the Operation phase of the lifecycle for MCP client-server connections.
|
||||
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, mcpVersion string, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch mcpVersion {
|
||||
case v20250618.PROTOCOL_VERSION:
|
||||
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
return v20250618.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
|
||||
case v20250326.PROTOCOL_VERSION:
|
||||
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
return v20250326.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
|
||||
default:
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, authServices, body, header)
|
||||
return v20241105.ProcessMethod(ctx, id, method, toolset, tools, promptset, prompts, authServices, body, header)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,8 @@ type ClientCapabilities struct {
|
||||
// capabilities are defined here, in this schema, but this is not a closed set: any
|
||||
// server can define its own, additional capabilities.
|
||||
type ServerCapabilities struct {
|
||||
Tools *ListChanged `json:"tools,omitempty"`
|
||||
Tools *ListChanged `json:"tools,omitempty"`
|
||||
Prompts *ListChanged `json:"prompts,omitempty"`
|
||||
}
|
||||
|
||||
// Base interface for metadata with name (identifier) and title (display name) properties.
|
||||
|
||||
@@ -24,13 +24,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
case PROMPTS_LIST:
|
||||
return promptsListHandler(ctx, id, promptset, body)
|
||||
case PROMPTS_GET:
|
||||
return promptsGetHandler(ctx, id, prompts, body)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
// Missing authService tokens.
|
||||
if errors.Is(err, tools.ErrUnauthorized) {
|
||||
if errors.Is(err, util.ErrUnauthorized) {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
// Upstream auth error
|
||||
@@ -212,3 +217,99 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
Result: CallToolResult{Content: content},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsListHandler handles the "prompts/list" method.
|
||||
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/list request")
|
||||
|
||||
var req ListPromptsRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
result := ListPromptsResult{
|
||||
Prompts: promptset.McpManifest,
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsGetHandler handles the "prompts/get" method.
|
||||
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/get request")
|
||||
|
||||
var req GetPromptRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
promptName := req.Params.Name
|
||||
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
|
||||
prompt, ok := promptsMap[promptName]
|
||||
if !ok {
|
||||
err := fmt.Errorf("prompt with name %q does not exist", promptName)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Parse the arguments provided in the request.
|
||||
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
|
||||
|
||||
// Substitute the argument values into the prompt's messages.
|
||||
substituted, err := prompt.SubstituteParams(argValues)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "substituted params successfully")
|
||||
|
||||
// Cast the result to the expected []prompts.Message type.
|
||||
substitutedMessages, ok := substituted.([]prompts.Message)
|
||||
if !ok {
|
||||
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Format the response messages into the required structure.
|
||||
promptMessages := make([]PromptMessage, len(substitutedMessages))
|
||||
for i, msg := range substitutedMessages {
|
||||
promptMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: msg.Content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := GetPromptResult{
|
||||
Description: prompt.Manifest().Description,
|
||||
Messages: promptMessages,
|
||||
}
|
||||
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package v20241105
|
||||
|
||||
import (
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2024-11-05"
|
||||
|
||||
// methods that are supported.
|
||||
const (
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PROMPTS_LIST = "prompts/list"
|
||||
PROMPTS_GET = "prompts/get"
|
||||
)
|
||||
|
||||
/* Empty result */
|
||||
@@ -136,3 +139,38 @@ type CallToolResult struct {
|
||||
// If not set, this is assumed to be false (the call was successful).
|
||||
IsError bool `json:"isError,omitempty"`
|
||||
}
|
||||
|
||||
/* Prompts */
|
||||
|
||||
// Sent from the client to request a list of prompts the server has.
|
||||
type ListPromptsRequest struct {
|
||||
PaginatedRequest
|
||||
}
|
||||
|
||||
// The server's response to a prompts/list request from the client.
|
||||
type ListPromptsResult struct {
|
||||
PaginatedResult
|
||||
Prompts []prompts.McpManifest `json:"prompts"`
|
||||
}
|
||||
|
||||
// Used by the client to get a prompt provided by the server.
|
||||
type GetPromptRequest struct {
|
||||
jsonrpc.Request
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
// The server's response to a prompts/get request from the client.
|
||||
type GetPromptResult struct {
|
||||
jsonrpc.Result
|
||||
Description string `json:"description,omitempty"`
|
||||
Messages []PromptMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// Describes a message returned as part of a prompt.
|
||||
type PromptMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content TextContent `json:"content"`
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
case PROMPTS_LIST:
|
||||
return promptsListHandler(ctx, id, promptset, body)
|
||||
case PROMPTS_GET:
|
||||
return promptsGetHandler(ctx, id, prompts, body)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
// Missing authService tokens.
|
||||
if errors.Is(err, tools.ErrUnauthorized) {
|
||||
if errors.Is(err, util.ErrUnauthorized) {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
// Upstream auth error
|
||||
@@ -211,3 +216,99 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
Result: CallToolResult{Content: content},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsListHandler handles the "prompts/list" method.
|
||||
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/list request")
|
||||
|
||||
var req ListPromptsRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
result := ListPromptsResult{
|
||||
Prompts: promptset.McpManifest,
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsGetHandler handles the "prompts/get" method.
|
||||
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/get request")
|
||||
|
||||
var req GetPromptRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
promptName := req.Params.Name
|
||||
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
|
||||
prompt, ok := promptsMap[promptName]
|
||||
if !ok {
|
||||
err := fmt.Errorf("prompt with name %q does not exist", promptName)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Parse the arguments provided in the request.
|
||||
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
|
||||
|
||||
// Substitute the argument values into the prompt's messages.
|
||||
substituted, err := prompt.SubstituteParams(argValues)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "substituted params successfully")
|
||||
|
||||
// Cast the result to the expected []prompts.Message type.
|
||||
substitutedMessages, ok := substituted.([]prompts.Message)
|
||||
if !ok {
|
||||
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Format the response messages into the required structure.
|
||||
promptMessages := make([]PromptMessage, len(substitutedMessages))
|
||||
for i, msg := range substitutedMessages {
|
||||
promptMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: msg.Content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := GetPromptResult{
|
||||
Description: prompt.Manifest().Description,
|
||||
Messages: promptMessages,
|
||||
}
|
||||
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package v20250326
|
||||
|
||||
import (
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2025-03-26"
|
||||
|
||||
// methods that are supported.
|
||||
const (
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PROMPTS_LIST = "prompts/list"
|
||||
PROMPTS_GET = "prompts/get"
|
||||
)
|
||||
|
||||
/* Empty result */
|
||||
@@ -168,3 +171,38 @@ type ToolAnnotations struct {
|
||||
// Default: true
|
||||
OpenWorldHint bool `json:"openWorldHint,omitempty"`
|
||||
}
|
||||
|
||||
/* Prompts */
|
||||
|
||||
// Sent from the client to request a list of prompts the server has.
|
||||
type ListPromptsRequest struct {
|
||||
PaginatedRequest
|
||||
}
|
||||
|
||||
// The server's response to a prompts/list request from the client.
|
||||
type ListPromptsResult struct {
|
||||
PaginatedResult
|
||||
Prompts []prompts.McpManifest `json:"prompts"`
|
||||
}
|
||||
|
||||
// Used by the client to get a prompt provided by the server.
|
||||
type GetPromptRequest struct {
|
||||
jsonrpc.Request
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
// The server's response to a prompts/get request from the client.
|
||||
type GetPromptResult struct {
|
||||
jsonrpc.Result
|
||||
Description string `json:"description,omitempty"`
|
||||
Messages []PromptMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// Describes a message returned as part of a prompt.
|
||||
type PromptMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content TextContent `json:"content"`
|
||||
}
|
||||
|
||||
@@ -24,13 +24,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
"github.com/googleapis/genai-toolbox/internal/util"
|
||||
)
|
||||
|
||||
// ProcessMethod returns a response for the request.
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, toolset tools.Toolset, tools map[string]tools.Tool, promptset prompts.Promptset, prompts map[string]prompts.Prompt, authServices map[string]auth.AuthService, body []byte, header http.Header) (any, error) {
|
||||
switch method {
|
||||
case PING:
|
||||
return pingHandler(id)
|
||||
@@ -38,6 +39,10 @@ func ProcessMethod(ctx context.Context, id jsonrpc.RequestId, method string, too
|
||||
return toolsListHandler(id, toolset, body)
|
||||
case TOOLS_CALL:
|
||||
return toolsCallHandler(ctx, id, tools, authServices, body, header)
|
||||
case PROMPTS_LIST:
|
||||
return promptsListHandler(ctx, id, promptset, body)
|
||||
case PROMPTS_GET:
|
||||
return promptsGetHandler(ctx, id, prompts, body)
|
||||
default:
|
||||
err := fmt.Errorf("invalid method %s", method)
|
||||
return jsonrpc.NewError(id, jsonrpc.METHOD_NOT_FOUND, err.Error(), nil), err
|
||||
@@ -99,7 +104,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if this specific tool requires the standard authorization header
|
||||
if tool.RequiresClientAuthorization() {
|
||||
if accessToken == "" {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), tools.ErrUnauthorized
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, "missing access token in the 'Authorization' header", nil), util.ErrUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +152,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
// Check if any of the specified auth services is verified
|
||||
isAuthorized := tool.Authorized(verifiedAuthServices)
|
||||
if !isAuthorized {
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", tools.ErrUnauthorized)
|
||||
err = fmt.Errorf("unauthorized Tool call: Please make sure your specify correct auth headers: %w", util.ErrUnauthorized)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "tool invocation authorized")
|
||||
@@ -164,7 +169,7 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
// Missing authService tokens.
|
||||
if errors.Is(err, tools.ErrUnauthorized) {
|
||||
if errors.Is(err, util.ErrUnauthorized) {
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
// Upstream auth error
|
||||
@@ -211,3 +216,100 @@ func toolsCallHandler(ctx context.Context, id jsonrpc.RequestId, toolsMap map[st
|
||||
Result: CallToolResult{Content: content},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsListHandler handles the "prompts/list" method.
|
||||
func promptsListHandler(ctx context.Context, id jsonrpc.RequestId, promptset prompts.Promptset, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/list request")
|
||||
|
||||
var req ListPromptsRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts list request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
result := ListPromptsResult{
|
||||
Prompts: promptset.McpManifest,
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("returning %d prompts", len(promptset.McpManifest)))
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// promptsGetHandler handles the "prompts/get" method.
|
||||
func promptsGetHandler(ctx context.Context, id jsonrpc.RequestId, promptsMap map[string]prompts.Prompt, body []byte) (any, error) {
|
||||
// retrieve logger from context
|
||||
logger, err := util.LoggerFromContext(ctx)
|
||||
if err != nil {
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "handling prompts/get request")
|
||||
|
||||
var req GetPromptRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
err = fmt.Errorf("invalid mcp prompts/get request: %w", err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_REQUEST, err.Error(), nil), err
|
||||
}
|
||||
|
||||
promptName := req.Params.Name
|
||||
logger.DebugContext(ctx, fmt.Sprintf("prompt name: %s", promptName))
|
||||
|
||||
prompt, ok := promptsMap[promptName]
|
||||
if !ok {
|
||||
err := fmt.Errorf("prompt with name %q does not exist", promptName)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Parse the arguments provided in the request.
|
||||
argValues, err := prompt.ParseArgs(req.Params.Arguments, nil)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("invalid arguments for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INVALID_PARAMS, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, fmt.Sprintf("parsed args: %v", argValues))
|
||||
|
||||
// Substitute the argument values into the prompt's messages.
|
||||
substituted, err := prompt.SubstituteParams(argValues)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error substituting params for prompt %q: %w", promptName, err)
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
|
||||
// Cast the result to the expected []prompts.Message type.
|
||||
substitutedMessages, ok := substituted.([]prompts.Message)
|
||||
if !ok {
|
||||
err = fmt.Errorf("internal error: SubstituteParams returned unexpected type")
|
||||
return jsonrpc.NewError(id, jsonrpc.INTERNAL_ERROR, err.Error(), nil), err
|
||||
}
|
||||
logger.DebugContext(ctx, "substituted params successfully")
|
||||
|
||||
// Format the response messages into the required structure.
|
||||
promptMessages := make([]PromptMessage, len(substitutedMessages))
|
||||
for i, msg := range substitutedMessages {
|
||||
promptMessages[i] = PromptMessage{
|
||||
Role: msg.Role,
|
||||
Content: TextContent{
|
||||
Type: "text",
|
||||
Text: msg.Content,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
result := GetPromptResult{
|
||||
Description: prompt.Manifest().Description,
|
||||
Messages: promptMessages,
|
||||
}
|
||||
|
||||
return jsonrpc.JSONRPCResponse{
|
||||
Jsonrpc: jsonrpc.JSONRPC_VERSION,
|
||||
Id: id,
|
||||
Result: result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
package v20250618
|
||||
|
||||
import (
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
@@ -27,9 +28,11 @@ const PROTOCOL_VERSION = "2025-06-18"
|
||||
|
||||
// methods that are supported.
|
||||
const (
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PING = "ping"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PROMPTS_LIST = "prompts/list"
|
||||
PROMPTS_GET = "prompts/get"
|
||||
)
|
||||
|
||||
/* Empty result */
|
||||
@@ -179,3 +182,38 @@ type ToolAnnotations struct {
|
||||
// Default: true
|
||||
OpenWorldHint bool `json:"openWorldHint,omitempty"`
|
||||
}
|
||||
|
||||
/* Prompts */
|
||||
|
||||
// Sent from the client to request a list of prompts the server has.
|
||||
type ListPromptsRequest struct {
|
||||
PaginatedRequest
|
||||
}
|
||||
|
||||
// The server's response to a prompts/list request from the client.
|
||||
type ListPromptsResult struct {
|
||||
PaginatedResult
|
||||
Prompts []prompts.McpManifest `json:"prompts"`
|
||||
}
|
||||
|
||||
// Used by the client to get a prompt provided by the server.
|
||||
type GetPromptRequest struct {
|
||||
jsonrpc.Request
|
||||
Params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
// The server's response to a prompts/get request from the client.
|
||||
type GetPromptResult struct {
|
||||
jsonrpc.Result
|
||||
Description string `json:"description,omitempty"`
|
||||
Messages []PromptMessage `json:"messages"`
|
||||
}
|
||||
|
||||
// Describes a message returned as part of a prompt.
|
||||
type PromptMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content TextContent `json:"content"`
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/server/mcp/jsonrpc"
|
||||
"github.com/googleapis/genai-toolbox/internal/telemetry"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
)
|
||||
|
||||
const jsonrpcVersion = "2.0"
|
||||
@@ -66,10 +65,19 @@ var tool3InputSchema = map[string]any{
|
||||
"required": []any{"my_array"},
|
||||
}
|
||||
|
||||
var prompt2Args = []any{
|
||||
map[string]any{
|
||||
"name": "arg1",
|
||||
"description": "This is the first argument.",
|
||||
"required": true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
mockPrompts := []MockPrompt{prompt1, prompt2}
|
||||
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets, promptsMap, promptsets)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -247,6 +255,82 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompts/list",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "prompts-list-uninitialized",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "prompts/list",
|
||||
},
|
||||
},
|
||||
isErr: false,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "prompts-list-uninitialized",
|
||||
"result": map[string]any{
|
||||
"prompts": []any{
|
||||
map[string]any{
|
||||
"name": "prompt1",
|
||||
},
|
||||
map[string]any{
|
||||
"name": "prompt2",
|
||||
"arguments": prompt2Args,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompts/get non-existent prompt",
|
||||
url: "/",
|
||||
isErr: true,
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "prompts-get-non-existent",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "prompts/get",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "non_existent_prompt",
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "prompts-get-non-existent",
|
||||
"error": map[string]any{
|
||||
"code": -32602.0,
|
||||
"message": `prompt with name "non_existent_prompt" does not exist`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompts/get with invalid arguments",
|
||||
url: "/",
|
||||
isErr: true,
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "prompts-get-invalid-args",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "prompts/get",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "prompt2",
|
||||
"arguments": map[string]any{
|
||||
"arg1": 42, // prompt2 expects a string, we send a number
|
||||
},
|
||||
},
|
||||
},
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "prompts-get-invalid-args",
|
||||
"error": map[string]any{
|
||||
"code": -32602.0,
|
||||
"message": `invalid arguments for prompt "prompt2": unable to parse value for "arg1": %!q(float64=42) not type "string"`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -254,7 +338,6 @@ func TestMcpEndpointWithoutInitialized(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during marshaling of body")
|
||||
}
|
||||
|
||||
resp, body, err := runRequest(ts, http.MethodPost, tc.url, bytes.NewBuffer(reqMarshal), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error during request: %s", err)
|
||||
@@ -337,8 +420,9 @@ func runInitializeLifecycle(t *testing.T, ts *httptest.Server, protocolVersion s
|
||||
|
||||
func TestMcpEndpoint(t *testing.T) {
|
||||
mockTools := []MockTool{tool1, tool2, tool3, tool4, tool5}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
mockPrompts := []MockPrompt{prompt1, prompt2}
|
||||
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets, promptsMap, promptsets)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -359,7 +443,8 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"result": map[string]any{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"prompts": map[string]any{"listChanged": false},
|
||||
},
|
||||
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
|
||||
},
|
||||
@@ -375,7 +460,8 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"result": map[string]any{
|
||||
"protocolVersion": "2025-03-26",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"prompts": map[string]any{"listChanged": false},
|
||||
},
|
||||
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
|
||||
},
|
||||
@@ -391,7 +477,8 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
"result": map[string]any{
|
||||
"protocolVersion": "2025-06-18",
|
||||
"capabilities": map[string]any{
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"tools": map[string]any{"listChanged": false},
|
||||
"prompts": map[string]any{"listChanged": false},
|
||||
},
|
||||
"serverInfo": map[string]any{"name": serverName, "version": fakeVersionString},
|
||||
},
|
||||
@@ -488,6 +575,66 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompts/list",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "prompts-list",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "prompts/list",
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "prompts-list",
|
||||
"result": map[string]any{
|
||||
"prompts": []any{
|
||||
map[string]any{
|
||||
"name": "prompt1",
|
||||
},
|
||||
map[string]any{
|
||||
"name": "prompt2",
|
||||
"arguments": prompt2Args,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "prompts/get",
|
||||
url: "/",
|
||||
body: jsonrpc.JSONRPCRequest{
|
||||
Jsonrpc: jsonrpcVersion,
|
||||
Id: "prompts-get-prompt2",
|
||||
Request: jsonrpc.Request{
|
||||
Method: "prompts/get",
|
||||
},
|
||||
Params: map[string]any{
|
||||
"name": "prompt2",
|
||||
"arguments": map[string]any{
|
||||
"arg1": "value1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantStatusCode: http.StatusOK,
|
||||
want: map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "prompts-get-prompt2",
|
||||
"result": map[string]any{
|
||||
"messages": []any{
|
||||
map[string]any{
|
||||
"role": "user",
|
||||
"content": map[string]any{
|
||||
"type": "text",
|
||||
"text": "substituted prompt2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tools/list on tool1_only",
|
||||
url: "/tool1_only",
|
||||
@@ -743,8 +890,7 @@ func TestMcpEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInvalidProtocolVersionHeader(t *testing.T) {
|
||||
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -770,8 +916,7 @@ func TestInvalidProtocolVersionHeader(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeleteEndpoint(t *testing.T) {
|
||||
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -786,8 +931,7 @@ func TestDeleteEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGetEndpoint(t *testing.T) {
|
||||
toolsMap, toolsets := map[string]tools.Tool{}, map[string]tools.Toolset{}
|
||||
r, shutdown := setUpServer(t, "mcp", toolsMap, toolsets)
|
||||
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -810,7 +954,7 @@ func TestGetEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSseEndpoint(t *testing.T) {
|
||||
r, shutdown := setUpServer(t, "mcp", nil, nil)
|
||||
r, shutdown := setUpServer(t, "mcp", nil, nil, nil, nil)
|
||||
defer shutdown()
|
||||
ts := runServer(r, false)
|
||||
defer ts.Close()
|
||||
@@ -849,6 +993,12 @@ func TestSseEndpoint(t *testing.T) {
|
||||
path: "/tool1_only/sse",
|
||||
event: fmt.Sprintf("event: endpoint\ndata: http://127.0.0.1:%s/mcp/tool1_only?sessionId=", tsPort),
|
||||
},
|
||||
{
|
||||
name: "promptset1",
|
||||
server: ts,
|
||||
path: "/prompt1_only/sse",
|
||||
event: fmt.Sprintf("event: endpoint\ndata: http://127.0.0.1:%s/mcp/prompt1_only?sessionId=", tsPort),
|
||||
},
|
||||
{
|
||||
name: "basic with http proto",
|
||||
server: ts,
|
||||
@@ -925,7 +1075,8 @@ func TestStdioSession(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
mockTools := []MockTool{tool1, tool2, tool3}
|
||||
toolsMap, toolsets := setUpResources(t, mockTools)
|
||||
mockPrompts := []MockPrompt{prompt1, prompt2}
|
||||
toolsMap, toolsets, promptsMap, promptsets := setUpResources(t, mockTools, mockPrompts)
|
||||
|
||||
pr, pw, err := os.Pipe()
|
||||
if err != nil {
|
||||
@@ -955,7 +1106,7 @@ func TestStdioSession(t *testing.T) {
|
||||
|
||||
sseManager := newSseManager(ctx)
|
||||
|
||||
resourceManager := NewResourceManager(nil, nil, toolsMap, toolsets)
|
||||
resourceManager := NewResourceManager(nil, nil, toolsMap, toolsets, promptsMap, promptsets)
|
||||
|
||||
server := &Server{
|
||||
version: fakeVersionString,
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/go-chi/httplog/v2"
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/telemetry"
|
||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||
@@ -57,12 +58,16 @@ type ResourceManager struct {
|
||||
authServices map[string]auth.AuthService
|
||||
tools map[string]tools.Tool
|
||||
toolsets map[string]tools.Toolset
|
||||
prompts map[string]prompts.Prompt
|
||||
promptsets map[string]prompts.Promptset
|
||||
}
|
||||
|
||||
func NewResourceManager(
|
||||
sourcesMap map[string]sources.Source,
|
||||
authServicesMap map[string]auth.AuthService,
|
||||
toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset,
|
||||
promptsMap map[string]prompts.Prompt, promptsetsMap map[string]prompts.Promptset,
|
||||
|
||||
) *ResourceManager {
|
||||
resourceMgr := &ResourceManager{
|
||||
mu: sync.RWMutex{},
|
||||
@@ -70,6 +75,8 @@ func NewResourceManager(
|
||||
authServices: authServicesMap,
|
||||
tools: toolsMap,
|
||||
toolsets: toolsetsMap,
|
||||
prompts: promptsMap,
|
||||
promptsets: promptsetsMap,
|
||||
}
|
||||
|
||||
return resourceMgr
|
||||
@@ -103,13 +110,29 @@ func (r *ResourceManager) GetToolset(toolsetName string) (tools.Toolset, bool) {
|
||||
return toolset, ok
|
||||
}
|
||||
|
||||
func (r *ResourceManager) SetResources(sourcesMap map[string]sources.Source, authServicesMap map[string]auth.AuthService, toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset) {
|
||||
func (r *ResourceManager) GetPrompt(promptName string) (prompts.Prompt, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
prompt, ok := r.prompts[promptName]
|
||||
return prompt, ok
|
||||
}
|
||||
|
||||
func (r *ResourceManager) GetPromptset(promptsetName string) (prompts.Promptset, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
promptset, ok := r.promptsets[promptsetName]
|
||||
return promptset, ok
|
||||
}
|
||||
|
||||
func (r *ResourceManager) SetResources(sourcesMap map[string]sources.Source, authServicesMap map[string]auth.AuthService, toolsMap map[string]tools.Tool, toolsetsMap map[string]tools.Toolset, promptsMap map[string]prompts.Prompt, promptsetsMap map[string]prompts.Promptset) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.sources = sourcesMap
|
||||
r.authServices = authServicesMap
|
||||
r.tools = toolsMap
|
||||
r.toolsets = toolsetsMap
|
||||
r.prompts = promptsMap
|
||||
r.promptsets = promptsetsMap
|
||||
}
|
||||
|
||||
func (r *ResourceManager) GetAuthServiceMap() map[string]auth.AuthService {
|
||||
@@ -124,11 +147,19 @@ func (r *ResourceManager) GetToolsMap() map[string]tools.Tool {
|
||||
return r.tools
|
||||
}
|
||||
|
||||
func (r *ResourceManager) GetPromptsMap() map[string]prompts.Prompt {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.prompts
|
||||
}
|
||||
|
||||
func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
map[string]sources.Source,
|
||||
map[string]auth.AuthService,
|
||||
map[string]tools.Tool,
|
||||
map[string]tools.Toolset,
|
||||
map[string]prompts.Prompt,
|
||||
map[string]prompts.Promptset,
|
||||
error,
|
||||
) {
|
||||
ctx = util.WithUserAgent(ctx, cfg.Version)
|
||||
@@ -160,7 +191,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
return s, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
sourcesMap[name] = s
|
||||
}
|
||||
@@ -188,7 +219,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
return a, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
authServicesMap[name] = a
|
||||
}
|
||||
@@ -216,7 +247,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
return t, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
toolsMap[name] = t
|
||||
}
|
||||
@@ -253,7 +284,7 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
return t, err
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
toolsetsMap[name] = t
|
||||
}
|
||||
@@ -267,7 +298,76 @@ func InitializeConfigs(ctx context.Context, cfg ServerConfig) (
|
||||
}
|
||||
l.InfoContext(ctx, fmt.Sprintf("Initialized %d toolsets: %s", len(toolsetsMap), strings.Join(toolsetNames, ", ")))
|
||||
|
||||
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, nil
|
||||
// initialize and validate the prompts from configs
|
||||
promptsMap := make(map[string]prompts.Prompt)
|
||||
for name, pc := range cfg.PromptConfigs {
|
||||
p, err := func() (prompts.Prompt, error) {
|
||||
_, span := instrumentation.Tracer.Start(
|
||||
ctx,
|
||||
"toolbox/server/prompt/init",
|
||||
trace.WithAttributes(attribute.String("prompt_kind", pc.PromptConfigKind())),
|
||||
trace.WithAttributes(attribute.String("prompt_name", name)),
|
||||
)
|
||||
defer span.End()
|
||||
p, err := pc.Initialize()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize prompt %q: %w", name, err)
|
||||
}
|
||||
return p, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
promptsMap[name] = p
|
||||
}
|
||||
promptNames := make([]string, 0, len(promptsMap))
|
||||
for name := range promptsMap {
|
||||
promptNames = append(promptNames, name)
|
||||
}
|
||||
l.InfoContext(ctx, fmt.Sprintf("Initialized %d prompts: %s", len(promptsMap), strings.Join(promptNames, ", ")))
|
||||
|
||||
// create a default promptset that contains all prompts
|
||||
allPromptNames := make([]string, 0, len(promptsMap))
|
||||
for name := range promptsMap {
|
||||
allPromptNames = append(allPromptNames, name)
|
||||
}
|
||||
if cfg.PromptsetConfigs == nil {
|
||||
cfg.PromptsetConfigs = make(PromptsetConfigs)
|
||||
}
|
||||
cfg.PromptsetConfigs[""] = prompts.PromptsetConfig{Name: "", PromptNames: allPromptNames}
|
||||
|
||||
// initialize and validate the promptsets from configs
|
||||
promptsetsMap := make(map[string]prompts.Promptset)
|
||||
for name, pc := range cfg.PromptsetConfigs {
|
||||
p, err := func() (prompts.Promptset, error) {
|
||||
_, span := instrumentation.Tracer.Start(
|
||||
ctx,
|
||||
"toolbox/server/prompset/init",
|
||||
trace.WithAttributes(attribute.String("prompset_name", name)),
|
||||
)
|
||||
defer span.End()
|
||||
p, err := pc.Initialize(cfg.Version, promptsMap)
|
||||
if err != nil {
|
||||
return prompts.Promptset{}, fmt.Errorf("unable to initialize promptset %q: %w", name, err)
|
||||
}
|
||||
return p, err
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, nil, nil, err
|
||||
}
|
||||
promptsetsMap[name] = p
|
||||
}
|
||||
promptsetNames := make([]string, 0, len(promptsetsMap))
|
||||
for name := range promptsetsMap {
|
||||
if name == "" {
|
||||
promptsetNames = append(promptsetNames, "default")
|
||||
} else {
|
||||
promptsetNames = append(promptsetNames, name)
|
||||
}
|
||||
}
|
||||
l.InfoContext(ctx, fmt.Sprintf("Initialized %d promptsets: %s", len(promptsetsMap), strings.Join(promptsetNames, ", ")))
|
||||
|
||||
return sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, nil
|
||||
}
|
||||
|
||||
// NewServer returns a Server object based on provided Config.
|
||||
@@ -319,7 +419,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
|
||||
httpLogger := httplog.NewLogger("httplog", httpOpts)
|
||||
r.Use(httplog.RequestLogger(httpLogger))
|
||||
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, err := InitializeConfigs(ctx, cfg)
|
||||
sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := InitializeConfigs(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize configs: %w", err)
|
||||
}
|
||||
@@ -329,7 +429,7 @@ func NewServer(ctx context.Context, cfg ServerConfig) (*Server, error) {
|
||||
|
||||
sseManager := newSseManager(ctx)
|
||||
|
||||
resourceManager := NewResourceManager(sourcesMap, authServicesMap, toolsMap, toolsetsMap)
|
||||
resourceManager := NewResourceManager(sourcesMap, authServicesMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
|
||||
|
||||
s := &Server{
|
||||
version: cfg.Version,
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||
"github.com/googleapis/genai-toolbox/internal/log"
|
||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||
"github.com/googleapis/genai-toolbox/internal/server"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||
"github.com/googleapis/genai-toolbox/internal/sources/alloydbpg"
|
||||
@@ -136,18 +137,29 @@ func TestUpdateServer(t *testing.T) {
|
||||
|
||||
newSources := map[string]sources.Source{
|
||||
"example-source": &alloydbpg.Source{
|
||||
Name: "example-alloydb-source",
|
||||
Kind: "alloydb-postgres",
|
||||
Config: alloydbpg.Config{
|
||||
Name: "example-alloydb-source",
|
||||
Kind: "alloydb-postgres",
|
||||
},
|
||||
},
|
||||
}
|
||||
newAuth := map[string]auth.AuthService{"example-auth": nil}
|
||||
newTools := map[string]tools.Tool{"example-tool": nil}
|
||||
newToolsets := map[string]tools.Toolset{
|
||||
"example-toolset": {
|
||||
Name: "example-toolset", Tools: []*tools.Tool{},
|
||||
ToolsetConfig: tools.ToolsetConfig{
|
||||
Name: "example-toolset",
|
||||
},
|
||||
Tools: []*tools.Tool{},
|
||||
},
|
||||
}
|
||||
s.ResourceMgr.SetResources(newSources, newAuth, newTools, newToolsets)
|
||||
newPrompts := map[string]prompts.Prompt{"example-prompt": nil}
|
||||
newPromptsets := map[string]prompts.Promptset{
|
||||
"example-promptset": {
|
||||
Name: "example-promptset", Prompts: []*prompts.Prompt{},
|
||||
},
|
||||
}
|
||||
s.ResourceMgr.SetResources(newSources, newAuth, newTools, newToolsets, newPrompts, newPromptsets)
|
||||
if err != nil {
|
||||
t.Errorf("error updating server: %s", err)
|
||||
}
|
||||
@@ -171,4 +183,14 @@ func TestUpdateServer(t *testing.T) {
|
||||
if diff := cmp.Diff(gotToolset, newToolsets["example-toolset"]); diff != "" {
|
||||
t.Errorf("error updating server, toolset (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
gotPrompt, _ := s.ResourceMgr.GetPrompt("example-prompt")
|
||||
if diff := cmp.Diff(gotPrompt, newPrompts["example-prompt"]); diff != "" {
|
||||
t.Errorf("error updating server, prompts (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
gotPromptset, _ := s.ResourceMgr.GetPromptset("example-promptset")
|
||||
if diff := cmp.Diff(gotPromptset, newPromptsets["example-promptset"]); diff != "" {
|
||||
t.Errorf("error updating server, promptset (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
|
||||
type Config struct {
|
||||
Name string `yaml:"name" validate:"required"`
|
||||
Kind string `yaml:"kind" validate:"required"`
|
||||
DefaultProject string `yaml:"defaultProject"`
|
||||
UseClientOAuth bool `yaml:"useClientOAuth"`
|
||||
}
|
||||
|
||||
@@ -111,11 +112,9 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
BaseURL: "https://alloydb.googleapis.com",
|
||||
Service: service,
|
||||
UseClientOAuth: r.UseClientOAuth,
|
||||
Config: r,
|
||||
BaseURL: "https://alloydb.googleapis.com",
|
||||
Service: service,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -124,17 +123,19 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Source struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
BaseURL string
|
||||
Service *alloydbrestapi.Service
|
||||
UseClientOAuth bool
|
||||
Config
|
||||
BaseURL string
|
||||
Service *alloydbrestapi.Service
|
||||
}
|
||||
|
||||
func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) ToConfig() sources.SourceConfig {
|
||||
return s.Config
|
||||
}
|
||||
|
||||
func (s *Source) GetService(ctx context.Context, accessToken string) (*alloydbrestapi.Service, error) {
|
||||
if s.UseClientOAuth {
|
||||
token := &oauth2.Token{AccessToken: accessToken}
|
||||
|
||||
@@ -76,9 +76,8 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
}
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
Pool: pool,
|
||||
Config: r,
|
||||
Pool: pool,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
@@ -86,8 +85,7 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Source struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Config
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
@@ -95,6 +93,10 @@ func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) ToConfig() sources.SourceConfig {
|
||||
return s.Config
|
||||
}
|
||||
|
||||
func (s *Source) PostgresPool() *pgxpool.Pool {
|
||||
return s.Pool
|
||||
}
|
||||
|
||||
@@ -110,18 +110,12 @@ func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.So
|
||||
var err error
|
||||
|
||||
s := &Source{
|
||||
Name: r.Name,
|
||||
Kind: SourceKind,
|
||||
Project: r.Project,
|
||||
Location: r.Location,
|
||||
Client: client,
|
||||
RestService: restService,
|
||||
TokenSource: tokenSource,
|
||||
MaxQueryResultRows: 50,
|
||||
WriteMode: r.WriteMode,
|
||||
UseClientOAuth: r.UseClientOAuth,
|
||||
ClientCreator: clientCreator,
|
||||
ImpersonateServiceAccount: r.ImpersonateServiceAccount,
|
||||
Config: r,
|
||||
Client: client,
|
||||
RestService: restService,
|
||||
TokenSource: tokenSource,
|
||||
MaxQueryResultRows: 50,
|
||||
ClientCreator: clientCreator,
|
||||
}
|
||||
|
||||
if r.UseClientOAuth {
|
||||
@@ -241,20 +235,13 @@ func setupClientCaching(s *Source, baseCreator BigqueryClientCreator) {
|
||||
var _ sources.Source = &Source{}
|
||||
|
||||
type Source struct {
|
||||
// BigQuery Google SQL struct with client
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Project string
|
||||
Location string
|
||||
Config
|
||||
Client *bigqueryapi.Client
|
||||
RestService *bigqueryrestapi.Service
|
||||
TokenSource oauth2.TokenSource
|
||||
MaxQueryResultRows int
|
||||
ClientCreator BigqueryClientCreator
|
||||
AllowedDatasets map[string]struct{}
|
||||
UseClientOAuth bool
|
||||
ImpersonateServiceAccount string
|
||||
WriteMode string
|
||||
sessionMutex sync.Mutex
|
||||
makeDataplexCatalogClient func() (*dataplexapi.CatalogClient, DataplexClientCreator, error)
|
||||
SessionProvider BigQuerySessionProvider
|
||||
@@ -279,6 +266,10 @@ func (s *Source) SourceKind() string {
|
||||
return SourceKind
|
||||
}
|
||||
|
||||
func (s *Source) ToConfig() sources.SourceConfig {
|
||||
return s.Config
|
||||
}
|
||||
|
||||
func (s *Source) BigQueryClient() *bigqueryapi.Client {
|
||||
return s.Client
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user