mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-05 04:35:14 -05:00
Compare commits
22 Commits
quickstart
...
adk-docs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86b4566ff2 | ||
|
|
6a90f3fb52 | ||
|
|
a212aedd19 | ||
|
|
9210e5555c | ||
|
|
b43af71793 | ||
|
|
da1f463dd1 | ||
|
|
3265f7e3a6 | ||
|
|
336743f747 | ||
|
|
911069ae8d | ||
|
|
cee59d52c3 | ||
|
|
9517daba09 | ||
|
|
3c61ee0597 | ||
|
|
19271eb9ee | ||
|
|
3a150c77ca | ||
|
|
ca6f31a192 | ||
|
|
d7faf7700f | ||
|
|
37a60ea2a6 | ||
|
|
8de16976ae | ||
|
|
49cb2f39f7 | ||
|
|
f169874e53 | ||
|
|
db8c3a3c77 | ||
|
|
8b33b0c67f |
@@ -0,0 +1,57 @@
|
|||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: "${_IMAGE}"
|
||||||
|
id: "py-pre-post-processing-test"
|
||||||
|
entrypoint: "bash"
|
||||||
|
args:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -ex
|
||||||
|
chmod +x .ci/sample_tests/run_tests.sh
|
||||||
|
.ci/sample_tests/run_tests.sh
|
||||||
|
env:
|
||||||
|
- "CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}"
|
||||||
|
- "GCP_PROJECT=${_GCP_PROJECT}"
|
||||||
|
- "DATABASE_NAME=${_DATABASE_NAME}"
|
||||||
|
- "DB_USER=${_DB_USER}"
|
||||||
|
- "TARGET_ROOT=${_TARGET_ROOT}"
|
||||||
|
- "TARGET_LANG=${_TARGET_LANG}"
|
||||||
|
- "TABLE_NAME=${_TABLE_NAME}"
|
||||||
|
- "SQL_FILE=${_SQL_FILE}"
|
||||||
|
- "AGENT_FILE_PATTERN=${_AGENT_FILE_PATTERN}"
|
||||||
|
secretEnv: ["TOOLS_YAML_CONTENT", "GOOGLE_API_KEY", "DB_PASSWORD"]
|
||||||
|
|
||||||
|
availableSecrets:
|
||||||
|
secretManager:
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_TOOLS_YAML_SECRET}/versions/5
|
||||||
|
env: "TOOLS_YAML_CONTENT"
|
||||||
|
- versionName: projects/${_GCP_PROJECT_NUMBER}/secrets/${_API_KEY_SECRET}/versions/latest
|
||||||
|
env: "GOOGLE_API_KEY"
|
||||||
|
- versionName: projects/${_GCP_PROJECT}/secrets/${_DB_PASS_SECRET}/versions/latest
|
||||||
|
env: "DB_PASSWORD"
|
||||||
|
|
||||||
|
timeout: 1200s
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
_TARGET_LANG: "python"
|
||||||
|
_IMAGE: "gcr.io/google.com/cloudsdktool/cloud-sdk:537.0.0"
|
||||||
|
_TARGET_ROOT: "docs/en/samples/pre_post_processing/python"
|
||||||
|
_TABLE_NAME: "hotels_py_pre_post_processing"
|
||||||
|
_SQL_FILE: ".ci/sample_tests/setup_hotels.sql"
|
||||||
|
_AGENT_FILE_PATTERN: "agent.py"
|
||||||
|
|
||||||
|
options:
|
||||||
|
logging: CLOUD_LOGGING_ONLY
|
||||||
@@ -23,8 +23,8 @@ steps:
|
|||||||
- |
|
- |
|
||||||
set -ex
|
set -ex
|
||||||
export VERSION=$(cat ./cmd/version.txt)
|
export VERSION=$(cat ./cmd/version.txt)
|
||||||
chmod +x .ci/sample_tests/run_tests.sh
|
chmod +x .ci/sample_tests/run_py_tests.sh
|
||||||
.ci/sample_tests/run_tests.sh
|
.ci/sample_tests/run_py_tests.sh
|
||||||
env:
|
env:
|
||||||
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
- 'CLOUD_SQL_INSTANCE=${_CLOUD_SQL_INSTANCE}'
|
||||||
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
- 'GCP_PROJECT=${_GCP_PROJECT}'
|
||||||
|
|||||||
4
.github/workflows/deploy_dev_docs.yaml
vendored
4
.github/workflows/deploy_dev_docs.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
group: docs-deployment
|
group: docs-deployment
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout main branch (for latest templates and theme)
|
- name: Checkout main branch (for latest templates and theme)
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
ref: 'main'
|
ref: 'main'
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout old content from tag into a temporary directory
|
- name: Checkout old content from tag into a temporary directory
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.inputs.version_tag }}
|
ref: ${{ github.event.inputs.version_tag }}
|
||||||
path: 'old_version_source' # Checkout into a temp subdir
|
path: 'old_version_source' # Checkout into a temp subdir
|
||||||
|
|||||||
2
.github/workflows/deploy_versioned_docs.yaml
vendored
2
.github/workflows/deploy_versioned_docs.yaml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code at Tag
|
- name: Checkout Code at Tag
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.release.tag_name }}
|
ref: ${{ github.event.release.tag_name }}
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/docs_preview_clean.yaml
vendored
2
.github/workflows/docs_preview_clean.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
group: "preview-${{ github.event.number }}"
|
group: "preview-${{ github.event.number }}"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
ref: versioned-gh-pages
|
ref: versioned-gh-pages
|
||||||
|
|
||||||
|
|||||||
4
.github/workflows/docs_preview_deploy.yaml
vendored
4
.github/workflows/docs_preview_deploy.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
|||||||
group: "preview-${{ github.event.number }}"
|
group: "preview-${{ github.event.number }}"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
with:
|
with:
|
||||||
# Checkout the PR's HEAD commit (supports forks).
|
# Checkout the PR's HEAD commit (supports forks).
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
@@ -67,7 +67,7 @@ jobs:
|
|||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
|||||||
4
.github/workflows/link_checker_workflow.yaml
vendored
4
.github/workflows/link_checker_workflow.yaml
vendored
@@ -22,10 +22,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
|
|
||||||
- name: Restore lychee cache
|
- name: Restore lychee cache
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
|
||||||
with:
|
with:
|
||||||
path: .lycheecache
|
path: .lycheecache
|
||||||
key: cache-lychee-${{ github.sha }}
|
key: cache-lychee-${{ github.sha }}
|
||||||
|
|||||||
2
.github/workflows/publish-mcp.yml
vendored
2
.github/workflows/publish-mcp.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
|
|
||||||
- name: Wait for image in Artifact Registry
|
- name: Wait for image in Artifact Registry
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import (
|
|||||||
yaml "github.com/goccy/go-yaml"
|
yaml "github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/auth"
|
"github.com/googleapis/genai-toolbox/internal/auth"
|
||||||
"github.com/googleapis/genai-toolbox/internal/cli/invoke"
|
"github.com/googleapis/genai-toolbox/internal/cli/invoke"
|
||||||
"github.com/googleapis/genai-toolbox/internal/cli/skills"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/log"
|
"github.com/googleapis/genai-toolbox/internal/log"
|
||||||
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
|
"github.com/googleapis/genai-toolbox/internal/prebuiltconfigs"
|
||||||
@@ -402,8 +401,6 @@ func NewCommand(opts ...Option) *Command {
|
|||||||
|
|
||||||
// Register subcommands for tool invocation
|
// Register subcommands for tool invocation
|
||||||
baseCmd.AddCommand(invoke.NewCommand(cmd))
|
baseCmd.AddCommand(invoke.NewCommand(cmd))
|
||||||
// Register subcommands for skill generation
|
|
||||||
baseCmd.AddCommand(skills.NewCommand(cmd))
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright 2026 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateSkill(t *testing.T) {
|
|
||||||
// Create a temporary directory for tests
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
outputDir := filepath.Join(tmpDir, "skills")
|
|
||||||
|
|
||||||
// Create a tools.yaml file with a sqlite tool
|
|
||||||
toolsFileContent := `
|
|
||||||
sources:
|
|
||||||
my-sqlite:
|
|
||||||
kind: sqlite
|
|
||||||
database: test.db
|
|
||||||
tools:
|
|
||||||
hello-sqlite:
|
|
||||||
kind: sqlite-sql
|
|
||||||
source: my-sqlite
|
|
||||||
description: "hello tool"
|
|
||||||
statement: "SELECT 'hello' as greeting"
|
|
||||||
`
|
|
||||||
|
|
||||||
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
|
|
||||||
if err := os.WriteFile(toolsFilePath, []byte(toolsFileContent), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to write tools file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"skills-generate",
|
|
||||||
"--tools-file", toolsFilePath,
|
|
||||||
"--output-dir", outputDir,
|
|
||||||
"--name", "hello-sqlite",
|
|
||||||
"--description", "hello tool",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
_, got, err := invokeCommandWithContext(ctx, args)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("command failed: %v\nOutput: %s", err, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify generated directory structure
|
|
||||||
skillPath := filepath.Join(outputDir, "hello-sqlite")
|
|
||||||
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
|
|
||||||
t.Fatalf("skill directory not created: %s", skillPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check SKILL.md
|
|
||||||
skillMarkdown := filepath.Join(skillPath, "SKILL.md")
|
|
||||||
content, err := os.ReadFile(skillMarkdown)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read SKILL.md: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedFrontmatter := `---
|
|
||||||
name: hello-sqlite
|
|
||||||
description: hello tool
|
|
||||||
---`
|
|
||||||
if !strings.HasPrefix(string(content), expectedFrontmatter) {
|
|
||||||
t.Errorf("SKILL.md does not have expected frontmatter format.\nExpected prefix:\n%s\nGot:\n%s", expectedFrontmatter, string(content))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(content), "## Usage") {
|
|
||||||
t.Errorf("SKILL.md does not contain '## Usage' section")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(content), "## Scripts") {
|
|
||||||
t.Errorf("SKILL.md does not contain '## Scripts' section")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(string(content), "### hello-sqlite") {
|
|
||||||
t.Errorf("SKILL.md does not contain '### hello-sqlite' tool header")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check script file
|
|
||||||
scriptFilename := "hello-sqlite.js"
|
|
||||||
scriptPath := filepath.Join(skillPath, "scripts", scriptFilename)
|
|
||||||
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
||||||
t.Fatalf("script file not created: %s", scriptPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptContent, err := os.ReadFile(scriptPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read script file: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(scriptContent), "hello-sqlite") {
|
|
||||||
t.Errorf("script file does not contain expected tool name")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check assets
|
|
||||||
assetPath := filepath.Join(skillPath, "assets", "hello-sqlite.yaml")
|
|
||||||
if _, err := os.Stat(assetPath); os.IsNotExist(err) {
|
|
||||||
t.Fatalf("asset file not created: %s", assetPath)
|
|
||||||
}
|
|
||||||
assetContent, err := os.ReadFile(assetPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to read asset file: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(assetContent), "hello-sqlite") {
|
|
||||||
t.Errorf("asset file does not contain expected tool name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateSkill_NoConfig(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
outputDir := filepath.Join(tmpDir, "skills")
|
|
||||||
|
|
||||||
args := []string{
|
|
||||||
"skills-generate",
|
|
||||||
"--output-dir", outputDir,
|
|
||||||
"--name", "test",
|
|
||||||
"--description", "test",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err := invokeCommandWithContext(context.Background(), args)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected command to fail when no configuration is provided and tools.yaml is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should not have created the directory if no config was processed
|
|
||||||
if _, err := os.Stat(outputDir); !os.IsNotExist(err) {
|
|
||||||
t.Errorf("output directory should not have been created")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateSkill_MissingArguments(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
|
|
||||||
if err := os.WriteFile(toolsFilePath, []byte("tools: {}"), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to write tools file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "missing name",
|
|
||||||
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--description", "test"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing description",
|
|
||||||
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--name", "test"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_, got, err := invokeCommandWithContext(context.Background(), tt.args)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected command to fail due to missing arguments, but it succeeded\nOutput: %s", got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Generate Agent Skills"
|
|
||||||
type: docs
|
|
||||||
weight: 10
|
|
||||||
description: >
|
|
||||||
How to generate agent skills from a toolset.
|
|
||||||
---
|
|
||||||
|
|
||||||
The `skills-generate` command allows you to convert a **toolset** into an **Agent Skill**. A toolset is a collection of tools, and the generated skill will contain metadata and execution scripts for all tools within that toolset, complying with the [Agent Skill specification](https://agentskills.io/specification).
|
|
||||||
|
|
||||||
## Before you begin
|
|
||||||
|
|
||||||
1. Make sure you have the `toolbox` executable in your PATH.
|
|
||||||
2. Make sure you have [Node.js](https://nodejs.org/) installed on your system.
|
|
||||||
|
|
||||||
## Generating a Skill from a Toolset
|
|
||||||
|
|
||||||
A skill package consists of a `SKILL.md` file (with required YAML frontmatter) and a set of Node.js scripts. Each tool defined in your toolset maps to a corresponding script in the generated Node.js scripts (`.js`) that work across different platforms (Linux, macOS, Windows).
|
|
||||||
|
|
||||||
|
|
||||||
### Command Usage
|
|
||||||
|
|
||||||
The basic syntax for the command is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox <tool-source> skills-generate \
|
|
||||||
--name <skill-name> \
|
|
||||||
--toolset <toolset-name> \
|
|
||||||
--description <description> \
|
|
||||||
--output-dir <output-directory>
|
|
||||||
```
|
|
||||||
|
|
||||||
- `<tool-source>`: Can be `--tools-file`, `--tools-files`, `--tools-folder`, and `--prebuilt`. See the [CLI Reference](../reference/cli.md) for details.
|
|
||||||
- `--name`: Name of the generated skill.
|
|
||||||
- `--description`: Description of the generated skill.
|
|
||||||
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, all tools will be included.
|
|
||||||
- `--output-dir`: (Optional) Directory to output generated skills (default: "skills").
|
|
||||||
|
|
||||||
{{< notice note >}}
|
|
||||||
**Note:** The `<skill-name>` must follow the Agent Skill [naming convention](https://agentskills.io/specification): it must contain only lowercase alphanumeric characters and hyphens, cannot start or end with a hyphen, and cannot contain consecutive hyphens (e.g., `my-skill`, `data-processing`).
|
|
||||||
{{< /notice >}}
|
|
||||||
|
|
||||||
### Example: Custom Tools File
|
|
||||||
|
|
||||||
1. Create a `tools.yaml` file with a toolset and some tools:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
tools:
|
|
||||||
tool_a:
|
|
||||||
description: "First tool"
|
|
||||||
run:
|
|
||||||
command: "echo 'Tool A'"
|
|
||||||
tool_b:
|
|
||||||
description: "Second tool"
|
|
||||||
run:
|
|
||||||
command: "echo 'Tool B'"
|
|
||||||
toolsets:
|
|
||||||
my_toolset:
|
|
||||||
tools:
|
|
||||||
- tool_a
|
|
||||||
- tool_b
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Generate the skill:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --tools-file tools.yaml skills-generate \
|
|
||||||
--name "my-skill" \
|
|
||||||
--toolset "my_toolset" \
|
|
||||||
--description "A skill containing multiple tools" \
|
|
||||||
--output-dir "generated-skills"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. The generated skill directory structure:
|
|
||||||
|
|
||||||
```text
|
|
||||||
generated-skills/
|
|
||||||
└── my-skill/
|
|
||||||
├── SKILL.md
|
|
||||||
├── assets/
|
|
||||||
│ ├── tool_a.yaml
|
|
||||||
│ └── tool_b.yaml
|
|
||||||
└── scripts/
|
|
||||||
├── tool_a.js
|
|
||||||
└── tool_b.js
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, the skill contains two Node.js scripts (`tool_a.js` and `tool_b.js`), each mapping to a tool in the original toolset.
|
|
||||||
|
|
||||||
### Example: Prebuilt Configuration
|
|
||||||
|
|
||||||
You can also generate skills from prebuilt toolsets:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --prebuilt alloydb-postgres-admin skills-generate \
|
|
||||||
--name "alloydb-postgres-admin" \
|
|
||||||
--description "skill for performing administrative operations on alloydb"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installing the Generated Skill in Gemini CLI
|
|
||||||
|
|
||||||
Once you have generated a skill, you can install it into the Gemini CLI using the `gemini skills install` command.
|
|
||||||
|
|
||||||
### Installation Command
|
|
||||||
|
|
||||||
Provide the path to the directory containing the generated skill:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gemini skills install /path/to/generated-skills/my-skill
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, use ~/.gemini/skills as the `--output-dir` to generate the skill straight to the Gemini CLI.
|
|
||||||
@@ -20,15 +20,14 @@ The `invoke` command allows you to invoke tools defined in your configuration di
|
|||||||
1. Make sure you have the `toolbox` binary installed or built.
|
1. Make sure you have the `toolbox` binary installed or built.
|
||||||
2. Make sure you have a valid tool configuration file (e.g., `tools.yaml`).
|
2. Make sure you have a valid tool configuration file (e.g., `tools.yaml`).
|
||||||
|
|
||||||
### Command Usage
|
## Basic Usage
|
||||||
|
|
||||||
The basic syntax for the command is:
|
The basic syntax for the command is:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
toolbox <tool-source> invoke <tool-name> [params]
|
toolbox [--tools-file <path> | --prebuilt <name>] invoke <tool-name> [params]
|
||||||
```
|
```
|
||||||
|
|
||||||
- `<tool-source>`: Can be `--tools-file`, `--tools-files`, `--tools-folder`, and `--prebuilt`. See the [CLI Reference](../reference/cli.md) for details.
|
|
||||||
- `<tool-name>`: The name of the tool you want to call. This must match the name defined in your `tools.yaml`.
|
- `<tool-name>`: The name of the tool you want to call. This must match the name defined in your `tools.yaml`.
|
||||||
- `[params]`: (Optional) A JSON string representing the arguments for the tool.
|
- `[params]`: (Optional) A JSON string representing the arguments for the tool.
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ description: >
|
|||||||
|
|
||||||
## Sub Commands
|
## Sub Commands
|
||||||
|
|
||||||
<details>
|
### `invoke`
|
||||||
<summary><code>invoke</code></summary>
|
|
||||||
|
|
||||||
Executes a tool directly with the provided parameters. This is useful for testing tool configurations and parameters without needing a full client setup.
|
Executes a tool directly with the provided parameters. This is useful for testing tool configurations and parameters without needing a full client setup.
|
||||||
|
|
||||||
@@ -43,36 +42,8 @@ Executes a tool directly with the provided parameters. This is useful for testin
|
|||||||
toolbox invoke <tool-name> [params]
|
toolbox invoke <tool-name> [params]
|
||||||
```
|
```
|
||||||
|
|
||||||
**Arguments:**
|
- `<tool-name>`: The name of the tool to execute (as defined in your configuration).
|
||||||
|
- `[params]`: (Optional) A JSON string containing the parameters for the tool.
|
||||||
- `tool-name`: The name of the tool to execute (as defined in your configuration).
|
|
||||||
- `params`: (Optional) A JSON string containing the parameters for the tool.
|
|
||||||
|
|
||||||
For more detailed instructions, see [Invoke Tools via CLI](../how-to/invoke_tool.md).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><code>skills-generate</code></summary>
|
|
||||||
|
|
||||||
Generates a skill package from a specified toolset. Each tool in the toolset will have a corresponding Node.js execution script in the generated skill.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox skills-generate --name <name> --description <description> --toolset <toolset> --output-dir <output>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flags:**
|
|
||||||
|
|
||||||
- `--name`: Name of the generated skill.
|
|
||||||
- `--description`: Description of the generated skill.
|
|
||||||
- `--toolset`: (Optional) Name of the toolset to convert into a skill. If not provided, all tools will be included.
|
|
||||||
- `--output-dir`: (Optional) Directory to output generated skills (default: "skills").
|
|
||||||
|
|
||||||
For more detailed instructions, see [Generate Agent Skills](../how-to/generate_skill.md).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Cloud Logging Admin"
|
|
||||||
linkTitle: "Cloud Logging Admin"
|
|
||||||
type: docs
|
|
||||||
weight: 1
|
|
||||||
description: >
|
|
||||||
Tools that work with Cloud Logging Admin Sources.
|
|
||||||
---
|
|
||||||
47
docs/en/samples/pre_post_processing/_index.md
Normal file
47
docs/en/samples/pre_post_processing/_index.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: "Pre and Post processing"
|
||||||
|
type: docs
|
||||||
|
weight: 1
|
||||||
|
description: >
|
||||||
|
Pre and Post processing in GenAI applications.
|
||||||
|
---
|
||||||
|
|
||||||
|
Pre and post processing allow developers to intercept and modify interactions between the agent and its tools or the user.
|
||||||
|
|
||||||
|
> **Note**: These capabilities are typically features of **orchestration frameworks** (like LangChain, LangGraph, or Agent Builder) rather than the Toolbox SDK itself. However, Toolbox tools are designed to fully leverage these framework capabilities to support robust, secure, and compliant agent architectures.
|
||||||
|
|
||||||
|
## Types of Processing
|
||||||
|
|
||||||
|
### Pre-processing
|
||||||
|
|
||||||
|
Pre-processing occurs before a tool is executed or an agent processes a message. Key types include:
|
||||||
|
|
||||||
|
- **Input Sanitization & Redaction**: Detecting and masking sensitive information (like PII) in user queries or tool arguments to prevent it from being logged or sent to unauthorized systems.
|
||||||
|
- **Business Logic Validation**: Verifying that the proposed action complies with business rules (e.g., ensuring a requested hotel stay does not exceed 14 days, or checking if a user has sufficient permission).
|
||||||
|
- **Security Guardrails**: Analyzing inputs for potential prompt injection attacks or malicious payloads.
|
||||||
|
|
||||||
|
### Post-processing
|
||||||
|
|
||||||
|
Post-processing occurs after a tool has executed or the model has generated a response. Key types include:
|
||||||
|
|
||||||
|
- **Response Enrichment**: Injecting additional data into the tool output that wasn't part of the raw API response (e.g., calculating loyalty points earned based on the booking value).
|
||||||
|
- **Output Formatting**: Transforming raw data (like JSON or XML) into a more human-readable or model-friendly format to improve the agent's understanding.
|
||||||
|
- **Compliance Auditing**: Logging the final outcome of transactions, including the original request and the result, to a secure audit trail.
|
||||||
|
|
||||||
|
## Processing Scopes
|
||||||
|
|
||||||
|
While processing logic can be applied at various levels (Agent, Model, Tool), this guide primarily focuses on **Tool Level** processing, which is most relevant for granular control over tool execution.
|
||||||
|
|
||||||
|
### Tool Level (Primary Focus)
|
||||||
|
|
||||||
|
Wraps individual tool executions. This is best for logic specific to a single tool or a set of tools.
|
||||||
|
|
||||||
|
- **Scope**: Intercepts the raw inputs (arguments) to a tool and its outputs.
|
||||||
|
- **Use Cases**: Argument validation, output formatting, specific privacy rules for sensitive tools.
|
||||||
|
|
||||||
|
### Comparison with Other Levels
|
||||||
|
|
||||||
|
It is helpful to understand how tool-level processing differs from other scopes:
|
||||||
|
|
||||||
|
- **Model Level**: Intercepts individual calls to the LLM (prompts and responses). Unlike tool-level, this applies globally to all text sent/received, making it better for global PII redaction or token tracking.
|
||||||
|
- **Agent Level**: Wraps the high-level execution loop (e.g., a "turn" in the conversation). Unlike tool-level, this envelopes the entire turn (user input to final response), making it suitable for session management or end-to-end auditing.
|
||||||
4
docs/en/samples/pre_post_processing/golden.txt
Normal file
4
docs/en/samples/pre_post_processing/golden.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Final Client Response:
|
||||||
|
AI:
|
||||||
|
Loyalty Points
|
||||||
|
POLICY CHECK: Intercepting 'update-hotel'
|
||||||
31
docs/en/samples/pre_post_processing/python.md
Normal file
31
docs/en/samples/pre_post_processing/python.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: "(Python) Pre and post processing"
|
||||||
|
type: docs
|
||||||
|
weight: 4
|
||||||
|
description: >
|
||||||
|
How to add pre and post processing to your Python toolbox applications.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
This tutorial assumes that you have set up a basic toolbox application as described in the [local quickstart](../../getting-started/local_quickstart).
|
||||||
|
|
||||||
|
This guide demonstrates how to implement these patterns in your Toolbox applications.
|
||||||
|
|
||||||
|
## Python
|
||||||
|
|
||||||
|
{{< tabpane persist=header >}}
|
||||||
|
{{% tab header="ADK" text=true %}}
|
||||||
|
Coming soon.
|
||||||
|
{{% /tab %}}
|
||||||
|
{{% tab header="Langchain" text=true %}}
|
||||||
|
The following example demonstrates how to use `ToolboxClient` with LangChain's middleware to implement pre and post processing for tool calls.
|
||||||
|
|
||||||
|
```py
|
||||||
|
{{< include "python/langchain/agent.py" >}}
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, see the [LangChain Middleware documentation](https://docs.langchain.com/oss/python/langchain/middleware/custom#wrap-style-hooks).
|
||||||
|
You can also add model-level (`wrap_model`) and agent-level (`before_agent`, `after_agent`) hooks to intercept messages at different stages of the execution loop. See the [LangChain Middleware documentation](https://docs.langchain.com/oss/python/langchain/middleware/custom#wrap-style-hooks) for details on these additional hook types.
|
||||||
|
{{% /tab %}}
|
||||||
|
{{< /tabpane >}}
|
||||||
4
docs/en/samples/pre_post_processing/python/__init__.py
Normal file
4
docs/en/samples/pre_post_processing/python/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# This file makes the 'pre_post_processing/python' directory a Python package.
|
||||||
|
|
||||||
|
# You can include any package-level initialization logic here if needed.
|
||||||
|
# For now, this file is empty.
|
||||||
110
docs/en/samples/pre_post_processing/python/adk/agent.py
Normal file
110
docs/en/samples/pre_post_processing/python/adk/agent.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, Optional, Awaitable
|
||||||
|
|
||||||
|
from google.adk import Agent
|
||||||
|
from google.adk.apps import App
|
||||||
|
from google.adk.runners import Runner
|
||||||
|
from google.adk.sessions.in_memory_session_service import InMemorySessionService
|
||||||
|
from google.genai import types
|
||||||
|
from toolbox_adk import ToolboxToolset, CredentialStrategy
|
||||||
|
|
||||||
|
system_prompt = """
|
||||||
|
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||||
|
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||||
|
location and price tier. Always mention hotel ids while performing any
|
||||||
|
searches. This is very important for any operations. For any bookings or
|
||||||
|
cancellations, please provide the appropriate confirmation. Be sure to
|
||||||
|
update checkin or checkout dates if mentioned by the user.
|
||||||
|
Don't ask for confirmations from the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def before_tool_callback(context: Any, args: Dict[str, Any]):
|
||||||
|
"""Enforces business logic: Max stay duration is 14 days."""
|
||||||
|
tool_name = getattr(context, "name", "unknown_tool")
|
||||||
|
print(f"POLICY CHECK: Intercepting '{tool_name}'")
|
||||||
|
|
||||||
|
if tool_name == "update-hotel" or ("checkin_date" in args and "checkout_date" in args):
|
||||||
|
try:
|
||||||
|
start = datetime.fromisoformat(args["checkin_date"])
|
||||||
|
end = datetime.fromisoformat(args["checkout_date"])
|
||||||
|
if (end - start).days > 14:
|
||||||
|
print("BLOCKED: Stay too long")
|
||||||
|
raise ValueError("Error: Maximum stay duration is 14 days.")
|
||||||
|
except ValueError as e:
|
||||||
|
if "Maximum stay duration" in str(e): raise
|
||||||
|
return args
|
||||||
|
|
||||||
|
async def after_tool_callback(context: Any, args: Dict[str, Any], result: Any, error: Optional[Exception]) -> Awaitable[Any]:
|
||||||
|
"""Enriches response for successful bookings."""
|
||||||
|
tool_name = getattr(context, "name", "unknown_tool")
|
||||||
|
if error:
|
||||||
|
print(f"[Tool-Level] Tool '{tool_name}' failed: {error}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(result, str) and "Error" not in result:
|
||||||
|
is_booking = tool_name == "book-hotel" or "booking" in str(result).lower()
|
||||||
|
if is_booking:
|
||||||
|
return f"Booking Confirmed!\n You earned 500 Loyalty Points with this stay.\n\nSystem Details: {result}"
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def run_turn(runner: Runner, user_id: str, session_id: str, text: str):
|
||||||
|
"""Helper to run a single turn."""
|
||||||
|
print(f"\nUSER: '{text}'")
|
||||||
|
response_text = ""
|
||||||
|
async for event in runner.run_async(
|
||||||
|
user_id=user_id, session_id=session_id,
|
||||||
|
new_message=types.Content(role="user", parts=[types.Part(text=text)])
|
||||||
|
):
|
||||||
|
if event.content and event.content.parts:
|
||||||
|
for part in event.content.parts:
|
||||||
|
if part.text:
|
||||||
|
response_text += part.text
|
||||||
|
pass
|
||||||
|
print(f"AI: {response_text}")
|
||||||
|
|
||||||
|
queries = [
|
||||||
|
"Book hotel with id 3.",
|
||||||
|
"Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-02-10",
|
||||||
|
]
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
print("🚀 Initializing ADK Agent with Toolbox...")
|
||||||
|
|
||||||
|
toolset = ToolboxToolset(
|
||||||
|
server_url="http://127.0.0.1:5000",
|
||||||
|
toolset_name="my-toolset",
|
||||||
|
credentials=CredentialStrategy.toolbox_identity(),
|
||||||
|
pre_hook=before_tool_callback,
|
||||||
|
post_hook=after_tool_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
app = App(
|
||||||
|
root_agent=Agent(name='root_agent', model='gemini-2.5-flash', instruction=system_prompt, tools=[toolset]),
|
||||||
|
name="my_agent"
|
||||||
|
)
|
||||||
|
runner = Runner(app=app, session_service=InMemorySessionService())
|
||||||
|
|
||||||
|
user_id, session_id = "test-user", "test-session"
|
||||||
|
await runner.session_service.create_session(app_name=app.name, user_id=user_id, session_id=session_id)
|
||||||
|
|
||||||
|
for query in queries:
|
||||||
|
await run_turn(runner, user_id, session_id, query)
|
||||||
|
print("-" * 50)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
google-adk[toolbox]==1.23.0
|
||||||
|
pytest==9.0.2
|
||||||
59
docs/en/samples/pre_post_processing/python/agent_test.py
Normal file
59
docs/en/samples/pre_post_processing/python/agent_test.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ORCH_NAME = os.environ.get("ORCH_NAME")
|
||||||
|
module_path = f"python.{ORCH_NAME}.agent"
|
||||||
|
agent = importlib.import_module(module_path)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def golden_keywords():
|
||||||
|
"""Loads expected keywords from the golden.txt file."""
|
||||||
|
golden_file_path = Path(__file__).resolve().parent.parent / "golden.txt"
|
||||||
|
if not golden_file_path.exists():
|
||||||
|
pytest.fail(f"Golden file not found: {golden_file_path}")
|
||||||
|
try:
|
||||||
|
with open(golden_file_path, "r") as f:
|
||||||
|
return [line.strip() for line in f.readlines() if line.strip()]
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Could not read golden.txt: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Execution Tests ---
|
||||||
|
class TestExecution:
|
||||||
|
"""Test framework execution and output validation."""
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def script_output(self, capsys):
|
||||||
|
"""Run the agent function and return its output."""
|
||||||
|
asyncio.run(agent.main())
|
||||||
|
return capsys.readouterr()
|
||||||
|
|
||||||
|
def test_script_runs_without_errors(self, script_output):
|
||||||
|
"""Test that the script runs and produces no stderr."""
|
||||||
|
assert script_output.err == "", f"Script produced stderr: {script_output.err}"
|
||||||
|
|
||||||
|
def test_keywords_in_output(self, script_output, golden_keywords):
|
||||||
|
"""Test that expected keywords are present in the script's output."""
|
||||||
|
output = script_output.out
|
||||||
|
print(f"\nAgent Output:\n{output}\n")
|
||||||
|
missing_keywords = [kw for kw in golden_keywords if kw not in output]
|
||||||
|
assert not missing_keywords, f"Missing keywords in output: {missing_keywords}"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# Empty init for package resolution
|
||||||
120
docs/en/samples/pre_post_processing/python/langchain/agent.py
Normal file
120
docs/en/samples/pre_post_processing/python/langchain/agent.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# Copyright 2026 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from langchain.agents import create_agent
|
||||||
|
from langchain.agents.middleware import wrap_tool_call
|
||||||
|
from langchain_core.messages import ToolMessage
|
||||||
|
from langchain_google_vertexai import ChatVertexAI
|
||||||
|
from toolbox_langchain import ToolboxClient
|
||||||
|
|
||||||
|
system_prompt = """
|
||||||
|
You're a helpful hotel assistant. You handle hotel searching, booking and
|
||||||
|
cancellations. When the user searches for a hotel, mention it's name, id,
|
||||||
|
location and price tier. Always mention hotel ids while performing any
|
||||||
|
searches. This is very important for any operations. For any bookings or
|
||||||
|
cancellations, please provide the appropriate confirmation. Be sure to
|
||||||
|
update checkin or checkout dates if mentioned by the user.
|
||||||
|
Don't ask for confirmations from the user.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Pre processing
|
||||||
|
@wrap_tool_call
|
||||||
|
async def enforce_business_rules(request, handler):
|
||||||
|
"""
|
||||||
|
Business Logic Validation:
|
||||||
|
Enforces max stay duration (e.g., max 14 days).
|
||||||
|
"""
|
||||||
|
tool_call = request.tool_call
|
||||||
|
name = tool_call["name"]
|
||||||
|
args = tool_call["args"]
|
||||||
|
|
||||||
|
print(f"POLICY CHECK: Intercepting '{name}'")
|
||||||
|
|
||||||
|
if name == "update-hotel":
|
||||||
|
if "checkin_date" in args and "checkout_date" in args:
|
||||||
|
try:
|
||||||
|
start = datetime.fromisoformat(args["checkin_date"])
|
||||||
|
end = datetime.fromisoformat(args["checkout_date"])
|
||||||
|
duration = (end - start).days
|
||||||
|
|
||||||
|
if duration > 14:
|
||||||
|
print("BLOCKED: Stay too long")
|
||||||
|
return ToolMessage(
|
||||||
|
content="Error: Maximum stay duration is 14 days.",
|
||||||
|
tool_call_id=tool_call["id"],
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass # Ignore invalid date formats
|
||||||
|
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
|
||||||
|
# Post processing
|
||||||
|
@wrap_tool_call
|
||||||
|
async def enrich_response(request, handler):
|
||||||
|
"""
|
||||||
|
Post-Processing & Enrichment:
|
||||||
|
Adds loyalty points information to successful bookings.
|
||||||
|
Standardizes output format.
|
||||||
|
"""
|
||||||
|
result = await handler(request)
|
||||||
|
|
||||||
|
if isinstance(result, ToolMessage):
|
||||||
|
content = str(result.content)
|
||||||
|
tool_name = request.tool_call["name"]
|
||||||
|
|
||||||
|
if tool_name == "book-hotel" and "Error" not in content:
|
||||||
|
loyalty_bonus = 500
|
||||||
|
result.content = f"Booking Confirmed!\n You earned {loyalty_bonus} Loyalty Points with this stay.\n\nSystem Details: {content}"
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with ToolboxClient("http://127.0.0.1:5000") as client:
|
||||||
|
tools = await client.aload_toolset("my-toolset")
|
||||||
|
model = ChatVertexAI(model="gemini-2.5-flash")
|
||||||
|
agent = create_agent(
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
model=model,
|
||||||
|
tools=tools,
|
||||||
|
middleware=[enforce_business_rules, enrich_response],
|
||||||
|
)
|
||||||
|
|
||||||
|
user_input = "Book hotel with id 3."
|
||||||
|
response = await agent.ainvoke(
|
||||||
|
{"messages": [{"role": "user", "content": user_input}]}
|
||||||
|
)
|
||||||
|
|
||||||
|
print("-" * 50)
|
||||||
|
print("Final Client Response:")
|
||||||
|
last_ai_msg = response["messages"][-1].content
|
||||||
|
print(f"AI: {last_ai_msg}")
|
||||||
|
|
||||||
|
# Test Pre-processing
|
||||||
|
print("-" * 50)
|
||||||
|
user_input = "Update my hotel with id 3 with checkin date 2025-01-18 and checkout date 2025-01-20"
|
||||||
|
response = await agent.ainvoke(
|
||||||
|
{"messages": [{"role": "user", "content": user_input}]}
|
||||||
|
)
|
||||||
|
last_ai_msg = response["messages"][-1].content
|
||||||
|
print(f"AI: {last_ai_msg}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
langchain==1.2.6
|
||||||
|
langchain-google-vertexai==3.2.2
|
||||||
|
toolbox-langchain==0.5.8
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
// Copyright 2026 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package skills
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/log"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/resources"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RootCommand defines the interface for required by skills-generate subcommand.
|
|
||||||
// This allows subcommands to access shared resources and functionality without
|
|
||||||
// direct coupling to the root command's implementation.
|
|
||||||
type RootCommand interface {
|
|
||||||
// Config returns a copy of the current server configuration.
|
|
||||||
Config() server.ServerConfig
|
|
||||||
|
|
||||||
// LoadConfig loads and merges the configuration from files, folders, and prebuilts.
|
|
||||||
LoadConfig(ctx context.Context) error
|
|
||||||
|
|
||||||
// Setup initializes the runtime environment, including logging and telemetry.
|
|
||||||
// It returns the updated context and a shutdown function to be called when finished.
|
|
||||||
Setup(ctx context.Context) (context.Context, func(context.Context) error, error)
|
|
||||||
|
|
||||||
// Logger returns the logger instance.
|
|
||||||
Logger() log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Command is the command for generating skills.
|
|
||||||
type Command struct {
|
|
||||||
*cobra.Command
|
|
||||||
rootCmd RootCommand
|
|
||||||
name string
|
|
||||||
description string
|
|
||||||
toolset string
|
|
||||||
outputDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCommand creates a new Command.
|
|
||||||
func NewCommand(rootCmd RootCommand) *cobra.Command {
|
|
||||||
cmd := &Command{
|
|
||||||
rootCmd: rootCmd,
|
|
||||||
}
|
|
||||||
cmd.Command = &cobra.Command{
|
|
||||||
Use: "skills-generate",
|
|
||||||
Short: "Generate skills from tool configurations",
|
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
|
||||||
return cmd.run(c)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&cmd.name, "name", "", "Name of the generated skill.")
|
|
||||||
cmd.Flags().StringVar(&cmd.description, "description", "", "Description of the generated skill")
|
|
||||||
cmd.Flags().StringVar(&cmd.toolset, "toolset", "", "Name of the toolset to convert into a skill. If not provided, all tools will be included.")
|
|
||||||
cmd.Flags().StringVar(&cmd.outputDir, "output-dir", "skills", "Directory to output generated skills")
|
|
||||||
|
|
||||||
_ = cmd.MarkFlagRequired("name")
|
|
||||||
_ = cmd.MarkFlagRequired("description")
|
|
||||||
return cmd.Command
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) run(cmd *cobra.Command) error {
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
ctx, shutdown, err := c.rootCmd.Setup(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = shutdown(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
logger := c.rootCmd.Logger()
|
|
||||||
|
|
||||||
// Load and merge tool configurations
|
|
||||||
if err := c.rootCmd.LoadConfig(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(c.outputDir, 0755); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error creating output directory: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.InfoContext(ctx, fmt.Sprintf("Generating skill '%s'...", c.name))
|
|
||||||
|
|
||||||
// Initialize toolbox and collect tools
|
|
||||||
allTools, err := c.collectTools(ctx)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error collecting tools: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(allTools) == 0 {
|
|
||||||
logger.InfoContext(ctx, "No tools found to generate.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the combined skill directory
|
|
||||||
skillPath := filepath.Join(c.outputDir, c.name)
|
|
||||||
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error creating skill directory: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate assets directory
|
|
||||||
assetsPath := filepath.Join(skillPath, "assets")
|
|
||||||
if err := os.MkdirAll(assetsPath, 0755); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error creating assets dir: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate scripts directory
|
|
||||||
scriptsPath := filepath.Join(skillPath, "scripts")
|
|
||||||
if err := os.MkdirAll(scriptsPath, 0755); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error creating scripts dir: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate over keys to ensure deterministic order
|
|
||||||
var toolNames []string
|
|
||||||
for name := range allTools {
|
|
||||||
toolNames = append(toolNames, name)
|
|
||||||
}
|
|
||||||
sort.Strings(toolNames)
|
|
||||||
|
|
||||||
for _, toolName := range toolNames {
|
|
||||||
// Generate YAML config in asset directory
|
|
||||||
minimizedContent, err := generateToolConfigYAML(c.rootCmd.Config(), toolName)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error generating filtered config for %s: %w", toolName, err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
specificToolsFileName := fmt.Sprintf("%s.yaml", toolName)
|
|
||||||
if minimizedContent != nil {
|
|
||||||
destPath := filepath.Join(assetsPath, specificToolsFileName)
|
|
||||||
if err := os.WriteFile(destPath, minimizedContent, 0644); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error writing filtered config for %s: %w", toolName, err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate wrapper script in scripts directory
|
|
||||||
scriptContent, err := generateScriptContent(toolName, specificToolsFileName)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error generating script content for %s: %w", toolName, err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptFilename := filepath.Join(scriptsPath, fmt.Sprintf("%s.js", toolName))
|
|
||||||
if err := os.WriteFile(scriptFilename, []byte(scriptContent), 0755); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error writing script %s: %w", scriptFilename, err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate SKILL.md
|
|
||||||
skillContent, err := generateSkillMarkdown(c.name, c.description, allTools)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error generating SKILL.md content: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
skillMdPath := filepath.Join(skillPath, "SKILL.md")
|
|
||||||
if err := os.WriteFile(skillMdPath, []byte(skillContent), 0644); err != nil {
|
|
||||||
errMsg := fmt.Errorf("error writing SKILL.md: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.InfoContext(ctx, fmt.Sprintf("Successfully generated skill '%s' with %d tools.", c.name, len(allTools)))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Command) collectTools(ctx context.Context) (map[string]tools.Tool, error) {
|
|
||||||
// Initialize Resources
|
|
||||||
sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := server.InitializeConfigs(ctx, c.rootCmd.Config())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to initialize resources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceMgr := resources.NewResourceManager(sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
|
|
||||||
|
|
||||||
result := make(map[string]tools.Tool)
|
|
||||||
|
|
||||||
if c.toolset == "" {
|
|
||||||
return toolsMap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ts, ok := resourceMgr.GetToolset(c.toolset)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("toolset %q not found", c.toolset)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range ts.Tools {
|
|
||||||
if t != nil {
|
|
||||||
tool := *t
|
|
||||||
result[tool.McpManifest().Name] = tool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
// Copyright 2026 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package skills
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
|
||||||
)
|
|
||||||
|
|
||||||
const skillTemplate = `---
|
|
||||||
name: {{.SkillName}}
|
|
||||||
description: {{.SkillDescription}}
|
|
||||||
---
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
All scripts can be executed using Node.js. Replace ` + "`" + `<param_name>` + "`" + ` and ` + "`" + `<param_value>` + "`" + ` with actual values.
|
|
||||||
|
|
||||||
**Bash:**
|
|
||||||
` + "`" + `node scripts/<script_name>.js '{"<param_name>": "<param_value>"}'` + "`" + `
|
|
||||||
|
|
||||||
**PowerShell:**
|
|
||||||
` + "`" + `node scripts/<script_name>.js '{\"<param_name>\": \"<param_value>\"}'` + "`" + `
|
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
{{range .Tools}}
|
|
||||||
### {{.Name}}
|
|
||||||
|
|
||||||
{{.Description}}
|
|
||||||
|
|
||||||
{{.ParametersSchema}}
|
|
||||||
|
|
||||||
---
|
|
||||||
{{end}}
|
|
||||||
`
|
|
||||||
|
|
||||||
type toolTemplateData struct {
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
ParametersSchema string
|
|
||||||
}
|
|
||||||
|
|
||||||
type skillTemplateData struct {
|
|
||||||
SkillName string
|
|
||||||
SkillDescription string
|
|
||||||
Tools []toolTemplateData
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateSkillMarkdown generates the content of the SKILL.md file.
|
|
||||||
// It includes usage instructions and a reference section for each tool in the skill,
|
|
||||||
// detailing its description and parameters.
|
|
||||||
func generateSkillMarkdown(skillName, skillDescription string, toolsMap map[string]tools.Tool) (string, error) {
|
|
||||||
var toolsData []toolTemplateData
|
|
||||||
|
|
||||||
// Order tools based on name
|
|
||||||
var toolNames []string
|
|
||||||
for name := range toolsMap {
|
|
||||||
toolNames = append(toolNames, name)
|
|
||||||
}
|
|
||||||
sort.Strings(toolNames)
|
|
||||||
|
|
||||||
for _, name := range toolNames {
|
|
||||||
tool := toolsMap[name]
|
|
||||||
manifest := tool.Manifest()
|
|
||||||
|
|
||||||
parametersSchema, err := formatParameters(manifest.Parameters)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
toolsData = append(toolsData, toolTemplateData{
|
|
||||||
Name: name,
|
|
||||||
Description: manifest.Description,
|
|
||||||
ParametersSchema: parametersSchema,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
data := skillTemplateData{
|
|
||||||
SkillName: skillName,
|
|
||||||
SkillDescription: skillDescription,
|
|
||||||
Tools: toolsData,
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("markdown").Parse(skillTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error parsing markdown template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
if err := tmpl.Execute(&buf, data); err != nil {
|
|
||||||
return "", fmt.Errorf("error executing markdown template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodeScriptTemplate = `#!/usr/bin/env node
|
|
||||||
|
|
||||||
const { spawn, execSync } = require('child_process');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
|
|
||||||
const toolName = "{{.Name}}";
|
|
||||||
const toolsFileName = "{{.ToolsFileName}}";
|
|
||||||
|
|
||||||
function getToolboxPath() {
|
|
||||||
try {
|
|
||||||
const checkCommand = process.platform === 'win32' ? 'where toolbox' : 'which toolbox';
|
|
||||||
const globalPath = execSync(checkCommand, { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
||||||
if (globalPath) {
|
|
||||||
return globalPath.split('\n')[0].trim();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore error;
|
|
||||||
}
|
|
||||||
const localPath = path.resolve(__dirname, '../../../toolbox');
|
|
||||||
if (fs.existsSync(localPath)) {
|
|
||||||
return localPath;
|
|
||||||
}
|
|
||||||
throw new Error("Toolbox binary not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolboxBinary;
|
|
||||||
try {
|
|
||||||
toolboxBinary = getToolboxPath();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error:", err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let configArgs = [];
|
|
||||||
if (toolsFileName) {
|
|
||||||
configArgs.push("--tools-file", path.join(__dirname, "..", "assets", toolsFileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const toolboxArgs = [...configArgs, "invoke", toolName, ...args];
|
|
||||||
|
|
||||||
const child = spawn(toolboxBinary, toolboxArgs, { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('close', (code) => {
|
|
||||||
process.exit(code);
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on('error', (err) => {
|
|
||||||
console.error("Error executing toolbox:", err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
`
|
|
||||||
|
|
||||||
type scriptData struct {
|
|
||||||
Name string
|
|
||||||
ToolsFileName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateScriptContent creates the content for a Node.js wrapper script.
|
|
||||||
// This script invokes the toolbox CLI with the appropriate configuration
|
|
||||||
// (using a generated tools file) and arguments to execute the specific tool.
|
|
||||||
func generateScriptContent(name string, toolsFileName string) (string, error) {
|
|
||||||
data := scriptData{
|
|
||||||
Name: name,
|
|
||||||
ToolsFileName: toolsFileName,
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl, err := template.New("script").Parse(nodeScriptTemplate)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error parsing script template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf strings.Builder
|
|
||||||
if err := tmpl.Execute(&buf, data); err != nil {
|
|
||||||
return "", fmt.Errorf("error executing script template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// formatParameters converts a list of parameter manifests into a formatted JSON schema string.
|
|
||||||
// This schema is used in the skill documentation to describe the input parameters for a tool.
|
|
||||||
func formatParameters(params []parameters.ParameterManifest) (string, error) {
|
|
||||||
if len(params) == 0 {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
properties := make(map[string]interface{})
|
|
||||||
var required []string
|
|
||||||
|
|
||||||
for _, p := range params {
|
|
||||||
paramMap := map[string]interface{}{
|
|
||||||
"type": p.Type,
|
|
||||||
"description": p.Description,
|
|
||||||
}
|
|
||||||
if p.Default != nil {
|
|
||||||
paramMap["default"] = p.Default
|
|
||||||
}
|
|
||||||
properties[p.Name] = paramMap
|
|
||||||
if p.Required {
|
|
||||||
required = append(required, p.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
schema := map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": properties,
|
|
||||||
}
|
|
||||||
if len(required) > 0 {
|
|
||||||
schema["required"] = required
|
|
||||||
}
|
|
||||||
|
|
||||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error generating parameters schema: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("#### Parameters\n\n```json\n%s\n```", string(schemaJSON)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateToolConfigYAML generates the YAML configuration for a single tool and its dependency (source).
|
|
||||||
// It extracts the relevant tool and source configurations from the server config and formats them
|
|
||||||
// into a YAML document suitable for inclusion in the skill's assets.
|
|
||||||
func generateToolConfigYAML(cfg server.ServerConfig, toolName string) ([]byte, error) {
|
|
||||||
toolCfg, ok := cfg.ToolConfigs[toolName]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error finding tool config: %s", toolName)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
encoder := yaml.NewEncoder(&buf)
|
|
||||||
|
|
||||||
// Process Tool Config
|
|
||||||
toolWrapper := struct {
|
|
||||||
Kind string `yaml:"kind"`
|
|
||||||
Config tools.ToolConfig `yaml:",inline"`
|
|
||||||
}{
|
|
||||||
Kind: "tools",
|
|
||||||
Config: toolCfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(toolWrapper); err != nil {
|
|
||||||
return nil, fmt.Errorf("error encoding tool config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process Source Config
|
|
||||||
var toolMap map[string]interface{}
|
|
||||||
b, err := yaml.Marshal(toolCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error marshaling tool config: %w", err)
|
|
||||||
}
|
|
||||||
if err := yaml.Unmarshal(b, &toolMap); err != nil {
|
|
||||||
return nil, fmt.Errorf("error unmarshaling tool config map: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if sourceName, ok := toolMap["source"].(string); ok && sourceName != "" {
|
|
||||||
sourceCfg, ok := cfg.SourceConfigs[sourceName]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error finding source config: %s", sourceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceWrapper := struct {
|
|
||||||
Kind string `yaml:"kind"`
|
|
||||||
Config sources.SourceConfig `yaml:",inline"`
|
|
||||||
}{
|
|
||||||
Kind: "sources",
|
|
||||||
Config: sourceCfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := encoder.Encode(sourceWrapper); err != nil {
|
|
||||||
return nil, fmt.Errorf("error encoding source config: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
// Copyright 2026 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package skills
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MockToolConfig struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
Source string `yaml:"source"`
|
|
||||||
Other string `yaml:"other"`
|
|
||||||
Parameters parameters.Parameters `yaml:"parameters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MockToolConfig) ToolConfigType() string {
|
|
||||||
return m.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MockToolConfig) Initialize(map[string]sources.Source) (tools.Tool, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockSourceConfig struct {
|
|
||||||
Name string `yaml:"name"`
|
|
||||||
Type string `yaml:"type"`
|
|
||||||
ConnectionString string `yaml:"connection_string"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MockSourceConfig) SourceConfigType() string {
|
|
||||||
return m.Type
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m MockSourceConfig) Initialize(context.Context, trace.Tracer) (sources.Source, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
params []parameters.ParameterManifest
|
|
||||||
wantContains []string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty parameters",
|
|
||||||
params: []parameters.ParameterManifest{},
|
|
||||||
wantContains: []string{""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single required string parameter",
|
|
||||||
params: []parameters.ParameterManifest{
|
|
||||||
{
|
|
||||||
Name: "param1",
|
|
||||||
Description: "A test parameter",
|
|
||||||
Type: "string",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
"## Parameters",
|
|
||||||
"```json",
|
|
||||||
`"type": "object"`,
|
|
||||||
`"properties": {`,
|
|
||||||
`"param1": {`,
|
|
||||||
`"type": "string"`,
|
|
||||||
`"description": "A test parameter"`,
|
|
||||||
`"required": [`,
|
|
||||||
`"param1"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed parameters with defaults",
|
|
||||||
params: []parameters.ParameterManifest{
|
|
||||||
{
|
|
||||||
Name: "param1",
|
|
||||||
Description: "Param 1",
|
|
||||||
Type: "string",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "param2",
|
|
||||||
Description: "Param 2",
|
|
||||||
Type: "integer",
|
|
||||||
Default: 42,
|
|
||||||
Required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantContains: []string{
|
|
||||||
`"param1": {`,
|
|
||||||
`"param2": {`,
|
|
||||||
`"default": 42`,
|
|
||||||
`"required": [`,
|
|
||||||
`"param1"`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := formatParameters(tt.params)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("formatParameters() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.wantErr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tt.params) == 0 {
|
|
||||||
if got != "" {
|
|
||||||
t.Errorf("formatParameters() = %v, want empty string", got)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
if !strings.Contains(got, want) {
|
|
||||||
t.Errorf("formatParameters() result missing expected string: %s\nGot:\n%s", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateSkillMarkdown(t *testing.T) {
|
|
||||||
toolsMap := map[string]tools.Tool{
|
|
||||||
"tool1": server.MockTool{
|
|
||||||
Description: "First tool",
|
|
||||||
Params: []parameters.Parameter{
|
|
||||||
parameters.NewStringParameter("p1", "d1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := generateSkillMarkdown("MySkill", "My Description", toolsMap)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generateSkillMarkdown() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedSubstrings := []string{
|
|
||||||
"name: MySkill",
|
|
||||||
"description: My Description",
|
|
||||||
"## Usage",
|
|
||||||
"All scripts can be executed using Node.js",
|
|
||||||
"**Bash:**",
|
|
||||||
"`node scripts/<script_name>.js '{\"<param_name>\": \"<param_value>\"}'`",
|
|
||||||
"**PowerShell:**",
|
|
||||||
"`node scripts/<script_name>.js '{\"<param_name>\": \"<param_value>\"}'`",
|
|
||||||
"## Scripts",
|
|
||||||
"### tool1",
|
|
||||||
"First tool",
|
|
||||||
"## Parameters",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range expectedSubstrings {
|
|
||||||
if !strings.Contains(got, s) {
|
|
||||||
t.Errorf("generateSkillMarkdown() missing substring %q", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateScriptContent(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
toolName string
|
|
||||||
toolsFileName string
|
|
||||||
wantContains []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "basic script",
|
|
||||||
toolName: "test-tool",
|
|
||||||
toolsFileName: "",
|
|
||||||
wantContains: []string{
|
|
||||||
`const toolName = "test-tool";`,
|
|
||||||
`const toolsFileName = "";`,
|
|
||||||
`const toolboxArgs = [...configArgs, "invoke", toolName, ...args];`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "script with tools file",
|
|
||||||
toolName: "complex-tool",
|
|
||||||
toolsFileName: "tools.yaml",
|
|
||||||
wantContains: []string{
|
|
||||||
`const toolName = "complex-tool";`,
|
|
||||||
`const toolsFileName = "tools.yaml";`,
|
|
||||||
`configArgs.push("--tools-file", path.join(__dirname, "..", "assets", toolsFileName));`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := generateScriptContent(tt.toolName, tt.toolsFileName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generateScriptContent() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range tt.wantContains {
|
|
||||||
if !strings.Contains(got, s) {
|
|
||||||
t.Errorf("generateScriptContent() missing substring %q\nGot:\n%s", s, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateToolConfigYAML(t *testing.T) {
|
|
||||||
cfg := server.ServerConfig{
|
|
||||||
ToolConfigs: server.ToolConfigs{
|
|
||||||
"tool1": MockToolConfig{
|
|
||||||
Name: "tool1",
|
|
||||||
Type: "custom-tool",
|
|
||||||
Source: "src1",
|
|
||||||
Other: "foo",
|
|
||||||
},
|
|
||||||
"toolNoSource": MockToolConfig{
|
|
||||||
Name: "toolNoSource",
|
|
||||||
Type: "http",
|
|
||||||
},
|
|
||||||
"toolWithParams": MockToolConfig{
|
|
||||||
Name: "toolWithParams",
|
|
||||||
Type: "custom-tool",
|
|
||||||
Parameters: []parameters.Parameter{
|
|
||||||
parameters.NewStringParameter("param1", "desc1"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"toolWithMissingSource": MockToolConfig{
|
|
||||||
Name: "toolWithMissingSource",
|
|
||||||
Type: "custom-tool",
|
|
||||||
Source: "missing-src",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SourceConfigs: server.SourceConfigs{
|
|
||||||
"src1": MockSourceConfig{
|
|
||||||
Name: "src1",
|
|
||||||
Type: "postgres",
|
|
||||||
ConnectionString: "conn1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
toolName string
|
|
||||||
wantContains []string
|
|
||||||
wantErr bool
|
|
||||||
wantNil bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tool with source",
|
|
||||||
toolName: "tool1",
|
|
||||||
wantContains: []string{
|
|
||||||
"kind: tools",
|
|
||||||
"name: tool1",
|
|
||||||
"type: custom-tool",
|
|
||||||
"source: src1",
|
|
||||||
"other: foo",
|
|
||||||
"---",
|
|
||||||
"kind: sources",
|
|
||||||
"name: src1",
|
|
||||||
"type: postgres",
|
|
||||||
"connection_string: conn1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tool without source",
|
|
||||||
toolName: "toolNoSource",
|
|
||||||
wantContains: []string{
|
|
||||||
"kind: tools",
|
|
||||||
"name: toolNoSource",
|
|
||||||
"type: http",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tool with parameters",
|
|
||||||
toolName: "toolWithParams",
|
|
||||||
wantContains: []string{
|
|
||||||
"kind: tools",
|
|
||||||
"name: toolWithParams",
|
|
||||||
"type: custom-tool",
|
|
||||||
"parameters:",
|
|
||||||
"- name: param1",
|
|
||||||
"type: string",
|
|
||||||
"description: desc1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-existent tool",
|
|
||||||
toolName: "missing-tool",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tool with missing source config",
|
|
||||||
toolName: "toolWithMissingSource",
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotBytes, err := generateToolConfigYAML(cfg, tt.toolName)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("generateToolConfigYAML() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.wantErr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantNil {
|
|
||||||
if gotBytes != nil {
|
|
||||||
t.Errorf("generateToolConfigYAML() expected nil, got %s", string(gotBytes))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
got := string(gotBytes)
|
|
||||||
for _, want := range tt.wantContains {
|
|
||||||
if !strings.Contains(got, want) {
|
|
||||||
t.Errorf("generateToolConfigYAML() result missing expected string: %q\nGot:\n%s", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/log"
|
"github.com/googleapis/genai-toolbox/internal/log"
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
"github.com/googleapis/genai-toolbox/internal/prompts"
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/resources"
|
"github.com/googleapis/genai-toolbox/internal/server/resources"
|
||||||
@@ -40,6 +41,140 @@ var (
|
|||||||
_ prompts.Prompt = MockPrompt{}
|
_ prompts.Prompt = MockPrompt{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MockTool is used to mock tools in tests
|
||||||
|
type MockTool struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Params []parameters.Parameter
|
||||||
|
manifest tools.Manifest
|
||||||
|
unauthorized bool
|
||||||
|
requiresClientAuthrorization bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) Invoke(context.Context, tools.SourceProvider, 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) (parameters.ParamValues, error) {
|
||||||
|
return parameters.ParseParams(t.Params, data, claimsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
|
||||||
|
return parameters.EmbedParams(ctx, t.Params, paramValues, embeddingModelsMap, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) Manifest() tools.Manifest {
|
||||||
|
pMs := make([]parameters.ParameterManifest, 0, len(t.Params))
|
||||||
|
for _, p := range t.Params {
|
||||||
|
pMs = append(pMs, p.Manifest())
|
||||||
|
}
|
||||||
|
return tools.Manifest{Description: t.Description, Parameters: pMs}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) Authorized(verifiedAuthServices []string) bool {
|
||||||
|
// defaulted to true
|
||||||
|
return !t.unauthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) RequiresClientAuthorization(tools.SourceProvider) (bool, error) {
|
||||||
|
// defaulted to false
|
||||||
|
return t.requiresClientAuthrorization, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) GetParameters() parameters.Parameters {
|
||||||
|
return t.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) McpManifest() tools.McpManifest {
|
||||||
|
properties := make(map[string]parameters.ParameterMcpManifest)
|
||||||
|
required := make([]string, 0)
|
||||||
|
authParams := make(map[string][]string)
|
||||||
|
|
||||||
|
for _, p := range t.Params {
|
||||||
|
name := p.GetName()
|
||||||
|
paramManifest, authParamList := p.McpManifest()
|
||||||
|
properties[name] = paramManifest
|
||||||
|
required = append(required, name)
|
||||||
|
|
||||||
|
if len(authParamList) > 0 {
|
||||||
|
authParams[name] = authParamList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toolsSchema := parameters.McpToolsSchema{
|
||||||
|
Type: "object",
|
||||||
|
Properties: properties,
|
||||||
|
Required: required,
|
||||||
|
}
|
||||||
|
|
||||||
|
mcpManifest := tools.McpManifest{
|
||||||
|
Name: t.Name,
|
||||||
|
Description: t.Description,
|
||||||
|
InputSchema: toolsSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(authParams) > 0 {
|
||||||
|
mcpManifest.Metadata = map[string]any{
|
||||||
|
"toolbox/authParams": authParams,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mcpManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t MockTool) GetAuthTokenHeaderName(tools.SourceProvider) (string, error) {
|
||||||
|
return "Authorization", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p MockPrompt) ToConfig() prompts.PromptConfig {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var tool1 = MockTool{
|
var tool1 = MockTool{
|
||||||
Name: "no_params",
|
Name: "no_params",
|
||||||
Params: []parameters.Parameter{},
|
Params: []parameters.Parameter{},
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
// Copyright 2026 Google LLC
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/prompts"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockTool is used to mock tools in tests
|
|
||||||
type MockTool struct {
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
Params []parameters.Parameter
|
|
||||||
manifest tools.Manifest
|
|
||||||
unauthorized bool
|
|
||||||
requiresClientAuthrorization bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) Invoke(context.Context, tools.SourceProvider, 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) (parameters.ParamValues, error) {
|
|
||||||
return parameters.ParseParams(t.Params, data, claimsMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) EmbedParams(ctx context.Context, paramValues parameters.ParamValues, embeddingModelsMap map[string]embeddingmodels.EmbeddingModel) (parameters.ParamValues, error) {
|
|
||||||
return parameters.EmbedParams(ctx, t.Params, paramValues, embeddingModelsMap, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) Manifest() tools.Manifest {
|
|
||||||
pMs := make([]parameters.ParameterManifest, 0, len(t.Params))
|
|
||||||
for _, p := range t.Params {
|
|
||||||
pMs = append(pMs, p.Manifest())
|
|
||||||
}
|
|
||||||
return tools.Manifest{Description: t.Description, Parameters: pMs}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) Authorized(verifiedAuthServices []string) bool {
|
|
||||||
// defaulted to true
|
|
||||||
return !t.unauthorized
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) RequiresClientAuthorization(tools.SourceProvider) (bool, error) {
|
|
||||||
// defaulted to false
|
|
||||||
return t.requiresClientAuthrorization, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) GetParameters() parameters.Parameters {
|
|
||||||
return t.Params
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) McpManifest() tools.McpManifest {
|
|
||||||
properties := make(map[string]parameters.ParameterMcpManifest)
|
|
||||||
required := make([]string, 0)
|
|
||||||
authParams := make(map[string][]string)
|
|
||||||
|
|
||||||
for _, p := range t.Params {
|
|
||||||
name := p.GetName()
|
|
||||||
paramManifest, authParamList := p.McpManifest()
|
|
||||||
properties[name] = paramManifest
|
|
||||||
required = append(required, name)
|
|
||||||
|
|
||||||
if len(authParamList) > 0 {
|
|
||||||
authParams[name] = authParamList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toolsSchema := parameters.McpToolsSchema{
|
|
||||||
Type: "object",
|
|
||||||
Properties: properties,
|
|
||||||
Required: required,
|
|
||||||
}
|
|
||||||
|
|
||||||
mcpManifest := tools.McpManifest{
|
|
||||||
Name: t.Name,
|
|
||||||
Description: t.Description,
|
|
||||||
InputSchema: toolsSchema,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(authParams) > 0 {
|
|
||||||
mcpManifest.Metadata = map[string]any{
|
|
||||||
"toolbox/authParams": authParams,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mcpManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t MockTool) GetAuthTokenHeaderName(tools.SourceProvider) (string, error) {
|
|
||||||
return "Authorization", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p MockPrompt) ToConfig() prompts.PromptConfig {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user