Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ require (
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
golang.org/x/sync v0.7.0
golang.org/x/term v0.19.0
gopkg.in/yaml.v3 v3.0.1
sigs.k8s.io/yaml v1.4.0
)

require (
Expand Down Expand Up @@ -80,7 +82,6 @@ require (
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.20.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
mvdan.cc/gofumpt v0.6.0 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -488,3 +488,5 @@ mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
76 changes: 73 additions & 3 deletions pkg/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import (
"path"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"unicode/utf8"

"github.com/getkin/kin-openapi/openapi2"

"github.com/getkin/kin-openapi/openapi2conv"
"github.com/getkin/kin-openapi/openapi3"
"github.com/gptscript-ai/gptscript/pkg/assemble"
"github.com/gptscript-ai/gptscript/pkg/builtin"
Expand All @@ -24,6 +28,8 @@ import (
"github.com/gptscript-ai/gptscript/pkg/parser"
"github.com/gptscript-ai/gptscript/pkg/system"
"github.com/gptscript-ai/gptscript/pkg/types"
"gopkg.in/yaml.v3"
kyaml "sigs.k8s.io/yaml"
)

const CacheTimeout = time.Hour
Expand Down Expand Up @@ -142,9 +148,34 @@ func loadOpenAPI(prg *types.Program, data []byte) *openapi3.T {
prg.OpenAPICache = map[string]any{}
}

openAPIDocument, err = openapi3.NewLoader().LoadFromData(data)
if err != nil || openAPIDocument.Paths.Len() == 0 {
openAPIDocument = nil
switch isOpenAPI(data) {
case 2:
// Convert OpenAPI v2 to v3
jsondata := data
if !json.Valid(data) {
jsondata, err = kyaml.YAMLToJSON(data)
if err != nil {
return nil
}
}

doc := &openapi2.T{}
if err := doc.UnmarshalJSON(jsondata); err != nil {
return nil
}

openAPIDocument, err = openapi2conv.ToV3(doc)
if err != nil {
return nil
}
case 3:
// Use OpenAPI v3 as is
openAPIDocument, err = openapi3.NewLoader().LoadFromData(data)
if err != nil {
return nil
}
default:
return nil
}

prg.OpenAPICache[openAPICacheKey] = openAPIDocument
Expand Down Expand Up @@ -399,3 +430,42 @@ func input(ctx context.Context, cache *cache.Client, base *source, name string)

return nil, fmt.Errorf("can not load tools path=%s name=%s", base.Path, name)
}

// isOpenAPI checks if the data is an OpenAPI definition and returns the version if it is.
func isOpenAPI(data []byte) int {
var fragment struct {
Paths map[string]any `json:"paths,omitempty"`
Swagger string `json:"swagger,omitempty"`
OpenAPI string `json:"openapi,omitempty"`
}

if err := json.Unmarshal(data, &fragment); err != nil {
if err := yaml.Unmarshal(data, &fragment); err != nil {
return 0
}
}
if len(fragment.Paths) == 0 {
return 0
}

if v, _, _ := strings.Cut(fragment.OpenAPI, "."); v != "" {
ver, err := strconv.Atoi(v)
if err != nil {
log.Debugf("invalid OpenAPI version: openapi=%q", fragment.OpenAPI)
return 0
}
return ver
}

if v, _, _ := strings.Cut(fragment.Swagger, "."); v != "" {
ver, err := strconv.Atoi(v)
if err != nil {
log.Debugf("invalid Swagger version: swagger=%q", fragment.Swagger)
return 0
}
return ver
}

log.Debugf("no OpenAPI version found in input data: openapi=%q, swagger=%q", fragment.OpenAPI, fragment.Swagger)
return 0
}
60 changes: 60 additions & 0 deletions pkg/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package loader
import (
"context"
"encoding/json"
"os"
"testing"

"github.com/gptscript-ai/gptscript/pkg/types"
"github.com/hexops/autogold/v2"
"github.com/stretchr/testify/require"
)
Expand All @@ -17,6 +19,64 @@ func toString(obj any) string {
return string(s)
}

func TestIsOpenAPI(t *testing.T) {
datav2, err := os.ReadFile("testdata/openapi_v2.yaml")
require.NoError(t, err)
v := isOpenAPI(datav2)
require.Equal(t, 2, v, "(yaml) expected openapi v2")

datav2, err = os.ReadFile("testdata/openapi_v2.json")
require.NoError(t, err)
v = isOpenAPI(datav2)
require.Equal(t, 2, v, "(json) expected openapi v2")

datav3, err := os.ReadFile("testdata/openapi_v3.yaml")
require.NoError(t, err)
v = isOpenAPI(datav3)
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://github.com/ibuildthecloud/test/bafe5a62174e8a0ea162277dcfe3a2ddb7eea928/example/sub/tool.gpt",
Expand Down
153 changes: 153 additions & 0 deletions pkg/loader/testdata/openapi_v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"license": {
"name": "MIT"
}
},
"host": "petstore.swagger.io",
"basePath": "/v1",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/pets": {
"get": {
"summary": "List all pets",
"operationId": "listPets",
"tags": [
"pets"
],
"parameters": [
{
"name": "limit",
"in": "query",
"description": "How many items to return at one time (max 100)",
"required": false,
"type": "integer",
"format": "int32"
}
],
"responses": {
"200": {
"description": "An paged array of pets",
"headers": {
"x-next": {
"type": "string",
"description": "A link to the next page of responses"
}
},
"schema": {
"$ref": "#/definitions/Pets"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"post": {
"summary": "Create a pet",
"operationId": "createPets",
"tags": [
"pets"
],
"responses": {
"201": {
"description": "Null response"
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/pets/{petId}": {
"get": {
"summary": "Info for a specific pet",
"operationId": "showPetById",
"tags": [
"pets"
],
"parameters": [
{
"name": "petId",
"in": "path",
"required": true,
"description": "The id of the pet to retrieve",
"type": "string"
}
],
"responses": {
"200": {
"description": "Expected response to a valid request",
"schema": {
"$ref": "#/definitions/Pets"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
}
},
"definitions": {
"Pet": {
"required": [
"id",
"name"
],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
},
"Error": {
"required": [
"code",
"message"
],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}
Loading