From cd2262affa79695f478924cf7b096bd59feef86d Mon Sep 17 00:00:00 2001 From: Robby Date: Tue, 21 May 2024 23:55:21 -0400 Subject: [PATCH 1/5] start streaming --- examples/function_calling/main.go | 4 - examples/streaming/main.go | 150 ++++++++++++++++++++++++++++++ pkg/instructor/anthropic.go | 5 + pkg/instructor/client.go | 12 +-- pkg/instructor/instructor.go | 105 ++++++++++++--------- pkg/instructor/messages.go | 16 ++-- pkg/instructor/openai.go | 130 ++++++++++++++++++-------- pkg/instructor/utils.go | 48 ++++++++-- 8 files changed, 359 insertions(+), 111 deletions(-) create mode 100644 examples/streaming/main.go diff --git a/examples/function_calling/main.go b/examples/function_calling/main.go index 1e6d7f2..9f9eed4 100644 --- a/examples/function_calling/main.go +++ b/examples/function_calling/main.go @@ -29,10 +29,6 @@ func (s *Search) execute() { type Searches = []Search -// type Searches struct { -// Items []Search `json:"searches" jsonschema:"title=Searches,description=A list of search results"` -// } - func segment(ctx context.Context, data string) *Searches { client, err := instructor.FromOpenAI( diff --git a/examples/streaming/main.go b/examples/streaming/main.go new file mode 100644 index 0000000..77ff3c5 --- /dev/null +++ b/examples/streaming/main.go @@ -0,0 +1,150 @@ +// package main +// +// import ( +// "context" +// "fmt" +// "os" +// +// "github.com/instructor-ai/instructor-go/pkg/instructor" +// openai "github.com/sashabaranov/go-openai" +// ) +// +// type Product struct { +// ID string `json:"product_id" jsonschema:"title=Product ID,description=ID of the product,required=True"` +// Name string `json:"product_name" jsonschema:"title=Product Name,description=Name of the product,required=True"` +// } +// +// func (p *Product) String() string { +// return fmt.Sprintf("product[id=%s,name=%s]", p.ID, p.Name) +// } +// +// func main() { +// ctx := context.Background() +// +// client, err := instructor.FromOpenAI( +// openai.NewClient(os.Getenv("OPENAI_API_KEY")), +// instructor.WithMode(instructor.ModeJSON), +// ) +// if err != nil { +// panic(err) +// } +// +// profileData := ` +// Customer ID: 12345 +// Recent Purchases: [Laptop, Wireless Headphones, Smart Watch] +// Frequently Browsed Categories: [Electronics, Books, Fitness Equipment] +// Product Ratings: {Laptop: 5 stars, Wireless Headphones: 4 stars} +// Recent Search History: [best budget laptops 2023, latest sci-fi books, yoga mats] +// Preferred Brands: [Apple, AllBirds, Bench] +// Responses to Previous Recommendations: {Philips: Not Interested, Adidas: Not Interested} +// Loyalty Program Status: Gold Member +// Average Monthly Spend: $500 +// Preferred Shopping Times: Weekend Evenings +// ... +// ` +// +// products := []Product{ +// {ID: "1", Name: "Sony WH-1000XM4 Wireless Headphones - Noise-canceling, long battery life"}, +// {ID: "2", Name: "Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem"}, +// {ID: "3", Name: "Kindle Oasis - Premium e-reader with adjustable warm light"}, +// {ID: "4", Name: "AllBirds Wool Runners - Comfortable, eco-friendly sneakers"}, +// {ID: "5", Name: "Manduka PRO Yoga Mat - High-quality, durable, eco-friendly"}, +// {ID: "6", Name: "Bench Hooded Jacket - Stylish, durable, suitable for outdoor activities"}, +// {ID: "7", Name: "Apple MacBook Air (2023) - Latest model, high performance, portable"}, +// {ID: "8", Name: "GoPro HERO9 Black - 5K video, waterproof, for action photography"}, +// {ID: "9", Name: "Nespresso Vertuo Next Coffee Machine - Quality coffee, easy to use, compact design"}, +// {ID: "10", Name: "Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author"}, +// } +// +// productList := "" +// for _, product := range products { +// productList += product.String() + "\n" +// } +// +// recommendationsChan := make(chan Product) +// err = instructor.CreateChatCompletionStream( +// client, +// ctx, +// instructor.Request{ +// Model: openai.GPT4o20240513, +// Messages: []instructor.Message{ +// { +// Role: instructor.RoleSystem, +// Content: fmt.Sprintf(`Generate the product recommendations from the product list based on the customer profile. +// Return in order of highest recommended first. +// Product list: +// %s`, productList), +// }, +// { +// Role: instructor.RoleUser, +// Content: fmt.Sprintf("User profile:\n%s", profileData), +// }, +// }, +// Stream: true, +// }, +// recommendationsChan, +// ) +// if err != nil { +// panic(err) +// } +// +// for product := range recommendationsChan { +// println(product.String()) +// } +// } + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + openai "github.com/sashabaranov/go-openai" +) + +type Number struct { + Value int `json:"value"` +} + +func (n *Number) String() string { + return fmt.Sprintf("Number[value=%d]", n.Value) +} + +func main() { + ctx := context.Background() + + client, err := instructor.FromOpenAI( + openai.NewClient(os.Getenv("OPENAI_API_KEY")), + instructor.WithMode(instructor.ModeJSON), + ) + if err != nil { + panic(err) + } + + numCh := make(chan Number) + err = instructor.CreateChatCompletionStream( + client, + ctx, + instructor.Request{ + Model: openai.GPT4o, + Messages: []instructor.Message{ + { + Role: instructor.RoleSystem, + Content: "Count to 5 starting at 1", + + }, + }, + Stream: true, + }, + numCh, + ) + if err != nil { + panic(err) + } + + for num := range numCh { + fmt.Println(num.String()) + } +} diff --git a/pkg/instructor/anthropic.go b/pkg/instructor/anthropic.go index 5906ba2..8b4b208 100644 --- a/pkg/instructor/anthropic.go +++ b/pkg/instructor/anthropic.go @@ -7,6 +7,7 @@ import ( "fmt" anthropic "github.com/liushuangls/go-anthropic/v2" + openai "github.com/sashabaranov/go-openai" ) type AnthropicClient struct { @@ -163,3 +164,7 @@ func toAnthropicMessages(request *Request) (*[]anthropic.Message, error) { return &messages, nil } + +func (a *AnthropicClient) CreateChatCompletionStream(ctx context.Context, request openai.ChatCompletionRequest, mode string, schema *Schema) (<-chan string, error) { + panic("unimplemented") +} diff --git a/pkg/instructor/client.go b/pkg/instructor/client.go index dfeb75a..7d7a657 100644 --- a/pkg/instructor/client.go +++ b/pkg/instructor/client.go @@ -12,10 +12,10 @@ type Client interface { schema *Schema, ) (string, error) - // TODO: implement streaming - // CreateChatCompletionStream( - // ctx context.Context, - // request ChatCompletionRequest, - // opts ...ClientOptions, - // ) (*T, error) + CreateChatCompletionStream( + ctx context.Context, + request Request, + mode Mode, + schema *Schema, + ) (<-chan string, error) } diff --git a/pkg/instructor/instructor.go b/pkg/instructor/instructor.go index e119e21..dbfba94 100644 --- a/pkg/instructor/instructor.go +++ b/pkg/instructor/instructor.go @@ -1,11 +1,12 @@ package instructor import ( + "bytes" "context" "encoding/json" "errors" + "io" "reflect" - "strings" anthropic "github.com/liushuangls/go-anthropic/v2" openai "github.com/sashabaranov/go-openai" @@ -90,59 +91,71 @@ func (i *Instructor) CreateChatCompletion(ctx context.Context, request Request, return errors.New("hit max retry attempts") } -func processResponse(responseStr string, response *any) error { +func CreateChatCompletionStream[T any](i *Instructor, ctx context.Context, request Request, ch chan T) error { + go func() { + defer close(ch) - err := json.Unmarshal([]byte(responseStr), response) - if err != nil { - return err - } + t := reflect.TypeOf(new(T)).Elem() - // TODO: if direct unmarshal fails: check common erors like wrapping struct with key name of struct, instead of just the value + schema, err := NewSchema(t) + if err != nil { + ch <- *new(T) // send a zero value of type T to signal error + return + } - return nil -} + request.Stream = true -// Removes any prefixes before the JSON (like "Sure, here you go:") -func trimPrefix(jsonStr string) string { - startObject := strings.IndexByte(jsonStr, '{') - startArray := strings.IndexByte(jsonStr, '[') - - var start int - if startObject == -1 && startArray == -1 { - return jsonStr // No opening brace or bracket found, return the original string - } else if startObject == -1 { - start = startArray - } else if startArray == -1 { - start = startObject - } else { - start = min(startObject, startArray) - } + streamCh, err := i.Client.CreateChatCompletionStream(ctx, request, i.Mode, schema) + if err != nil { + ch <- *new(T) // send a zero value of type T to signal error + return + } + + var buffer bytes.Buffer + + for { + select { + case <-ctx.Done(): + return + default: + text, ok := <-streamCh + if !ok { + return + } + + println(text) + text = extractJSON(text) + + buffer.WriteString(text) + + for { + var chunk T + err = json.Unmarshal(buffer.Bytes(), &chunk) + if err == nil { + ch <- chunk + buffer.Reset() + break + } + + if err != io.EOF { + break + } + } + } + } + }() - return jsonStr[start:] + return nil } -// Removes any postfixes after the JSON -func trimPostfix(jsonStr string) string { - endObject := strings.LastIndexByte(jsonStr, '}') - endArray := strings.LastIndexByte(jsonStr, ']') - - var end int - if endObject == -1 && endArray == -1 { - return jsonStr // No closing brace or bracket found, return the original string - } else if endObject == -1 { - end = endArray - } else if endArray == -1 { - end = endObject - } else { - end = max(endObject, endArray) +func processResponse(responseStr string, response *any) error { + + err := json.Unmarshal([]byte(responseStr), response) + if err != nil { + return err } - return jsonStr[:end+1] -} + // TODO: if direct unmarshal fails: check common erors like wrapping struct with key name of struct, instead of just the value -// Extracts the JSON by trimming prefixes and postfixes -func extractJSON(jsonStr string) string { - trimmedPrefix := trimPrefix(jsonStr) - trimmedJSON := trimPostfix(trimmedPrefix) - return trimmedJSON + return nil } diff --git a/pkg/instructor/messages.go b/pkg/instructor/messages.go index 637d8b6..fec52d9 100644 --- a/pkg/instructor/messages.go +++ b/pkg/instructor/messages.go @@ -4,15 +4,13 @@ import ( openai "github.com/sashabaranov/go-openai" ) -type Message = openai.ChatCompletionMessage - -type Request = openai.ChatCompletionRequest - -type ChatMessagePart = openai.ChatMessagePart - -type ChatMessageImageURL = openai.ChatMessageImageURL - -type ChatMessagePartType = openai.ChatMessagePartType +type ( + Message = openai.ChatCompletionMessage + Request = openai.ChatCompletionRequest + ChatMessagePart = openai.ChatMessagePart + ChatMessageImageURL = openai.ChatMessageImageURL + ChatMessagePartType = openai.ChatMessagePartType +) const ( ChatMessagePartTypeText ChatMessagePartType = "text" diff --git a/pkg/instructor/openai.go b/pkg/instructor/openai.go index 8f6eaf2..4f7319a 100644 --- a/pkg/instructor/openai.go +++ b/pkg/instructor/openai.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" openai "github.com/sashabaranov/go-openai" ) @@ -26,6 +27,10 @@ func NewOpenAIClient(client *openai.Client) (*OpenAIClient, error) { } func (o *OpenAIClient) CreateChatCompletion(ctx context.Context, request Request, mode Mode, schema *Schema) (string, error) { + if request.Stream { + return "", errors.New("streaming is not supported by this method; use CreateChatCompletionStream instead") + } + switch mode { case ModeToolCall: return o.completionToolCall(ctx, request, schema) @@ -40,22 +45,7 @@ func (o *OpenAIClient) CreateChatCompletion(ctx context.Context, request Request func (o *OpenAIClient) completionToolCall(ctx context.Context, request Request, schema *Schema) (string, error) { - tools := []openai.Tool{} - - for _, function := range schema.Functions { - f := openai.FunctionDefinition{ - Name: function.Name, - Description: function.Description, - Parameters: function.Parameters, - } - t := openai.Tool{ - Type: "function", - Function: &f, - } - tools = append(tools, t) - } - - request.Tools = tools + request.Tools = createTools(schema) resp, err := o.Client.CreateChatCompletion(ctx, request) if err != nil { @@ -103,23 +93,25 @@ func (o *OpenAIClient) completionToolCall(ctx context.Context, request Request, } func (o *OpenAIClient) completionJSON(ctx context.Context, request Request, schema *Schema) (string, error) { - message := fmt.Sprintf(` -Please responsd with json in the following json_schema: -%s + request.Messages = prepend(request.Messages, *createJSONMessage(schema)) -Make sure to return an instance of the JSON, not the schema itself -`, schema.String) + // Set JSON mode + request.ResponseFormat = &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject} - msg := Message{ - Role: RoleSystem, - Content: message, + resp, err := o.Client.CreateChatCompletion(ctx, request) + if err != nil { + return "", err } - request.Messages = prepend(request.Messages, msg) + text := resp.Choices[0].Message.Content + + return text, nil +} - // Set JSON mode - request.ResponseFormat = &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject} +func (o *OpenAIClient) completionJSONSchema(ctx context.Context, request Request, schema *Schema) (string, error) { + + request.Messages = prepend(request.Messages, *createJSONMessage(schema)) resp, err := o.Client.CreateChatCompletion(ctx, request) if err != nil { @@ -131,29 +123,91 @@ Make sure to return an instance of the JSON, not the schema itself return text, nil } -func (o *OpenAIClient) completionJSONSchema(ctx context.Context, request Request, schema *Schema) (string, error) { +func (o *OpenAIClient) CreateChatCompletionStream(ctx context.Context, request Request, mode Mode, schema *Schema) (<-chan string, error) { + switch mode { + case ModeToolCall: + return o.completionToolCallStream(ctx, request, schema) + case ModeJSON: + return o.completionJSONStream(ctx, request, schema) + case ModeJSONSchema: + return o.completionJSONSchemaStream(ctx, request, schema) + default: + return nil, fmt.Errorf("mode '%s' is not supported for %s", mode, o.Name) + } +} + +func (o *OpenAIClient) completionToolCallStream(ctx context.Context, request Request, schema *Schema) (<-chan string, error) { + request.Tools = createTools(schema) + return o.createStream(ctx, request) +} +func (o *OpenAIClient) completionJSONStream(ctx context.Context, request Request, schema *Schema) (<-chan string, error) { + request.Messages = prepend(request.Messages, *createJSONMessage(schema)) + // Set JSON mode + request.ResponseFormat = &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject} + return o.createStream(ctx, request) +} + +func (o *OpenAIClient) completionJSONSchemaStream(ctx context.Context, request Request, schema *Schema) (<-chan string, error) { + request.Messages = prepend(request.Messages, *createJSONMessage(schema)) + return o.createStream(ctx, request) +} + +func createTools(schema *Schema) []openai.Tool { + tools := make([]openai.Tool, 0, len(schema.Functions)) + for _, function := range schema.Functions { + f := openai.FunctionDefinition{ + Name: function.Name, + Description: function.Description, + Parameters: function.Parameters, + } + t := openai.Tool{ + Type: "function", + Function: &f, + } + tools = append(tools, t) + } + return tools +} + +func createJSONMessage(schema *Schema) *Message { message := fmt.Sprintf(` -Please responsd with json in the following json_schema: +Please respond with JSON in the following JSON schema: %s Make sure to return an instance of the JSON, not the schema itself `, schema.String) - - msg := Message{ + return &Message{ Role: RoleSystem, Content: message, } +} - request.Messages = prepend(request.Messages, msg) - - resp, err := o.Client.CreateChatCompletion(ctx, request) +func (o *OpenAIClient) createStream(ctx context.Context, request Request) (<-chan string, error) { + stream, err := o.Client.CreateChatCompletionStream(ctx, request) if err != nil { - return "", err + return nil, err } - text := resp.Choices[0].Message.Content - - return text, nil + ch := make(chan string) + + go func() { + defer stream.Close() + defer close(ch) + for { + response, err := stream.Recv() + if errors.Is(err, io.EOF) { + println("closing channel") + return + } + if err != nil { + println("channel errored") + return + } + text := response.Choices[0].Delta.Content + ch <- text + } + }() + return ch, nil } diff --git a/pkg/instructor/utils.go b/pkg/instructor/utils.go index c4a07a0..7489b60 100644 --- a/pkg/instructor/utils.go +++ b/pkg/instructor/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" ) func toPtr[T any](val T) *T { @@ -45,16 +46,47 @@ func urlToBase64(url string) (string, error) { return base64.StdEncoding.EncodeToString(data), nil } -func min(a, b int) int { - if a < b { - return a +// Removes any prefixes before the JSON (like "Sure, here you go:") +func trimPrefixBeforeJSON(jsonStr string) string { + startObject := strings.IndexByte(jsonStr, '{') + startArray := strings.IndexByte(jsonStr, '[') + + var start int + if startObject == -1 && startArray == -1 { + return jsonStr // No opening brace or bracket found, return the original string + } else if startObject == -1 { + start = startArray + } else if startArray == -1 { + start = startObject + } else { + start = min(startObject, startArray) } - return b + + return jsonStr[start:] } -func max(a, b int) int { - if a > b { - return a +// Removes any postfixes after the JSON +func trimPostfixAfterJSON(jsonStr string) string { + endObject := strings.LastIndexByte(jsonStr, '}') + endArray := strings.LastIndexByte(jsonStr, ']') + + var end int + if endObject == -1 && endArray == -1 { + return jsonStr // No closing brace or bracket found, return the original string + } else if endObject == -1 { + end = endArray + } else if endArray == -1 { + end = endObject + } else { + end = max(endObject, endArray) } - return b + + return jsonStr[:end+1] +} + +// Extracts the JSON by trimming prefixes and postfixes +func extractJSON(jsonStr string) string { + trimmedPrefix := trimPrefixBeforeJSON(jsonStr) + trimmedJSON := trimPostfixAfterJSON(trimmedPrefix) + return trimmedJSON } From 9993388a52579232c1ee06586fa9b7f09e23974e Mon Sep 17 00:00:00 2001 From: Robby Date: Wed, 22 May 2024 17:47:08 -0400 Subject: [PATCH 2/5] messy but working --- examples/streaming/main.go | 194 +++++++++++++++-------------------- pkg/instructor/instructor.go | 84 ++++++++------- pkg/instructor/openai.go | 26 +++-- 3 files changed, 152 insertions(+), 152 deletions(-) diff --git a/examples/streaming/main.go b/examples/streaming/main.go index 77ff3c5..88795cf 100644 --- a/examples/streaming/main.go +++ b/examples/streaming/main.go @@ -1,115 +1,35 @@ -// package main -// -// import ( -// "context" -// "fmt" -// "os" -// -// "github.com/instructor-ai/instructor-go/pkg/instructor" -// openai "github.com/sashabaranov/go-openai" -// ) -// -// type Product struct { -// ID string `json:"product_id" jsonschema:"title=Product ID,description=ID of the product,required=True"` -// Name string `json:"product_name" jsonschema:"title=Product Name,description=Name of the product,required=True"` -// } -// -// func (p *Product) String() string { -// return fmt.Sprintf("product[id=%s,name=%s]", p.ID, p.Name) -// } -// -// func main() { -// ctx := context.Background() -// -// client, err := instructor.FromOpenAI( -// openai.NewClient(os.Getenv("OPENAI_API_KEY")), -// instructor.WithMode(instructor.ModeJSON), -// ) -// if err != nil { -// panic(err) -// } -// -// profileData := ` -// Customer ID: 12345 -// Recent Purchases: [Laptop, Wireless Headphones, Smart Watch] -// Frequently Browsed Categories: [Electronics, Books, Fitness Equipment] -// Product Ratings: {Laptop: 5 stars, Wireless Headphones: 4 stars} -// Recent Search History: [best budget laptops 2023, latest sci-fi books, yoga mats] -// Preferred Brands: [Apple, AllBirds, Bench] -// Responses to Previous Recommendations: {Philips: Not Interested, Adidas: Not Interested} -// Loyalty Program Status: Gold Member -// Average Monthly Spend: $500 -// Preferred Shopping Times: Weekend Evenings -// ... -// ` -// -// products := []Product{ -// {ID: "1", Name: "Sony WH-1000XM4 Wireless Headphones - Noise-canceling, long battery life"}, -// {ID: "2", Name: "Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem"}, -// {ID: "3", Name: "Kindle Oasis - Premium e-reader with adjustable warm light"}, -// {ID: "4", Name: "AllBirds Wool Runners - Comfortable, eco-friendly sneakers"}, -// {ID: "5", Name: "Manduka PRO Yoga Mat - High-quality, durable, eco-friendly"}, -// {ID: "6", Name: "Bench Hooded Jacket - Stylish, durable, suitable for outdoor activities"}, -// {ID: "7", Name: "Apple MacBook Air (2023) - Latest model, high performance, portable"}, -// {ID: "8", Name: "GoPro HERO9 Black - 5K video, waterproof, for action photography"}, -// {ID: "9", Name: "Nespresso Vertuo Next Coffee Machine - Quality coffee, easy to use, compact design"}, -// {ID: "10", Name: "Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author"}, -// } -// -// productList := "" -// for _, product := range products { -// productList += product.String() + "\n" -// } -// -// recommendationsChan := make(chan Product) -// err = instructor.CreateChatCompletionStream( -// client, -// ctx, -// instructor.Request{ -// Model: openai.GPT4o20240513, -// Messages: []instructor.Message{ -// { -// Role: instructor.RoleSystem, -// Content: fmt.Sprintf(`Generate the product recommendations from the product list based on the customer profile. -// Return in order of highest recommended first. -// Product list: -// %s`, productList), -// }, -// { -// Role: instructor.RoleUser, -// Content: fmt.Sprintf("User profile:\n%s", profileData), -// }, -// }, -// Stream: true, -// }, -// recommendationsChan, -// ) -// if err != nil { -// panic(err) -// } -// -// for product := range recommendationsChan { -// println(product.String()) -// } -// } - package main import ( "context" "fmt" "os" + "reflect" "github.com/instructor-ai/instructor-go/pkg/instructor" openai "github.com/sashabaranov/go-openai" ) -type Number struct { - Value int `json:"value"` +type Product struct { + ID string `json:"product_id" jsonschema:"title=Product ID,description=ID of the product,required=True"` + Name string `json:"product_name" jsonschema:"title=Product Name,description=Name of the product,required=True"` +} + +func (p *Product) String() string { + return fmt.Sprintf("product=[id=%s,name=%s]", p.ID, p.Name) +} + +type Recommendation struct { + Product + Reason string `json:"reason" jsonschema:"title=Recommendation Reason,description=Reason for the product recommendation"` } -func (n *Number) String() string { - return fmt.Sprintf("Number[value=%d]", n.Value) +func (r *Recommendation) String() string { + return fmt.Sprintf("recommendation=[%s, reason=%s]", r.Product.String(), r.Reason) +} + +type Recommendations struct { + Items []Recommendation `json:"items" jsonschema:"title=Product Recommendations,description=List of product recommendations"` } func main() { @@ -123,28 +43,84 @@ func main() { panic(err) } - numCh := make(chan Number) - err = instructor.CreateChatCompletionStream( - client, + profileData := ` +Customer ID: 12345 +Recent Purchases: [Laptop, Wireless Headphones, Smart Watch] +Frequently Browsed Categories: [Electronics, Books, Fitness Equipment] +Product Ratings: {Laptop: 5 stars, Wireless Headphones: 4 stars} +Recent Search History: [best budget laptops 2023, latest sci-fi books, yoga mats] +Preferred Brands: [Apple, AllBirds, Bench] +Responses to Previous Recommendations: {Philips: Not Interested, Adidas: Not Interested} +Loyalty Program Status: Gold Member +Average Monthly Spend: $500 +Preferred Shopping Times: Weekend Evenings +... +` + + products := []Product{ + {ID: "1", Name: "Sony WH-1000XM4 Wireless Headphones - Noise-canceling, long battery life"}, + {ID: "2", Name: "Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem"}, + {ID: "3", Name: "Kindle Oasis - Premium e-reader with adjustable warm light"}, + {ID: "4", Name: "AllBirds Wool Runners - Comfortable, eco-friendly sneakers"}, + {ID: "5", Name: "Manduka PRO Yoga Mat - High-quality, durable, eco-friendly"}, + {ID: "6", Name: "Bench Hooded Jacket - Stylish, durable, suitable for outdoor activities"}, + {ID: "7", Name: "Apple MacBook Air (2023) - Latest model, high performance, portable"}, + {ID: "8", Name: "GoPro HERO9 Black - 5K video, waterproof, for action photography"}, + {ID: "9", Name: "Nespresso Vertuo Next Coffee Machine - Quality coffee, easy to use, compact design"}, + {ID: "10", Name: "Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author"}, + } + + productList := "" + for _, product := range products { + productList += product.String() + "\n" + } + + recommendationChan, err := client.CreateChatCompletionStream( ctx, instructor.Request{ - Model: openai.GPT4o, + Model: openai.GPT4o20240513, Messages: []instructor.Message{ { - Role: instructor.RoleSystem, - Content: "Count to 5 starting at 1", - + Role: instructor.RoleSystem, + Content: fmt.Sprintf(`Generate the top 3 product recommendations from the product list based on the customer profile. +Return in order of highest recommended first. +Product list: +%s`, productList), + }, + { + Role: instructor.RoleUser, + Content: fmt.Sprintf("User profile:\n%s", profileData), }, }, Stream: true, }, - numCh, + new(Recommendations), + *new(Recommendation), ) if err != nil { panic(err) } - for num := range numCh { - fmt.Println(num.String()) + for instance := range recommendationChan { + recommendation, ok := instance.(*Recommendation) + if !ok { + // Handle error: the received value is not a *Recommendations + println("channel is not of correct type. Actual type: " + reflect.TypeOf(instance).String()) + continue + } + + println(recommendation.String()) } + + // for instance := range recommendationsChan { + // recommendations, ok := instance.(*Recommendations) + // if !ok { + // // Handle error: the received value is not a *Recommendations + // continue + // } + // + // for _, recommendation := range recommendations.Items { + // println(recommendation.String()) + // } + // } } diff --git a/pkg/instructor/instructor.go b/pkg/instructor/instructor.go index dbfba94..12e1cd6 100644 --- a/pkg/instructor/instructor.go +++ b/pkg/instructor/instructor.go @@ -1,12 +1,11 @@ package instructor import ( - "bytes" "context" "encoding/json" "errors" - "io" "reflect" + "strings" anthropic "github.com/liushuangls/go-anthropic/v2" openai "github.com/sashabaranov/go-openai" @@ -91,71 +90,84 @@ func (i *Instructor) CreateChatCompletion(ctx context.Context, request Request, return errors.New("hit max retry attempts") } -func CreateChatCompletionStream[T any](i *Instructor, ctx context.Context, request Request, ch chan T) error { - go func() { - defer close(ch) +func (i *Instructor) CreateChatCompletionStream(ctx context.Context, request Request, responseSlice, responseElem any) (chan any, error) { - t := reflect.TypeOf(new(T)).Elem() + // used to signal to model to send a stream of the elements of that type + sliceType := reflect.TypeOf(responseSlice) + // used to create the channel to parse elements of this type and send them + elemType := reflect.TypeOf(responseElem) - schema, err := NewSchema(t) - if err != nil { - ch <- *new(T) // send a zero value of type T to signal error - return - } + schema, err := NewSchema(sliceType) + if err != nil { + return nil, err + } - request.Stream = true + request.Stream = true - streamCh, err := i.Client.CreateChatCompletionStream(ctx, request, i.Mode, schema) - if err != nil { - ch <- *new(T) // send a zero value of type T to signal error - return - } + ch, err := i.Client.CreateChatCompletionStream(ctx, request, i.Mode, schema) + if err != nil { + return nil, err + } - var buffer bytes.Buffer + parsedChan := make(chan any) // Buffered channel for parsed objects + + go func() { + defer close(parsedChan) + var buffer strings.Builder + inArray := false for { select { case <-ctx.Done(): return - default: - text, ok := <-streamCh + case text, ok := <-ch: if !ok { return } - - println(text) - text = extractJSON(text) - buffer.WriteString(text) - for { - var chunk T - err = json.Unmarshal(buffer.Bytes(), &chunk) - if err == nil { - ch <- chunk - buffer.Reset() - break + // eat all input until elements stream starts + if !inArray { + idx := strings.Index(buffer.String(), `[`) + if idx == -1 { + continue } - if err != io.EOF { - break + inArray = true + bufferStr := buffer.String() + trimmed := strings.TrimSpace(bufferStr[idx+1:]) + buffer.Reset() + buffer.WriteString(trimmed) + } + + data := buffer.String() + decoder := json.NewDecoder(strings.NewReader(data)) + + for decoder.More() { + instance := reflect.New(elemType).Interface() + err := decoder.Decode(instance) + if err != nil { + break // Stop on error } + parsedChan <- instance + + buffer.Reset() + buffer.WriteString(data[len(data):]) } } } }() - return nil + return parsedChan, nil } func processResponse(responseStr string, response *any) error { - err := json.Unmarshal([]byte(responseStr), response) if err != nil { return err } - // TODO: if direct unmarshal fails: check common erors like wrapping struct with key name of struct, instead of just the value + // TODO: if direct unmarshal fails: check common errors like wrapping struct with key name of struct, instead of just the value return nil } diff --git a/pkg/instructor/openai.go b/pkg/instructor/openai.go index 4f7319a..1dcdc8e 100644 --- a/pkg/instructor/openai.go +++ b/pkg/instructor/openai.go @@ -123,6 +123,20 @@ func (o *OpenAIClient) completionJSONSchema(ctx context.Context, request Request return text, nil } +func createJSONMessage(schema *Schema) *Message { + message := fmt.Sprintf(` +Please respond with JSON in the following JSON schema: + +%s + +Make sure to return an instance of the JSON, not the schema itself +`, schema.String) + return &Message{ + Role: RoleSystem, + Content: message, + } +} + func (o *OpenAIClient) CreateChatCompletionStream(ctx context.Context, request Request, mode Mode, schema *Schema) (<-chan string, error) { switch mode { case ModeToolCall: @@ -142,14 +156,14 @@ func (o *OpenAIClient) completionToolCallStream(ctx context.Context, request Req } func (o *OpenAIClient) completionJSONStream(ctx context.Context, request Request, schema *Schema) (<-chan string, error) { - request.Messages = prepend(request.Messages, *createJSONMessage(schema)) + request.Messages = prepend(request.Messages, *createJSONMessageStream(schema)) // Set JSON mode request.ResponseFormat = &openai.ChatCompletionResponseFormat{Type: openai.ChatCompletionResponseFormatTypeJSONObject} return o.createStream(ctx, request) } func (o *OpenAIClient) completionJSONSchemaStream(ctx context.Context, request Request, schema *Schema) (<-chan string, error) { - request.Messages = prepend(request.Messages, *createJSONMessage(schema)) + request.Messages = prepend(request.Messages, *createJSONMessageStream(schema)) return o.createStream(ctx, request) } @@ -170,13 +184,13 @@ func createTools(schema *Schema) []openai.Tool { return tools } -func createJSONMessage(schema *Schema) *Message { +func createJSONMessageStream(schema *Schema) *Message { message := fmt.Sprintf(` -Please respond with JSON in the following JSON schema: +Please respond with a JSON array where the elements following JSON schema: %s -Make sure to return an instance of the JSON, not the schema itself +Make sure to return an array with the elements an instance of the JSON, not the schema itself. `, schema.String) return &Message{ Role: RoleSystem, @@ -198,11 +212,9 @@ func (o *OpenAIClient) createStream(ctx context.Context, request Request) (<-cha for { response, err := stream.Recv() if errors.Is(err, io.EOF) { - println("closing channel") return } if err != nil { - println("channel errored") return } text := response.Choices[0].Delta.Content From cc234fc3447324b2abd21db388767e8c8fb13094 Mon Sep 17 00:00:00 2001 From: Robby Date: Fri, 24 May 2024 17:07:21 -0400 Subject: [PATCH 3/5] use generic stream array wrapper struct --- examples/streaming/main.go | 27 ++++++------------- pkg/instructor/instructor.go | 52 +++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 29 deletions(-) diff --git a/examples/streaming/main.go b/examples/streaming/main.go index 88795cf..97f9817 100644 --- a/examples/streaming/main.go +++ b/examples/streaming/main.go @@ -16,7 +16,7 @@ type Product struct { } func (p *Product) String() string { - return fmt.Sprintf("product=[id=%s,name=%s]", p.ID, p.Name) + return fmt.Sprintf("Product [ID: %s, Name: %s]", p.ID, p.Name) } type Recommendation struct { @@ -25,11 +25,12 @@ type Recommendation struct { } func (r *Recommendation) String() string { - return fmt.Sprintf("recommendation=[%s, reason=%s]", r.Product.String(), r.Reason) -} - -type Recommendations struct { - Items []Recommendation `json:"items" jsonschema:"title=Product Recommendations,description=List of product recommendations"` + return fmt.Sprintf(` +Recommendation [ + %s + Reason [%s] +] +`, r.Product.String(), r.Reason) } func main() { @@ -82,7 +83,7 @@ Preferred Shopping Times: Weekend Evenings Messages: []instructor.Message{ { Role: instructor.RoleSystem, - Content: fmt.Sprintf(`Generate the top 3 product recommendations from the product list based on the customer profile. + Content: fmt.Sprintf(`Generate the product recommendations from the product list based on the customer profile. Return in order of highest recommended first. Product list: %s`, productList), @@ -94,7 +95,6 @@ Product list: }, Stream: true, }, - new(Recommendations), *new(Recommendation), ) if err != nil { @@ -112,15 +112,4 @@ Product list: println(recommendation.String()) } - // for instance := range recommendationsChan { - // recommendations, ok := instance.(*Recommendations) - // if !ok { - // // Handle error: the received value is not a *Recommendations - // continue - // } - // - // for _, recommendation := range recommendations.Items { - // println(recommendation.String()) - // } - // } } diff --git a/pkg/instructor/instructor.go b/pkg/instructor/instructor.go index 12e1cd6..c1a2fcb 100644 --- a/pkg/instructor/instructor.go +++ b/pkg/instructor/instructor.go @@ -90,14 +90,26 @@ func (i *Instructor) CreateChatCompletion(ctx context.Context, request Request, return errors.New("hit max retry attempts") } -func (i *Instructor) CreateChatCompletionStream(ctx context.Context, request Request, responseSlice, responseElem any) (chan any, error) { +const WRAPPER_END = `"items": [` - // used to signal to model to send a stream of the elements of that type - sliceType := reflect.TypeOf(responseSlice) - // used to create the channel to parse elements of this type and send them - elemType := reflect.TypeOf(responseElem) +type StreamWrapper[T any] struct { + Items []T `json:"items"` +} + +func (i *Instructor) CreateChatCompletionStream(ctx context.Context, request Request, response any) (chan any, error) { + + responseType := reflect.TypeOf(response) - schema, err := NewSchema(sliceType) + streamWrapperType := reflect.StructOf([]reflect.StructField{ + { + Name: "Items", + Type: reflect.SliceOf(responseType), + Tag: `json:"items"`, + Anonymous: false, + }, + }) + + schema, err := NewSchema(streamWrapperType) if err != nil { return nil, err } @@ -122,20 +134,40 @@ func (i *Instructor) CreateChatCompletionStream(ctx context.Context, request Req return case text, ok := <-ch: if !ok { + // Steeam closed + + // Get last element out of stream wrapper + + data := buffer.String() + + if idx := strings.LastIndex(data, "]"); idx != -1 { + data = data[:idx] + data[idx+1:] + } + + // Process the remaining data in the buffer + decoder := json.NewDecoder(strings.NewReader(data)) + for decoder.More() { + instance := reflect.New(responseType).Interface() + err := decoder.Decode(instance) + if err != nil { + break + } + parsedChan <- instance + } return } buffer.WriteString(text) // eat all input until elements stream starts if !inArray { - idx := strings.Index(buffer.String(), `[`) + idx := strings.Index(buffer.String(), WRAPPER_END) if idx == -1 { continue } inArray = true bufferStr := buffer.String() - trimmed := strings.TrimSpace(bufferStr[idx+1:]) + trimmed := strings.TrimSpace(bufferStr[idx+len(WRAPPER_END):]) buffer.Reset() buffer.WriteString(trimmed) } @@ -144,10 +176,10 @@ func (i *Instructor) CreateChatCompletionStream(ctx context.Context, request Req decoder := json.NewDecoder(strings.NewReader(data)) for decoder.More() { - instance := reflect.New(elemType).Interface() + instance := reflect.New(responseType).Interface() err := decoder.Decode(instance) if err != nil { - break // Stop on error + break } parsedChan <- instance From 39bd2887819bf392d632366a7aa7c6a35eb9205b Mon Sep 17 00:00:00 2001 From: Robby Date: Fri, 24 May 2024 17:09:59 -0400 Subject: [PATCH 4/5] cleanup print --- examples/streaming/main.go | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/examples/streaming/main.go b/examples/streaming/main.go index 97f9817..5a5220f 100644 --- a/examples/streaming/main.go +++ b/examples/streaming/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "reflect" "github.com/instructor-ai/instructor-go/pkg/instructor" openai "github.com/sashabaranov/go-openai" @@ -29,8 +28,7 @@ func (r *Recommendation) String() string { Recommendation [ %s Reason [%s] -] -`, r.Product.String(), r.Reason) +]`, r.Product.String(), r.Reason) } func main() { @@ -102,14 +100,33 @@ Product list: } for instance := range recommendationChan { - recommendation, ok := instance.(*Recommendation) - if !ok { - // Handle error: the received value is not a *Recommendations - println("channel is not of correct type. Actual type: " + reflect.TypeOf(instance).String()) - continue - } - + recommendation, _ := instance.(*Recommendation) println(recommendation.String()) } + /* + Recommendation [ + Product [ID: 7, Name: Apple MacBook Air (2023) - Latest model, high performance, portable] + Reason [As you have recently searched for budget laptops of 2023 and previously purchased a laptop, we believe the latest Apple MacBook Air will meet your high-performance requirements. Additionally, Apple is one of your preferred brands.] + ] + + Recommendation [ + Product [ID: 2, Name: Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem] + Reason [Based on your recent purchase history which includes a smart watch and your preference for Apple products, we recommend the Apple Watch Series 7 for its advanced fitness tracking features.] + ] + + Recommendation [ + Product [ID: 10, Name: Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author] + Reason [Given your recent search for the latest sci-fi books and frequent browsing in the Books category, 'Project Hail Mary' by Andy Weir may interest you.] + ] + + Recommendation [ + Product [ID: 5, Name: Manduka PRO Yoga Mat - High-quality, durable, eco-friendly] + Reason [Since you recently searched for yoga mats and frequently browse fitness equipment, we recommend the Manduka PRO Yoga Mat to support your fitness activities.] + ] + Recommendation [ + Product [ID: 4, Name: AllBirds Wool Runners - Comfortable, eco-friendly sneakers] + Reason [Considering your preference for the AllBirds brand and your frequent browsing in fitness categories, the AllBirds Wool Runners would be a great fit for your lifestyle.] + ] + */ } From 7db61faa55f77f03b408fde4638bdd3580b2b7a0 Mon Sep 17 00:00:00 2001 From: Robby Date: Fri, 24 May 2024 17:31:15 -0400 Subject: [PATCH 5/5] Update readme --- README.md | 659 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 657 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 108b545..778a816 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,16 @@ As shown in the example below, by adding extra metadata to each struct field (vi > For more information on the `jsonschema` tags available, see the [`jsonschema` godoc](https://pkg.go.dev/github.com/invopop/jsonschema?utm_source=godoc). +
+Running + +```bash +export OPENAI_API_KEY= +go run examples/user/main.go +``` + +
+ ```go package main @@ -76,9 +86,654 @@ Age: %d } ``` -## Coming Soon +### Other Examples + +
+Function Calling with OpenAI + +
+Running + +```bash +export OPENAI_API_KEY= +go run examples/function_calling/main.go +``` + +
+ +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + openai "github.com/sashabaranov/go-openai" +) + +type SearchType string + +const ( + Web SearchType = "web" + Image SearchType = "image" + Video SearchType = "video" +) + +type Search struct { + Topic string `json:"topic" jsonschema:"title=Topic,description=Topic of the search,example=golang"` + Query string `json:"query" jsonschema:"title=Query,description=Query to search for relevant content,example=what is golang"` + Type SearchType `json:"type" jsonschema:"title=Type,description=Type of search,default=web,enum=web,enum=image,enum=video"` +} + +func (s *Search) execute() { + fmt.Printf("Searching for `%s` with query `%s` using `%s`\n", s.Topic, s.Query, s.Type) +} + +type Searches = []Search + +func segment(ctx context.Context, data string) *Searches { + + client, err := instructor.FromOpenAI( + openai.NewClient(os.Getenv("OPENAI_API_KEY")), + instructor.WithMode(instructor.ModeToolCall), + instructor.WithMaxRetries(3), + ) + if err != nil { + panic(err) + } + + var searches Searches + err = client.CreateChatCompletion( + ctx, + instructor.Request{ + Model: openai.GPT4o, + Messages: []instructor.Message{ + { + Role: instructor.RoleUser, + Content: fmt.Sprintf("Consider the data below: '\n%s' and segment it into multiple search queries", data), + }, + }, + }, + &searches, + ) + if err != nil { + panic(err) + } + + return &searches +} + +func main() { + ctx := context.Background() + + q := "Search for a picture of a cat, a video of a dog, and the taxonomy of each" + for _, search := range *segment(ctx, q) { + search.execute() + } + /* + Searching for `cat` with query `picture of a cat` using `image` + Searching for `dog` with query `video of a dog` using `video` + Searching for `cat` with query `taxonomy of a cat` using `web` + Searching for `dog` with query `taxonomy of a dog` using `web` + */ +} +``` + +
+ +
+Text Classification with Anthropic + +
+Running + +```bash +export ANTHROPIC_API_KEY= +go run examples/classification/main.go +``` + +
+ +```go + package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + anthropic "github.com/liushuangls/go-anthropic/v2" +) + +type LabelType string + +const ( + LabelTechIssue LabelType = "tech_issue" + LabelBilling LabelType = "billing" + LabelGeneralQuery LabelType = "general_query" +) + +type Label struct { + Type LabelType `json:"type" jsonschema:"title=Label type,description=Type of the label,enum=tech_issue,enum=billing,enum=general_query"` +} + +type Prediction struct { + Labels []Label `json:"labels" jsonschema:"title=Predicted labels,description=Labels of the prediction"` +} + +func classify(data string) *Prediction { + ctx := context.Background() + + client, err := instructor.FromAnthropic( + anthropic.NewClient(os.Getenv("ANTHROPIC_API_KEY")), + instructor.WithMode(instructor.ModeToolCall), + instructor.WithMaxRetries(3), + ) + if err != nil { + panic(err) + } + + var prediction Prediction + err = client.CreateChatCompletion( + ctx, + instructor.Request{ + Model: anthropic.ModelClaude3Haiku20240307, + Messages: []instructor.Message{ + { + Role: instructor.RoleUser, + Content: fmt.Sprintf("Classify the following support ticket: %s", data), + }, + }, + }, + &prediction, + ) + if err != nil { + panic(err) + } + + return &prediction +} + +func main() { + + ticket := "My account is locked and I can't access my billing info." + prediction := classify(ticket) + + assert(prediction.contains(LabelTechIssue), "Expected ticket to be related to tech issue") + assert(prediction.contains(LabelBilling), "Expected ticket to be related to billing") + assert(!prediction.contains(LabelGeneralQuery), "Expected ticket NOT to be a general query") + + fmt.Printf("%+v\n", prediction) + /* + &{Labels:[{Type:tech_issue} {Type:billing}]} + */ +} + +/******/ + +func (p *Prediction) contains(label LabelType) bool { + for _, l := range p.Labels { + if l.Type == label { + return true + } + } + return false +} + +func assert(condition bool, message string) { + if !condition { + fmt.Println("Assertion failed:", message) + } +} +``` + +
+ +
+Images with OpenAI + +
+Running + +```bash +export OPENAI_API_KEY= +go run examples/images/openai/main.go +``` + +
+ +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + openai "github.com/sashabaranov/go-openai" +) + +type Book struct { + Title string `json:"title,omitempty" jsonschema:"title=title,description=The title of the book,example=Harry Potter and the Philosopher's Stone"` + Author *string `json:"author,omitempty" jsonschema:"title=author,description=The author of the book,example=J.K. Rowling"` +} + +type BookCatalog struct { + Catalog []Book `json:"catalog"` +} + +func (bc *BookCatalog) PrintCatalog() { + fmt.Printf("Number of books in the catalog: %d\n\n", len(bc.Catalog)) + for _, book := range bc.Catalog { + fmt.Printf("Title: %s\n", book.Title) + fmt.Printf("Author: %s\n", *book.Author) + fmt.Println("--------------------") + } +} + +func main() { + ctx := context.Background() + + client, err := instructor.FromOpenAI( + openai.NewClient(os.Getenv("OPENAI_API_KEY")), + instructor.WithMode(instructor.ModeJSON), + instructor.WithMaxRetries(3), + ) + if err != nil { + panic(err) + } + + url := "https://utfs.io/f/fe55d6bd-e920-4a6f-8e93-a4c9dd851b90-eivhb2.png" + + var bookCatalog BookCatalog + err = client.CreateChatCompletion( + ctx, + instructor.Request{ + Model: openai.GPT4o, + Messages: []instructor.Message{ + { + Role: instructor.RoleUser, + MultiContent: []instructor.ChatMessagePart{ + { + Type: instructor.ChatMessagePartTypeText, + Text: "Extract book catelog from the image", + }, + { + Type: instructor.ChatMessagePartTypeImageURL, + ImageURL: &instructor.ChatMessageImageURL{ + URL: url, + }, + }, + }, + }, + }, + }, + &bookCatalog, + ) + + if err != nil { + panic(err) + } + + bookCatalog.PrintCatalog() + /* + Number of books in the catalog: 15 + + Title: Pride and Prejudice + Author: Jane Austen + -------------------- + Title: The Great Gatsby + Author: F. Scott Fitzgerald + -------------------- + Title: The Catcher in the Rye + Author: J. D. Salinger + -------------------- + Title: Don Quixote + Author: Miguel de Cervantes + -------------------- + Title: One Hundred Years of Solitude + Author: Gabriel García Márquez + -------------------- + Title: To Kill a Mockingbird + Author: Harper Lee + -------------------- + Title: Beloved + Author: Toni Morrison + -------------------- + Title: Ulysses + Author: James Joyce + -------------------- + Title: Harry Potter and the Cursed Child + Author: J.K. Rowling + -------------------- + Title: The Grapes of Wrath + Author: John Steinbeck + -------------------- + Title: 1984 + Author: George Orwell + -------------------- + Title: Lolita + Author: Vladimir Nabokov + -------------------- + Title: Anna Karenina + Author: Leo Tolstoy + -------------------- + Title: Moby-Dick + Author: Herman Melville + -------------------- + Title: Wuthering Heights + Author: Emily Brontë + -------------------- + */ +} +``` + +
+ +
+Images with Anthropic + +
+Running + +```bash +export ANTHROPIC_API_KEY= +go run examples/images/anthropic/main.go +``` + +
+ +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + "github.com/liushuangls/go-anthropic/v2" +) + +type Movie struct { + Title string `json:"title" jsonschema:"title=title,description=The title of the movie,required=true,example=Ex Machina"` + Year int `json:"year,omitempty" jsonschema:"title=year,description=The year of the movie,required=false,example=2014"` +} + +type MovieCatalog struct { + Catalog []Movie `json:"catalog"` +} + +func (bc *MovieCatalog) PrintCatalog() { + fmt.Printf("Number of movies in the catalog: %d\n\n", len(bc.Catalog)) + for _, movie := range bc.Catalog { + fmt.Printf("Title: %s\n", movie.Title) + if movie.Year != 0 { + fmt.Printf("Year: %d\n", movie.Year) + } + fmt.Println("--------------------") + } +} + +func main() { + ctx := context.Background() + + client, err := instructor.FromAnthropic( + anthropic.NewClient(os.Getenv("ANTHROPIC_API_KEY")), + instructor.WithMode(instructor.ModeJSONSchema), + instructor.WithMaxRetries(3), + ) + if err != nil { + panic(err) + } + + url := "https://utfs.io/f/bd0dbae6-27e3-4604-b640-fd2ffea891b8-fxyywt.jpeg" + + var movieCatalog MovieCatalog + err = client.CreateChatCompletion( + ctx, + instructor.Request{ + Model: "claude-3-haiku-20240307", + Messages: []instructor.Message{ + { + Role: instructor.RoleUser, + MultiContent: []instructor.ChatMessagePart{ + { + Type: instructor.ChatMessagePartTypeText, + Text: "Extract the movie catalog from the screenshot", + }, + { + Type: instructor.ChatMessagePartTypeImageURL, + ImageURL: &instructor.ChatMessageImageURL{ + URL: url, + }, + }, + }, + }, + }, + }, + &movieCatalog, + ) + if err != nil { + panic(err) + } + + movieCatalog.PrintCatalog() + /* + Number of movies in the catalog: 18 + + Title: Oppenheimer + Year: 2023 + -------------------- + Title: The Dark Knight + Year: 2008 + -------------------- + Title: Interstellar + Year: 2014 + -------------------- + Title: Inception + Year: 2010 + -------------------- + Title: Tenet + Year: 2020 + -------------------- + Title: Dunkirk + Year: 2017 + -------------------- + Title: Memento + Year: 2000 + -------------------- + Title: The Dark Knight Rises + Year: 2012 + -------------------- + Title: Batman Begins + Year: 2005 + -------------------- + Title: The Prestige + Year: 2006 + -------------------- + Title: Insomnia + Year: 2002 + -------------------- + Title: Following + Year: 1998 + -------------------- + Title: Man of Steel + Year: 2013 + -------------------- + Title: Transcendence + Year: 2014 + -------------------- + Title: Justice League + Year: 2017 + -------------------- + Title: Batman v Superman: Dawn of Justice + Year: 2016 + -------------------- + Title: Ending the Knight + Year: 2016 + -------------------- + Title: Larceny + -------------------- + */ +} +``` + +
+ +
+Streaming with OpenAI + +
+Running + +```bash +export OPENAI_API_KEY= +go run examples/streaming/main.go +``` + +
+ +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/instructor-ai/instructor-go/pkg/instructor" + openai "github.com/sashabaranov/go-openai" +) + +type Product struct { + ID string `json:"product_id" jsonschema:"title=Product ID,description=ID of the product,required=True"` + Name string `json:"product_name" jsonschema:"title=Product Name,description=Name of the product,required=True"` +} + +func (p *Product) String() string { + return fmt.Sprintf("Product [ID: %s, Name: %s]", p.ID, p.Name) +} + +type Recommendation struct { + Product + Reason string `json:"reason" jsonschema:"title=Recommendation Reason,description=Reason for the product recommendation"` +} + +func (r *Recommendation) String() string { + return fmt.Sprintf(` +Recommendation [ + %s + Reason [%s] +]`, r.Product.String(), r.Reason) +} + +func main() { + ctx := context.Background() + + client, err := instructor.FromOpenAI( + openai.NewClient(os.Getenv("OPENAI_API_KEY")), + instructor.WithMode(instructor.ModeJSON), + ) + if err != nil { + panic(err) + } + + profileData := ` +Customer ID: 12345 +Recent Purchases: [Laptop, Wireless Headphones, Smart Watch] +Frequently Browsed Categories: [Electronics, Books, Fitness Equipment] +Product Ratings: {Laptop: 5 stars, Wireless Headphones: 4 stars} +Recent Search History: [best budget laptops 2023, latest sci-fi books, yoga mats] +Preferred Brands: [Apple, AllBirds, Bench] +Responses to Previous Recommendations: {Philips: Not Interested, Adidas: Not Interested} +Loyalty Program Status: Gold Member +Average Monthly Spend: $500 +Preferred Shopping Times: Weekend Evenings +... +` + + products := []Product{ + {ID: "1", Name: "Sony WH-1000XM4 Wireless Headphones - Noise-canceling, long battery life"}, + {ID: "2", Name: "Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem"}, + {ID: "3", Name: "Kindle Oasis - Premium e-reader with adjustable warm light"}, + {ID: "4", Name: "AllBirds Wool Runners - Comfortable, eco-friendly sneakers"}, + {ID: "5", Name: "Manduka PRO Yoga Mat - High-quality, durable, eco-friendly"}, + {ID: "6", Name: "Bench Hooded Jacket - Stylish, durable, suitable for outdoor activities"}, + {ID: "7", Name: "Apple MacBook Air (2023) - Latest model, high performance, portable"}, + {ID: "8", Name: "GoPro HERO9 Black - 5K video, waterproof, for action photography"}, + {ID: "9", Name: "Nespresso Vertuo Next Coffee Machine - Quality coffee, easy to use, compact design"}, + {ID: "10", Name: "Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author"}, + } + + productList := "" + for _, product := range products { + productList += product.String() + "\n" + } + + recommendationChan, err := client.CreateChatCompletionStream( + ctx, + instructor.Request{ + Model: openai.GPT4o20240513, + Messages: []instructor.Message{ + { + Role: instructor.RoleSystem, + Content: fmt.Sprintf(`Generate the product recommendations from the product list based on the customer profile. +Return in order of highest recommended first. +Product list: +%s`, productList), + }, + { + Role: instructor.RoleUser, + Content: fmt.Sprintf("User profile:\n%s", profileData), + }, + }, + Stream: true, + }, + *new(Recommendation), + ) + if err != nil { + panic(err) + } + + for instance := range recommendationChan { + recommendation, _ := instance.(*Recommendation) + println(recommendation.String()) + } + /* + Recommendation [ + Product [ID: 7, Name: Apple MacBook Air (2023) - Latest model, high performance, portable] + Reason [As you have recently searched for budget laptops of 2023 and previously purchased a laptop, we believe the latest Apple MacBook Air will meet your high-performance requirements. Additionally, Apple is one of your preferred brands.] + ] + + Recommendation [ + Product [ID: 2, Name: Apple Watch Series 7 - Advanced fitness tracking, seamless integration with Apple ecosystem] + Reason [Based on your recent purchase history which includes a smart watch and your preference for Apple products, we recommend the Apple Watch Series 7 for its advanced fitness tracking features.] + ] + + Recommendation [ + Product [ID: 10, Name: Project Hail Mary by Andy Weir - Latest sci-fi book from a renowned author] + Reason [Given your recent search for the latest sci-fi books and frequent browsing in the Books category, 'Project Hail Mary' by Andy Weir may interest you.] + ] + + Recommendation [ + Product [ID: 5, Name: Manduka PRO Yoga Mat - High-quality, durable, eco-friendly] + Reason [Since you recently searched for yoga mats and frequently browse fitness equipment, we recommend the Manduka PRO Yoga Mat to support your fitness activities.] + ] + + Recommendation [ + Product [ID: 4, Name: AllBirds Wool Runners - Comfortable, eco-friendly sneakers] + Reason [Considering your preference for the AllBirds brand and your frequent browsing in fitness categories, the AllBirds Wool Runners would be a great fit for your lifestyle.] + ] + */ +} +``` -1. Streaming support +
## Providers