diff --git a/docs/content/reference/llm.md b/docs/content/reference/llm.md index fe885ebd..7e8ac4fc 100644 --- a/docs/content/reference/llm.md +++ b/docs/content/reference/llm.md @@ -118,6 +118,60 @@ fmt.Println(myThread) LinGoose allows you to bind a function describing its scope and input's schema. The function will be called by the OpenAI LLM automatically depending on the user's input. Here we force the tool choice to be "auto" to let OpenAI decide which tool to use. If, after an LLM generation, the last message is a tool call, you can enrich the thread with a new LLM generation based on the tool call result. +## OpenAI structured outputs + +LinGoose supports structured outputs to ensure responses adhere to a JSON schema. You can define the response format type as `json_object` or `json_schema` to control output format. Here is examples of how to use structured outputs feature: + +### JSON mode (json_object) + +```go + openaillm := openai.New().WithModel(openai.GPT4o).WithResponseFormat(openai.ResponseFormatJSONObject).WithMaxTokens(1000) + + t := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Give me a JSON object that describes a person"), + ), + ) + + err := openaillm.Generate(context.Background(), t) +``` + +### Structured outputs (json_schema) + +```go + import "github.com/sashabaranov/go-openai/jsonschema" + + + type Result struct { + FirstName string + LastName string + Age int + Job string + } + var result Result + schema, err := jsonschema.GenerateSchemaForType(result) + if err != nil { + panic(err) + } + + openaillm := openai.New(). + WithModel("gpt-4.1-nano"). + WithResponseFormat(openai.ResponseFormatTypeJSONSchema). + WithResponseFormatJSONSchema(&openai.ResponseFormatJSONSchema{ + Name: "Person", + Schema: schema, + Strict: true, + }). + WithMaxTokens(1000) + + t := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Give me a JSON object that describes a person"), + ), + ) + + err = openaillm.Generate(context.Background(), t) +``` ## Private LLMs If you want to run your model or use a private LLM provider, you have many options. @@ -147,4 +201,4 @@ if err != nil { } fmt.Println(myThread) -``` \ No newline at end of file +``` diff --git a/examples/llm/openai/response_format/main.go b/examples/llm/openai/response_format/json-mode/main.go similarity index 100% rename from examples/llm/openai/response_format/main.go rename to examples/llm/openai/response_format/json-mode/main.go diff --git a/examples/llm/openai/response_format/structured-outputs/main.go b/examples/llm/openai/response_format/structured-outputs/main.go new file mode 100644 index 00000000..91cad760 --- /dev/null +++ b/examples/llm/openai/response_format/structured-outputs/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/thread" + "github.com/sashabaranov/go-openai/jsonschema" +) + +func main() { + type Result struct { + FirstName string + LastName string + Age int + Job string + } + var result Result + schema, err := jsonschema.GenerateSchemaForType(result) + if err != nil { + panic(err) + } + + openaillm := openai.New(). + WithModel("gpt-4.1-nano"). + WithResponseFormat(openai.ResponseFormatTypeJSONSchema). + WithResponseFormatJSONSchema(&openai.ResponseFormatJSONSchema{ + Name: "Person", + Schema: schema, + Strict: true, + }). + WithMaxTokens(1000) + + t := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Give me a JSON object that describes a person"), + ), + ) + + err = openaillm.Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t) +} diff --git a/go.mod b/go.mod index 4131f84e..6c63251a 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/henomis/qdrant-go v1.1.0 github.com/henomis/restclientgo v1.2.0 github.com/invopop/jsonschema v0.7.0 - github.com/sashabaranov/go-openai v1.24.0 + github.com/sashabaranov/go-openai v1.38.2 golang.org/x/net v0.25.0 ) diff --git a/go.sum b/go.sum index a79137ce..9bbaaa05 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/sashabaranov/go-openai v1.24.0 h1:4H4Pg8Bl2RH/YSnU8DYumZbuHnnkfioor/dtNlB20D4= -github.com/sashabaranov/go-openai v1.24.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo= +github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= diff --git a/llm/openai/common.go b/llm/openai/common.go index d7cbc84b..0bd2637d 100644 --- a/llm/openai/common.go +++ b/llm/openai/common.go @@ -61,6 +61,9 @@ type StreamCallback func(string) type ResponseFormat = openai.ChatCompletionResponseFormatType const ( - ResponseFormatJSONObject ResponseFormat = openai.ChatCompletionResponseFormatTypeJSONObject - ResponseFormatText ResponseFormat = openai.ChatCompletionResponseFormatTypeText + ResponseFormatTypeJSONSchema ResponseFormat = openai.ChatCompletionResponseFormatTypeJSONSchema + ResponseFormatJSONObject ResponseFormat = openai.ChatCompletionResponseFormatTypeJSONObject + ResponseFormatText ResponseFormat = openai.ChatCompletionResponseFormatTypeText ) + +type ResponseFormatJSONSchema = openai.ChatCompletionResponseFormatJSONSchema diff --git a/llm/openai/openai.go b/llm/openai/openai.go index f0da97b8..58bc1ed9 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -30,18 +30,19 @@ var threadRoleToOpenAIRole = map[thread.Role]string{ } type OpenAI struct { - openAIClient *openai.Client - model Model - temperature float32 - maxTokens int - stop []string - usageCallback UsageCallback - functions map[string]Function - streamCallbackFn StreamCallback - responseFormat *ResponseFormat - toolChoice *string - cache *cache.Cache - Name string + openAIClient *openai.Client + model Model + temperature float32 + maxTokens int + stop []string + usageCallback UsageCallback + functions map[string]Function + streamCallbackFn StreamCallback + responseFormat *ResponseFormat + responseFormatJSONSchema *ResponseFormatJSONSchema + toolChoice *string + cache *cache.Cache + Name string } // WithModel sets the model to use for the OpenAI instance. @@ -105,6 +106,11 @@ func (o *OpenAI) WithResponseFormat(responseFormat ResponseFormat) *OpenAI { return o } +func (o *OpenAI) WithResponseFormatJSONSchema(jsonSchema *ResponseFormatJSONSchema) *OpenAI { + o.responseFormatJSONSchema = jsonSchema + return o +} + // SetStop sets the stop sequences for the completion. func (o *OpenAI) SetStop(stop []string) { o.stop = stop @@ -358,7 +364,8 @@ func (o *OpenAI) buildChatCompletionRequest(t *thread.Thread) openai.ChatComplet var responseFormat *openai.ChatCompletionResponseFormat if o.responseFormat != nil { responseFormat = &openai.ChatCompletionResponseFormat{ - Type: *o.responseFormat, + Type: *o.responseFormat, + JSONSchema: o.responseFormatJSONSchema, } } diff --git a/llm/openai/openai_test.go b/llm/openai/openai_test.go new file mode 100644 index 00000000..6f53d825 --- /dev/null +++ b/llm/openai/openai_test.go @@ -0,0 +1,47 @@ +package openai + +import ( + "reflect" + "testing" + + "github.com/henomis/lingoose/thread" + "github.com/sashabaranov/go-openai" +) + +func TestWithResponseFormatJSONSchema(t *testing.T) { + tests := []struct { + name string + jsonSchema *ResponseFormatJSONSchema + want *ResponseFormatJSONSchema + }{ + { + name: "should set JSON schema", + jsonSchema: &ResponseFormatJSONSchema{ + Name: "FirstName", + Description: "First of your name", + Strict: true, + }, + }, + { + name: "should set nil JSON schema", + jsonSchema: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := New(). + WithResponseFormatJSONSchema(tt.jsonSchema). + WithResponseFormat(openai.ChatCompletionResponseFormatTypeJSONSchema) + if !reflect.DeepEqual(tt.jsonSchema, o.responseFormatJSONSchema) { + t.Fatalf("New OpenAI WithResponseFormatJSONSchema doesn't set value correctly") + } + + myThread := thread.New() + request := o.buildChatCompletionRequest(myThread) + if !reflect.DeepEqual(tt.jsonSchema, request.ResponseFormat.JSONSchema) { + t.Fatalf("New OpenAI WithResponseFormatJSONSchema doesn't generate correct chat request correctly") + } + }) + } +}