From 7d1e71e72b8c55d5b7228b72d967e4cae8165280 Mon Sep 17 00:00:00 2001 From: Jacob Zimmerman Date: Thu, 29 Aug 2024 12:40:26 -0400 Subject: [PATCH] feat(examples/structure-outputs): created an example for using structured outputs Basic example which uses the invopop/jsonschema library --- examples/go.mod | 6 ++ examples/go.sum | 21 +++++++ examples/structured-outputs/main.go | 91 +++++++++++++++++++++++++++++ go.work.sum | 3 + shared/shared.go | 2 +- 5 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 examples/structured-outputs/main.go diff --git a/examples/go.mod b/examples/go.mod index be82650..a060547 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -6,14 +6,20 @@ go 1.22.4 require ( github.com/ebitengine/oto/v3 v3.2.0 + github.com/invopop/jsonschema v0.12.0 github.com/openai/openai-go v0.0.0-00010101000000-000000000000 ) require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/ebitengine/purego v0.7.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect golang.org/x/sys v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 447fab2..a357a05 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,7 +1,22 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/ebitengine/oto/v3 v3.2.0 h1:FuggTJTSI3/3hEYwZEIN0CZVXYT29ZOdCu+z/f4QjTw= github.com/ebitengine/oto/v3 v3.2.0/go.mod h1:dOKXShvy1EQbIXhXPFcKLargdnFqH0RjptecvyAxhyw= github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -12,5 +27,11 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/structured-outputs/main.go b/examples/structured-outputs/main.go new file mode 100644 index 0000000..6270ec1 --- /dev/null +++ b/examples/structured-outputs/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/invopop/jsonschema" + "github.com/openai/openai-go" +) + +// A struct that will be converted to a Structured Outputs response schema +type HistoricalComputer struct { + Origin Origin `json:"origin" jsonschema_description:"The origin of the computer"` + Name string `json:"full_name" jsonschema_description:"The name of the device model"` + Legacy string `json:"legacy" jsonschema:"enum=positive,enum=neutral,enum=negative" jsonschema_description:"Its influence on the field of computing"` + NotableFacts []string `json:"notable_facts" jsonschema_description:"A few key facts about the computer"` +} + +type Origin struct { + YearBuilt int64 `json:"year_of_construction" jsonschema_description:"The year it was made"` + Organization string `json:"organization" jsonschema_description:"The organization that was in charge of its development"` +} + +func GenerateSchema[T any]() interface{} { + // Structured Outputs uses a subset of JSON schema + // These flags are necessary to comply with the subset + reflector := jsonschema.Reflector{ + AllowAdditionalProperties: false, + DoNotReference: true, + } + var v T + schema := reflector.Reflect(v) + return schema +} + +// Generate the JSON schema at initialization time +var HistoricalComputerResponseSchema = GenerateSchema[HistoricalComputer]() + +func main() { + client := openai.NewClient() + ctx := context.Background() + + question := "What computer ran the first neural network?" + + print("> ") + println(question) + + schemaParam := openai.ResponseFormatJSONSchemaJSONSchemaParam{ + Name: openai.F("biography"), + Description: openai.F("Notable information about a person"), + Schema: openai.F(HistoricalComputerResponseSchema), + Strict: openai.Bool(true), + } + + // Query the Chat Completions API + chat, err := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{ + Messages: openai.F([]openai.ChatCompletionMessageParamUnion{ + openai.UserMessage(question), + }), + ResponseFormat: openai.F[openai.ChatCompletionNewParamsResponseFormatUnion]( + openai.ResponseFormatJSONSchemaParam{ + Type: openai.F(openai.ResponseFormatJSONSchemaTypeJSONSchema), + JSONSchema: openai.F(schemaParam), + }, + ), + // Only certain models can perform structured outputs + Model: openai.F(openai.ChatModelGPT4o2024_08_06), + }) + + if err != nil { + panic(err.Error()) + } + + // The model responds with a JSON string, so parse it into a struct + historicalComputer := HistoricalComputer{} + err = json.Unmarshal([]byte(chat.Choices[0].Message.Content), &historicalComputer) + if err != nil { + panic(err.Error()) + } + + // Use the model's structured response with a native Go struct + fmt.Printf("Name: %v\n", historicalComputer.Name) + fmt.Printf("Year: %v\n", historicalComputer.Origin.YearBuilt) + fmt.Printf("Org: %v\n", historicalComputer.Origin.Organization) + fmt.Printf("Legacy: %v\n", historicalComputer.Legacy) + fmt.Printf("Facts:\n") + for i, fact := range historicalComputer.NotableFacts { + fmt.Printf("%v. %v\n", i+1, fact) + } +} diff --git a/go.work.sum b/go.work.sum index 4efc870..0d57a9b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -8,6 +8,7 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -16,6 +17,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= diff --git a/shared/shared.go b/shared/shared.go index 18331e2..9bf1ed3 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -151,7 +151,7 @@ type ResponseFormatJSONSchemaJSONSchemaParam struct { // how to respond in the format. Description param.Field[string] `json:"description"` // The schema for the response format, described as a JSON Schema object. - Schema param.Field[map[string]interface{}] `json:"schema"` + Schema param.Field[interface{}] `json:"schema"` // Whether to enable strict schema adherence when generating the output. If set to // true, the model will always follow the exact schema defined in the `schema` // field. Only a subset of JSON Schema is supported when `strict` is `true`. To