mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-11 07:35:05 -05:00
## Description This PR introduces a new skills-generate command that enables users to generate standardized agent skills from their existing Toolbox tool configurations. This facilitates the integration of Toolbox tools into agentic workflows by automatically creating skill descriptions (SKILL.md) and executable wrappers. - New Subcommand: Implemented skills-generate, which automates the creation of agent skill packages including metadata and executable scripts. - Skill Generation: Added logic to generate SKILL.md files with parameter schemas and Node.js wrappers for cross-platform tool execution. - Toolset Integration: Supports selective generation of skills based on defined toolsets, including support for both local files and prebuilt configurations. - Testing: Added unit tests for the generation logic and integration tests for the CLI command. - Documentation: Created a new "how-to" guide for generating skills and updated the CLI reference documentation.
238 lines
7.3 KiB
Go
238 lines
7.3 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package skills
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/googleapis/genai-toolbox/internal/log"
|
|
"github.com/googleapis/genai-toolbox/internal/server"
|
|
"github.com/googleapis/genai-toolbox/internal/server/resources"
|
|
"github.com/googleapis/genai-toolbox/internal/tools"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// RootCommand defines the interface for required by skills-generate subcommand.
|
|
// This allows subcommands to access shared resources and functionality without
|
|
// direct coupling to the root command's implementation.
|
|
type RootCommand interface {
|
|
// Config returns a copy of the current server configuration.
|
|
Config() server.ServerConfig
|
|
|
|
// LoadConfig loads and merges the configuration from files, folders, and prebuilts.
|
|
LoadConfig(ctx context.Context) error
|
|
|
|
// Setup initializes the runtime environment, including logging and telemetry.
|
|
// It returns the updated context and a shutdown function to be called when finished.
|
|
Setup(ctx context.Context) (context.Context, func(context.Context) error, error)
|
|
|
|
// Logger returns the logger instance.
|
|
Logger() log.Logger
|
|
}
|
|
|
|
// Command is the command for generating skills.
|
|
type Command struct {
|
|
*cobra.Command
|
|
rootCmd RootCommand
|
|
name string
|
|
description string
|
|
toolset string
|
|
outputDir string
|
|
}
|
|
|
|
// NewCommand creates a new Command.
|
|
func NewCommand(rootCmd RootCommand) *cobra.Command {
|
|
cmd := &Command{
|
|
rootCmd: rootCmd,
|
|
}
|
|
cmd.Command = &cobra.Command{
|
|
Use: "skills-generate",
|
|
Short: "Generate skills from tool configurations",
|
|
RunE: func(c *cobra.Command, args []string) error {
|
|
return cmd.run(c)
|
|
},
|
|
}
|
|
|
|
cmd.Flags().StringVar(&cmd.name, "name", "", "Name of the generated skill.")
|
|
cmd.Flags().StringVar(&cmd.description, "description", "", "Description of the generated skill")
|
|
cmd.Flags().StringVar(&cmd.toolset, "toolset", "", "Name of the toolset to convert into a skill. If not provided, all tools will be included.")
|
|
cmd.Flags().StringVar(&cmd.outputDir, "output-dir", "skills", "Directory to output generated skills")
|
|
|
|
_ = cmd.MarkFlagRequired("name")
|
|
_ = cmd.MarkFlagRequired("description")
|
|
return cmd.Command
|
|
}
|
|
|
|
func (c *Command) run(cmd *cobra.Command) error {
|
|
ctx, cancel := context.WithCancel(cmd.Context())
|
|
defer cancel()
|
|
|
|
ctx, shutdown, err := c.rootCmd.Setup(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = shutdown(ctx)
|
|
}()
|
|
|
|
logger := c.rootCmd.Logger()
|
|
|
|
// Load and merge tool configurations
|
|
if err := c.rootCmd.LoadConfig(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := os.MkdirAll(c.outputDir, 0755); err != nil {
|
|
errMsg := fmt.Errorf("error creating output directory: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
logger.InfoContext(ctx, fmt.Sprintf("Generating skill '%s'...", c.name))
|
|
|
|
// Initialize toolbox and collect tools
|
|
allTools, err := c.collectTools(ctx)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error collecting tools: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
if len(allTools) == 0 {
|
|
logger.InfoContext(ctx, "No tools found to generate.")
|
|
return nil
|
|
}
|
|
|
|
// Generate the combined skill directory
|
|
skillPath := filepath.Join(c.outputDir, c.name)
|
|
if err := os.MkdirAll(skillPath, 0755); err != nil {
|
|
errMsg := fmt.Errorf("error creating skill directory: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
// Generate assets directory
|
|
assetsPath := filepath.Join(skillPath, "assets")
|
|
if err := os.MkdirAll(assetsPath, 0755); err != nil {
|
|
errMsg := fmt.Errorf("error creating assets dir: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
// Generate scripts directory
|
|
scriptsPath := filepath.Join(skillPath, "scripts")
|
|
if err := os.MkdirAll(scriptsPath, 0755); err != nil {
|
|
errMsg := fmt.Errorf("error creating scripts dir: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
// Iterate over keys to ensure deterministic order
|
|
var toolNames []string
|
|
for name := range allTools {
|
|
toolNames = append(toolNames, name)
|
|
}
|
|
sort.Strings(toolNames)
|
|
|
|
for _, toolName := range toolNames {
|
|
// Generate YAML config in asset directory
|
|
minimizedContent, err := generateToolConfigYAML(c.rootCmd.Config(), toolName)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error generating filtered config for %s: %w", toolName, err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
specificToolsFileName := fmt.Sprintf("%s.yaml", toolName)
|
|
if minimizedContent != nil {
|
|
destPath := filepath.Join(assetsPath, specificToolsFileName)
|
|
if err := os.WriteFile(destPath, minimizedContent, 0644); err != nil {
|
|
errMsg := fmt.Errorf("error writing filtered config for %s: %w", toolName, err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
}
|
|
|
|
// Generate wrapper script in scripts directory
|
|
scriptContent, err := generateScriptContent(toolName, specificToolsFileName)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error generating script content for %s: %w", toolName, err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
scriptFilename := filepath.Join(scriptsPath, fmt.Sprintf("%s.js", toolName))
|
|
if err := os.WriteFile(scriptFilename, []byte(scriptContent), 0755); err != nil {
|
|
errMsg := fmt.Errorf("error writing script %s: %w", scriptFilename, err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
}
|
|
|
|
// Generate SKILL.md
|
|
skillContent, err := generateSkillMarkdown(c.name, c.description, allTools)
|
|
if err != nil {
|
|
errMsg := fmt.Errorf("error generating SKILL.md content: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
skillMdPath := filepath.Join(skillPath, "SKILL.md")
|
|
if err := os.WriteFile(skillMdPath, []byte(skillContent), 0644); err != nil {
|
|
errMsg := fmt.Errorf("error writing SKILL.md: %w", err)
|
|
logger.ErrorContext(ctx, errMsg.Error())
|
|
return errMsg
|
|
}
|
|
|
|
logger.InfoContext(ctx, fmt.Sprintf("Successfully generated skill '%s' with %d tools.", c.name, len(allTools)))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Command) collectTools(ctx context.Context) (map[string]tools.Tool, error) {
|
|
// Initialize Resources
|
|
sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap, err := server.InitializeConfigs(ctx, c.rootCmd.Config())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize resources: %w", err)
|
|
}
|
|
|
|
resourceMgr := resources.NewResourceManager(sourcesMap, authServicesMap, embeddingModelsMap, toolsMap, toolsetsMap, promptsMap, promptsetsMap)
|
|
|
|
result := make(map[string]tools.Tool)
|
|
|
|
if c.toolset == "" {
|
|
return toolsMap, nil
|
|
}
|
|
|
|
ts, ok := resourceMgr.GetToolset(c.toolset)
|
|
if !ok {
|
|
return nil, fmt.Errorf("toolset %q not found", c.toolset)
|
|
}
|
|
|
|
for _, t := range ts.Tools {
|
|
if t != nil {
|
|
tool := *t
|
|
result[tool.McpManifest().Name] = tool
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|