Skip to content

Commit

Permalink
enhance: generate name for OpenAPI tools when operation ID is blank (#…
Browse files Browse the repository at this point in the history
…601)

Signed-off-by: Grant Linville <[email protected]>
  • Loading branch information
g-linville authored Jul 2, 2024
1 parent a0013e4 commit d39962e
Show file tree
Hide file tree
Showing 7 changed files with 593 additions and 43 deletions.
42 changes: 0 additions & 42 deletions pkg/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"path/filepath"
"testing"

"github.com/gptscript-ai/gptscript/pkg/types"
"github.com/hexops/autogold/v2"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -68,47 +67,6 @@ func TestIsOpenAPI(t *testing.T) {
require.Equal(t, 3, v, "(json) expected openapi v3")
}

func TestLoadOpenAPI(t *testing.T) {
numOpenAPITools := func(set types.ToolSet) int {
num := 0
for _, v := range set {
if v.IsOpenAPI() {
num++
}
}
return num
}

prgv3 := types.Program{
ToolSet: types.ToolSet{},
}
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
require.NoError(t, err, "failed to read openapi v3")
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")

prgv2json := types.Program{
ToolSet: types.ToolSet{},
}
datav2, err := os.ReadFile("testdata/openapi_v2.json")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
require.NoError(t, err, "failed to read openapi v2")
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")

prgv2yaml := types.Program{
ToolSet: types.ToolSet{},
}
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
require.NoError(t, err, "failed to read openapi v2 (yaml)")
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")

require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
}

func TestHelloWorld(t *testing.T) {
prg, err := Program(context.Background(),
"https://raw.githubusercontent.com/ibuildthecloud/test/bafe5a62174e8a0ea162277dcfe3a2ddb7eea928/example/sub/tool.gpt",
Expand Down
12 changes: 11 additions & 1 deletion pkg/loader/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/url"
"regexp"
"slices"
"sort"
"strings"
Expand All @@ -14,6 +15,8 @@ import (
"github.com/gptscript-ai/gptscript/pkg/types"
)

var toolNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)

// getOpenAPITools parses an OpenAPI definition and generates a set of tools from it.
// Each operation will become a tool definition.
// The tool's Instructions will be in the format "#!sys.openapi '{JSON Instructions}'",
Expand Down Expand Up @@ -115,6 +118,13 @@ func getOpenAPITools(t *openapi3.T, defaultHost string) ([]types.Tool, error) {
toolDesc = toolDesc[:1024]
}

toolName := operation.OperationID
if toolName == "" {
// When there is no operation ID, we use the method + path as the tool name and remove all characters
// except letters, numbers, underscores, and hyphens.
toolName = toolNameRegex.ReplaceAllString(strings.ToLower(method)+strings.ReplaceAll(pathString, "/", "_"), "")
}

var (
// auths are represented as a list of maps, where each map contains the names of the required security schemes.
// Items within the same map are a logical AND. The maps themselves are a logical OR. For example:
Expand All @@ -133,7 +143,7 @@ func getOpenAPITools(t *openapi3.T, defaultHost string) ([]types.Tool, error) {
tool := types.Tool{
ToolDef: types.ToolDef{
Parameters: types.Parameters{
Name: operation.OperationID,
Name: toolName,
Description: toolDesc,
Arguments: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Expand Down
88 changes: 88 additions & 0 deletions pkg/loader/openapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package loader

import (
"context"
"os"
"testing"

"github.com/gptscript-ai/gptscript/pkg/types"
"github.com/hexops/autogold/v2"
"github.com/stretchr/testify/require"
)

func TestLoadOpenAPI(t *testing.T) {
numOpenAPITools := func(set types.ToolSet) int {
num := 0
for _, v := range set {
if v.IsOpenAPI() {
num++
}
}
return num
}

prgv3 := types.Program{
ToolSet: types.ToolSet{},
}
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
require.NoError(t, err, "failed to read openapi v3")
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")

prgv2json := types.Program{
ToolSet: types.ToolSet{},
}
datav2, err := os.ReadFile("testdata/openapi_v2.json")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
require.NoError(t, err, "failed to read openapi v2")
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")

prgv2yaml := types.Program{
ToolSet: types.ToolSet{},
}
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
require.NoError(t, err, "failed to read openapi v2 (yaml)")
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")

require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
}

func TestOpenAPIv3(t *testing.T) {
prgv3 := types.Program{
ToolSet: types.ToolSet{},
}
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
require.NoError(t, err)

autogold.ExpectFile(t, prgv3.ToolSet, autogold.Dir("testdata/openapi"))
}

func TestOpenAPIv3NoOperationIDs(t *testing.T) {
prgv3 := types.Program{
ToolSet: types.ToolSet{},
}
datav3, err := os.ReadFile("testdata/openapi_v3_no_operation_ids.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
require.NoError(t, err)

autogold.ExpectFile(t, prgv3.ToolSet, autogold.Dir("testdata/openapi"))
}

func TestOpenAPIv2(t *testing.T) {
prgv2 := types.Program{
ToolSet: types.ToolSet{},
}
datav2, err := os.ReadFile("testdata/openapi_v2.yaml")
require.NoError(t, err)
_, err = readTool(context.Background(), nil, &prgv2, &source{Content: datav2}, "")
require.NoError(t, err)

autogold.ExpectFile(t, prgv2.ToolSet, autogold.Dir("testdata/openapi"))
}
110 changes: 110 additions & 0 deletions pkg/loader/testdata/openapi/TestOpenAPIv2.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
types.ToolSet{
":": types.Tool{
ToolDef: types.ToolDef{Parameters: types.Parameters{
Description: "This is a tool set for the Swagger Petstore OpenAPI spec",
ModelName: "gpt-4o",
Export: []string{
"listPets",
"createPets",
"showPetById",
},
}},
ID: ":",
ToolMapping: map[string][]types.ToolReference{
"createPets": {{
Reference: "createPets",
ToolID: ":createPets",
}},
"listPets": {{
Reference: "listPets",
ToolID: ":listPets",
}},
"showPetById": {{
Reference: "showPetById",
ToolID: ":showPetById",
}},
},
LocalTools: map[string]string{
"": ":",
"createpets": ":createPets",
"listpets": ":listPets",
"showpetbyid": ":showPetById",
},
},
":createPets": types.Tool{
ToolDef: types.ToolDef{
Parameters: types.Parameters{
Name: "createPets",
Description: "Create a pet",
ModelName: "gpt-4o",
},
Instructions: `#!sys.openapi '{"server":"http://petstore.swagger.io/v1","path":"/pets","method":"POST","bodyContentMIME":"","apiKeyInfos":null,"queryParameters":null,"pathParameters":null,"headerParameters":null,"cookieParameters":null}'`,
},
ID: ":createPets",
ToolMapping: map[string][]types.ToolReference{},
LocalTools: map[string]string{
"": ":",
"createpets": ":createPets",
"listpets": ":listPets",
"showpetbyid": ":showPetById",
},
Source: types.ToolSource{LineNo: 2},
},
":listPets": types.Tool{
ToolDef: types.ToolDef{
Parameters: types.Parameters{
Name: "listPets",
Description: "List all pets",
ModelName: "gpt-4o",
Arguments: &openapi3.Schema{
Type: &openapi3.Types{
"object",
},
Required: []string{},
Properties: openapi3.Schemas{"limit": &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"integer"},
Format: "int32",
Description: "How many items to return at one time (max 100)",
}}},
},
},
Instructions: `#!sys.openapi '{"server":"http://petstore.swagger.io/v1","path":"/pets","method":"GET","bodyContentMIME":"","apiKeyInfos":null,"queryParameters":[{"name":"limit","style":"","explode":null}],"pathParameters":null,"headerParameters":null,"cookieParameters":null}'`,
},
ID: ":listPets",
ToolMapping: map[string][]types.ToolReference{},
LocalTools: map[string]string{
"": ":",
"createpets": ":createPets",
"listpets": ":listPets",
"showpetbyid": ":showPetById",
},
Source: types.ToolSource{LineNo: 1},
},
":showPetById": types.Tool{
ToolDef: types.ToolDef{
Parameters: types.Parameters{
Name: "showPetById",
Description: "Info for a specific pet",
ModelName: "gpt-4o",
Arguments: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Required: []string{"petId"},
Properties: openapi3.Schemas{"petId": &openapi3.SchemaRef{Value: &openapi3.Schema{
Type: &openapi3.Types{"string"},
Description: "The id of the pet to retrieve",
}}},
},
},
Instructions: `#!sys.openapi '{"server":"http://petstore.swagger.io/v1","path":"/pets/{petId}","method":"GET","bodyContentMIME":"","apiKeyInfos":null,"queryParameters":null,"pathParameters":[{"name":"petId","style":"","explode":null}],"headerParameters":null,"cookieParameters":null}'`,
},
ID: ":showPetById",
ToolMapping: map[string][]types.ToolReference{},
LocalTools: map[string]string{
"": ":",
"createpets": ":createPets",
"listpets": ":listPets",
"showpetbyid": ":showPetById",
},
Source: types.ToolSource{LineNo: 3},
},
}
Loading

0 comments on commit d39962e

Please sign in to comment.