Skip to content

Commit d39962e

Browse files
authored
enhance: generate name for OpenAPI tools when operation ID is blank (#601)
Signed-off-by: Grant Linville <[email protected]>
1 parent a0013e4 commit d39962e

7 files changed

+593
-43
lines changed

pkg/loader/loader_test.go

-42
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"path/filepath"
1111
"testing"
1212

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

71-
func TestLoadOpenAPI(t *testing.T) {
72-
numOpenAPITools := func(set types.ToolSet) int {
73-
num := 0
74-
for _, v := range set {
75-
if v.IsOpenAPI() {
76-
num++
77-
}
78-
}
79-
return num
80-
}
81-
82-
prgv3 := types.Program{
83-
ToolSet: types.ToolSet{},
84-
}
85-
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
86-
require.NoError(t, err)
87-
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
88-
require.NoError(t, err, "failed to read openapi v3")
89-
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")
90-
91-
prgv2json := types.Program{
92-
ToolSet: types.ToolSet{},
93-
}
94-
datav2, err := os.ReadFile("testdata/openapi_v2.json")
95-
require.NoError(t, err)
96-
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
97-
require.NoError(t, err, "failed to read openapi v2")
98-
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")
99-
100-
prgv2yaml := types.Program{
101-
ToolSet: types.ToolSet{},
102-
}
103-
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
104-
require.NoError(t, err)
105-
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
106-
require.NoError(t, err, "failed to read openapi v2 (yaml)")
107-
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")
108-
109-
require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
110-
}
111-
11270
func TestHelloWorld(t *testing.T) {
11371
prg, err := Program(context.Background(),
11472
"https://raw.githubusercontent.com/ibuildthecloud/test/bafe5a62174e8a0ea162277dcfe3a2ddb7eea928/example/sub/tool.gpt",

pkg/loader/openapi.go

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"net/url"
7+
"regexp"
78
"slices"
89
"sort"
910
"strings"
@@ -14,6 +15,8 @@ import (
1415
"github.com/gptscript-ai/gptscript/pkg/types"
1516
)
1617

18+
var toolNameRegex = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
19+
1720
// getOpenAPITools parses an OpenAPI definition and generates a set of tools from it.
1821
// Each operation will become a tool definition.
1922
// The tool's Instructions will be in the format "#!sys.openapi '{JSON Instructions}'",
@@ -115,6 +118,13 @@ func getOpenAPITools(t *openapi3.T, defaultHost string) ([]types.Tool, error) {
115118
toolDesc = toolDesc[:1024]
116119
}
117120

121+
toolName := operation.OperationID
122+
if toolName == "" {
123+
// When there is no operation ID, we use the method + path as the tool name and remove all characters
124+
// except letters, numbers, underscores, and hyphens.
125+
toolName = toolNameRegex.ReplaceAllString(strings.ToLower(method)+strings.ReplaceAll(pathString, "/", "_"), "")
126+
}
127+
118128
var (
119129
// auths are represented as a list of maps, where each map contains the names of the required security schemes.
120130
// Items within the same map are a logical AND. The maps themselves are a logical OR. For example:
@@ -133,7 +143,7 @@ func getOpenAPITools(t *openapi3.T, defaultHost string) ([]types.Tool, error) {
133143
tool := types.Tool{
134144
ToolDef: types.ToolDef{
135145
Parameters: types.Parameters{
136-
Name: operation.OperationID,
146+
Name: toolName,
137147
Description: toolDesc,
138148
Arguments: &openapi3.Schema{
139149
Type: &openapi3.Types{"object"},

pkg/loader/openapi_test.go

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package loader
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/gptscript-ai/gptscript/pkg/types"
9+
"github.com/hexops/autogold/v2"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestLoadOpenAPI(t *testing.T) {
14+
numOpenAPITools := func(set types.ToolSet) int {
15+
num := 0
16+
for _, v := range set {
17+
if v.IsOpenAPI() {
18+
num++
19+
}
20+
}
21+
return num
22+
}
23+
24+
prgv3 := types.Program{
25+
ToolSet: types.ToolSet{},
26+
}
27+
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
28+
require.NoError(t, err)
29+
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
30+
require.NoError(t, err, "failed to read openapi v3")
31+
require.Equal(t, 3, numOpenAPITools(prgv3.ToolSet), "expected 3 openapi tools")
32+
33+
prgv2json := types.Program{
34+
ToolSet: types.ToolSet{},
35+
}
36+
datav2, err := os.ReadFile("testdata/openapi_v2.json")
37+
require.NoError(t, err)
38+
_, err = readTool(context.Background(), nil, &prgv2json, &source{Content: datav2}, "")
39+
require.NoError(t, err, "failed to read openapi v2")
40+
require.Equal(t, 3, numOpenAPITools(prgv2json.ToolSet), "expected 3 openapi tools")
41+
42+
prgv2yaml := types.Program{
43+
ToolSet: types.ToolSet{},
44+
}
45+
datav2, err = os.ReadFile("testdata/openapi_v2.yaml")
46+
require.NoError(t, err)
47+
_, err = readTool(context.Background(), nil, &prgv2yaml, &source{Content: datav2}, "")
48+
require.NoError(t, err, "failed to read openapi v2 (yaml)")
49+
require.Equal(t, 3, numOpenAPITools(prgv2yaml.ToolSet), "expected 3 openapi tools")
50+
51+
require.EqualValuesf(t, prgv2json.ToolSet, prgv2yaml.ToolSet, "expected same toolset for openapi v2 json and yaml")
52+
}
53+
54+
func TestOpenAPIv3(t *testing.T) {
55+
prgv3 := types.Program{
56+
ToolSet: types.ToolSet{},
57+
}
58+
datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
59+
require.NoError(t, err)
60+
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
61+
require.NoError(t, err)
62+
63+
autogold.ExpectFile(t, prgv3.ToolSet, autogold.Dir("testdata/openapi"))
64+
}
65+
66+
func TestOpenAPIv3NoOperationIDs(t *testing.T) {
67+
prgv3 := types.Program{
68+
ToolSet: types.ToolSet{},
69+
}
70+
datav3, err := os.ReadFile("testdata/openapi_v3_no_operation_ids.yaml")
71+
require.NoError(t, err)
72+
_, err = readTool(context.Background(), nil, &prgv3, &source{Content: datav3}, "")
73+
require.NoError(t, err)
74+
75+
autogold.ExpectFile(t, prgv3.ToolSet, autogold.Dir("testdata/openapi"))
76+
}
77+
78+
func TestOpenAPIv2(t *testing.T) {
79+
prgv2 := types.Program{
80+
ToolSet: types.ToolSet{},
81+
}
82+
datav2, err := os.ReadFile("testdata/openapi_v2.yaml")
83+
require.NoError(t, err)
84+
_, err = readTool(context.Background(), nil, &prgv2, &source{Content: datav2}, "")
85+
require.NoError(t, err)
86+
87+
autogold.ExpectFile(t, prgv2.ToolSet, autogold.Dir("testdata/openapi"))
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
types.ToolSet{
2+
":": types.Tool{
3+
ToolDef: types.ToolDef{Parameters: types.Parameters{
4+
Description: "This is a tool set for the Swagger Petstore OpenAPI spec",
5+
ModelName: "gpt-4o",
6+
Export: []string{
7+
"listPets",
8+
"createPets",
9+
"showPetById",
10+
},
11+
}},
12+
ID: ":",
13+
ToolMapping: map[string][]types.ToolReference{
14+
"createPets": {{
15+
Reference: "createPets",
16+
ToolID: ":createPets",
17+
}},
18+
"listPets": {{
19+
Reference: "listPets",
20+
ToolID: ":listPets",
21+
}},
22+
"showPetById": {{
23+
Reference: "showPetById",
24+
ToolID: ":showPetById",
25+
}},
26+
},
27+
LocalTools: map[string]string{
28+
"": ":",
29+
"createpets": ":createPets",
30+
"listpets": ":listPets",
31+
"showpetbyid": ":showPetById",
32+
},
33+
},
34+
":createPets": types.Tool{
35+
ToolDef: types.ToolDef{
36+
Parameters: types.Parameters{
37+
Name: "createPets",
38+
Description: "Create a pet",
39+
ModelName: "gpt-4o",
40+
},
41+
Instructions: `#!sys.openapi '{"server":"http://petstore.swagger.io/v1","path":"/pets","method":"POST","bodyContentMIME":"","apiKeyInfos":null,"queryParameters":null,"pathParameters":null,"headerParameters":null,"cookieParameters":null}'`,
42+
},
43+
ID: ":createPets",
44+
ToolMapping: map[string][]types.ToolReference{},
45+
LocalTools: map[string]string{
46+
"": ":",
47+
"createpets": ":createPets",
48+
"listpets": ":listPets",
49+
"showpetbyid": ":showPetById",
50+
},
51+
Source: types.ToolSource{LineNo: 2},
52+
},
53+
":listPets": types.Tool{
54+
ToolDef: types.ToolDef{
55+
Parameters: types.Parameters{
56+
Name: "listPets",
57+
Description: "List all pets",
58+
ModelName: "gpt-4o",
59+
Arguments: &openapi3.Schema{
60+
Type: &openapi3.Types{
61+
"object",
62+
},
63+
Required: []string{},
64+
Properties: openapi3.Schemas{"limit": &openapi3.SchemaRef{Value: &openapi3.Schema{
65+
Type: &openapi3.Types{"integer"},
66+
Format: "int32",
67+
Description: "How many items to return at one time (max 100)",
68+
}}},
69+
},
70+
},
71+
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}'`,
72+
},
73+
ID: ":listPets",
74+
ToolMapping: map[string][]types.ToolReference{},
75+
LocalTools: map[string]string{
76+
"": ":",
77+
"createpets": ":createPets",
78+
"listpets": ":listPets",
79+
"showpetbyid": ":showPetById",
80+
},
81+
Source: types.ToolSource{LineNo: 1},
82+
},
83+
":showPetById": types.Tool{
84+
ToolDef: types.ToolDef{
85+
Parameters: types.Parameters{
86+
Name: "showPetById",
87+
Description: "Info for a specific pet",
88+
ModelName: "gpt-4o",
89+
Arguments: &openapi3.Schema{
90+
Type: &openapi3.Types{"object"},
91+
Required: []string{"petId"},
92+
Properties: openapi3.Schemas{"petId": &openapi3.SchemaRef{Value: &openapi3.Schema{
93+
Type: &openapi3.Types{"string"},
94+
Description: "The id of the pet to retrieve",
95+
}}},
96+
},
97+
},
98+
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}'`,
99+
},
100+
ID: ":showPetById",
101+
ToolMapping: map[string][]types.ToolReference{},
102+
LocalTools: map[string]string{
103+
"": ":",
104+
"createpets": ":createPets",
105+
"listpets": ":listPets",
106+
"showpetbyid": ":showPetById",
107+
},
108+
Source: types.ToolSource{LineNo: 3},
109+
},
110+
}

0 commit comments

Comments
 (0)