mirror of
https://github.com/googleapis/genai-toolbox.git
synced 2026-02-10 23:25:17 -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.
180 lines
5.0 KiB
Go
180 lines
5.0 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 cmd
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGenerateSkill(t *testing.T) {
|
|
// Create a temporary directory for tests
|
|
tmpDir := t.TempDir()
|
|
outputDir := filepath.Join(tmpDir, "skills")
|
|
|
|
// Create a tools.yaml file with a sqlite tool
|
|
toolsFileContent := `
|
|
sources:
|
|
my-sqlite:
|
|
kind: sqlite
|
|
database: test.db
|
|
tools:
|
|
hello-sqlite:
|
|
kind: sqlite-sql
|
|
source: my-sqlite
|
|
description: "hello tool"
|
|
statement: "SELECT 'hello' as greeting"
|
|
`
|
|
|
|
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
|
|
if err := os.WriteFile(toolsFilePath, []byte(toolsFileContent), 0644); err != nil {
|
|
t.Fatalf("failed to write tools file: %v", err)
|
|
}
|
|
|
|
args := []string{
|
|
"skills-generate",
|
|
"--tools-file", toolsFilePath,
|
|
"--output-dir", outputDir,
|
|
"--name", "hello-sqlite",
|
|
"--description", "hello tool",
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
_, got, err := invokeCommandWithContext(ctx, args)
|
|
if err != nil {
|
|
t.Fatalf("command failed: %v\nOutput: %s", err, got)
|
|
}
|
|
|
|
// Verify generated directory structure
|
|
skillPath := filepath.Join(outputDir, "hello-sqlite")
|
|
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
|
|
t.Fatalf("skill directory not created: %s", skillPath)
|
|
}
|
|
|
|
// Check SKILL.md
|
|
skillMarkdown := filepath.Join(skillPath, "SKILL.md")
|
|
content, err := os.ReadFile(skillMarkdown)
|
|
if err != nil {
|
|
t.Fatalf("failed to read SKILL.md: %v", err)
|
|
}
|
|
|
|
expectedFrontmatter := `---
|
|
name: hello-sqlite
|
|
description: hello tool
|
|
---`
|
|
if !strings.HasPrefix(string(content), expectedFrontmatter) {
|
|
t.Errorf("SKILL.md does not have expected frontmatter format.\nExpected prefix:\n%s\nGot:\n%s", expectedFrontmatter, string(content))
|
|
}
|
|
|
|
if !strings.Contains(string(content), "## Usage") {
|
|
t.Errorf("SKILL.md does not contain '## Usage' section")
|
|
}
|
|
|
|
if !strings.Contains(string(content), "## Scripts") {
|
|
t.Errorf("SKILL.md does not contain '## Scripts' section")
|
|
}
|
|
|
|
if !strings.Contains(string(content), "### hello-sqlite") {
|
|
t.Errorf("SKILL.md does not contain '### hello-sqlite' tool header")
|
|
}
|
|
|
|
// Check script file
|
|
scriptFilename := "hello-sqlite.js"
|
|
scriptPath := filepath.Join(skillPath, "scripts", scriptFilename)
|
|
if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
|
|
t.Fatalf("script file not created: %s", scriptPath)
|
|
}
|
|
|
|
scriptContent, err := os.ReadFile(scriptPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read script file: %v", err)
|
|
}
|
|
if !strings.Contains(string(scriptContent), "hello-sqlite") {
|
|
t.Errorf("script file does not contain expected tool name")
|
|
}
|
|
|
|
// Check assets
|
|
assetPath := filepath.Join(skillPath, "assets", "hello-sqlite.yaml")
|
|
if _, err := os.Stat(assetPath); os.IsNotExist(err) {
|
|
t.Fatalf("asset file not created: %s", assetPath)
|
|
}
|
|
assetContent, err := os.ReadFile(assetPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to read asset file: %v", err)
|
|
}
|
|
if !strings.Contains(string(assetContent), "hello-sqlite") {
|
|
t.Errorf("asset file does not contain expected tool name")
|
|
}
|
|
}
|
|
|
|
func TestGenerateSkill_NoConfig(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
outputDir := filepath.Join(tmpDir, "skills")
|
|
|
|
args := []string{
|
|
"skills-generate",
|
|
"--output-dir", outputDir,
|
|
"--name", "test",
|
|
"--description", "test",
|
|
}
|
|
|
|
_, _, err := invokeCommandWithContext(context.Background(), args)
|
|
if err == nil {
|
|
t.Fatal("expected command to fail when no configuration is provided and tools.yaml is missing")
|
|
}
|
|
|
|
// Should not have created the directory if no config was processed
|
|
if _, err := os.Stat(outputDir); !os.IsNotExist(err) {
|
|
t.Errorf("output directory should not have been created")
|
|
}
|
|
}
|
|
|
|
func TestGenerateSkill_MissingArguments(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
toolsFilePath := filepath.Join(tmpDir, "tools.yaml")
|
|
if err := os.WriteFile(toolsFilePath, []byte("tools: {}"), 0644); err != nil {
|
|
t.Fatalf("failed to write tools file: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{
|
|
name: "missing name",
|
|
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--description", "test"},
|
|
},
|
|
{
|
|
name: "missing description",
|
|
args: []string{"skills-generate", "--tools-file", toolsFilePath, "--name", "test"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, got, err := invokeCommandWithContext(context.Background(), tt.args)
|
|
if err == nil {
|
|
t.Fatalf("expected command to fail due to missing arguments, but it succeeded\nOutput: %s", got)
|
|
}
|
|
})
|
|
}
|
|
}
|