mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-01-30 09:48:27 -05:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6fa798610 | ||
|
|
bb58baff70 | ||
|
|
32b2c9366d |
@@ -1,131 +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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInvokeTool(t *testing.T) {
|
|
||||||
// Create a temporary tools file
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
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"
|
|
||||||
echo-tool:
|
|
||||||
kind: sqlite-sql
|
|
||||||
source: my-sqlite
|
|
||||||
description: "echo tool"
|
|
||||||
statement: "SELECT ? as msg"
|
|
||||||
parameters:
|
|
||||||
- name: message
|
|
||||||
type: string
|
|
||||||
description: message to echo
|
|
||||||
`
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
tcs := []struct {
|
|
||||||
desc string
|
|
||||||
args []string
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
errStr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
desc: "success - basic tool call",
|
|
||||||
args: []string{"invoke", "hello-sqlite", "--tools-file", toolsFilePath},
|
|
||||||
want: `"greeting": "hello"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "success - tool call with parameters",
|
|
||||||
args: []string{"invoke", "echo-tool", `{"message": "world"}`, "--tools-file", toolsFilePath},
|
|
||||||
want: `"msg": "world"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "error - tool not found",
|
|
||||||
args: []string{"invoke", "non-existent", "--tools-file", toolsFilePath},
|
|
||||||
wantErr: true,
|
|
||||||
errStr: `tool "non-existent" not found`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
desc: "error - invalid JSON params",
|
|
||||||
args: []string{"invoke", "echo-tool", `invalid-json`, "--tools-file", toolsFilePath},
|
|
||||||
wantErr: true,
|
|
||||||
errStr: `params must be a valid JSON string`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tcs {
|
|
||||||
t.Run(tc.desc, func(t *testing.T) {
|
|
||||||
_, got, err := invokeCommandWithContext(context.Background(), tc.args)
|
|
||||||
if (err != nil) != tc.wantErr {
|
|
||||||
t.Fatalf("got error %v, wantErr %v", err, tc.wantErr)
|
|
||||||
}
|
|
||||||
if tc.wantErr && !strings.Contains(err.Error(), tc.errStr) {
|
|
||||||
t.Fatalf("got error %v, want error containing %q", err, tc.errStr)
|
|
||||||
}
|
|
||||||
if !tc.wantErr && !strings.Contains(got, tc.want) {
|
|
||||||
t.Fatalf("got %q, want it to contain %q", got, tc.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeTool_AuthUnsupported(t *testing.T) {
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
toolsFileContent := `
|
|
||||||
sources:
|
|
||||||
my-bq:
|
|
||||||
kind: bigquery
|
|
||||||
project: my-project
|
|
||||||
useClientOAuth: true
|
|
||||||
tools:
|
|
||||||
bq-tool:
|
|
||||||
kind: bigquery-sql
|
|
||||||
source: my-bq
|
|
||||||
description: "bq tool"
|
|
||||||
statement: "SELECT 1"
|
|
||||||
`
|
|
||||||
toolsFilePath := filepath.Join(tmpDir, "auth_tools.yaml")
|
|
||||||
if err := os.WriteFile(toolsFilePath, []byte(toolsFileContent), 0644); err != nil {
|
|
||||||
t.Fatalf("failed to write tools file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := []string{"invoke", "bq-tool", "--tools-file", toolsFilePath}
|
|
||||||
_, _, err := invokeCommandWithContext(context.Background(), args)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error for tool requiring client auth, but got nil")
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "client authorization is not supported") {
|
|
||||||
t.Fatalf("unexpected error message: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
205
cmd/root.go
205
cmd/root.go
@@ -34,8 +34,6 @@ import (
|
|||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
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/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"
|
||||||
@@ -367,44 +365,37 @@ func NewCommand(opts ...Option) *Command {
|
|||||||
baseCmd.SetErr(cmd.errStream)
|
baseCmd.SetErr(cmd.errStream)
|
||||||
|
|
||||||
flags := cmd.Flags()
|
flags := cmd.Flags()
|
||||||
persistentFlags := cmd.PersistentFlags()
|
|
||||||
|
|
||||||
flags.StringVarP(&cmd.cfg.Address, "address", "a", "127.0.0.1", "Address of the interface the server will listen on.")
|
flags.StringVarP(&cmd.cfg.Address, "address", "a", "127.0.0.1", "Address of the interface the server will listen on.")
|
||||||
flags.IntVarP(&cmd.cfg.Port, "port", "p", 5000, "Port the server will listen on.")
|
flags.IntVarP(&cmd.cfg.Port, "port", "p", 5000, "Port the server will listen on.")
|
||||||
|
|
||||||
flags.StringVar(&cmd.tools_file, "tools_file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
flags.StringVar(&cmd.tools_file, "tools_file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
||||||
// deprecate tools_file
|
// deprecate tools_file
|
||||||
_ = flags.MarkDeprecated("tools_file", "please use --tools-file instead")
|
_ = flags.MarkDeprecated("tools_file", "please use --tools-file instead")
|
||||||
persistentFlags.StringVar(&cmd.tools_file, "tools-file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
flags.StringVar(&cmd.tools_file, "tools-file", "", "File path specifying the tool configuration. Cannot be used with --tools-files, or --tools-folder.")
|
||||||
persistentFlags.StringSliceVar(&cmd.tools_files, "tools-files", []string{}, "Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --tools-file, or --tools-folder.")
|
flags.StringSliceVar(&cmd.tools_files, "tools-files", []string{}, "Multiple file paths specifying tool configurations. Files will be merged. Cannot be used with --tools-file, or --tools-folder.")
|
||||||
persistentFlags.StringVar(&cmd.tools_folder, "tools-folder", "", "Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --tools-file, or --tools-files.")
|
flags.StringVar(&cmd.tools_folder, "tools-folder", "", "Directory path containing YAML tool configuration files. All .yaml and .yml files in the directory will be loaded and merged. Cannot be used with --tools-file, or --tools-files.")
|
||||||
persistentFlags.Var(&cmd.cfg.LogLevel, "log-level", "Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'.")
|
flags.Var(&cmd.cfg.LogLevel, "log-level", "Specify the minimum level logged. Allowed: 'DEBUG', 'INFO', 'WARN', 'ERROR'.")
|
||||||
persistentFlags.Var(&cmd.cfg.LoggingFormat, "logging-format", "Specify logging format to use. Allowed: 'standard' or 'JSON'.")
|
flags.Var(&cmd.cfg.LoggingFormat, "logging-format", "Specify logging format to use. Allowed: 'standard' or 'JSON'.")
|
||||||
persistentFlags.BoolVar(&cmd.cfg.TelemetryGCP, "telemetry-gcp", false, "Enable exporting directly to Google Cloud Monitoring.")
|
flags.BoolVar(&cmd.cfg.TelemetryGCP, "telemetry-gcp", false, "Enable exporting directly to Google Cloud Monitoring.")
|
||||||
persistentFlags.StringVar(&cmd.cfg.TelemetryOTLP, "telemetry-otlp", "", "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318')")
|
flags.StringVar(&cmd.cfg.TelemetryOTLP, "telemetry-otlp", "", "Enable exporting using OpenTelemetry Protocol (OTLP) to the specified endpoint (e.g. 'http://127.0.0.1:4318')")
|
||||||
persistentFlags.StringVar(&cmd.cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
|
flags.StringVar(&cmd.cfg.TelemetryServiceName, "telemetry-service-name", "toolbox", "Sets the value of the service.name resource attribute for telemetry data.")
|
||||||
// Fetch prebuilt tools sources to customize the help description
|
// Fetch prebuilt tools sources to customize the help description
|
||||||
prebuiltHelp := fmt.Sprintf(
|
prebuiltHelp := fmt.Sprintf(
|
||||||
"Use a prebuilt tool configuration by source type. Allowed: '%s'. Can be specified multiple times.",
|
"Use a prebuilt tool configuration by source type. Allowed: '%s'. Can be specified multiple times.",
|
||||||
strings.Join(prebuiltconfigs.GetPrebuiltSources(), "', '"),
|
strings.Join(prebuiltconfigs.GetPrebuiltSources(), "', '"),
|
||||||
)
|
)
|
||||||
persistentFlags.StringSliceVar(&cmd.prebuiltConfigs, "prebuilt", []string{}, prebuiltHelp)
|
flags.StringSliceVar(&cmd.prebuiltConfigs, "prebuilt", []string{}, prebuiltHelp)
|
||||||
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
|
flags.BoolVar(&cmd.cfg.Stdio, "stdio", false, "Listens via MCP STDIO instead of acting as a remote HTTP server.")
|
||||||
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
|
flags.BoolVar(&cmd.cfg.DisableReload, "disable-reload", false, "Disables dynamic reloading of tools file.")
|
||||||
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
|
flags.BoolVar(&cmd.cfg.UI, "ui", false, "Launches the Toolbox UI web server.")
|
||||||
// TODO: Insecure by default. Might consider updating this for v1.0.0
|
// TODO: Insecure by default. Might consider updating this for v1.0.0
|
||||||
flags.StringSliceVar(&cmd.cfg.AllowedOrigins, "allowed-origins", []string{"*"}, "Specifies a list of origins permitted to access this server. Defaults to '*'.")
|
flags.StringSliceVar(&cmd.cfg.AllowedOrigins, "allowed-origins", []string{"*"}, "Specifies a list of origins permitted to access this server. Defaults to '*'.")
|
||||||
flags.StringSliceVar(&cmd.cfg.AllowedHosts, "allowed-hosts", []string{"*"}, "Specifies a list of hosts permitted to access this server. Defaults to '*'.")
|
flags.StringSliceVar(&cmd.cfg.AllowedHosts, "allowed-hosts", []string{"*"}, "Specifies a list of hosts permitted to access this server. Defaults to '*'.")
|
||||||
persistentFlags.StringSliceVar(&cmd.cfg.UserAgentMetadata, "user-agent-metadata", []string{}, "Appends additional metadata to the User-Agent.")
|
flags.StringSliceVar(&cmd.cfg.UserAgentMetadata, "user-agent-metadata", []string{}, "Appends additional metadata to the User-Agent.")
|
||||||
|
|
||||||
// wrap RunE command so that we have access to original Command object
|
// wrap RunE command so that we have access to original Command object
|
||||||
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
|
cmd.RunE = func(*cobra.Command, []string) error { return run(cmd) }
|
||||||
|
|
||||||
// Register subcommands for tool invocation
|
|
||||||
baseCmd.AddCommand(invoke.NewCommand(cmd))
|
|
||||||
// Register subcommands for skill generation
|
|
||||||
baseCmd.AddCommand(skills.NewCommand(cmd))
|
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,24 +919,71 @@ func resolveWatcherInputs(toolsFile string, toolsFiles []string, toolsFolder str
|
|||||||
return watchDirs, watchedFiles
|
return watchDirs, watchedFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cmd *Command) Config() server.ServerConfig {
|
func run(cmd *Command) error {
|
||||||
return cmd.cfg
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
}
|
defer cancel()
|
||||||
|
|
||||||
func (cmd *Command) Out() io.Writer {
|
// watch for sigterm / sigint signals
|
||||||
return cmd.outStream
|
signals := make(chan os.Signal, 1)
|
||||||
}
|
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
go func(sCtx context.Context) {
|
||||||
|
var s os.Signal
|
||||||
|
select {
|
||||||
|
case <-sCtx.Done():
|
||||||
|
// this should only happen when the context supplied when testing is canceled
|
||||||
|
return
|
||||||
|
case s = <-signals:
|
||||||
|
}
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
cmd.logger.DebugContext(sCtx, "Received SIGINT signal to shutdown.")
|
||||||
|
case syscall.SIGTERM:
|
||||||
|
cmd.logger.DebugContext(sCtx, "Sending SIGTERM signal to shutdown.")
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
func (cmd *Command) Logger() log.Logger {
|
// If stdio, set logger's out stream (usually DEBUG and INFO logs) to errStream
|
||||||
return cmd.logger
|
loggerOut := cmd.outStream
|
||||||
}
|
if cmd.cfg.Stdio {
|
||||||
|
loggerOut = cmd.errStream
|
||||||
func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|
||||||
logger, err := util.LoggerFromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle logger separately from config
|
||||||
|
switch strings.ToLower(cmd.cfg.LoggingFormat.String()) {
|
||||||
|
case "json":
|
||||||
|
logger, err := log.NewStructuredLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize logger: %w", err)
|
||||||
|
}
|
||||||
|
cmd.logger = logger
|
||||||
|
case "standard":
|
||||||
|
logger, err := log.NewStdLogger(loggerOut, cmd.errStream, cmd.cfg.LogLevel.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to initialize logger: %w", err)
|
||||||
|
}
|
||||||
|
cmd.logger = logger
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("logging format invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = util.WithLogger(ctx, cmd.logger)
|
||||||
|
|
||||||
|
// Set up OpenTelemetry
|
||||||
|
otelShutdown, err := telemetry.SetupOTel(ctx, cmd.cfg.Version, cmd.cfg.TelemetryOTLP, cmd.cfg.TelemetryGCP, cmd.cfg.TelemetryServiceName)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Errorf("error setting up OpenTelemetry: %w", err)
|
||||||
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
|
return errMsg
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := otelShutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errMsg := fmt.Errorf("error shutting down OpenTelemetry: %w", err)
|
||||||
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
var allToolsFiles []ToolsFile
|
var allToolsFiles []ToolsFile
|
||||||
|
|
||||||
// Load Prebuilt Configuration
|
// Load Prebuilt Configuration
|
||||||
@@ -954,12 +992,12 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
slices.Sort(cmd.prebuiltConfigs)
|
slices.Sort(cmd.prebuiltConfigs)
|
||||||
sourcesList := strings.Join(cmd.prebuiltConfigs, ", ")
|
sourcesList := strings.Join(cmd.prebuiltConfigs, ", ")
|
||||||
logMsg := fmt.Sprintf("Using prebuilt tool configurations for: %s", sourcesList)
|
logMsg := fmt.Sprintf("Using prebuilt tool configurations for: %s", sourcesList)
|
||||||
logger.InfoContext(ctx, logMsg)
|
cmd.logger.InfoContext(ctx, logMsg)
|
||||||
|
|
||||||
for _, configName := range cmd.prebuiltConfigs {
|
for _, configName := range cmd.prebuiltConfigs {
|
||||||
buf, err := prebuiltconfigs.Get(configName)
|
buf, err := prebuiltconfigs.Get(configName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, err.Error())
|
cmd.logger.ErrorContext(ctx, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -967,7 +1005,7 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
parsed, err := parseToolsFile(ctx, buf)
|
parsed, err := parseToolsFile(ctx, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Errorf("unable to parse prebuilt tool configuration for '%s': %w", configName, err)
|
errMsg := fmt.Errorf("unable to parse prebuilt tool configuration for '%s': %w", configName, err)
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
return errMsg
|
return errMsg
|
||||||
}
|
}
|
||||||
allToolsFiles = append(allToolsFiles, parsed)
|
allToolsFiles = append(allToolsFiles, parsed)
|
||||||
@@ -993,7 +1031,7 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
(cmd.tools_file != "" && cmd.tools_folder != "") ||
|
(cmd.tools_file != "" && cmd.tools_folder != "") ||
|
||||||
(len(cmd.tools_files) > 0 && cmd.tools_folder != "") {
|
(len(cmd.tools_files) > 0 && cmd.tools_folder != "") {
|
||||||
errMsg := fmt.Errorf("--tools-file, --tools-files, and --tools-folder flags cannot be used simultaneously")
|
errMsg := fmt.Errorf("--tools-file, --tools-files, and --tools-folder flags cannot be used simultaneously")
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
return errMsg
|
return errMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,18 +1040,18 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
|
|
||||||
if len(cmd.tools_files) > 0 {
|
if len(cmd.tools_files) > 0 {
|
||||||
// Use tools-files
|
// Use tools-files
|
||||||
logger.InfoContext(ctx, fmt.Sprintf("Loading and merging %d tool configuration files", len(cmd.tools_files)))
|
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging %d tool configuration files", len(cmd.tools_files)))
|
||||||
customTools, err = loadAndMergeToolsFiles(ctx, cmd.tools_files)
|
customTools, err = loadAndMergeToolsFiles(ctx, cmd.tools_files)
|
||||||
} else if cmd.tools_folder != "" {
|
} else if cmd.tools_folder != "" {
|
||||||
// Use tools-folder
|
// Use tools-folder
|
||||||
logger.InfoContext(ctx, fmt.Sprintf("Loading and merging all YAML files from directory: %s", cmd.tools_folder))
|
cmd.logger.InfoContext(ctx, fmt.Sprintf("Loading and merging all YAML files from directory: %s", cmd.tools_folder))
|
||||||
customTools, err = loadAndMergeToolsFolder(ctx, cmd.tools_folder)
|
customTools, err = loadAndMergeToolsFolder(ctx, cmd.tools_folder)
|
||||||
} else {
|
} else {
|
||||||
// Use single file (tools-file or default `tools.yaml`)
|
// Use single file (tools-file or default `tools.yaml`)
|
||||||
buf, readFileErr := os.ReadFile(cmd.tools_file)
|
buf, readFileErr := os.ReadFile(cmd.tools_file)
|
||||||
if readFileErr != nil {
|
if readFileErr != nil {
|
||||||
errMsg := fmt.Errorf("unable to read tool file at %q: %w", cmd.tools_file, readFileErr)
|
errMsg := fmt.Errorf("unable to read tool file at %q: %w", cmd.tools_file, readFileErr)
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
return errMsg
|
return errMsg
|
||||||
}
|
}
|
||||||
customTools, err = parseToolsFile(ctx, buf)
|
customTools, err = parseToolsFile(ctx, buf)
|
||||||
@@ -1023,7 +1061,7 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, err.Error())
|
cmd.logger.ErrorContext(ctx, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
allToolsFiles = append(allToolsFiles, customTools)
|
allToolsFiles = append(allToolsFiles, customTools)
|
||||||
@@ -1045,7 +1083,7 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
// This will error if custom tools collide with prebuilt tools
|
// This will error if custom tools collide with prebuilt tools
|
||||||
finalToolsFile, err := mergeToolsFiles(allToolsFiles...)
|
finalToolsFile, err := mergeToolsFiles(allToolsFiles...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, err.Error())
|
cmd.logger.ErrorContext(ctx, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1056,91 +1094,15 @@ func (cmd *Command) LoadConfig(ctx context.Context) error {
|
|||||||
cmd.cfg.ToolsetConfigs = finalToolsFile.Toolsets
|
cmd.cfg.ToolsetConfigs = finalToolsFile.Toolsets
|
||||||
cmd.cfg.PromptConfigs = finalToolsFile.Prompts
|
cmd.cfg.PromptConfigs = finalToolsFile.Prompts
|
||||||
|
|
||||||
return nil
|
instrumentation, err := telemetry.CreateTelemetryInstrumentation(versionString)
|
||||||
}
|
|
||||||
|
|
||||||
func (cmd *Command) Setup(ctx context.Context) (context.Context, func(context.Context) error, error) {
|
|
||||||
// If stdio, set logger's out stream (usually DEBUG and INFO logs) to errStream
|
|
||||||
loggerOut := cmd.outStream
|
|
||||||
if cmd.cfg.Stdio {
|
|
||||||
loggerOut = cmd.errStream
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle logger separately from config
|
|
||||||
logger, err := log.NewLogger(cmd.cfg.LoggingFormat.String(), cmd.cfg.LogLevel.String(), loggerOut, cmd.errStream)
|
|
||||||
if err != nil {
|
|
||||||
return ctx, nil, fmt.Errorf("unable to initialize logger: %w", err)
|
|
||||||
}
|
|
||||||
cmd.logger = logger
|
|
||||||
|
|
||||||
ctx = util.WithLogger(ctx, cmd.logger)
|
|
||||||
|
|
||||||
// Set up OpenTelemetry
|
|
||||||
otelShutdown, err := telemetry.SetupOTel(ctx, cmd.cfg.Version, cmd.cfg.TelemetryOTLP, cmd.cfg.TelemetryGCP, cmd.cfg.TelemetryServiceName)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error setting up OpenTelemetry: %w", err)
|
|
||||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return ctx, nil, errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownFunc := func(ctx context.Context) error {
|
|
||||||
err := otelShutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error shutting down OpenTelemetry: %w", err)
|
|
||||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
instrumentation, err := telemetry.CreateTelemetryInstrumentation(cmd.cfg.Version)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMsg := fmt.Errorf("unable to create telemetry instrumentation: %w", err)
|
errMsg := fmt.Errorf("unable to create telemetry instrumentation: %w", err)
|
||||||
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
cmd.logger.ErrorContext(ctx, errMsg.Error())
|
||||||
return ctx, shutdownFunc, errMsg
|
return errMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = util.WithInstrumentation(ctx, instrumentation)
|
ctx = util.WithInstrumentation(ctx, instrumentation)
|
||||||
|
|
||||||
return ctx, shutdownFunc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(cmd *Command) error {
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// watch for sigterm / sigint signals
|
|
||||||
signals := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
|
|
||||||
go func(sCtx context.Context) {
|
|
||||||
var s os.Signal
|
|
||||||
select {
|
|
||||||
case <-sCtx.Done():
|
|
||||||
// this should only happen when the context supplied when testing is canceled
|
|
||||||
return
|
|
||||||
case s = <-signals:
|
|
||||||
}
|
|
||||||
switch s {
|
|
||||||
case syscall.SIGINT:
|
|
||||||
cmd.logger.DebugContext(sCtx, "Received SIGINT signal to shutdown.")
|
|
||||||
case syscall.SIGTERM:
|
|
||||||
cmd.logger.DebugContext(sCtx, "Sending SIGTERM signal to shutdown.")
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
}(ctx)
|
|
||||||
|
|
||||||
ctx, shutdown, err := cmd.Setup(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = shutdown(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := cmd.LoadConfig(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start server
|
// start server
|
||||||
s, err := server.NewServer(ctx, cmd.cfg)
|
s, err := server.NewServer(ctx, cmd.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1180,9 +1142,6 @@ func run(cmd *Command) error {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if Custom Files are configured (re-check as loadAndMergeConfig might have updated defaults)
|
|
||||||
isCustomConfigured := cmd.tools_file != "" || len(cmd.tools_files) > 0 || cmd.tools_folder != ""
|
|
||||||
|
|
||||||
if isCustomConfigured && !cmd.cfg.DisableReload {
|
if isCustomConfigured && !cmd.cfg.DisableReload {
|
||||||
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
|
watchDirs, watchedFiles := resolveWatcherInputs(cmd.tools_file, cmd.tools_files, cmd.tools_folder)
|
||||||
// start watching the file(s) or folder for changes to trigger dynamic reloading
|
// start watching the file(s) or folder for changes to trigger dynamic reloading
|
||||||
|
|||||||
@@ -1,166 +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",
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need a longer timeout because generate-skill starts a server and polls it
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(content), "name: hello-sqlite") {
|
|
||||||
t.Errorf("SKILL.md does not contain expected name")
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(content), "description: hello tool") {
|
|
||||||
t.Errorf("SKILL.md does not contain expected description")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,107 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Generate a Skill"
|
|
||||||
type: docs
|
|
||||||
weight: 10
|
|
||||||
description: >
|
|
||||||
How to generate a skill from a toolset configuration.
|
|
||||||
---
|
|
||||||
|
|
||||||
The `skills-generate` command allows you to convert a **toolset** into a **skill**. A toolset is a collection of tools, and the generated skill will contain metadata and execution scripts for all tools within that toolset.
|
|
||||||
|
|
||||||
## 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 and a set of Node.js scripts. Each tool defined in your toolset maps to a corresponding script in the generated skill.
|
|
||||||
|
|
||||||
### Command Signature
|
|
||||||
|
|
||||||
The `skills-generate` command follows this signature:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox [--tools-file <path> | --prebuilt <name>] skills-generate \
|
|
||||||
--name <skill-name> \
|
|
||||||
--toolset <toolset-name> \
|
|
||||||
--description <description> \
|
|
||||||
--output-dir <output-directory>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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-multi-tool-skill" \
|
|
||||||
--toolset "my_toolset" \
|
|
||||||
--description "A skill containing multiple tools" \
|
|
||||||
--output-dir "generated-skills"
|
|
||||||
```
|
|
||||||
|
|
||||||
3. The generated skill directory structure:
|
|
||||||
|
|
||||||
```text
|
|
||||||
generated-skills/
|
|
||||||
└── my-multi-tool-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"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Output Directory
|
|
||||||
|
|
||||||
By default, skills are generated in the `skills` directory. You can specify a different output directory using the `--output-dir` flag.
|
|
||||||
|
|
||||||
## Shared Node.js Scripts
|
|
||||||
|
|
||||||
The `skills-generate` command generates shared Node.js scripts (`.js`) that work across different platforms (Linux, macOS, Windows). This ensures that the generated skills are portable.
|
|
||||||
|
|
||||||
## 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-multi-tool-skill
|
|
||||||
```
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Invoke Tools via CLI"
|
|
||||||
type: docs
|
|
||||||
weight: 10
|
|
||||||
description: >
|
|
||||||
Learn how to invoke your tools directly from the command line using the `invoke` command.
|
|
||||||
---
|
|
||||||
|
|
||||||
The `invoke` command allows you to invoke tools defined in your configuration directly from the CLI. This is useful for:
|
|
||||||
|
|
||||||
- **Ephemeral Invocation:** Executing a tool without spinning up a full MCP server/client.
|
|
||||||
- **Debugging:** Isolating tool execution logic and testing with various parameter combinations.
|
|
||||||
|
|
||||||
{{< notice tip >}}
|
|
||||||
**Keep configurations minimal:** The `invoke` command initializes *all* resources (sources, tools, etc.) defined in your configuration files during execution. To ensure fast response times, consider using a minimal configuration file containing only the tools you need for the specific invocation.
|
|
||||||
{{< notice tip >}}
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- You have the `toolbox` binary installed or built.
|
|
||||||
- You have a valid tool configuration file (e.g., `tools.yaml`).
|
|
||||||
|
|
||||||
## Basic Usage
|
|
||||||
|
|
||||||
The basic syntax for the command is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox [--tools-file <path> | --prebuilt <name>] invoke <tool-name> [params]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `<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.
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### 1. Calling a Tool without Parameters
|
|
||||||
|
|
||||||
If your tool takes no parameters, simply provide the tool name:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --tools-file tools.yaml invoke my-simple-tool
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Calling a Tool with Parameters
|
|
||||||
|
|
||||||
For tools that require arguments, pass them as a JSON string. Ensure you escape quotes correctly for your shell.
|
|
||||||
|
|
||||||
**Example: A tool that takes parameters**
|
|
||||||
|
|
||||||
Assuming a tool named `mytool` taking `a` and `b`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --tools-file tools.yaml invoke mytool '{"a": 10, "b": 20}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example: A tool that queries a database**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --tools-file tools.yaml invoke db-query '{"sql": "SELECT * FROM users LIMIT 5"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Using Prebuilt Configurations
|
|
||||||
|
|
||||||
You can also use the `--prebuilt` flag to load prebuilt toolsets.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox --prebuilt cloudsql-postgres invoke cloudsql-postgres-list-instances
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **Tool not found:** Ensure the `<tool-name>` matches exactly what is in your YAML file and that the file is correctly loaded via `--tools-file`.
|
|
||||||
- **Invalid parameters:** Double-check your JSON syntax. The error message will usually indicate if the JSON parsing failed or if the parameters didn't match the tool's schema.
|
|
||||||
- **Auth errors:** The `invoke` command currently does not support flows requiring client-side authorization (like OAuth flow initiation via the CLI). It works best for tools using service-side authentication (e.g., Application Default Credentials).
|
|
||||||
@@ -30,46 +30,6 @@ description: >
|
|||||||
| | `--user-agent-metadata` | Appends additional metadata to the User-Agent. | |
|
| | `--user-agent-metadata` | Appends additional metadata to the User-Agent. | |
|
||||||
| `-v` | `--version` | version for toolbox | |
|
| `-v` | `--version` | version for toolbox | |
|
||||||
|
|
||||||
## Sub Commands
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<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.
|
|
||||||
|
|
||||||
**Syntax:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
toolbox invoke <tool-name> [params]
|
|
||||||
```
|
|
||||||
|
|
||||||
- `<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.
|
|
||||||
|
|
||||||
</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 [--tools-file <path> | --prebuilt <name>] skills-generate --name <name> --toolset <toolset> --description <description> --output-dir <output>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Flags:**
|
|
||||||
|
|
||||||
- `--name`: (Required) Name of the generated skill.
|
|
||||||
- `--toolset`: (Required) Name of the toolset to convert into a skill.
|
|
||||||
- `--description`: (Required) Description of the generated skill.
|
|
||||||
- `--output-dir`: Directory to output generated skills (default: "skills").
|
|
||||||
|
|
||||||
For more detailed instructions, see [Generate a Skill](../how-to/generate_skill.md).
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Transport Configuration
|
### Transport Configuration
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -52,7 +52,7 @@ require (
|
|||||||
github.com/trinodb/trino-go-client v0.330.0
|
github.com/trinodb/trino-go-client v0.330.0
|
||||||
github.com/valkey-io/valkey-go v1.0.68
|
github.com/valkey-io/valkey-go v1.0.68
|
||||||
github.com/yugabyte/pgx/v5 v5.5.3-yb-5
|
github.com/yugabyte/pgx/v5 v5.5.3-yb-5
|
||||||
go.mongodb.org/mongo-driver/v2 v2.4.2
|
go.mongodb.org/mongo-driver v1.17.4
|
||||||
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
|
go.opentelemetry.io/contrib/propagators/autoprop v0.62.0
|
||||||
go.opentelemetry.io/otel v1.38.0
|
go.opentelemetry.io/otel v1.38.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0
|
||||||
@@ -211,6 +211,7 @@ require (
|
|||||||
github.com/moby/term v0.5.2 // indirect
|
github.com/moby/term v0.5.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
github.com/mtibben/percent v0.2.1 // indirect
|
github.com/mtibben/percent v0.2.1 // indirect
|
||||||
github.com/nakagami/chacha20 v0.1.0 // indirect
|
github.com/nakagami/chacha20 v0.1.0 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1238,6 +1238,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
|||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||||
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
|
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
|
||||||
@@ -1402,8 +1404,8 @@ github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaD
|
|||||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
|
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
|
||||||
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
|
||||||
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.4.2 h1:HrJ+Auygxceby9MLp3YITobef5a8Bv4HcPFIkml1U7U=
|
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.4.2/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
|
|||||||
@@ -1,161 +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 invoke
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"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/util/parameters"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RootCommand defines the interface for required by invoke 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
|
|
||||||
|
|
||||||
// Out returns the writer used for standard output.
|
|
||||||
Out() io.Writer
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCommand(rootCmd RootCommand) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "invoke <tool-name> [params]",
|
|
||||||
Short: "Execute a tool directly",
|
|
||||||
Long: `Execute a tool directly with parameters.
|
|
||||||
Params must be a JSON string.
|
|
||||||
Example:
|
|
||||||
toolbox invoke my-tool '{"param1": "value1"}'`,
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
|
||||||
return runInvoke(c, args, rootCmd)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func runInvoke(cmd *cobra.Command, args []string, rootCmd RootCommand) error {
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
ctx, shutdown, err := rootCmd.Setup(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = shutdown(ctx)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Load and merge tool configurations
|
|
||||||
if err := rootCmd.LoadConfig(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Resources
|
|
||||||
sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := server.InitializeConfigs(ctx, rootCmd.Config())
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("failed to initialize resources: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceMgr := resources.NewResourceManager(sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
|
|
||||||
|
|
||||||
// Execute Tool
|
|
||||||
toolName := args[0]
|
|
||||||
tool, ok := resourceMgr.GetTool(toolName)
|
|
||||||
if !ok {
|
|
||||||
errMsg := fmt.Errorf("tool %q not found", toolName)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
var paramsInput string
|
|
||||||
if len(args) > 1 {
|
|
||||||
paramsInput = args[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
params := make(map[string]any)
|
|
||||||
if paramsInput != "" {
|
|
||||||
if err := json.Unmarshal([]byte(paramsInput), ¶ms); err != nil {
|
|
||||||
errMsg := fmt.Errorf("params must be a valid JSON string: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedParams, err := parameters.ParseParams(tool.GetParameters(), params, nil)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("invalid parameters: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedParams, err = tool.EmbedParams(ctx, parsedParams, resourceMgr.GetEmbeddingModelMap())
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error embedding parameters: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client Auth not supported for ephemeral CLI call
|
|
||||||
requiresAuth, err := tool.RequiresClientAuthorization(resourceMgr)
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("failed to check auth requirements: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
if requiresAuth {
|
|
||||||
errMsg := fmt.Errorf("client authorization is not supported")
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := tool.Invoke(ctx, resourceMgr, parsedParams, "")
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("tool execution failed: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print Result
|
|
||||||
output, err := json.MarshalIndent(result, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("failed to marshal result: %w", err)
|
|
||||||
rootCmd.Logger().ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
fmt.Fprintln(rootCmd.Out(), string(output))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,298 +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"
|
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/log"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server"
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/server/resources"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RootCommand defines the interface for required by invoke 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parameter represents a parameter of a tool.
|
|
||||||
type Parameter struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Default interface{} `json:"default"`
|
|
||||||
Required bool `json:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tool represents a tool.
|
|
||||||
type Tool struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Parameters []Parameter `json:"parameters"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config represents the structure of the tools.yaml file.
|
|
||||||
type Config struct {
|
|
||||||
Sources map[string]interface{} `yaml:"sources,omitempty"`
|
|
||||||
Tools map[string]map[string]interface{} `yaml:"tools"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serverConfig holds the configuration used to start the toolbox server.
|
|
||||||
type serverConfig struct {
|
|
||||||
prebuiltConfigs []string
|
|
||||||
toolsFile 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.outputDir, "output-dir", "skills", "Directory to output generated skills")
|
|
||||||
cmd.Flags().StringVar(&cmd.toolset, "toolset", "", "Name of the toolset (and generated skill folder). If provided, only tools in this toolset are generated.")
|
|
||||||
cmd.Flags().StringVar(&cmd.name, "name", "", "Name of the generated skill.")
|
|
||||||
cmd.Flags().StringVar(&cmd.description, "description", "", "Description of the generated skill")
|
|
||||||
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()
|
|
||||||
|
|
||||||
toolsFile, err := cmd.Flags().GetString("tools-file")
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error getting tools-file flag: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
prebuiltConfigs, err := cmd.Flags().GetStringSlice("prebuilt")
|
|
||||||
if err != nil {
|
|
||||||
errMsg := fmt.Errorf("error getting prebuilt flag: %w", err)
|
|
||||||
logger.ErrorContext(ctx, errMsg.Error())
|
|
||||||
return errMsg
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and merge tool configurations
|
|
||||||
if err := c.rootCmd.LoadConfig(ctx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(prebuiltConfigs) == 0 && toolsFile == "" {
|
|
||||||
logger.InfoContext(ctx, "No configurations found to process. Use --tools-file or --prebuilt.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
|
|
||||||
config := serverConfig{
|
|
||||||
prebuiltConfigs: prebuiltConfigs,
|
|
||||||
toolsFile: toolsFile,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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 if needed
|
|
||||||
assetsPath := filepath.Join(skillPath, "assets")
|
|
||||||
if toolsFile != "" {
|
|
||||||
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
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tool := range allTools {
|
|
||||||
specificToolsFileName := ""
|
|
||||||
if toolsFile != "" {
|
|
||||||
minimizedContent, err := generateFilteredConfig(toolsFile, tool.Name)
|
|
||||||
if err != nil {
|
|
||||||
logger.ErrorContext(ctx, fmt.Sprintf("Error generating filtered config for %s: %v", tool.Name, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if minimizedContent != nil {
|
|
||||||
specificToolsFileName = fmt.Sprintf("%s.yaml", tool.Name)
|
|
||||||
destPath := filepath.Join(assetsPath, specificToolsFileName)
|
|
||||||
if err := os.WriteFile(destPath, minimizedContent, 0644); err != nil {
|
|
||||||
logger.ErrorContext(ctx, fmt.Sprintf("Error writing filtered config for %s: %v", tool.Name, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptContent, err := generateShellScriptContent(tool.Name, config, specificToolsFileName)
|
|
||||||
if err != nil {
|
|
||||||
logger.ErrorContext(ctx, fmt.Sprintf("Error generating script content for %s: %v", tool.Name, err))
|
|
||||||
} else {
|
|
||||||
scriptFilename := filepath.Join(scriptsPath, fmt.Sprintf("%s.js", tool.Name))
|
|
||||||
if err := os.WriteFile(scriptFilename, []byte(scriptContent), 0755); err != nil {
|
|
||||||
logger.ErrorContext(ctx, fmt.Sprintf("Error writing script %s: %v", scriptFilename, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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]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]Tool)
|
|
||||||
|
|
||||||
var toolsToProcess []string
|
|
||||||
|
|
||||||
if c.toolset != "" {
|
|
||||||
ts, ok := resourceMgr.GetToolset(c.toolset)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("toolset %q not found", c.toolset)
|
|
||||||
}
|
|
||||||
toolsToProcess = ts.ToolNames
|
|
||||||
} else {
|
|
||||||
// All tools
|
|
||||||
for name := range toolsMap {
|
|
||||||
toolsToProcess = append(toolsToProcess, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, toolName := range toolsToProcess {
|
|
||||||
t, ok := resourceMgr.GetTool(toolName)
|
|
||||||
if !ok {
|
|
||||||
// Should happen only if toolset refers to non-existent tool, but good to check
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
params := []Parameter{}
|
|
||||||
for _, p := range t.GetParameters() {
|
|
||||||
manifest := p.Manifest()
|
|
||||||
params = append(params, Parameter{
|
|
||||||
Name: p.GetName(),
|
|
||||||
Description: manifest.Description, // Use description from manifest
|
|
||||||
Type: p.GetType(),
|
|
||||||
Default: p.GetDefault(),
|
|
||||||
Required: p.GetRequired(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest := t.Manifest()
|
|
||||||
result[toolName] = Tool{
|
|
||||||
Name: toolName,
|
|
||||||
Description: manifest.Description,
|
|
||||||
Parameters: params,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,247 +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 (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
)
|
|
||||||
|
|
||||||
const skillTemplate = `---
|
|
||||||
name: {{.SkillName}}
|
|
||||||
description: {{.SkillDescription}}
|
|
||||||
---
|
|
||||||
|
|
||||||
Here is a list of scripts which can be used.
|
|
||||||
|
|
||||||
{{range .Tools}}
|
|
||||||
# {{.Name}}
|
|
||||||
|
|
||||||
{{.Description}}
|
|
||||||
|
|
||||||
{{.ParametersSchema}}
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
{{.Usage}}
|
|
||||||
|
|
||||||
---
|
|
||||||
{{end}}
|
|
||||||
`
|
|
||||||
|
|
||||||
type toolTemplateData struct {
|
|
||||||
Name string
|
|
||||||
Description string
|
|
||||||
ParametersSchema string
|
|
||||||
Usage string
|
|
||||||
}
|
|
||||||
|
|
||||||
type skillTemplateData struct {
|
|
||||||
SkillName string
|
|
||||||
SkillDescription string
|
|
||||||
Tools []toolTemplateData
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSkillMarkdown(skillName, skillDescription string, toolsMap map[string]Tool) (string, error) {
|
|
||||||
var toolsData []toolTemplateData
|
|
||||||
|
|
||||||
// Order tools based on name
|
|
||||||
var tools []Tool
|
|
||||||
for _, tool := range toolsMap {
|
|
||||||
tools = append(tools, tool)
|
|
||||||
}
|
|
||||||
sort.Slice(tools, func(i, j int) bool {
|
|
||||||
return tools[i].Name < tools[j].Name
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, tool := range tools {
|
|
||||||
parametersSchema, err := formatParameters(tool.Parameters)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
usage := fmt.Sprintf("`bash\nnode scripts/%s.js '{\"<param_name>\": \"<param_value>\"}'\n`", tool.Name)
|
|
||||||
|
|
||||||
toolsData = append(toolsData, toolTemplateData{
|
|
||||||
Name: tool.Name,
|
|
||||||
Description: tool.Description,
|
|
||||||
ParametersSchema: parametersSchema,
|
|
||||||
Usage: usage,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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 } = require('child_process');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const toolName = "{{.Name}}";
|
|
||||||
const prebuiltNames = {{.PrebuiltNamesJSON}};
|
|
||||||
const toolsFileName = "{{.ToolsFileName}}";
|
|
||||||
|
|
||||||
let configArgs = [];
|
|
||||||
if (prebuiltNames.length > 0) {
|
|
||||||
prebuiltNames.forEach(name => {
|
|
||||||
configArgs.push("--prebuilt", name);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toolsFileName) {
|
|
||||||
configArgs.push("--tools-file", path.join(__dirname, "..", "assets", toolsFileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
const toolboxArgs = [...configArgs, "invoke", toolName, ...args];
|
|
||||||
|
|
||||||
const command = process.platform === 'win32' ? 'toolbox.exe' : 'toolbox';
|
|
||||||
|
|
||||||
const child = spawn(command, 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
|
|
||||||
PrebuiltNamesJSON string
|
|
||||||
ToolsFileName string
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateShellScriptContent(name string, config serverConfig, toolsFileName string) (string, error) {
|
|
||||||
prebuiltJSON, _ := json.Marshal(config.prebuiltConfigs)
|
|
||||||
|
|
||||||
data := scriptData{
|
|
||||||
Name: name,
|
|
||||||
PrebuiltNamesJSON: string(prebuiltJSON),
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatParameters(params []Parameter) (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
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateFilteredConfig(toolsFile, toolName string) ([]byte, error) {
|
|
||||||
data, err := os.ReadFile(toolsFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading %s: %w", toolsFile, err)
|
|
||||||
}
|
|
||||||
var cfg Config
|
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing YAML from %s: %w", toolsFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cfg.Tools[toolName]; !ok {
|
|
||||||
return nil, nil // Tool not found in this file
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredCfg := Config{
|
|
||||||
Tools: map[string]map[string]interface{}{
|
|
||||||
toolName: cfg.Tools[toolName],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add relevant source if exists
|
|
||||||
if src, ok := cfg.Tools[toolName]["source"].(string); ok && src != "" {
|
|
||||||
if sourceData, exists := cfg.Sources[src]; exists {
|
|
||||||
if filteredCfg.Sources == nil {
|
|
||||||
filteredCfg.Sources = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
filteredCfg.Sources[src] = sourceData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredData, err := yaml.Marshal(filteredCfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error marshaling filtered tools for %s: %w", toolName, err)
|
|
||||||
}
|
|
||||||
return filteredData, nil
|
|
||||||
}
|
|
||||||
@@ -1,300 +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 (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/goccy/go-yaml"
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFormatParameters(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
params []Parameter
|
|
||||||
wantContains []string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty parameters",
|
|
||||||
params: []Parameter{},
|
|
||||||
wantContains: []string{""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single required string parameter",
|
|
||||||
params: []Parameter{
|
|
||||||
{
|
|
||||||
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: []Parameter{
|
|
||||||
{
|
|
||||||
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) {
|
|
||||||
tools := map[string]Tool{
|
|
||||||
"tool1": {
|
|
||||||
Name: "tool1",
|
|
||||||
Description: "First tool",
|
|
||||||
Parameters: []Parameter{
|
|
||||||
{Name: "p1", Type: "string", Description: "d1", Required: true},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := generateSkillMarkdown("MySkill", "My Description", tools)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generateSkillMarkdown() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedSubstrings := []string{
|
|
||||||
"name: MySkill",
|
|
||||||
"description: My Description",
|
|
||||||
"# tool1",
|
|
||||||
"First tool",
|
|
||||||
"## Parameters",
|
|
||||||
"node scripts/tool1.js '{\"<param_name>\": \"<param_value>\"}'",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range expectedSubstrings {
|
|
||||||
if !strings.Contains(got, s) {
|
|
||||||
t.Errorf("generateSkillMarkdown() missing substring %q", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateShellScriptContent(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
toolName string
|
|
||||||
config serverConfig
|
|
||||||
toolsFileName string
|
|
||||||
wantContains []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "basic script",
|
|
||||||
toolName: "test-tool",
|
|
||||||
config: serverConfig{
|
|
||||||
prebuiltConfigs: []string{},
|
|
||||||
},
|
|
||||||
toolsFileName: "",
|
|
||||||
wantContains: []string{
|
|
||||||
`const toolName = "test-tool";`,
|
|
||||||
`const prebuiltNames = [];`,
|
|
||||||
`const toolsFileName = "";`,
|
|
||||||
`const toolboxArgs = [...configArgs, "invoke", toolName, ...args];`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "script with prebuilts and tools file",
|
|
||||||
toolName: "complex-tool",
|
|
||||||
config: serverConfig{
|
|
||||||
prebuiltConfigs: []string{"pre1", "pre2"},
|
|
||||||
},
|
|
||||||
toolsFileName: "tools.yaml",
|
|
||||||
wantContains: []string{
|
|
||||||
`const toolName = "complex-tool";`,
|
|
||||||
`const prebuiltNames = ["pre1","pre2"];`,
|
|
||||||
`const toolsFileName = "tools.yaml";`,
|
|
||||||
`configArgs.push("--prebuilt", name);`,
|
|
||||||
`configArgs.push("--tools-file", path.join(__dirname, "..", "assets", toolsFileName));`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := generateShellScriptContent(tt.toolName, tt.config, tt.toolsFileName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("generateShellScriptContent() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range tt.wantContains {
|
|
||||||
if !strings.Contains(got, s) {
|
|
||||||
t.Errorf("generateShellScriptContent() missing substring %q\nGot:\n%s", s, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateFilteredConfig(t *testing.T) {
|
|
||||||
// Setup temporary directory and file
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
toolsFile := filepath.Join(tmpDir, "tools.yaml")
|
|
||||||
|
|
||||||
configContent := `
|
|
||||||
sources:
|
|
||||||
src1:
|
|
||||||
type: "postgres"
|
|
||||||
connection_string: "conn1"
|
|
||||||
src2:
|
|
||||||
type: "mysql"
|
|
||||||
connection_string: "conn2"
|
|
||||||
tools:
|
|
||||||
tool1:
|
|
||||||
source: "src1"
|
|
||||||
query: "SELECT 1"
|
|
||||||
tool2:
|
|
||||||
source: "src2"
|
|
||||||
query: "SELECT 2"
|
|
||||||
tool3:
|
|
||||||
type: "http" # No source
|
|
||||||
`
|
|
||||||
if err := os.WriteFile(toolsFile, []byte(configContent), 0644); err != nil {
|
|
||||||
t.Fatalf("Failed to create temp tools file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
toolName string
|
|
||||||
wantCfg Config
|
|
||||||
wantErr bool
|
|
||||||
wantNil bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tool with source",
|
|
||||||
toolName: "tool1",
|
|
||||||
wantCfg: Config{
|
|
||||||
Sources: map[string]interface{}{
|
|
||||||
"src1": map[string]interface{}{
|
|
||||||
"type": "postgres",
|
|
||||||
"connection_string": "conn1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tools: map[string]map[string]interface{}{
|
|
||||||
"tool1": {
|
|
||||||
"source": "src1",
|
|
||||||
"query": "SELECT 1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tool without source",
|
|
||||||
toolName: "tool3",
|
|
||||||
wantCfg: Config{
|
|
||||||
Tools: map[string]map[string]interface{}{
|
|
||||||
"tool3": {
|
|
||||||
"type": "http",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-existent tool",
|
|
||||||
toolName: "missing-tool",
|
|
||||||
wantNil: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
gotBytes, err := generateFilteredConfig(toolsFile, tt.toolName)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("generateFilteredConfig() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.wantErr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantNil {
|
|
||||||
if gotBytes != nil {
|
|
||||||
t.Errorf("generateFilteredConfig() expected nil, got %s", string(gotBytes))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotCfg Config
|
|
||||||
if err := yaml.Unmarshal(gotBytes, &gotCfg); err != nil {
|
|
||||||
t.Errorf("Failed to unmarshal result: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.wantCfg, gotCfg); diff != "" {
|
|
||||||
t.Errorf("generateFilteredConfig() mismatch (-want +got):\n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,18 +22,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewLogger creates a new logger based on the provided format and level.
|
|
||||||
func NewLogger(format, level string, out, err io.Writer) (Logger, error) {
|
|
||||||
switch strings.ToLower(format) {
|
|
||||||
case "json":
|
|
||||||
return NewStructuredLogger(out, err, level)
|
|
||||||
case "standard":
|
|
||||||
return NewStdLogger(out, err, level)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("logging format invalid: %s", format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StdLogger is the standard logger
|
// StdLogger is the standard logger
|
||||||
type StdLogger struct {
|
type StdLogger struct {
|
||||||
outLogger *slog.Logger
|
outLogger *slog.Logger
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util"
|
"github.com/googleapis/genai-toolbox/internal/util"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
"go.opentelemetry.io/otel/trace"
|
"go.opentelemetry.io/otel/trace"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ func (s *Source) Aggregate(ctx context.Context, pipelineString string, canonical
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) Find(ctx context.Context, filterString, database, collection string, opts *options.FindOptionsBuilder) ([]any, error) {
|
func (s *Source) Find(ctx context.Context, filterString, database, collection string, opts *options.FindOptions) ([]any, error) {
|
||||||
var filter = bson.D{}
|
var filter = bson.D{}
|
||||||
err := bson.UnmarshalExtJSON([]byte(filterString), false, &filter)
|
err := bson.UnmarshalExtJSON([]byte(filterString), false, &filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,7 +163,7 @@ func (s *Source) Find(ctx context.Context, filterString, database, collection st
|
|||||||
return parseData(ctx, cur)
|
return parseData(ctx, cur)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) FindOne(ctx context.Context, filterString, database, collection string, opts *options.FindOneOptionsBuilder) ([]any, error) {
|
func (s *Source) FindOne(ctx context.Context, filterString, database, collection string, opts *options.FindOneOptions) ([]any, error) {
|
||||||
var filter = bson.D{}
|
var filter = bson.D{}
|
||||||
err := bson.UnmarshalExtJSON([]byte(filterString), false, &filter)
|
err := bson.UnmarshalExtJSON([]byte(filterString), false, &filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -233,7 +233,7 @@ func (s *Source) UpdateMany(ctx context.Context, filterString string, canonical
|
|||||||
return nil, fmt.Errorf("unable to unmarshal update string: %w", err)
|
return nil, fmt.Errorf("unable to unmarshal update string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.MongoClient().Database(database).Collection(collection).UpdateMany(ctx, filter, update, options.UpdateMany().SetUpsert(upsert))
|
res, err := s.MongoClient().Database(database).Collection(collection).UpdateMany(ctx, filter, update, options.Update().SetUpsert(upsert))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error updating collection: %w", err)
|
return nil, fmt.Errorf("error updating collection: %w", err)
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@ func (s *Source) UpdateOne(ctx context.Context, filterString string, canonical b
|
|||||||
return nil, fmt.Errorf("unable to unmarshal update string: %w", err)
|
return nil, fmt.Errorf("unable to unmarshal update string: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.MongoClient().Database(database).Collection(collection).UpdateOne(ctx, filter, update, options.UpdateOne().SetUpsert(upsert))
|
res, err := s.MongoClient().Database(database).Collection(collection).UpdateOne(ctx, filter, update, options.Update().SetUpsert(upsert))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error updating collection: %w", err)
|
return nil, fmt.Errorf("error updating collection: %w", err)
|
||||||
}
|
}
|
||||||
@@ -266,7 +266,7 @@ func (s *Source) DeleteMany(ctx context.Context, filterString, database, collect
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.MongoClient().Database(database).Collection(collection).DeleteMany(ctx, filter, options.DeleteMany())
|
res, err := s.MongoClient().Database(database).Collection(collection).DeleteMany(ctx, filter, options.Delete())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ func (s *Source) DeleteOne(ctx context.Context, filterString, database, collecti
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.MongoClient().Database(database).Collection(collection).DeleteOne(ctx, filter, options.DeleteOne())
|
res, err := s.MongoClient().Database(database).Collection(collection).DeleteOne(ctx, filter, options.Delete())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -303,7 +303,7 @@ func initMongoDBClient(ctx context.Context, tracer trace.Tracer, name, uri strin
|
|||||||
|
|
||||||
// Create a new MongoDB client
|
// Create a new MongoDB client
|
||||||
clientOpts := options.Client().ApplyURI(uri).SetAppName(userAgent)
|
clientOpts := options.Client().ApplyURI(uri).SetAppName(userAgent)
|
||||||
client, err := mongo.Connect(clientOpts)
|
client, err := mongo.Connect(ctx, clientOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to create MongoDB client: %w", err)
|
return nil, fmt.Errorf("unable to create MongoDB client: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ import (
|
|||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util"
|
"github.com/googleapis/genai-toolbox/internal/util"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
@@ -48,7 +48,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
|||||||
|
|
||||||
type compatibleSource interface {
|
type compatibleSource interface {
|
||||||
MongoClient() *mongo.Client
|
MongoClient() *mongo.Client
|
||||||
Find(context.Context, string, string, string, *options.FindOptionsBuilder) ([]any, error)
|
Find(context.Context, string, string, string, *options.FindOptions) ([]any, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -118,7 +118,7 @@ type Tool struct {
|
|||||||
mcpManifest tools.McpManifest
|
mcpManifest tools.McpManifest
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOptions(ctx context.Context, sortParameters parameters.Parameters, projectPayload string, limit int64, paramsMap map[string]any) (*options.FindOptionsBuilder, error) {
|
func getOptions(ctx context.Context, sortParameters parameters.Parameters, projectPayload string, limit int64, paramsMap map[string]any) (*options.FindOptions, error) {
|
||||||
logger, err := util.LoggerFromContext(ctx)
|
logger, err := util.LoggerFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import (
|
|||||||
"github.com/goccy/go-yaml"
|
"github.com/goccy/go-yaml"
|
||||||
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
"github.com/googleapis/genai-toolbox/internal/embeddingmodels"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/bson"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
@@ -47,7 +47,7 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (tools.T
|
|||||||
|
|
||||||
type compatibleSource interface {
|
type compatibleSource interface {
|
||||||
MongoClient() *mongo.Client
|
MongoClient() *mongo.Client
|
||||||
FindOne(context.Context, string, string, string, *options.FindOneOptionsBuilder) ([]any, error)
|
FindOne(context.Context, string, string, string, *options.FindOneOptions) ([]any, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const resourceType string = "mongodb-insert-many"
|
const resourceType string = "mongodb-insert-many"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const resourceType string = "mongodb-insert-one"
|
const resourceType string = "mongodb-insert-one"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const resourceType string = "mongodb-update-many"
|
const resourceType string = "mongodb-update-many"
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/googleapis/genai-toolbox/internal/sources"
|
"github.com/googleapis/genai-toolbox/internal/sources"
|
||||||
"github.com/googleapis/genai-toolbox/internal/tools"
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
||||||
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
"github.com/googleapis/genai-toolbox/internal/util/parameters"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
)
|
)
|
||||||
|
|
||||||
const resourceType string = "mongodb-update-one"
|
const resourceType string = "mongodb-update-one"
|
||||||
|
|||||||
60
internal/util/errors.go
Normal file
60
internal/util/errors.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// 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 util
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type ErrorCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CategoryAgent ErrorCategory = "AGENT_ERROR"
|
||||||
|
CategoryServer ErrorCategory = "SERVER_ERROR"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToolboxError is the interface all custom errors must satisfy
|
||||||
|
type ToolboxError interface {
|
||||||
|
error
|
||||||
|
Category() ErrorCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent Errors
|
||||||
|
type AgentError struct {
|
||||||
|
Msg string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AgentError) Error() string { return e.Msg }
|
||||||
|
|
||||||
|
func (e *AgentError) Category() ErrorCategory { return CategoryAgent }
|
||||||
|
|
||||||
|
func (e *AgentError) Unwrap() error { return e.Cause }
|
||||||
|
|
||||||
|
func NewAgentError(msg string, args ...any) *AgentError {
|
||||||
|
return &AgentError{Msg: fmt.Sprintf(msg, args...)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server Errors
|
||||||
|
type ServerError struct {
|
||||||
|
Msg string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ServerError) Error() string { return fmt.Sprintf("%s: %v", e.Msg, e.Cause) }
|
||||||
|
|
||||||
|
func (e *ServerError) Category() ErrorCategory { return CategoryServer }
|
||||||
|
|
||||||
|
func (e *ServerError) Unwrap() error { return e.Cause }
|
||||||
|
|
||||||
|
func NewServerError(msg string, cause error) *ServerError {
|
||||||
|
return &ServerError{Msg: msg, Cause: cause}
|
||||||
|
}
|
||||||
@@ -28,8 +28,8 @@ import (
|
|||||||
|
|
||||||
"github.com/googleapis/genai-toolbox/internal/testutils"
|
"github.com/googleapis/genai-toolbox/internal/testutils"
|
||||||
"github.com/googleapis/genai-toolbox/tests"
|
"github.com/googleapis/genai-toolbox/tests"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -55,7 +55,7 @@ func getMongoDBVars(t *testing.T) map[string]any {
|
|||||||
|
|
||||||
func initMongoDbDatabase(ctx context.Context, uri, database string) (*mongo.Database, error) {
|
func initMongoDbDatabase(ctx context.Context, uri, database string) (*mongo.Database, error) {
|
||||||
// Create a new mongodb Database
|
// Create a new mongodb Database
|
||||||
client, err := mongo.Connect(options.Client().ApplyURI(uri))
|
client, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to connect to mongodb: %s", err)
|
return nil, fmt.Errorf("unable to connect to mongodb: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user