From bd0f27529488b095e506f4aed9ad96ffec2fce04 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 8 May 2024 10:54:52 +0400 Subject: [PATCH 01/21] feat(openai): add text to embedding operation --- providers/openai/helpers.go | 50 +++++++++++++++++++++++ providers/openai/helpers_test.go | 41 +++++++++++++++++++ providers/openai/text_to_embedding.go | 57 +++++++++++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 providers/openai/helpers.go create mode 100644 providers/openai/helpers_test.go create mode 100644 providers/openai/text_to_embedding.go diff --git a/providers/openai/helpers.go b/providers/openai/helpers.go new file mode 100644 index 0000000..67ff44e --- /dev/null +++ b/providers/openai/helpers.go @@ -0,0 +1,50 @@ +package openai + +import ( + "encoding/binary" + "fmt" + "math" +) + +func EmbeddingToBytes(dimensions int, embeddings [][]float32) ([]byte, error) { + if len(embeddings) == 0 { + return nil, fmt.Errorf("embeddings is empty") + } + + buf := make([]byte, len(embeddings)*dimensions*4) + + for i, embedding := range embeddings { + if len(embedding) != dimensions { + return nil, fmt.Errorf("invalid embedding length: %d, expected %d", len(embedding), dimensions) + } + + for j, f := range embedding { + u := math.Float32bits(f) + binary.LittleEndian.PutUint32(buf[(i*dimensions+j)*4:], u) + } + } + + return buf, nil +} + +func BytesToEmbedding(dimensions int, buf []byte) ([][]float32, error) { + if mltp := len(buf) % (dimensions * 4); mltp != 0 { + return nil, fmt.Errorf("invalid buffer length: got %d, but expected multiple of %d", len(buf), dimensions*4) + } + + embeddings := make([][]float32, len(buf)/dimensions/4) + for i := range embeddings { + embeddings[i] = make([]float32, dimensions) + for j := 0; j < dimensions; j++ { + index := (i*dimensions + j) * 4 + + if index+4 > len(buf) { + return nil, fmt.Errorf("buffer is too small for expected number of embeddings") + } + + embeddings[i][j] = math.Float32frombits(binary.LittleEndian.Uint32(buf[index:])) + } + } + + return embeddings, nil +} diff --git a/providers/openai/helpers_test.go b/providers/openai/helpers_test.go new file mode 100644 index 0000000..9817173 --- /dev/null +++ b/providers/openai/helpers_test.go @@ -0,0 +1,41 @@ +package openai + +import ( + "reflect" + "testing" +) + +func TestEmbeddingToBytes(t *testing.T) { + floats := [][]float32{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}} + + bytes, err := EmbeddingToBytes(3, floats) + if err != nil { + t.Errorf("EmbeddingToBytes error %v", err) + } + + newFloats, err := BytesToEmbedding(3, bytes) + if err != nil { + t.Errorf("EmbeddingToBytes error %v", err) + } + + if !reflect.DeepEqual(floats, newFloats) { + t.Errorf("floats and newFloats are not equal %v %v", floats, newFloats) + } + + wrongFloats := [][]float32{{4.4, 5.5, 6.6, 7.7}} + + _, err = EmbeddingToBytes(3, wrongFloats) + if err == nil { + t.Errorf("EmbeddingToBytes should has error") + } + + bytes, err = EmbeddingToBytes(4, wrongFloats) + if err != nil { + t.Errorf("EmbeddingToBytes error %v", err) + } + + _, err = BytesToEmbedding(3, bytes) + if err == nil { + t.Errorf("BytesToEmbedding should has error") + } +} diff --git a/providers/openai/text_to_embedding.go b/providers/openai/text_to_embedding.go new file mode 100644 index 0000000..87e153d --- /dev/null +++ b/providers/openai/text_to_embedding.go @@ -0,0 +1,57 @@ +package openai + +import ( + "context" + "fmt" + + "github.com/sashabaranov/go-openai" + + "github.com/neurocult/agency" +) + +type EmbeddingModel = openai.EmbeddingModel + +const AdaEmbeddingV2 EmbeddingModel = openai.AdaEmbeddingV2 + +type TextToEmbeddingParams struct { + Model EmbeddingModel +} + +func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operation { + return agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { + //TODO: we have to convert string to model and then model to string. Can we optimize it? + messages := append(cfg.Messages, msg) + texts := make([]string, len(messages)) + + for i, m := range messages { + texts[i] = m.String() + } + + resp, err := p.client.CreateEmbeddings( + ctx, + openai.EmbeddingRequest{ + Input: texts, + Model: params.Model, + }, + ) + if err != nil { + return agency.Message{}, err + } + + vectors := make([][]float32, len(resp.Data)) + for i, vector := range resp.Data { + vectors[i] = vector.Embedding + } + + bytes, err := EmbeddingToBytes(1536, vectors) + if err != nil { + return agency.Message{}, fmt.Errorf("failed to convert embedding to bytes: %w", err) + } + + return agency.Message{ + Role: agency.AssistantRole, + //TODO: we have to convert []float32 to []byte. Can we optimize it? + Content: bytes, + }, nil + }) +} From 5efe77bd5ac1288abb44cced00fc7ec73f13bbb8 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 8 May 2024 10:55:11 +0400 Subject: [PATCH 02/21] feat(openai): add text to stream operation --- providers/openai/text_to_stream.go | 181 +++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 51 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index ee81b6f..4d2cd9c 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -2,7 +2,9 @@ package openai import ( "context" + "encoding/json" "errors" + "fmt" "io" "github.com/neurocult/agency" @@ -13,67 +15,144 @@ type TextToStreamParams struct { Model string Temperature NullableFloat32 MaxTokens int + FuncDefs []FuncDef + Stream chan<- string } -type streamHandler func(delta string) error +var ToolAnswerShouldBeFinal = errors.New("tool answer should be final") -func (p Provider) TextToStream(params TextToStreamParams, handler streamHandler) *agency.Operation { - return agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { - openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) +func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { + openAITools := castFuncDefsToOpenAITools(params.FuncDefs) - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleSystem, - Content: cfg.Prompt, - }) + return agency.NewOperation( + func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { + openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) - for _, textMsg := range cfg.Messages { openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: string(textMsg.Role), - Content: string(textMsg.Content), + Role: openai.ChatMessageRoleSystem, + Content: cfg.Prompt, }) - } - - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: msg.String(), - }) - - resp, err := p.client.CreateChatCompletionStream( - ctx, - openai.ChatCompletionRequest{ - Model: params.Model, - Temperature: nullableToFloat32(params.Temperature), - MaxTokens: params.MaxTokens, - Messages: openAIMessages, - Stream: true, - }, - ) - if err != nil { - return agency.Message{}, err - } - defer resp.Close() - - var content string - for { - response, err := resp.Recv() - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return agency.Message{}, nil + for _, textMsg := range cfg.Messages { + openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ + Role: string(textMsg.Role), + Content: string(textMsg.Content), + }) } - if err := handler(response.Choices[0].Delta.Content); err != nil { - return agency.Message{}, err - } + openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: msg.String(), + }) + + for { + openAIResponse, err := p.client.CreateChatCompletionStream( + ctx, + openai.ChatCompletionRequest{ + Model: params.Model, + Temperature: nullableToFloat32(params.Temperature), + MaxTokens: params.MaxTokens, + Messages: openAIMessages, + Tools: openAITools, + Stream: params.Stream != nil, + }, + ) + if err != nil { + return agency.Message{}, fmt.Errorf("create chat completion stream: %w", err) + } + + var content string + var accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools)) + for { + recv, err := openAIResponse.Recv() + if errors.Is(err, io.EOF) { + if len(accumulatedStreamedFunctions) == 0 { + return agency.Message{ + Role: agency.AssistantRole, + Content: []byte(content), + }, nil + } + + break + } + + if err != nil { + return agency.Message{}, err + } + + if len(recv.Choices) < 1 { + return agency.Message{}, errors.New("no choice") + } + + firstChoice := recv.Choices[0] + + if len(firstChoice.Delta.Content) > 0 { + params.Stream <- firstChoice.Delta.Content + content += firstChoice.Delta.Content + } - content += response.Choices[0].Delta.Content - } + for index, toolCall := range firstChoice.Delta.ToolCalls { + if len(accumulatedStreamedFunctions) < index+1 { + accumulatedStreamedFunctions = append(accumulatedStreamedFunctions, openai.ToolCall{ + Index: toolCall.Index, + ID: toolCall.ID, + Type: toolCall.Type, + Function: openai.FunctionCall{ + Name: toolCall.Function.Name, + Arguments: toolCall.Function.Arguments, + }, + }) + } + accumulatedStreamedFunctions[index].Function.Arguments += toolCall.Function.Arguments + } - return agency.Message{ - Role: agency.AssistantRole, - Content: []byte(content), - }, nil - }) + if firstChoice.FinishReason != openai.FinishReasonToolCalls { + continue + } + + openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + ToolCalls: accumulatedStreamedFunctions, + }) + + for _, toolCall := range accumulatedStreamedFunctions { + + funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) + if funcToCall == nil { + return agency.Message{}, errors.New("function not found") + } + + funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) + var isFinal = errors.Is(err, ToolAnswerShouldBeFinal) + if err != nil && !isFinal { + return agency.Message{}, fmt.Errorf("call function %s: %w", funcToCall.Name, err) + } + + escapedFuncResult, err := json.Marshal(funcResult) + if err != nil { + return agency.Message{}, fmt.Errorf("marshal function result: %w", err) + } + + if isFinal { + params.Stream <- string(escapedFuncResult) + + return agency.Message{ + Role: openai.ChatMessageRoleTool, + Content: escapedFuncResult, + }, nil + } + + openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: string(escapedFuncResult), + Name: toolCall.Function.Name, + ToolCallID: toolCall.ID, + }) + } + } + + openAIResponse.Close() + } + }, + ) } From e7366202bfba9c514bd3c4070d3ece525638d72b Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 8 May 2024 11:53:48 +0400 Subject: [PATCH 03/21] feat(agency): change message struct to interface --- agency.go | 2 +- examples/chat/main.go | 2 +- examples/cli/main.go | 2 +- examples/completion_streaming/main.go | 28 +++++++----- examples/custom_operation/main.go | 8 ++-- examples/func_call/main.go | 6 +-- examples/image_to_text/main.go | 4 +- examples/logging/main.go | 2 +- examples/prompt_template/main.go | 2 +- examples/rag_vector_database/main.go | 19 ++++---- examples/speech_to_text/main.go | 4 +- examples/speech_to_text_multi_model/main.go | 4 +- examples/speech_to_text_to_image/main.go | 4 +- examples/text_to_image_dalle2/main.go | 4 +- examples/text_to_speech/main.go | 10 +++-- examples/translate_text/main.go | 4 +- messages.go | 50 ++++++++++++++------- process.go | 2 +- providers/openai/helpers.go | 21 +++++++++ providers/openai/image_to_text.go | 13 +++--- providers/openai/provider.go | 25 ----------- providers/openai/speech_to_text.go | 9 ++-- providers/openai/text_to_embedding.go | 13 +++--- providers/openai/text_to_image.go | 11 ++--- providers/openai/text_to_speech.go | 11 ++--- providers/openai/text_to_stream.go | 28 +++++------- providers/openai/text_to_text.go | 25 ++++++----- 27 files changed, 159 insertions(+), 154 deletions(-) diff --git a/agency.go b/agency.go index 106c355..e527b76 100644 --- a/agency.go +++ b/agency.go @@ -37,7 +37,7 @@ func NewOperation(handler OperationHandler) *Operation { func (p *Operation) Execute(ctx context.Context, input Message) (Message, error) { output, err := p.handler(ctx, input, p.config) if err != nil { - return Message{}, err + return nil, err } return output, nil } diff --git a/examples/chat/main.go b/examples/chat/main.go index b938421..75d8c4f 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -30,7 +30,7 @@ func main() { panic(err) } - input := agency.UserMessage(text) + input := agency.NewMessage(agency.UserRole, agency.TextKind, []byte(text)) answer, err := assistant.SetMessages(messages).Execute(ctx, input) if err != nil { panic(err) diff --git a/examples/cli/main.go b/examples/cli/main.go index 2441b72..038b17d 100644 --- a/examples/cli/main.go +++ b/examples/cli/main.go @@ -38,7 +38,7 @@ func main() { MaxTokens: *maxTokens, }). SetPrompt(*prompt). - Execute(context.Background(), agency.UserMessage(content)) + Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte(content))) if err != nil { fmt.Println(err) diff --git a/examples/completion_streaming/main.go b/examples/completion_streaming/main.go index 2d1e1d9..9b9ed47 100644 --- a/examples/completion_streaming/main.go +++ b/examples/completion_streaming/main.go @@ -14,18 +14,24 @@ import ( func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) + stream := make(chan string) - result, err := factory. - TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo}, func(delta string) error { - fmt.Printf(delta) - return nil - }). - SetPrompt("Write a few sentences about topic"). - Execute(context.Background(), agency.UserMessage("I love programming.")) + go func() { + defer close(stream) - if err != nil { - panic(err) - } + result, err := factory. + TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo, Stream: stream}). + SetPrompt("Write a few sentences about topic"). + Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming."))) + + if err != nil { + panic(err) + } - fmt.Println("\nFinal result:", string(result.Content)) + fmt.Println("\nFinal result:", string(result.Content())) + }() + + for s := range stream { + fmt.Println(s) + } } diff --git a/examples/custom_operation/main.go b/examples/custom_operation/main.go index 4369a43..3ecd675 100644 --- a/examples/custom_operation/main.go +++ b/examples/custom_operation/main.go @@ -13,7 +13,7 @@ func main() { msg, err := agency.NewProcess( increment, increment, increment, - ).Execute(context.Background(), agency.UserMessage("0")) + ).Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("0"))) if err != nil { panic(err) @@ -23,10 +23,10 @@ func main() { } func incrementFunc(ctx context.Context, msg agency.Message, _ *agency.OperationConfig) (agency.Message, error) { - i, err := strconv.ParseInt(string(msg.Content), 10, 10) + i, err := strconv.ParseInt(string(msg.Content()), 10, 10) if err != nil { - return agency.Message{}, err + return nil, err } inc := strconv.Itoa(int(i) + 1) - return agency.SystemMessage(inc), nil + return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(inc)), nil } diff --git a/examples/func_call/main.go b/examples/func_call/main.go index 4e6aa26..ea921c5 100644 --- a/examples/func_call/main.go +++ b/examples/func_call/main.go @@ -64,7 +64,7 @@ Examples: // test for first function call answer, err := t2tOp.Execute( ctx, - agency.UserMessage("what is the meaning of life?"), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("what is the meaning of life?")), ) if err != nil { panic(err) @@ -74,7 +74,7 @@ Examples: // test for second function call answer, err = t2tOp.Execute( ctx, - agency.UserMessage("1+1?"), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("1+1?")), ) if err != nil { panic(err) @@ -84,7 +84,7 @@ Examples: // test for both function calls at the same time answer, err = t2tOp.Execute( ctx, - agency.UserMessage("1+1 and what is the meaning of life?"), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("1+1 and what is the meaning of life?")), ) if err != nil { panic(err) diff --git a/examples/image_to_text/main.go b/examples/image_to_text/main.go index 84fb35a..b1e7016 100644 --- a/examples/image_to_text/main.go +++ b/examples/image_to_text/main.go @@ -6,8 +6,8 @@ import ( "os" _ "github.com/joho/godotenv/autoload" - "github.com/neurocult/agency" + "github.com/neurocult/agency/providers/openai" ) @@ -22,7 +22,7 @@ func main() { SetPrompt("describe what you see"). Execute( context.Background(), - agency.Message{Content: imgBytes}, + agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), ) if err != nil { panic(err) diff --git a/examples/logging/main.go b/examples/logging/main.go index 57e35f1..13e092f 100644 --- a/examples/logging/main.go +++ b/examples/logging/main.go @@ -22,7 +22,7 @@ func main() { ). Execute( context.Background(), - agency.UserMessage("Kazakhstan alga!"), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("Kazakhstan alga!")), Logger, ) diff --git a/examples/prompt_template/main.go b/examples/prompt_template/main.go index 46ce6b1..e851d62 100644 --- a/examples/prompt_template/main.go +++ b/examples/prompt_template/main.go @@ -22,7 +22,7 @@ func main() { ). Execute( context.Background(), - agency.UserMessage("%s", "I love programming."), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming.")), ) if err != nil { diff --git a/examples/rag_vector_database/main.go b/examples/rag_vector_database/main.go index e4f112c..dd8b9eb 100644 --- a/examples/rag_vector_database/main.go +++ b/examples/rag_vector_database/main.go @@ -6,11 +6,11 @@ import ( "os" _ "github.com/joho/godotenv/autoload" + "github.com/neurocult/agency" "github.com/weaviate/weaviate-go-client/v4/weaviate" "github.com/weaviate/weaviate-go-client/v4/weaviate/graphql" "github.com/weaviate/weaviate/entities/models" - "github.com/neurocult/agency" "github.com/neurocult/agency/providers/openai" ) @@ -36,7 +36,7 @@ func main() { retrieve, summarize, voice, - ).Execute(ctx, agency.UserMessage("programming")) + ).Execute(ctx, agency.NewMessage(agency.UserRole, agency.TextKind, []byte("programming"))) if err != nil { panic(err) } @@ -49,7 +49,7 @@ func main() { // RAGoperation retrieves relevant objects from vector store and builds a text message to pass further to the process func RAGoperation(client *weaviate.Client) *agency.Operation { return agency.NewOperation(func(ctx context.Context, msg agency.Message, po *agency.OperationConfig) (agency.Message, error) { - input := msg.String() + input := string(msg.Content()) result, err := client.GraphQL().Get(). WithClassName("Records"). @@ -71,15 +71,16 @@ func RAGoperation(client *weaviate.Client) *agency.Operation { for _, obj := range result.Data { bb, err := json.Marshal(&obj) if err != nil { - return agency.Message{}, err + return nil, err } content += string(bb) } - return agency.Message{ - Role: agency.AssistantRole, - Content: []byte(content), - }, nil + return agency.NewMessage( + agency.AssistantRole, + agency.TextKind, + []byte(content), + ), nil }) } @@ -125,7 +126,7 @@ func saveToDisk(msg agency.Message) error { } defer file.Close() - _, err = file.Write(msg.Content) + _, err = file.Write(msg.Content()) if err != nil { return err } diff --git a/examples/speech_to_text/main.go b/examples/speech_to_text/main.go index de2922b..38b0895 100644 --- a/examples/speech_to_text/main.go +++ b/examples/speech_to_text/main.go @@ -26,9 +26,7 @@ func main() { Model: goopenai.Whisper1, }).Execute( context.Background(), - agency.Message{ - Content: data, - }, + agency.NewMessage(agency.UserRole, agency.VoiceKind, data), ) if err != nil { diff --git a/examples/speech_to_text_multi_model/main.go b/examples/speech_to_text_multi_model/main.go index 92cec8d..6e37972 100644 --- a/examples/speech_to_text_multi_model/main.go +++ b/examples/speech_to_text_multi_model/main.go @@ -52,7 +52,7 @@ func main() { } ctx := context.Background() - speechMsg := agency.Message{Content: sound} + speechMsg := agency.NewMessage(agency.UserRole, agency.VoiceKind, sound) _, err = agency.NewProcess( hear, @@ -64,6 +64,6 @@ func main() { } for _, msg := range saver { - fmt.Println(msg.String()) + fmt.Println(string(msg.Content())) } } diff --git a/examples/speech_to_text_to_image/main.go b/examples/speech_to_text_to_image/main.go index 420429e..a8e5d39 100644 --- a/examples/speech_to_text_to_image/main.go +++ b/examples/speech_to_text_to_image/main.go @@ -28,7 +28,7 @@ func main() { Model: goopenai.CreateImageModelDallE2, ImageSize: goopenai.CreateImageSize256x256, }), - ).Execute(context.Background(), agency.Message{Content: data}) + ).Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.VoiceKind, data)) if err != nil { panic(err) } @@ -39,7 +39,7 @@ func main() { } func saveImgToDisk(msg agency.Message) error { - r := bytes.NewReader(msg.Content) + r := bytes.NewReader(msg.Content()) imgData, err := png.Decode(r) if err != nil { diff --git a/examples/text_to_image_dalle2/main.go b/examples/text_to_image_dalle2/main.go index 57cfafd..3124fd9 100644 --- a/examples/text_to_image_dalle2/main.go +++ b/examples/text_to_image_dalle2/main.go @@ -26,7 +26,7 @@ func main() { Style: "vivid", }).Execute( context.Background(), - agency.UserMessage("Halloween night at a haunted museum"), + agency.NewMessage(agency.UserRole, agency.TextKind, []byte("Halloween night at a haunted museum")), ) if err != nil { panic(err) @@ -40,7 +40,7 @@ func main() { } func saveToDisk(msg agency.Message) error { - r := bytes.NewReader(msg.Content) + r := bytes.NewReader(msg.Content()) // for dall-e-3 use third party libraries due to lack of webp support in go stdlib imgData, format, err := image.Decode(r) diff --git a/examples/text_to_speech/main.go b/examples/text_to_speech/main.go index 89d2e15..5cbfd84 100644 --- a/examples/text_to_speech/main.go +++ b/examples/text_to_speech/main.go @@ -11,11 +11,15 @@ import ( ) func main() { - input := agency.UserMessage(` + input := agency.NewMessage( + + agency.UserRole, + agency.TextKind, + []byte(` One does not simply walk into Mordor. Its black gates are guarded by more than just Orcs. There is evil there that does not sleep, and the Great Eye is ever watchful. - `) + `)) msg, err := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}). TextToSpeech(openai.TextToSpeechParams{ @@ -42,7 +46,7 @@ func saveToDisk(msg agency.Message) error { } defer file.Close() - _, err = file.Write(msg.Content) + _, err = file.Write(msg.Content()) if err != nil { return err } diff --git a/examples/translate_text/main.go b/examples/translate_text/main.go index 90a9f6c..60f5485 100644 --- a/examples/translate_text/main.go +++ b/examples/translate_text/main.go @@ -18,11 +18,11 @@ func main() { result, err := factory. TextToText(openai.TextToTextParams{Model: goopenai.GPT3Dot5Turbo}). SetPrompt("You are a helpful assistant that translates English to French"). - Execute(context.Background(), agency.UserMessage("I love programming.")) + Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming."))) if err != nil { panic(err) } - fmt.Println(string(result.Content)) + fmt.Println(string(result.Content())) } diff --git a/messages.go b/messages.go index f178aa3..d38b1f9 100644 --- a/messages.go +++ b/messages.go @@ -1,15 +1,19 @@ package agency -import "fmt" - -type Message struct { - Role Role - Content []byte +type Message interface { + Role() Role + Content() []byte + Kind() Kind } -func (m Message) String() string { - return string(m.Content) -} +type Kind string + +const ( + TextKind Kind = "text" + ImageKind Kind = "image" + VoiceKind Kind = "voice" + VectorKind Kind = "vector" +) type Role string @@ -17,15 +21,31 @@ const ( UserRole Role = "user" SystemRole Role = "system" AssistantRole Role = "assistant" + ToolRole Role = "tool" ) -// UserMessage creates new `Message` with the `Role` equal to `user` -func UserMessage(content string, args ...any) Message { - s := fmt.Sprintf(content, args...) - return Message{Role: UserRole, Content: []byte(s)} +type BaseMessage struct { + content []byte + role Role + kind Kind +} + +func (bm BaseMessage) Role() Role { + return bm.role +} + +func (bm BaseMessage) Kind() Kind { + return bm.kind +} +func (bm BaseMessage) Content() []byte { + return bm.content } -// SystemMessage creates new `Message` with the `Role` equal to `system` -func SystemMessage(content string) Message { - return Message{Role: SystemRole, Content: []byte(content)} +// NewMessage creates new `Message` with the specified `Role` and `Kind` +func NewMessage(role Role, kind Kind, content []byte) BaseMessage { + return BaseMessage{ + content: content, + role: role, + kind: kind, + } } diff --git a/process.go b/process.go index af300dd..7f4a5a6 100644 --- a/process.go +++ b/process.go @@ -26,7 +26,7 @@ func (p *Process) Execute(ctx context.Context, input Message, interceptors ...In for _, operation := range p.operations { output, err := operation.Execute(ctx, input) if err != nil { - return Message{}, err + return nil, err } // FIXME while these are called AFTER operation and not before it's impossible to modify configuration diff --git a/providers/openai/helpers.go b/providers/openai/helpers.go index 67ff44e..92427f4 100644 --- a/providers/openai/helpers.go +++ b/providers/openai/helpers.go @@ -48,3 +48,24 @@ func BytesToEmbedding(dimensions int, buf []byte) ([][]float32, error) { return embeddings, nil } + +// NullableFloat32 is a type that exists to distinguish between undefined values and real zeros. +// It fixes sashabaranov/go-openai issue with zero temp not included in api request due to how json unmarshal work. +type NullableFloat32 *float32 + +// Temperature is just a tiny helper to create nullable float32 value from regular float32 +func Temperature(v float32) NullableFloat32 { + return &v +} + +// nullableToFloat32 replaces nil with zero (in this case value won't be included in api request) +// and for real zeros it returns math.SmallestNonzeroFloat32 that is as close to zero as possible. +func nullableToFloat32(v NullableFloat32) float32 { + if v == nil { + return 0 + } + if *v == 0 { + return math.SmallestNonzeroFloat32 + } + return *v +} diff --git a/providers/openai/image_to_text.go b/providers/openai/image_to_text.go index 21fad01..d81322d 100644 --- a/providers/openai/image_to_text.go +++ b/providers/openai/image_to_text.go @@ -34,13 +34,13 @@ func (f *Provider) ImageToText(params ImageToTextParams) *agency.Operation { for _, cfgMsg := range cfg.Messages { openaiMsg.MultiContent = append( openaiMsg.MultiContent, - openAIBase64ImageMessage(cfgMsg.Content), + openAIBase64ImageMessage(cfgMsg.Content()), ) } openaiMsg.MultiContent = append( openaiMsg.MultiContent, - openAIBase64ImageMessage(msg.Content), + openAIBase64ImageMessage(msg.Content()), ) resp, err := f.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ @@ -53,18 +53,15 @@ func (f *Provider) ImageToText(params ImageToTextParams) *agency.Operation { PresencePenalty: nullableToFloat32(params.PresencePenalty), }) if err != nil { - return agency.Message{}, err + return nil, err } if len(resp.Choices) < 1 { - return agency.Message{}, errors.New("no choice") + return nil, errors.New("no choice") } choice := resp.Choices[0].Message - return agency.Message{ - Role: agency.AssistantRole, - Content: []byte(choice.Content), - }, nil + return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(choice.Content)), nil }) } diff --git a/providers/openai/provider.go b/providers/openai/provider.go index e8859c2..74dbbb7 100644 --- a/providers/openai/provider.go +++ b/providers/openai/provider.go @@ -1,8 +1,6 @@ package openai import ( - "math" - "github.com/sashabaranov/go-openai" ) @@ -28,26 +26,3 @@ func New(params Params) *Provider { client: openai.NewClientWithConfig(cfg), } } - -// === Helpers === - -// NullableFloat32 is a type that exists to distinguish between undefined values and real zeros. -// It fixes sashabaranov/go-openai issue with zero temp not included in api request due to how json unmarshal work. -type NullableFloat32 *float32 - -// Temperature is just a tiny helper to create nullable float32 value from regular float32 -func Temperature(v float32) NullableFloat32 { - return &v -} - -// nullableToFloat32 replaces nil with zero (in this case value won't be included in api request) -// and for real zeros it returns math.SmallestNonzeroFloat32 that is as close to zero as possible. -func nullableToFloat32(v NullableFloat32) float32 { - if v == nil { - return 0 - } - if *v == 0 { - return math.SmallestNonzeroFloat32 - } - return *v -} diff --git a/providers/openai/speech_to_text.go b/providers/openai/speech_to_text.go index 4d9c41f..93d7252 100644 --- a/providers/openai/speech_to_text.go +++ b/providers/openai/speech_to_text.go @@ -21,17 +21,14 @@ func (f Provider) SpeechToText(params SpeechToTextParams) *agency.Operation { Model: params.Model, Prompt: cfg.Prompt, FilePath: "speech.ogg", - Reader: bytes.NewReader(msg.Content), + Reader: bytes.NewReader(msg.Content()), Temperature: nullableToFloat32(params.Temperature), }) if err != nil { - return agency.Message{}, err + return nil, err } - return agency.Message{ - Role: agency.AssistantRole, - Content: []byte(resp.Text), - }, nil + return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(resp.Text)), nil }, ) } diff --git a/providers/openai/text_to_embedding.go b/providers/openai/text_to_embedding.go index 87e153d..ec1718e 100644 --- a/providers/openai/text_to_embedding.go +++ b/providers/openai/text_to_embedding.go @@ -24,7 +24,7 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio texts := make([]string, len(messages)) for i, m := range messages { - texts[i] = m.String() + texts[i] = string(m.Content()) } resp, err := p.client.CreateEmbeddings( @@ -35,7 +35,7 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio }, ) if err != nil { - return agency.Message{}, err + return nil, err } vectors := make([][]float32, len(resp.Data)) @@ -45,13 +45,10 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio bytes, err := EmbeddingToBytes(1536, vectors) if err != nil { - return agency.Message{}, fmt.Errorf("failed to convert embedding to bytes: %w", err) + return nil, fmt.Errorf("failed to convert embedding to bytes: %w", err) } - return agency.Message{ - Role: agency.AssistantRole, - //TODO: we have to convert []float32 to []byte. Can we optimize it? - Content: bytes, - }, nil + //TODO: we have to convert []float32 to []byte. Can we optimize it? + return agency.NewMessage(agency.AssistantRole, agency.VectorKind, bytes), nil }) } diff --git a/providers/openai/text_to_image.go b/providers/openai/text_to_image.go index ef8c54b..ecf36dd 100644 --- a/providers/openai/text_to_image.go +++ b/providers/openai/text_to_image.go @@ -21,7 +21,7 @@ func (p Provider) TextToImage(params TextToImageParams) *agency.Operation { return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { reqBase64 := openai.ImageRequest{ - Prompt: fmt.Sprintf("%s\n\n%s", cfg.Prompt, string(msg.Content)), + Prompt: fmt.Sprintf("%s\n\n%s", cfg.Prompt, string(msg.Content())), Size: params.ImageSize, ResponseFormat: openai.CreateImageResponseFormatB64JSON, N: 1, // DALLĀ·E-3 only support n=1, for other models support needed @@ -32,18 +32,15 @@ func (p Provider) TextToImage(params TextToImageParams) *agency.Operation { respBase64, err := p.client.CreateImage(ctx, reqBase64) if err != nil { - return agency.Message{}, err + return nil, err } imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON) if err != nil { - return agency.Message{}, err + return nil, err } - return agency.Message{ - Role: agency.AssistantRole, - Content: imgBytes, - }, nil + return agency.NewMessage(agency.AssistantRole, agency.ImageKind, imgBytes), nil }, ) } diff --git a/providers/openai/text_to_speech.go b/providers/openai/text_to_speech.go index f0cbd92..f758978 100644 --- a/providers/openai/text_to_speech.go +++ b/providers/openai/text_to_speech.go @@ -21,24 +21,21 @@ func (f Provider) TextToSpeech(params TextToSpeechParams) *agency.Operation { func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { resp, err := f.client.CreateSpeech(ctx, openai.CreateSpeechRequest{ Model: openai.SpeechModel(params.Model), - Input: msg.String(), + Input: string(msg.Content()), Voice: openai.SpeechVoice(params.Voice), ResponseFormat: openai.SpeechResponseFormat(params.ResponseFormat), Speed: params.Speed, }) if err != nil { - return agency.Message{}, err + return nil, err } bb, err := io.ReadAll(resp) if err != nil { - return agency.Message{}, err + return nil, err } - return agency.Message{ - Role: agency.AssistantRole, - Content: bb, - }, nil + return agency.NewMessage(agency.AssistantRole, agency.VoiceKind, bb), nil }, ) } diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 4d2cd9c..3fa3ddb 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -35,14 +35,14 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { for _, textMsg := range cfg.Messages { openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: string(textMsg.Role), - Content: string(textMsg.Content), + Role: string(textMsg.Role()), + Content: string(textMsg.Content()), }) } openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, - Content: msg.String(), + Content: string(msg.Content()), }) for { @@ -58,7 +58,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { }, ) if err != nil { - return agency.Message{}, fmt.Errorf("create chat completion stream: %w", err) + return nil, fmt.Errorf("create chat completion stream: %w", err) } var content string @@ -67,21 +67,18 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { recv, err := openAIResponse.Recv() if errors.Is(err, io.EOF) { if len(accumulatedStreamedFunctions) == 0 { - return agency.Message{ - Role: agency.AssistantRole, - Content: []byte(content), - }, nil + return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(content)), nil } break } if err != nil { - return agency.Message{}, err + return nil, err } if len(recv.Choices) < 1 { - return agency.Message{}, errors.New("no choice") + return nil, errors.New("no choice") } firstChoice := recv.Choices[0] @@ -119,27 +116,24 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) if funcToCall == nil { - return agency.Message{}, errors.New("function not found") + return nil, errors.New("function not found") } funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) var isFinal = errors.Is(err, ToolAnswerShouldBeFinal) if err != nil && !isFinal { - return agency.Message{}, fmt.Errorf("call function %s: %w", funcToCall.Name, err) + return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } escapedFuncResult, err := json.Marshal(funcResult) if err != nil { - return agency.Message{}, fmt.Errorf("marshal function result: %w", err) + return nil, fmt.Errorf("marshal function result: %w", err) } if isFinal { params.Stream <- string(escapedFuncResult) - return agency.Message{ - Role: openai.ChatMessageRoleTool, - Content: escapedFuncResult, - }, nil + return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(content)), nil } openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 756b985..642cfb6 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -48,14 +48,14 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { for _, textMsg := range cfg.Messages { openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: string(textMsg.Role), - Content: string(textMsg.Content), + Role: string(textMsg.Role()), + Content: string(textMsg.Content()), }) } openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleUser, - Content: msg.String(), + Content: string(msg.Content()), }) for { @@ -70,19 +70,20 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { }, ) if err != nil { - return agency.Message{}, err + return nil, err } if len(openAIResponse.Choices) < 1 { - return agency.Message{}, errors.New("no choice") + return nil, errors.New("no choice") } firstChoice := openAIResponse.Choices[0] if len(firstChoice.Message.ToolCalls) == 0 { - return agency.Message{ - Role: agency.Role(firstChoice.Message.Role), - Content: []byte(firstChoice.Message.Content), - }, nil + return agency.NewMessage( + agency.Role(firstChoice.Message.Role), + agency.TextKind, + []byte(firstChoice.Message.Content), + ), nil } openAIMessages = append(openAIMessages, firstChoice.Message) @@ -90,17 +91,17 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { for _, toolCall := range firstChoice.Message.ToolCalls { funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) if funcToCall == nil { - return agency.Message{}, errors.New("function not found") + return nil, errors.New("function not found") } funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) if err != nil { - return agency.Message{}, fmt.Errorf("call function %s: %w", funcToCall.Name, err) + return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } bb, err := json.Marshal(funcResult) if err != nil { - return agency.Message{}, fmt.Errorf("marshal function result: %w", err) + return nil, fmt.Errorf("marshal function result: %w", err) } openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ From 3e45bd6fc7908ee3b5d2545870688e429ded1c43 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 8 May 2024 12:18:25 +0400 Subject: [PATCH 04/21] feat(text2stream): add multicontent support --- examples/image_to_stream/main.go | 42 ++++++++++++++++++ examples/image_to_text/main.go | 2 +- .../main.go | 0 providers/openai/text_to_stream.go | 43 +++++++++++++++---- 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 examples/image_to_stream/main.go rename examples/{completion_streaming => text_to_stream}/main.go (100%) diff --git a/examples/image_to_stream/main.go b/examples/image_to_stream/main.go new file mode 100644 index 0000000..3cca38b --- /dev/null +++ b/examples/image_to_stream/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "context" + "fmt" + "os" + + _ "github.com/joho/godotenv/autoload" + "github.com/neurocult/agency" + + providers "github.com/neurocult/agency/providers/openai" + "github.com/sashabaranov/go-openai" +) + +func main() { + imgBytes, err := os.ReadFile("assets/dracula.png") + if err != nil { + panic(err) + } + + stream := make(chan string) + + go func() { + defer close(stream) + result, err := providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). + TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: openai.GPT4Turbo, Stream: stream}). + SetPrompt("describe what you see"). + Execute( + context.Background(), + agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), + ) + if err != nil { + panic(err) + } + + fmt.Println(string(result.Content())) + }() + + for s := range stream { + fmt.Println(s) + } +} diff --git a/examples/image_to_text/main.go b/examples/image_to_text/main.go index b1e7016..43b9292 100644 --- a/examples/image_to_text/main.go +++ b/examples/image_to_text/main.go @@ -12,7 +12,7 @@ import ( ) func main() { - imgBytes, err := os.ReadFile("example.png") + imgBytes, err := os.ReadFile("../example.png") if err != nil { panic(err) } diff --git a/examples/completion_streaming/main.go b/examples/text_to_stream/main.go similarity index 100% rename from examples/completion_streaming/main.go rename to examples/text_to_stream/main.go diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 3fa3ddb..efc24f3 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -33,17 +33,42 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { Content: cfg.Prompt, }) - for _, textMsg := range cfg.Messages { - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: string(textMsg.Role()), - Content: string(textMsg.Content()), - }) + for _, cfgMsg := range cfg.Messages { + openaiCfgMsg := openai.ChatCompletionMessage{ + Role: string(cfgMsg.Role()), + } + + switch cfgMsg.Kind() { + case agency.TextKind: + openaiCfgMsg.Content = string(cfgMsg.Content()) + case agency.ImageKind: + openaiCfgMsg.MultiContent = append( + openaiCfgMsg.MultiContent, + openAIBase64ImageMessage(cfgMsg.Content()), + ) + default: + return nil, fmt.Errorf("text to stream doesn't support %s kind", cfgMsg.Kind()) + } + openAIMessages = append(openAIMessages, openaiCfgMsg) } - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: string(msg.Content()), - }) + openaiMsg := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + } + + switch msg.Kind() { + case agency.TextKind: + openaiMsg.Content = string(msg.Content()) + case agency.ImageKind: + openaiMsg.MultiContent = append( + openaiMsg.MultiContent, + openAIBase64ImageMessage(msg.Content()), + ) + default: + return nil, fmt.Errorf("text to stream doesn't support %s kind", msg.Kind()) + } + + openAIMessages = append(openAIMessages, openaiMsg) for { openAIResponse, err := p.client.CreateChatCompletionStream( From 4cb6efaf09305cf9fbe2162de036a36470f83081 Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Fri, 14 Jun 2024 12:27:23 +0500 Subject: [PATCH 05/21] feat(openai): upd lower lvl lib --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index e7aec63..aa37842 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/neurocult/agency go 1.21.0 require ( - github.com/sashabaranov/go-openai v1.21.0 + github.com/sashabaranov/go-openai v1.25.0 github.com/weaviate/weaviate v1.24.8 github.com/weaviate/weaviate-go-client/v4 v4.13.1 ) diff --git a/go.sum b/go.sum index 6f72971..62686cb 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/sashabaranov/go-openai v1.21.0 h1:isAf3zPSD3VLd0pC2/2Q6ZyRK7jzPAaz+X3rjsviaYQ= github.com/sashabaranov/go-openai v1.21.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.25.0 h1:3h3DtJ55zQJqc+BR4y/iTcPhLk4pewJpyO+MXW2RdW0= +github.com/sashabaranov/go-openai v1.25.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/weaviate/weaviate v1.24.8 h1:obeBOJuXScDvUlbTKuqPwJl/cUB5csRhCN6q4smcQiM= From a4780e76e305b06ecd5ddf6a8e0874956a548db7 Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Sun, 16 Jun 2024 12:43:50 +0500 Subject: [PATCH 06/21] wip: adding usage --- providers/openai/text_to_stream.go | 33 +++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index efc24f3..c58aa83 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -24,6 +24,12 @@ var ToolAnswerShouldBeFinal = errors.New("tool answer should be final") func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) + var toolChoice *string + if len(openAITools) > 0 { + v := "required" + toolChoice = &v + } + return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) @@ -70,7 +76,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAIMessages = append(openAIMessages, openaiMsg) - for { + for { // streaming loop openAIResponse, err := p.client.CreateChatCompletionStream( ctx, openai.ChatCompletionRequest{ @@ -80,6 +86,10 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { Messages: openAIMessages, Tools: openAITools, Stream: params.Stream != nil, + ToolChoice: toolChoice, + StreamOptions: &openai.StreamOptions{ + IncludeUsage: true, + }, }, ) if err != nil { @@ -88,11 +98,20 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { var content string var accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools)) + var usage openai.Usage + for { recv, err := openAIResponse.Recv() - if errors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) { // last message if len(accumulatedStreamedFunctions) == 0 { - return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(content)), nil + // TODO return usage + fmt.Println(usage) + + return agency.NewMessage( + agency.AssistantRole, + agency.TextKind, + []byte(content), + ), nil } break @@ -102,6 +121,11 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { return nil, err } + if recv.Usage != nil { // penultimate message + usage = *recv.Usage + continue + } + if len(recv.Choices) < 1 { return nil, errors.New("no choice") } @@ -158,6 +182,9 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { if isFinal { params.Stream <- string(escapedFuncResult) + // TODO return usage + // fmt.Println(usage) + return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(content)), nil } From 12721d3089e6b1b37d59c36276b39707624e97cc Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Sun, 16 Jun 2024 12:46:28 +0500 Subject: [PATCH 07/21] fix: remove fmt println --- providers/openai/text_to_stream.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index c58aa83..acb20bd 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -104,8 +104,8 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { recv, err := openAIResponse.Recv() if errors.Is(err, io.EOF) { // last message if len(accumulatedStreamedFunctions) == 0 { - // TODO return usage - fmt.Println(usage) + // TODO update operation API and return usage along with message + _ = usage return agency.NewMessage( agency.AssistantRole, From 0b819254153322dd144b336a181899879cebd01e Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 26 Jun 2024 14:12:30 +0800 Subject: [PATCH 08/21] feat(text2stream): replace channel by handler --- examples/image_to_stream/main.go | 5 ++- examples/text_to_stream/main.go | 31 ++++++++++--------- providers/openai/helpers.go | 8 +++-- providers/openai/text_to_embedding.go | 2 +- providers/openai/text_to_stream.go | 44 ++++++++++++++++----------- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/examples/image_to_stream/main.go b/examples/image_to_stream/main.go index 3cca38b..4bb0488 100644 --- a/examples/image_to_stream/main.go +++ b/examples/image_to_stream/main.go @@ -9,11 +9,10 @@ import ( "github.com/neurocult/agency" providers "github.com/neurocult/agency/providers/openai" - "github.com/sashabaranov/go-openai" ) func main() { - imgBytes, err := os.ReadFile("assets/dracula.png") + imgBytes, err := os.ReadFile("assets/test.jpg") if err != nil { panic(err) } @@ -23,7 +22,7 @@ func main() { go func() { defer close(stream) result, err := providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). - TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: openai.GPT4Turbo, Stream: stream}). + TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: "gpt-4o", Stream: stream}). SetPrompt("describe what you see"). Execute( context.Background(), diff --git a/examples/text_to_stream/main.go b/examples/text_to_stream/main.go index 9b9ed47..0567ca6 100644 --- a/examples/text_to_stream/main.go +++ b/examples/text_to_stream/main.go @@ -14,24 +14,27 @@ import ( func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) - stream := make(chan string) - go func() { - defer close(stream) + result, err := factory. + TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo, StreamHandler: func(delta, total string, isFirst, isLast bool) error { + if isFirst { + fmt.Println("====Start streaming====\n") + } - result, err := factory. - TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo, Stream: stream}). - SetPrompt("Write a few sentences about topic"). - Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming."))) + fmt.Print(delta) - if err != nil { - panic(err) - } + if isLast { + fmt.Println("\n\n====Finish streaming====") + } - fmt.Println("\nFinal result:", string(result.Content())) - }() + return nil + }}). + SetPrompt("Write a few sentences about topic"). + Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming."))) - for s := range stream { - fmt.Println(s) + if err != nil { + panic(err) } + + fmt.Println("\nResult:", string(result.Content())) } diff --git a/providers/openai/helpers.go b/providers/openai/helpers.go index 92427f4..68d4bc0 100644 --- a/providers/openai/helpers.go +++ b/providers/openai/helpers.go @@ -6,7 +6,9 @@ import ( "math" ) -func EmbeddingToBytes(dimensions int, embeddings [][]float32) ([]byte, error) { +type Embedding []float32 + +func EmbeddingToBytes(dimensions int, embeddings []Embedding) ([]byte, error) { if len(embeddings) == 0 { return nil, fmt.Errorf("embeddings is empty") } @@ -27,12 +29,12 @@ func EmbeddingToBytes(dimensions int, embeddings [][]float32) ([]byte, error) { return buf, nil } -func BytesToEmbedding(dimensions int, buf []byte) ([][]float32, error) { +func BytesToEmbedding(dimensions int, buf []byte) ([]Embedding, error) { if mltp := len(buf) % (dimensions * 4); mltp != 0 { return nil, fmt.Errorf("invalid buffer length: got %d, but expected multiple of %d", len(buf), dimensions*4) } - embeddings := make([][]float32, len(buf)/dimensions/4) + embeddings := make([]Embedding, len(buf)/dimensions/4) for i := range embeddings { embeddings[i] = make([]float32, dimensions) for j := 0; j < dimensions; j++ { diff --git a/providers/openai/text_to_embedding.go b/providers/openai/text_to_embedding.go index ec1718e..fc32f85 100644 --- a/providers/openai/text_to_embedding.go +++ b/providers/openai/text_to_embedding.go @@ -38,7 +38,7 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio return nil, err } - vectors := make([][]float32, len(resp.Data)) + vectors := make([]Embedding, len(resp.Data)) for i, vector := range resp.Data { vectors[i] = vector.Embedding } diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index acb20bd..73c4f4c 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -12,11 +12,11 @@ import ( ) type TextToStreamParams struct { - Model string - Temperature NullableFloat32 - MaxTokens int - FuncDefs []FuncDef - Stream chan<- string + Model string + Temperature NullableFloat32 + MaxTokens int + FuncDefs []FuncDef + StreamHandler func(delta, total string, isFirst, isLast bool) error } var ToolAnswerShouldBeFinal = errors.New("tool answer should be final") @@ -85,7 +85,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { MaxTokens: params.MaxTokens, Messages: openAIMessages, Tools: openAITools, - Stream: params.Stream != nil, + Stream: params.StreamHandler != nil, ToolChoice: toolChoice, StreamOptions: &openai.StreamOptions{ IncludeUsage: true, @@ -99,10 +99,23 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { var content string var accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools)) var usage openai.Usage + var isFirstDelta = true + var isLastDelta = false + var lastDelta string for { recv, err := openAIResponse.Recv() - if errors.Is(err, io.EOF) { // last message + isLastDelta = errors.Is(err, io.EOF) + + if len(lastDelta) > 0 || isLastDelta { + if err = params.StreamHandler(lastDelta, content, isFirstDelta, isLastDelta); err != nil { + return nil, fmt.Errorf("handing stream: %w", err) + } + + isFirstDelta = false + } + + if isLastDelta { if len(accumulatedStreamedFunctions) == 0 { // TODO update operation API and return usage along with message _ = usage @@ -133,8 +146,10 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { firstChoice := recv.Choices[0] if len(firstChoice.Delta.Content) > 0 { - params.Stream <- firstChoice.Delta.Content - content += firstChoice.Delta.Content + lastDelta = firstChoice.Delta.Content + content += lastDelta + } else { + lastDelta = "" } for index, toolCall := range firstChoice.Delta.ToolCalls { @@ -169,8 +184,8 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { } funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) - var isFinal = errors.Is(err, ToolAnswerShouldBeFinal) - if err != nil && !isFinal { + var isFunctionCallShouldBeFinal = errors.Is(err, ToolAnswerShouldBeFinal) + if err != nil && !isFunctionCallShouldBeFinal { return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } @@ -179,12 +194,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { return nil, fmt.Errorf("marshal function result: %w", err) } - if isFinal { - params.Stream <- string(escapedFuncResult) - - // TODO return usage - // fmt.Println(usage) - + if isFunctionCallShouldBeFinal { return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(content)), nil } From 13c7faf04c09b6c18f19dc6dd0a0e56340caf80f Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 26 Jun 2024 14:26:47 +0800 Subject: [PATCH 09/21] fix(text2stream): empty handler call --- providers/openai/text_to_stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 73c4f4c..679de89 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -107,7 +107,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { recv, err := openAIResponse.Recv() isLastDelta = errors.Is(err, io.EOF) - if len(lastDelta) > 0 || isLastDelta { + if len(lastDelta) > 0 || (isLastDelta && len(content) > 0) { if err = params.StreamHandler(lastDelta, content, isFirstDelta, isLastDelta); err != nil { return nil, fmt.Errorf("handing stream: %w", err) } From 7abe29633fb00d9cf7e35ef68eb4e757873c0197 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 26 Jun 2024 14:56:10 +0800 Subject: [PATCH 10/21] fix(text2stream): remove tool message in output --- assets/test.jpg | Bin 0 -> 68100 bytes providers/openai/text_to_stream.go | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 assets/test.jpg diff --git a/assets/test.jpg b/assets/test.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2f47fc8c5da8278fb81ba0b663dd0b7e9ae5fc34 GIT binary patch literal 68100 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 literal 0 HcmV?d00001 diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 679de89..2816563 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -171,6 +171,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { continue } + // Saving tool call to history openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ Role: openai.ChatMessageRoleAssistant, ToolCalls: accumulatedStreamedFunctions, @@ -195,7 +196,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { } if isFunctionCallShouldBeFinal { - return agency.NewMessage(agency.ToolRole, agency.TextKind, []byte(content)), nil + return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(content)), nil } openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ From 666cd807e02918b90416ad8ffe936b2f85353033 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 26 Jun 2024 15:01:50 +0800 Subject: [PATCH 11/21] fix(assets): remove wrong image --- assets/test.jpg | Bin 68100 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 assets/test.jpg diff --git a/assets/test.jpg b/assets/test.jpg deleted file mode 100644 index 2f47fc8c5da8278fb81ba0b663dd0b7e9ae5fc34..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68100 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 From bda4674ea6d6d7cf6a2714918086da87ec8ac0cb Mon Sep 17 00:00:00 2001 From: Max Krou Date: Wed, 26 Jun 2024 15:18:56 +0800 Subject: [PATCH 12/21] fix(stream): make tool calls optional --- providers/openai/text_to_stream.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 2816563..35cdeeb 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -12,11 +12,12 @@ import ( ) type TextToStreamParams struct { - Model string - Temperature NullableFloat32 - MaxTokens int - FuncDefs []FuncDef - StreamHandler func(delta, total string, isFirst, isLast bool) error + Model string + Temperature NullableFloat32 + MaxTokens int + FuncDefs []FuncDef + StreamHandler func(delta, total string, isFirst, isLast bool) error + IsToolsCallRequired bool } var ToolAnswerShouldBeFinal = errors.New("tool answer should be final") @@ -25,7 +26,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) var toolChoice *string - if len(openAITools) > 0 { + if params.IsToolsCallRequired { v := "required" toolChoice = &v } From 495b7aabc13d741e074e3c854cef9e9bd9a7e301 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Tue, 2 Jul 2024 14:41:53 +0800 Subject: [PATCH 13/21] feat(toolCall): set type of tool response --- examples/func_call/main.go | 21 ++++++--- examples/image_to_stream/main.go | 32 ++++++-------- examples/text_to_stream/main.go | 4 +- messages.go | 17 ++++++++ providers/openai/helpers_test.go | 4 +- providers/openai/text_to_stream.go | 68 +++++++++++++++++------------- providers/openai/text_to_text.go | 2 +- 7 files changed, 86 insertions(+), 62 deletions(-) diff --git a/examples/func_call/main.go b/examples/func_call/main.go index ea921c5..bf99e47 100644 --- a/examples/func_call/main.go +++ b/examples/func_call/main.go @@ -23,8 +23,8 @@ func main() { { Name: "GetMeaningOfLife", Description: "Answer questions about meaning of life", - Body: func(ctx context.Context, _ []byte) (any, error) { - return 42, nil + Body: func(ctx context.Context, _ []byte) (agency.Message, error) { + return agency.NewTextMessage(agency.AssistantRole, "42"), nil }, }, // function with parameters @@ -38,12 +38,15 @@ func main() { "b": {Type: "integer"}, }, }, - Body: func(ctx context.Context, params []byte) (any, error) { + Body: func(ctx context.Context, params []byte) (agency.Message, error) { var pp struct{ A, B int } if err := json.Unmarshal(params, &pp); err != nil { return nil, err } - return (pp.A + pp.B) * 10, nil // *10 is just to distinguish from normal response + return agency.NewTextMessage( + agency.AssistantRole, + fmt.Sprintf("%d", (pp.A+pp.B)*10), + ), nil // *10 is just to distinguish from normal response }, }, }, @@ -69,7 +72,7 @@ Examples: if err != nil { panic(err) } - fmt.Println(answer) + printAnswer(answer) // test for second function call answer, err = t2tOp.Execute( @@ -79,7 +82,7 @@ Examples: if err != nil { panic(err) } - fmt.Println(answer) + printAnswer(answer) // test for both function calls at the same time answer, err = t2tOp.Execute( @@ -89,5 +92,9 @@ Examples: if err != nil { panic(err) } - fmt.Println(answer) + printAnswer(answer) +} + +func printAnswer(message agency.Message) { + fmt.Printf("Role: %s; Type: %s; Data: %s\n", message.Role(), message.Kind(), agency.GetStringContent(message)) } diff --git a/examples/image_to_stream/main.go b/examples/image_to_stream/main.go index 4bb0488..a1cb1af 100644 --- a/examples/image_to_stream/main.go +++ b/examples/image_to_stream/main.go @@ -17,25 +17,17 @@ func main() { panic(err) } - stream := make(chan string) - - go func() { - defer close(stream) - result, err := providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). - TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: "gpt-4o", Stream: stream}). - SetPrompt("describe what you see"). - Execute( - context.Background(), - agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), - ) - if err != nil { - panic(err) - } - - fmt.Println(string(result.Content())) - }() - - for s := range stream { - fmt.Println(s) + _, err = providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). + TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: "gpt-4o", StreamHandler: func(delta, total string, isFirst, isLast bool) error { + fmt.Println(delta) + return nil + }}). + SetPrompt("describe what you see"). + Execute( + context.Background(), + agency.NewMessage(agency.UserRole, agency.ImageKind, imgBytes), + ) + if err != nil { + panic(err) } } diff --git a/examples/text_to_stream/main.go b/examples/text_to_stream/main.go index 0567ca6..e86ceb4 100644 --- a/examples/text_to_stream/main.go +++ b/examples/text_to_stream/main.go @@ -18,13 +18,13 @@ func main() { result, err := factory. TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo, StreamHandler: func(delta, total string, isFirst, isLast bool) error { if isFirst { - fmt.Println("====Start streaming====\n") + fmt.Println("====Start streaming====") } fmt.Print(delta) if isLast { - fmt.Println("\n\n====Finish streaming====") + fmt.Println("\n====Finish streaming====") } return nil diff --git a/messages.go b/messages.go index d38b1f9..11981a0 100644 --- a/messages.go +++ b/messages.go @@ -49,3 +49,20 @@ func NewMessage(role Role, kind Kind, content []byte) BaseMessage { kind: kind, } } + +// NewTextMessage creates new `Message` with Text kind and the specified `Role` +func NewTextMessage(role Role, content string) BaseMessage { + return BaseMessage{ + content: []byte(content), + role: role, + kind: TextKind, + } +} + +func GetStringContent(msg Message) string { + if msg.Kind() == TextKind { + return string(msg.Content()) + } + + return "" +} diff --git a/providers/openai/helpers_test.go b/providers/openai/helpers_test.go index 9817173..e97abe2 100644 --- a/providers/openai/helpers_test.go +++ b/providers/openai/helpers_test.go @@ -6,7 +6,7 @@ import ( ) func TestEmbeddingToBytes(t *testing.T) { - floats := [][]float32{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}} + floats := []Embedding{{1.1, 2.2, 3.3}, {4.4, 5.5, 6.6}} bytes, err := EmbeddingToBytes(3, floats) if err != nil { @@ -22,7 +22,7 @@ func TestEmbeddingToBytes(t *testing.T) { t.Errorf("floats and newFloats are not equal %v %v", floats, newFloats) } - wrongFloats := [][]float32{{4.4, 5.5, 6.6, 7.7}} + wrongFloats := []Embedding{{4.4, 5.5, 6.6, 7.7}} _, err = EmbeddingToBytes(3, wrongFloats) if err == nil { diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 35cdeeb..8da5145 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -2,7 +2,6 @@ package openai import ( "context" - "encoding/json" "errors" "fmt" "io" @@ -20,7 +19,7 @@ type TextToStreamParams struct { IsToolsCallRequired bool } -var ToolAnswerShouldBeFinal = errors.New("tool answer should be final") +var ToolAnswerAsModelsAnswer = errors.New("tool answer should be final") func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) @@ -41,21 +40,11 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { }) for _, cfgMsg := range cfg.Messages { - openaiCfgMsg := openai.ChatCompletionMessage{ - Role: string(cfgMsg.Role()), + openaiCfgMsg, err := messageToOpenAI(cfgMsg) + if err != nil { + return nil, fmt.Errorf("openAI msg mapping: %w", err) } - switch cfgMsg.Kind() { - case agency.TextKind: - openaiCfgMsg.Content = string(cfgMsg.Content()) - case agency.ImageKind: - openaiCfgMsg.MultiContent = append( - openaiCfgMsg.MultiContent, - openAIBase64ImageMessage(cfgMsg.Content()), - ) - default: - return nil, fmt.Errorf("text to stream doesn't support %s kind", cfgMsg.Kind()) - } openAIMessages = append(openAIMessages, openaiCfgMsg) } @@ -179,33 +168,32 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { }) for _, toolCall := range accumulatedStreamedFunctions { - funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) if funcToCall == nil { return nil, errors.New("function not found") } - funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) - var isFunctionCallShouldBeFinal = errors.Is(err, ToolAnswerShouldBeFinal) - if err != nil && !isFunctionCallShouldBeFinal { + var funcResult agency.Message + funcResult, err = funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) + var isFunctionCallAsModelAnswer = errors.Is(err, ToolAnswerAsModelsAnswer) + if err != nil && !isFunctionCallAsModelAnswer { return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } - escapedFuncResult, err := json.Marshal(funcResult) - if err != nil { - return nil, fmt.Errorf("marshal function result: %w", err) + if isFunctionCallAsModelAnswer { + return funcResult, nil } - if isFunctionCallShouldBeFinal { - return agency.NewMessage(agency.AssistantRole, agency.TextKind, []byte(content)), nil + var openaiFuncResult openai.ChatCompletionMessage + openaiFuncResult, err = messageToOpenAI(funcResult) + if err != nil { + return nil, fmt.Errorf("openAI msg mapping: %w", err) } - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: string(escapedFuncResult), - Name: toolCall.Function.Name, - ToolCallID: toolCall.ID, - }) + openaiFuncResult.ToolCallID = toolCall.ID + openaiFuncResult.Name = toolCall.Function.Name + + openAIMessages = append(openAIMessages, openaiFuncResult) } } @@ -214,3 +202,23 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { }, ) } + +func messageToOpenAI(message agency.Message) (openai.ChatCompletionMessage, error) { + wrappedMessage := openai.ChatCompletionMessage{ + Role: string(message.Role()), + } + + switch message.Kind() { + case agency.TextKind: + wrappedMessage.Content = string(message.Content()) + case agency.ImageKind: + wrappedMessage.MultiContent = append( + wrappedMessage.MultiContent, + openAIBase64ImageMessage(message.Content()), + ) + default: + return openai.ChatCompletionMessage{}, fmt.Errorf("text to stream doesn't support %s kind", message.Kind()) + } + + return wrappedMessage, nil +} diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 642cfb6..05f5131 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -29,7 +29,7 @@ type FuncDef struct { // Body is the actual function that get's called. // Parameters passed are bytes that can be unmarshalled to type that implements provided json schema. // Returned result must be anything that can be marshalled, including primitive values. - Body func(ctx context.Context, params []byte) (any, error) + Body func(ctx context.Context, params []byte) (agency.Message, error) } // TextToText is an operation builder that creates operation than can convert text to text. From bcc9de3414a914cd6037fec3d99f43daef5f6d23 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Tue, 2 Jul 2024 14:50:00 +0800 Subject: [PATCH 14/21] feat(toolCall): handle empty tool result for ToolAnswerAsModelsAnswer --- providers/openai/text_to_stream.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 8da5145..41be250 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -181,6 +181,9 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { } if isFunctionCallAsModelAnswer { + if funcResult == nil { + return nil, fmt.Errorf("can't use ToolAnswerAsModelsAnswer with empty tool result") + } return funcResult, nil } From 701d059662118b91a9c67cfe0e2f043e951fb4f1 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Tue, 2 Jul 2024 14:59:00 +0800 Subject: [PATCH 15/21] feat(toolCall): handle empty tool result --- providers/openai/text_to_stream.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 41be250..81c38c9 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -180,10 +180,11 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) } + if funcResult == nil { + return nil, fmt.Errorf("tool response shouldn't be nil") + } + if isFunctionCallAsModelAnswer { - if funcResult == nil { - return nil, fmt.Errorf("can't use ToolAnswerAsModelsAnswer with empty tool result") - } return funcResult, nil } From d0c5c471f04062aea8c1bd2991d1de132ef54d2f Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Wed, 3 Jul 2024 11:54:57 +0500 Subject: [PATCH 16/21] feat(openai:tts): ability to pass seed from the outside --- providers/openai/text_to_stream.go | 2 ++ providers/openai/text_to_text.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 81c38c9..7969a46 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -17,6 +17,7 @@ type TextToStreamParams struct { FuncDefs []FuncDef StreamHandler func(delta, total string, isFirst, isLast bool) error IsToolsCallRequired bool + Seed *int } var ToolAnswerAsModelsAnswer = errors.New("tool answer should be final") @@ -80,6 +81,7 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { StreamOptions: &openai.StreamOptions{ IncludeUsage: true, }, + Seed: params.Seed, }, ) if err != nil { diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 05f5131..0201ec3 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -18,6 +18,7 @@ type TextToTextParams struct { Temperature NullableFloat32 MaxTokens int FuncDefs []FuncDef + Seed *int } // FuncDef represents a function definition that can be called during the conversation. @@ -67,6 +68,7 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { MaxTokens: params.MaxTokens, Messages: openAIMessages, Tools: openAITools, + Seed: params.Seed, }, ) if err != nil { From 5e75979ae213195c40aafb307a75f651b4c694f3 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Sat, 13 Jul 2024 15:21:21 +0500 Subject: [PATCH 17/21] chores(messages): add some sugar --- messages.go | 24 ++++++++++++++++++++---- providers/openai/text_to_embedding.go | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/messages.go b/messages.go index 11981a0..7604f85 100644 --- a/messages.go +++ b/messages.go @@ -1,5 +1,7 @@ package agency +import "encoding/json" + type Message interface { Role() Role Content() []byte @@ -9,10 +11,10 @@ type Message interface { type Kind string const ( - TextKind Kind = "text" - ImageKind Kind = "image" - VoiceKind Kind = "voice" - VectorKind Kind = "vector" + TextKind Kind = "text" + ImageKind Kind = "image" + VoiceKind Kind = "voice" + EmbeddingKind Kind = "embedding" ) type Role string @@ -59,6 +61,20 @@ func NewTextMessage(role Role, content string) BaseMessage { } } +// NewJsonMessage marshals content and creates new `Message` with text kind and the specified `Role` +func NewJsonMessage(role Role, content any) (BaseMessage, error) { + data, err := json.Marshal(content) + if err != nil { + return BaseMessage{}, err + } + + return BaseMessage{ + content: data, + role: role, + kind: TextKind, + }, nil +} + func GetStringContent(msg Message) string { if msg.Kind() == TextKind { return string(msg.Content()) diff --git a/providers/openai/text_to_embedding.go b/providers/openai/text_to_embedding.go index fc32f85..2f5117f 100644 --- a/providers/openai/text_to_embedding.go +++ b/providers/openai/text_to_embedding.go @@ -49,6 +49,6 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio } //TODO: we have to convert []float32 to []byte. Can we optimize it? - return agency.NewMessage(agency.AssistantRole, agency.VectorKind, bytes), nil + return agency.NewMessage(agency.AssistantRole, agency.EmbeddingKind, bytes), nil }) } From 5a064a3a5a49bf3bac188c961fcaf0bb6960d7f1 Mon Sep 17 00:00:00 2001 From: Max Krou Date: Sat, 13 Jul 2024 18:15:49 +0500 Subject: [PATCH 18/21] chores(openai): simplify tool calls --- examples/func_call/main.go | 4 +- examples/image_to_stream/main.go | 10 +- examples/text_to_stream/main.go | 30 +++--- providers/openai/text_to_stream.go | 123 +++++------------------ providers/openai/text_to_text.go | 153 ++++++++++++++++------------- providers/openai/tools.go | 39 ++++++++ 6 files changed, 177 insertions(+), 182 deletions(-) create mode 100644 providers/openai/tools.go diff --git a/examples/func_call/main.go b/examples/func_call/main.go index bf99e47..cc2239f 100644 --- a/examples/func_call/main.go +++ b/examples/func_call/main.go @@ -24,7 +24,7 @@ func main() { Name: "GetMeaningOfLife", Description: "Answer questions about meaning of life", Body: func(ctx context.Context, _ []byte) (agency.Message, error) { - return agency.NewTextMessage(agency.AssistantRole, "42"), nil + return agency.NewTextMessage(agency.ToolRole, "42"), nil }, }, // function with parameters @@ -44,7 +44,7 @@ func main() { return nil, err } return agency.NewTextMessage( - agency.AssistantRole, + agency.ToolRole, fmt.Sprintf("%d", (pp.A+pp.B)*10), ), nil // *10 is just to distinguish from normal response }, diff --git a/examples/image_to_stream/main.go b/examples/image_to_stream/main.go index a1cb1af..c7e7b0f 100644 --- a/examples/image_to_stream/main.go +++ b/examples/image_to_stream/main.go @@ -18,10 +18,12 @@ func main() { } _, err = providers.New(providers.Params{Key: os.Getenv("OPENAI_API_KEY")}). - TextToStream(providers.TextToStreamParams{MaxTokens: 300, Model: "gpt-4o", StreamHandler: func(delta, total string, isFirst, isLast bool) error { - fmt.Println(delta) - return nil - }}). + TextToStream(providers.TextToStreamParams{ + TextToTextParams: providers.TextToTextParams{MaxTokens: 300, Model: "gpt-4o"}, + StreamHandler: func(delta, total string, isFirst, isLast bool) error { + fmt.Println(delta) + return nil + }}). SetPrompt("describe what you see"). Execute( context.Background(), diff --git a/examples/text_to_stream/main.go b/examples/text_to_stream/main.go index e86ceb4..0303f6f 100644 --- a/examples/text_to_stream/main.go +++ b/examples/text_to_stream/main.go @@ -16,19 +16,23 @@ func main() { factory := openai.New(openai.Params{Key: os.Getenv("OPENAI_API_KEY")}) result, err := factory. - TextToStream(openai.TextToStreamParams{Model: goopenai.GPT3Dot5Turbo, StreamHandler: func(delta, total string, isFirst, isLast bool) error { - if isFirst { - fmt.Println("====Start streaming====") - } - - fmt.Print(delta) - - if isLast { - fmt.Println("\n====Finish streaming====") - } - - return nil - }}). + TextToStream(openai.TextToStreamParams{ + TextToTextParams: openai.TextToTextParams{ + Model: goopenai.GPT3Dot5Turbo, + }, + StreamHandler: func(delta, total string, isFirst, isLast bool) error { + if isFirst { + fmt.Println("====Start streaming====") + } + + fmt.Print(delta) + + if isLast { + fmt.Println("\n====Finish streaming====") + } + + return nil + }}). SetPrompt("Write a few sentences about topic"). Execute(context.Background(), agency.NewMessage(agency.UserRole, agency.TextKind, []byte("I love programming."))) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index 7969a46..c8fca05 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -11,62 +11,20 @@ import ( ) type TextToStreamParams struct { - Model string - Temperature NullableFloat32 - MaxTokens int - FuncDefs []FuncDef - StreamHandler func(delta, total string, isFirst, isLast bool) error - IsToolsCallRequired bool - Seed *int + TextToTextParams + StreamHandler func(delta, total string, isFirst, isLast bool) error } -var ToolAnswerAsModelsAnswer = errors.New("tool answer should be final") - func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) - var toolChoice *string - if params.IsToolsCallRequired { - v := "required" - toolChoice = &v - } - return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { - openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) - - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleSystem, - Content: cfg.Prompt, - }) - - for _, cfgMsg := range cfg.Messages { - openaiCfgMsg, err := messageToOpenAI(cfgMsg) - if err != nil { - return nil, fmt.Errorf("openAI msg mapping: %w", err) - } - - openAIMessages = append(openAIMessages, openaiCfgMsg) + openAIMessages, err := agencyToOpenaiMessages(cfg, msg) + if err != nil { + return nil, fmt.Errorf("text to stream: %w", err) } - openaiMsg := openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - } - - switch msg.Kind() { - case agency.TextKind: - openaiMsg.Content = string(msg.Content()) - case agency.ImageKind: - openaiMsg.MultiContent = append( - openaiMsg.MultiContent, - openAIBase64ImageMessage(msg.Content()), - ) - default: - return nil, fmt.Errorf("text to stream doesn't support %s kind", msg.Kind()) - } - - openAIMessages = append(openAIMessages, openaiMsg) - for { // streaming loop openAIResponse, err := p.client.CreateChatCompletionStream( ctx, @@ -77,11 +35,8 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { Messages: openAIMessages, Tools: openAITools, Stream: params.StreamHandler != nil, - ToolChoice: toolChoice, - StreamOptions: &openai.StreamOptions{ - IncludeUsage: true, - }, - Seed: params.Seed, + ToolChoice: params.ToolCallRequired(), + Seed: params.Seed, }, ) if err != nil { @@ -90,7 +45,6 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { var content string var accumulatedStreamedFunctions = make([]openai.ToolCall, 0, len(openAITools)) - var usage openai.Usage var isFirstDelta = true var isLastDelta = false var lastDelta string @@ -109,13 +63,9 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { if isLastDelta { if len(accumulatedStreamedFunctions) == 0 { - // TODO update operation API and return usage along with message - _ = usage - - return agency.NewMessage( + return agency.NewTextMessage( agency.AssistantRole, - agency.TextKind, - []byte(content), + content, ), nil } @@ -126,11 +76,6 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { return nil, err } - if recv.Usage != nil { // penultimate message - usage = *recv.Usage - continue - } - if len(recv.Choices) < 1 { return nil, errors.New("no choice") } @@ -169,37 +114,17 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { ToolCalls: accumulatedStreamedFunctions, }) - for _, toolCall := range accumulatedStreamedFunctions { - funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) - if funcToCall == nil { - return nil, errors.New("function not found") - } - - var funcResult agency.Message - funcResult, err = funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) - var isFunctionCallAsModelAnswer = errors.Is(err, ToolAnswerAsModelsAnswer) - if err != nil && !isFunctionCallAsModelAnswer { - return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) - } - - if funcResult == nil { - return nil, fmt.Errorf("tool response shouldn't be nil") - } - - if isFunctionCallAsModelAnswer { - return funcResult, nil - } - - var openaiFuncResult openai.ChatCompletionMessage - openaiFuncResult, err = messageToOpenAI(funcResult) + for _, call := range accumulatedStreamedFunctions { + toolResponse, err := callTool(ctx, call, params.FuncDefs) if err != nil { - return nil, fmt.Errorf("openAI msg mapping: %w", err) + return nil, fmt.Errorf("text to text call tool: %w", err) } - openaiFuncResult.ToolCallID = toolCall.ID - openaiFuncResult.Name = toolCall.Function.Name + if toolResponse.Role() != agency.ToolRole { + return toolResponse, nil + } - openAIMessages = append(openAIMessages, openaiFuncResult) + openAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID)) } } @@ -209,22 +134,28 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { ) } -func messageToOpenAI(message agency.Message) (openai.ChatCompletionMessage, error) { +func messageToOpenAI(message agency.Message) openai.ChatCompletionMessage { wrappedMessage := openai.ChatCompletionMessage{ Role: string(message.Role()), } switch message.Kind() { - case agency.TextKind: - wrappedMessage.Content = string(message.Content()) + case agency.ImageKind: wrappedMessage.MultiContent = append( wrappedMessage.MultiContent, openAIBase64ImageMessage(message.Content()), ) default: - return openai.ChatCompletionMessage{}, fmt.Errorf("text to stream doesn't support %s kind", message.Kind()) + wrappedMessage.Content = string(message.Content()) } - return wrappedMessage, nil + return wrappedMessage +} + +func toolMessageToOpenAI(message agency.Message, toolID string) openai.ChatCompletionMessage { + wrappedMessage := messageToOpenAI(message) + wrappedMessage.ToolCallID = toolID + + return wrappedMessage } diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 0201ec3..519bfde 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -2,7 +2,6 @@ package openai import ( "context" - "encoding/json" "errors" "fmt" @@ -14,23 +13,22 @@ import ( // TextToTextParams represents parameters that are specific for this operation. type TextToTextParams struct { - Model string - Temperature NullableFloat32 - MaxTokens int - FuncDefs []FuncDef - Seed *int + Model string + Temperature NullableFloat32 + MaxTokens int + FuncDefs []FuncDef + Seed *int + IsToolsCallRequired bool } -// FuncDef represents a function definition that can be called during the conversation. -type FuncDef struct { - Name string - Description string - // Parameters is an optional structure that defines the schema of the parameters that the function accepts. - Parameters *jsonschema.Definition - // Body is the actual function that get's called. - // Parameters passed are bytes that can be unmarshalled to type that implements provided json schema. - // Returned result must be anything that can be marshalled, including primitive values. - Body func(ctx context.Context, params []byte) (agency.Message, error) +func (p TextToTextParams) ToolCallRequired() *string { + var toolChoice *string + if p.IsToolsCallRequired { + v := "required" + toolChoice = &v + } + + return toolChoice } // TextToText is an operation builder that creates operation than can convert text to text. @@ -40,25 +38,11 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { - openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) - - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleSystem, - Content: cfg.Prompt, - }) - - for _, textMsg := range cfg.Messages { - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: string(textMsg.Role()), - Content: string(textMsg.Content()), - }) + openAIMessages, err := agencyToOpenaiMessages(cfg, msg) + if err != nil { + return nil, fmt.Errorf("text to stream: %w", err) } - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: string(msg.Content()), - }) - for { openAIResponse, err := p.client.CreateChatCompletion( ctx, @@ -69,49 +53,35 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { Messages: openAIMessages, Tools: openAITools, Seed: params.Seed, + ToolChoice: params.ToolCallRequired(), }, ) if err != nil { return nil, err } - if len(openAIResponse.Choices) < 1 { - return nil, errors.New("no choice") - } - firstChoice := openAIResponse.Choices[0] - - if len(firstChoice.Message.ToolCalls) == 0 { - return agency.NewMessage( - agency.Role(firstChoice.Message.Role), - agency.TextKind, - []byte(firstChoice.Message.Content), - ), nil + if len(openAIResponse.Choices) == 0 { + return nil, errors.New("get text to text response: no choice") } - openAIMessages = append(openAIMessages, firstChoice.Message) + responseMessage := openAIResponse.Choices[0].Message - for _, toolCall := range firstChoice.Message.ToolCalls { - funcToCall := getFuncDefByName(params.FuncDefs, toolCall.Function.Name) - if funcToCall == nil { - return nil, errors.New("function not found") - } + if len(responseMessage.ToolCalls) == 0 { + return OpenaiToAgencyMessage(responseMessage), nil + } - funcResult, err := funcToCall.Body(ctx, []byte(toolCall.Function.Arguments)) + openAIMessages = append(openAIMessages, responseMessage) + for _, call := range responseMessage.ToolCalls { + toolResponse, err := callTool(ctx, call, params.FuncDefs) if err != nil { - return nil, fmt.Errorf("call function %s: %w", funcToCall.Name, err) + return nil, fmt.Errorf("text to text call tool: %w", err) } - bb, err := json.Marshal(funcResult) - if err != nil { - return nil, fmt.Errorf("marshal function result: %w", err) + if toolResponse.Role() != agency.ToolRole { + return toolResponse, nil } - openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleTool, - Content: string(bb), - Name: toolCall.Function.Name, - ToolCallID: toolCall.ID, - }) + openAIMessages = append(openAIMessages, toolMessageToOpenAI(toolResponse, call.ID)) } } }, @@ -142,11 +112,60 @@ func castFuncDefsToOpenAITools(funcDefs []FuncDef) []openai.Tool { return tools } -func getFuncDefByName(funcDefs []FuncDef, name string) *FuncDef { - for _, f := range funcDefs { - if f.Name == name { - return &f - } +func agencyToOpenaiMessages(cfg *agency.OperationConfig, msg agency.Message) ([]openai.ChatCompletionMessage, error) { + openAIMessages := make([]openai.ChatCompletionMessage, 0, len(cfg.Messages)+2) + + openAIMessages = append(openAIMessages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: cfg.Prompt, + }) + + for _, cfgMsg := range cfg.Messages { + openAIMessages = append(openAIMessages, messageToOpenAI(cfgMsg)) + } + + openaiMsg := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, } - return nil + + switch msg.Kind() { + case agency.TextKind: + openaiMsg.Content = string(msg.Content()) + case agency.ImageKind: + openaiMsg.MultiContent = append( + openaiMsg.MultiContent, + openAIBase64ImageMessage(msg.Content()), + ) + default: + return nil, fmt.Errorf("operator doesn't support %s kind", msg.Kind()) + } + + openAIMessages = append(openAIMessages, openaiMsg) + + return openAIMessages, nil +} + +func callTool( + ctx context.Context, + call openai.ToolCall, + defs FuncDefs, +) (agency.Message, error) { + funcToCall := defs.getFuncDefByName(call.Function.Name) + if funcToCall == nil { + return nil, errors.New("function not found") + } + + funcResult, err := funcToCall.Body(ctx, []byte(call.Function.Arguments)) + if err != nil { + return funcResult, fmt.Errorf("call function %s: %w", funcToCall.Name, err) + } + + return funcResult, nil +} + +func OpenaiToAgencyMessage(msg openai.ChatCompletionMessage) agency.Message { + return agency.NewTextMessage( + agency.Role(msg.Role), + msg.Content, + ) } diff --git a/providers/openai/tools.go b/providers/openai/tools.go new file mode 100644 index 0000000..6c2aec4 --- /dev/null +++ b/providers/openai/tools.go @@ -0,0 +1,39 @@ +package openai + +import ( + "context" + + "github.com/neurocult/agency" + "github.com/sashabaranov/go-openai/jsonschema" +) + +type ToolResultMessage struct { + agency.Message + + ToolID string + ToolName string +} + +// FuncDef represents a function definition that can be called during the conversation. +type FuncDef struct { + Name string + Description string + // Parameters is an optional structure that defines the schema of the parameters that the function accepts. + Parameters *jsonschema.Definition + // Body is the actual function that get's called. + // Parameters passed are bytes that can be unmarshalled to type that implements provided json schema. + // Returned result must be anything that can be marshalled, including primitive values. + Body func(ctx context.Context, params []byte) (agency.Message, error) +} + +type FuncDefs []FuncDef + +func (ds FuncDefs) getFuncDefByName(name string) *FuncDef { + for _, f := range ds { + if f.Name == name { + return &f + } + } + + return nil +} From 0032e686670907a1e3671b9d75fd3b2791781058 Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Mon, 22 Jul 2024 23:55:14 +0500 Subject: [PATCH 19/21] feat(openai:t2t): add shouldReturnJSON flag --- providers/openai/text_to_text.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 519bfde..1612831 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -19,6 +19,7 @@ type TextToTextParams struct { FuncDefs []FuncDef Seed *int IsToolsCallRequired bool + ShouldReturnJSON bool } func (p TextToTextParams) ToolCallRequired() *string { @@ -36,6 +37,13 @@ func (p TextToTextParams) ToolCallRequired() *string { func (p Provider) TextToText(params TextToTextParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) + var responseFormat openai.ChatCompletionResponseFormatType + if params.ShouldReturnJSON { + responseFormat = openai.ChatCompletionResponseFormatTypeJSONObject + } else { + responseFormat = openai.ChatCompletionResponseFormatTypeText + } + return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openAIMessages, err := agencyToOpenaiMessages(cfg, msg) @@ -47,13 +55,14 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { openAIResponse, err := p.client.CreateChatCompletion( ctx, openai.ChatCompletionRequest{ - Model: params.Model, - Temperature: nullableToFloat32(params.Temperature), - MaxTokens: params.MaxTokens, - Messages: openAIMessages, - Tools: openAITools, - Seed: params.Seed, - ToolChoice: params.ToolCallRequired(), + Model: params.Model, + Temperature: nullableToFloat32(params.Temperature), + MaxTokens: params.MaxTokens, + Messages: openAIMessages, + Tools: openAITools, + Seed: params.Seed, + ToolChoice: params.ToolCallRequired(), + ResponseFormat: &openai.ChatCompletionResponseFormat{Type: responseFormat}, }, ) if err != nil { From 0ae3eec75e85d54df5b9a3a2c11e31dc2af67af4 Mon Sep 17 00:00:00 2001 From: Emil Valeev Date: Tue, 23 Jul 2024 12:14:49 +0500 Subject: [PATCH 20/21] feat(t2e): dimensions --- providers/openai/text_to_embedding.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/providers/openai/text_to_embedding.go b/providers/openai/text_to_embedding.go index 2f5117f..8fc220e 100644 --- a/providers/openai/text_to_embedding.go +++ b/providers/openai/text_to_embedding.go @@ -14,12 +14,24 @@ type EmbeddingModel = openai.EmbeddingModel const AdaEmbeddingV2 EmbeddingModel = openai.AdaEmbeddingV2 type TextToEmbeddingParams struct { - Model EmbeddingModel + Model EmbeddingModel + Dimensions EmbeddingDimensions +} + +type EmbeddingDimensions *int + +func NewDimensions(v int) EmbeddingDimensions { + return &v } func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operation { + var dimensions int + if params.Dimensions != nil { + dimensions = *params.Dimensions + } + return agency.NewOperation(func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { - //TODO: we have to convert string to model and then model to string. Can we optimize it? + // TODO: we have to convert string to model and then model to string. Can we optimize it? messages := append(cfg.Messages, msg) texts := make([]string, len(messages)) @@ -30,8 +42,9 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio resp, err := p.client.CreateEmbeddings( ctx, openai.EmbeddingRequest{ - Input: texts, - Model: params.Model, + Input: texts, + Model: params.Model, + Dimensions: dimensions, }, ) if err != nil { @@ -48,7 +61,7 @@ func (p Provider) TextToEmbedding(params TextToEmbeddingParams) *agency.Operatio return nil, fmt.Errorf("failed to convert embedding to bytes: %w", err) } - //TODO: we have to convert []float32 to []byte. Can we optimize it? + // TODO: we have to convert []float32 to []byte. Can we optimize it? return agency.NewMessage(agency.AssistantRole, agency.EmbeddingKind, bytes), nil }) } From 71be583f206ef29a0e78c739e151b6a20acc895b Mon Sep 17 00:00:00 2001 From: Max Krou Date: Tue, 3 Sep 2024 14:27:33 +0500 Subject: [PATCH 21/21] fix(openai): add rich format option --- providers/openai/text_to_stream.go | 17 +++++++++-------- providers/openai/text_to_text.go | 11 ++--------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/providers/openai/text_to_stream.go b/providers/openai/text_to_stream.go index c8fca05..fd1ebad 100644 --- a/providers/openai/text_to_stream.go +++ b/providers/openai/text_to_stream.go @@ -29,14 +29,15 @@ func (p Provider) TextToStream(params TextToStreamParams) *agency.Operation { openAIResponse, err := p.client.CreateChatCompletionStream( ctx, openai.ChatCompletionRequest{ - Model: params.Model, - Temperature: nullableToFloat32(params.Temperature), - MaxTokens: params.MaxTokens, - Messages: openAIMessages, - Tools: openAITools, - Stream: params.StreamHandler != nil, - ToolChoice: params.ToolCallRequired(), - Seed: params.Seed, + Model: params.Model, + Temperature: nullableToFloat32(params.Temperature), + MaxTokens: params.MaxTokens, + Messages: openAIMessages, + Tools: openAITools, + Stream: params.StreamHandler != nil, + ToolChoice: params.ToolCallRequired(), + Seed: params.Seed, + ResponseFormat: params.Format, }, ) if err != nil { diff --git a/providers/openai/text_to_text.go b/providers/openai/text_to_text.go index 1612831..e5caa66 100644 --- a/providers/openai/text_to_text.go +++ b/providers/openai/text_to_text.go @@ -19,7 +19,7 @@ type TextToTextParams struct { FuncDefs []FuncDef Seed *int IsToolsCallRequired bool - ShouldReturnJSON bool + Format *openai.ChatCompletionResponseFormat } func (p TextToTextParams) ToolCallRequired() *string { @@ -37,13 +37,6 @@ func (p TextToTextParams) ToolCallRequired() *string { func (p Provider) TextToText(params TextToTextParams) *agency.Operation { openAITools := castFuncDefsToOpenAITools(params.FuncDefs) - var responseFormat openai.ChatCompletionResponseFormatType - if params.ShouldReturnJSON { - responseFormat = openai.ChatCompletionResponseFormatTypeJSONObject - } else { - responseFormat = openai.ChatCompletionResponseFormatTypeText - } - return agency.NewOperation( func(ctx context.Context, msg agency.Message, cfg *agency.OperationConfig) (agency.Message, error) { openAIMessages, err := agencyToOpenaiMessages(cfg, msg) @@ -62,7 +55,7 @@ func (p Provider) TextToText(params TextToTextParams) *agency.Operation { Tools: openAITools, Seed: params.Seed, ToolChoice: params.ToolCallRequired(), - ResponseFormat: &openai.ChatCompletionResponseFormat{Type: responseFormat}, + ResponseFormat: params.Format, }, ) if err != nil {