From 934d34071d8477f23422bc749bf05f1d26c3dbf1 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Wed, 20 Dec 2023 14:27:46 +0100 Subject: [PATCH 01/65] chore: refactor --- llm/openai/newopenai.go | 187 ++++++++++++++++++++++++++++++++++++++++ pipeline/lmm.go | 1 + thread/thread.go | 37 ++++++++ 3 files changed, 225 insertions(+) create mode 100644 llm/openai/newopenai.go create mode 100644 thread/thread.go diff --git a/llm/openai/newopenai.go b/llm/openai/newopenai.go new file mode 100644 index 00000000..bc4d53fa --- /dev/null +++ b/llm/openai/newopenai.go @@ -0,0 +1,187 @@ +package openai + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/thread" + "github.com/sashabaranov/go-openai" +) + +func threadToChatCompletionMessages(thread *thread.Thread) []openai.ChatCompletionMessage { + chatCompletionMessages := make([]openai.ChatCompletionMessage, len(thread.Messages)) + for i, message := range thread.Messages { + chatMessageParts := threadContentsToChatMessageParts(thread.Messages[i]) + chatCompletionMessages[i] = openai.ChatCompletionMessage{ + Role: message.Role, + MultiContent: chatMessageParts, + } + } + + return chatCompletionMessages +} + +func threadContentsToChatMessageParts(m thread.Message) []openai.ChatMessagePart { + chatMessageParts := make([]openai.ChatMessagePart, len(m.Contents)) + + for i, content := range m.Contents { + var chatMessagePart *openai.ChatMessagePart + + switch content.Type { + case thread.ContentTypeText: + chatMessagePart = &openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeText, + Text: content.Data.(string), + } + case thread.ContentTypeImage: + chatMessagePart = &openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeImageURL, + ImageURL: &openai.ChatMessageImageURL{ + URL: content.Data.(string), + Detail: openai.ImageURLDetailAuto, + }, + } + case thread.ContentTypeTool: + toolData := content.Data.(thread.ToolData) + chatMessagePart = &openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeTool, + Tool: &openai.ChatMessageTool{ + ID: toolData.ID, + Name: toolData.Name, + Result: toolData.Result, + }, + } + default: + continue + } + + chatMessageParts[i] = *chatMessagePart + } + + return chatMessageParts +} + +func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) (*thread.Thread, error) { + if t == nil { + return nil, nil + } + + chatCompletionRequest := openai.ChatCompletionRequest{ + Model: string(o.model), + Messages: threadToChatCompletionMessages(t), + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, + } + + if len(o.functions) > 0 { + chatCompletionRequest.Tools = o.getChatCompletionRequestTools() + chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() + } + + response, err := o.openAIClient.CreateChatCompletion( + ctx, + chatCompletionRequest, + ) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + + if o.usageCallback != nil { + o.setUsageMetadata(response.Usage) + } + + if len(response.Choices) == 0 { + return nil, fmt.Errorf("%w: no choices returned", ErrOpenAIChat) + } + + var messages []thread.Message + if response.Choices[0].FinishReason == "tool_calls" { + messages = o.callTools(response) + } else { + if o.verbose { + //TODO + } + messages = []thread.Message{ + textContentToThreadMessage(response.Choices[0].Message.Content), + } + } + + t.Messages = append(t.Messages, messages...) + + return t, nil +} + +func (o *OpenAI) getChatCompletionRequestTools() []openai.Tool { + return o.getFunctions() +} + +func (o *OpenAI) getChatCompletionRequestToolChoice() any { + if o.toolChoice != nil { + return openai.ToolChoice{ + Type: openai.ToolTypeFunction, + Function: openai.ToolFunction{ + Name: *o.toolChoice, + }, + } + } + + return "auto" +} + +func (o *OpenAI) callTool(openai.ToolCall) (string, error) { + return "", nil +} + +func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []thread.Message { + if len(o.functions) == 0 || len(response.Choices[0].Message.ToolCalls) == 0 { + return nil + } + + var messages []thread.Message + for _, toolCall := range response.Choices[0].Message.ToolCalls { + if o.verbose { + fmt.Printf("Calling function %s\n", toolCall.Function.Name) + fmt.Printf("Function call arguments: %s\n", toolCall.Function.Arguments) + } + + result, err := o.callTool(toolCall) + if err != nil { + result = fmt.Sprintf("error: %s", err) + } + + messages = append(messages, toolCallResultToThreadMessage(toolCall, result)) + } + + return messages +} + +func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) thread.Message { + return thread.Message{ + Role: thread.RoleTool, + Contents: []thread.Content{ + { + Type: thread.ContentTypeTool, + Data: thread.ToolData{ + ID: toolCall.ID, + Name: toolCall.Function.Name, + Result: result, + }, + }, + }, + } +} + +func textContentToThreadMessage(content string) thread.Message { + return thread.Message{ + Role: thread.RoleAssistant, + Contents: []thread.Content{ + { + Type: thread.ContentTypeText, + Data: content, + }, + }, + } +} diff --git a/pipeline/lmm.go b/pipeline/lmm.go index ae9ac48c..353463c5 100644 --- a/pipeline/lmm.go +++ b/pipeline/lmm.go @@ -29,4 +29,5 @@ type Prompt interface { type LlmEngine interface { Completion(ctx context.Context, prompt string) (string, error) Chat(ctx context.Context, chat *chat.Chat) (string, error) + // Generate(ctx context.Context, thread *chat.Thread) (*chat.Thread, error) } diff --git a/thread/thread.go b/thread/thread.go new file mode 100644 index 00000000..74753ef6 --- /dev/null +++ b/thread/thread.go @@ -0,0 +1,37 @@ +package thread + +type Thread struct { + Messages []Message +} + +type ContentType string + +const ( + ContentTypeText ContentType = "text" + ContentTypeImage ContentType = "image" + ContentTypeTool ContentType = "tool" +) + +type Content struct { + Type ContentType + Data any +} + +type Role string + +const ( + RoleUser Role = "user" + RoleAssistant Role = "assistant" + RoleTool Role = "tool" +) + +type Message struct { + Role Role + Contents []Content +} + +type ToolData struct { + ID string + Name string + Result string +} From 52edba5b36e2b389b443384ef3181626447a6ac1 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Wed, 20 Dec 2023 15:55:40 +0100 Subject: [PATCH 02/65] refactor --- llm/openai/newopenai.go | 99 +++++++++++++++++++++++++---------------- thread/thread.go | 78 ++++++++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 41 deletions(-) diff --git a/llm/openai/newopenai.go b/llm/openai/newopenai.go index bc4d53fa..ab8bf526 100644 --- a/llm/openai/newopenai.go +++ b/llm/openai/newopenai.go @@ -5,23 +5,48 @@ import ( "fmt" "github.com/henomis/lingoose/thread" - "github.com/sashabaranov/go-openai" + openai "github.com/sashabaranov/go-openai" ) -func threadToChatCompletionMessages(thread *thread.Thread) []openai.ChatCompletionMessage { - chatCompletionMessages := make([]openai.ChatCompletionMessage, len(thread.Messages)) - for i, message := range thread.Messages { - chatMessageParts := threadContentsToChatMessageParts(thread.Messages[i]) +var threadRoleToOpenAIRole = map[thread.Role]string{ + thread.RoleUser: "user", + thread.RoleAssistant: "assistant", + thread.RoleTool: "tool", +} + +func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMessage { + chatCompletionMessages := make([]openai.ChatCompletionMessage, len(t.Messages)) + for i, message := range t.Messages { chatCompletionMessages[i] = openai.ChatCompletionMessage{ - Role: message.Role, - MultiContent: chatMessageParts, + Role: threadRoleToOpenAIRole[message.Role], + } + + if len(message.Contents) > 1 { + chatCompletionMessages[i].MultiContent = threadContentsToChatMessageParts(message) + continue + } + + switch message.Role { + case thread.RoleUser, thread.RoleAssistant: + if data, ok := message.Contents[0].Data.(string); ok { + chatCompletionMessages[i].Content = data + } else { + continue + } + case thread.RoleTool: + if data, ok := message.Contents[0].Data.(thread.ToolData); ok { + chatCompletionMessages[i].ToolCallID = data.ID + chatCompletionMessages[i].Content = data.Result + } else { + continue + } } } return chatCompletionMessages } -func threadContentsToChatMessageParts(m thread.Message) []openai.ChatMessagePart { +func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePart { chatMessageParts := make([]openai.ChatMessagePart, len(m.Contents)) for i, content := range m.Contents { @@ -41,16 +66,6 @@ func threadContentsToChatMessageParts(m thread.Message) []openai.ChatMessagePart Detail: openai.ImageURLDetailAuto, }, } - case thread.ContentTypeTool: - toolData := content.Data.(thread.ToolData) - chatMessagePart = &openai.ChatMessagePart{ - Type: openai.ChatMessagePartTypeTool, - Tool: &openai.ChatMessageTool{ - ID: toolData.ID, - Name: toolData.Name, - Result: toolData.Result, - }, - } default: continue } @@ -97,14 +112,14 @@ func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) (*thread.Thread return nil, fmt.Errorf("%w: no choices returned", ErrOpenAIChat) } - var messages []thread.Message + var messages []*thread.Message if response.Choices[0].FinishReason == "tool_calls" { messages = o.callTools(response) } else { if o.verbose { //TODO } - messages = []thread.Message{ + messages = []*thread.Message{ textContentToThreadMessage(response.Choices[0].Message.Content), } } @@ -131,16 +146,28 @@ func (o *OpenAI) getChatCompletionRequestToolChoice() any { return "auto" } -func (o *OpenAI) callTool(openai.ToolCall) (string, error) { - return "", nil +func (o *OpenAI) callTool(toolCall openai.ToolCall) (string, error) { + fn, ok := o.functions[toolCall.Function.Name] + if !ok { + return "", fmt.Errorf("unknown function %s", toolCall.Function.Name) + } + + resultAsJSON, err := callFnWithArgumentAsJSON(fn.Fn, toolCall.Function.Arguments) + if err != nil { + return "", err + } + + o.calledFunctionName = &fn.Name + + return resultAsJSON, nil } -func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []thread.Message { +func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []*thread.Message { if len(o.functions) == 0 || len(response.Choices[0].Message.ToolCalls) == 0 { return nil } - var messages []thread.Message + var messages []*thread.Message for _, toolCall := range response.Choices[0].Message.ToolCalls { if o.verbose { fmt.Printf("Calling function %s\n", toolCall.Function.Name) @@ -158,30 +185,26 @@ func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []thread.Mess return messages } -func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) thread.Message { - return thread.Message{ +func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thread.Message { + return &thread.Message{ Role: thread.RoleTool, - Contents: []thread.Content{ - { - Type: thread.ContentTypeTool, - Data: thread.ToolData{ + Contents: []*thread.Content{ + thread.NewToolContent( + &thread.ToolData{ ID: toolCall.ID, Name: toolCall.Function.Name, Result: result, }, - }, + ), }, } } -func textContentToThreadMessage(content string) thread.Message { - return thread.Message{ +func textContentToThreadMessage(content string) *thread.Message { + return &thread.Message{ Role: thread.RoleAssistant, - Contents: []thread.Content{ - { - Type: thread.ContentTypeText, - Data: content, - }, + Contents: []*thread.Content{ + thread.NewTextContent(content), }, } } diff --git a/thread/thread.go b/thread/thread.go index 74753ef6..074e29c0 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -1,7 +1,7 @@ package thread type Thread struct { - Messages []Message + Messages []*Message } type ContentType string @@ -9,7 +9,9 @@ type ContentType string const ( ContentTypeText ContentType = "text" ContentTypeImage ContentType = "image" - ContentTypeTool ContentType = "tool" + // ContentTypeVideo ContentType = "video" + // ContentTypeAudio ContentType = "audio" + ContentTypeTool ContentType = "tool" ) type Content struct { @@ -27,7 +29,7 @@ const ( type Message struct { Role Role - Contents []Content + Contents []*Content } type ToolData struct { @@ -35,3 +37,73 @@ type ToolData struct { Name string Result string } + +type MediaData struct { + Raw any + URL *string +} + +func NewTextContent(text string) *Content { + return &Content{ + Type: ContentTypeText, + Data: text, + } +} + +func NewImageContent(mediaData *MediaData) *Content { + return &Content{ + Type: ContentTypeImage, + Data: mediaData, + } +} + +// func NewVideoContent(mediaData *MediaData) *Content { +// return &Content{ +// Type: ContentTypeVideo, +// Data: mediaData, +// } +// } + +// func NewAudioContent(mediaData *MediaData) *Content { +// return &Content{ +// Type: ContentTypeAudio, +// Data: mediaData, +// } +// } + +func NewToolContent(toolData *ToolData) *Content { + return &Content{ + Type: ContentTypeTool, + Data: toolData, + } +} + +func (m *Message) AddContent(content *Content) { + m.Contents = append(m.Contents, content) +} + +func NewUserMessage() *Message { + return &Message{ + Role: RoleUser, + } +} + +func NewAssistantMessage() *Message { + return &Message{ + Role: RoleAssistant, + } +} + +func NewToolMessage() *Message { + return &Message{ + Role: RoleTool, + } +} + +func (t *Thread) AddMessage(message *Message) { + t.Messages = append(t.Messages, message) +} + +func NewThread() *Thread { + return &Thread{} +} From b458f6b38422a6fe4883af818c180b98590d62ce Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Wed, 20 Dec 2023 16:00:49 +0100 Subject: [PATCH 03/65] fix --- llm/openai/newopenai.go | 30 ++++++++++++------------------ thread/thread.go | 6 ++++-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/llm/openai/newopenai.go b/llm/openai/newopenai.go index ab8bf526..55fba736 100644 --- a/llm/openai/newopenai.go +++ b/llm/openai/newopenai.go @@ -186,25 +186,19 @@ func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []*thread.Mes } func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thread.Message { - return &thread.Message{ - Role: thread.RoleTool, - Contents: []*thread.Content{ - thread.NewToolContent( - &thread.ToolData{ - ID: toolCall.ID, - Name: toolCall.Function.Name, - Result: result, - }, - ), - }, - } + return thread.NewToolMessage().AddContent( + thread.NewToolContent( + &thread.ToolData{ + ID: toolCall.ID, + Name: toolCall.Function.Name, + Result: result, + }, + ), + ) } func textContentToThreadMessage(content string) *thread.Message { - return &thread.Message{ - Role: thread.RoleAssistant, - Contents: []*thread.Content{ - thread.NewTextContent(content), - }, - } + return thread.NewAssistantMessage().AddContent( + thread.NewTextContent(content), + ) } diff --git a/thread/thread.go b/thread/thread.go index 074e29c0..c81e66d4 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -78,8 +78,9 @@ func NewToolContent(toolData *ToolData) *Content { } } -func (m *Message) AddContent(content *Content) { +func (m *Message) AddContent(content *Content) *Message { m.Contents = append(m.Contents, content) + return m } func NewUserMessage() *Message { @@ -100,8 +101,9 @@ func NewToolMessage() *Message { } } -func (t *Thread) AddMessage(message *Message) { +func (t *Thread) AddMessage(message *Message) *Thread { t.Messages = append(t.Messages, message) + return t } func NewThread() *Thread { From 6ea2ba1427b78f1c7e107409de23066740166e7d Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Wed, 20 Dec 2023 16:12:01 +0100 Subject: [PATCH 04/65] fix --- examples/llm/openai/thread/main.go | 39 ++++++++++++++++++++++++++++++ thread/thread.go | 20 +++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 examples/llm/openai/thread/main.go diff --git a/examples/llm/openai/thread/main.go b/examples/llm/openai/thread/main.go new file mode 100644 index 00000000..a7d15d23 --- /dev/null +++ b/examples/llm/openai/thread/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/thread" +) + +func main() { + openaillm := openai.New( + openai.GPT3Dot5Turbo0613, + 0, + 256, + false, + ) + + t := thread.NewThread().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Hello, I'm a user"), + ).AddContent( + thread.NewTextContent("Can you greet me?"), + ), + ).AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("please reply as a pirate."), + ), + ) + + fmt.Println(t) + + t, err := openaillm.Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t) +} diff --git a/thread/thread.go b/thread/thread.go index c81e66d4..5accce32 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -109,3 +109,23 @@ func (t *Thread) AddMessage(message *Message) *Thread { func NewThread() *Thread { return &Thread{} } + +func (t *Thread) String() string { + str := "Thread:\n" + for _, message := range t.Messages { + str += string(message.Role) + ":\n" + for _, content := range message.Contents { + str += "\tType: " + string(content.Type) + "\n" + switch content.Type { + case ContentTypeText: + str += "\tText: " + content.Data.(string) + "\n" + case ContentTypeImage: + str += "\tImage URL: " + *content.Data.(*MediaData).URL + "\n" + case ContentTypeTool: + str += "\tTool Name: " + content.Data.(*ToolData).Name + "\n" + str += "\tTool Result: " + content.Data.(*ToolData).Result + "\n" + } + } + } + return str +} From f113af4f10a7379531536678444de8566cfdcda8 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 09:51:11 +0100 Subject: [PATCH 05/65] refactor --- examples/chat/functions/main.go | 2 +- examples/chat/simple/main.go | 2 +- examples/llm/openai/thread/main.go | 46 ++- llm/openai/common.go | 122 +++++++ llm/openai/formatters.go | 129 ++++++++ llm/openai/legacy.go | 386 ++++++++++++++++++++++ llm/openai/newopenai.go | 204 ------------ llm/openai/openai.go | 514 +++++++---------------------- thread/thread.go | 68 ++-- 9 files changed, 837 insertions(+), 636 deletions(-) create mode 100644 llm/openai/common.go create mode 100644 llm/openai/formatters.go create mode 100644 llm/openai/legacy.go delete mode 100644 llm/openai/newopenai.go diff --git a/examples/chat/functions/main.go b/examples/chat/functions/main.go index 65388f25..a0124429 100644 --- a/examples/chat/functions/main.go +++ b/examples/chat/functions/main.go @@ -33,7 +33,7 @@ func main() { }, ) - llmOpenAI := openai.New(openai.GPT3Dot5Turbo0613, openai.DefaultOpenAITemperature, openai.DefaultOpenAIMaxTokens, true). + llmOpenAI := openai.NewLegacy(openai.GPT3Dot5Turbo0613, openai.DefaultOpenAITemperature, openai.DefaultOpenAIMaxTokens, true). WithCallback(func(response types.Meta) { for k, v := range response { if k == "CompletionTokens" { diff --git a/examples/chat/simple/main.go b/examples/chat/simple/main.go index 1494edaf..79c90698 100644 --- a/examples/chat/simple/main.go +++ b/examples/chat/simple/main.go @@ -22,7 +22,7 @@ func main() { }, ) - llmOpenAI := openai.New(openai.GPT3Dot5Turbo, openai.DefaultOpenAITemperature, openai.DefaultOpenAIMaxTokens, true) + llmOpenAI := openai.NewLegacy(openai.GPT3Dot5Turbo, openai.DefaultOpenAITemperature, openai.DefaultOpenAIMaxTokens, true) response, err := llmOpenAI.Chat(context.Background(), chat) if err != nil { diff --git a/examples/llm/openai/thread/main.go b/examples/llm/openai/thread/main.go index a7d15d23..878b04af 100644 --- a/examples/llm/openai/thread/main.go +++ b/examples/llm/openai/thread/main.go @@ -8,12 +8,25 @@ import ( "github.com/henomis/lingoose/thread" ) +type Answer struct { + Answer string `json:"answer" jsonschema:"description=the pirate answer"` +} + +func getAnswer(a Answer) string { + return a.Answer +} + +func newStr(str string) *string { + return &str +} + func main() { - openaillm := openai.New( - openai.GPT3Dot5Turbo0613, - 0, - 256, - false, + openaillm := openai.New() + openaillm.WithToolChoice(newStr("getPirateAnswer")) + openaillm.BindFunction( + getAnswer, + "getPirateAnswer", + "use this function to get the pirate answer", ) t := thread.NewThread().AddMessage( @@ -24,13 +37,32 @@ func main() { ), ).AddMessage( thread.NewUserMessage().AddContent( - thread.NewTextContent("please reply as a pirate."), + thread.NewTextContent("please greet me as a pirate."), ), ) fmt.Println(t) - t, err := openaillm.Generate(context.Background(), t) + err := openaillm.Generate(context.Background(), t) + if err != nil { + panic(err) + } + + t.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent("now translate to italian as a poem"), + )) + + fmt.Println(t) + // disable functions + openaillm.WithToolChoice(nil) + + err = openaillm.Stream(context.Background(), t, func(a string) { + if a == openai.EOS { + fmt.Printf("\n") + return + } + fmt.Printf("%s", a) + }) if err != nil { panic(err) } diff --git a/llm/openai/common.go b/llm/openai/common.go new file mode 100644 index 00000000..d0ad5b33 --- /dev/null +++ b/llm/openai/common.go @@ -0,0 +1,122 @@ +package openai + +import ( + "fmt" + + "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/types" + "github.com/sashabaranov/go-openai" +) + +var ( + ErrOpenAICompletion = fmt.Errorf("openai completion error") + ErrOpenAIChat = fmt.Errorf("openai chat error") +) + +const ( + DefaultOpenAIMaxTokens = 256 + DefaultOpenAITemperature = 0.7 + DefaultOpenAINumResults = 1 + DefaultOpenAITopP = 1.0 + DefaultMaxIterations = 3 +) + +type Model string + +const ( + GPT432K0613 Model = openai.GPT432K0613 + GPT432K0314 Model = openai.GPT432K0314 + GPT432K Model = openai.GPT432K + GPT40613 Model = openai.GPT40613 + GPT40314 Model = openai.GPT40314 + GPT4TurboPreview Model = openai.GPT4TurboPreview + GPT4VisionPreview Model = openai.GPT4VisionPreview + GPT4 Model = openai.GPT4 + GPT3Dot5Turbo1106 Model = openai.GPT3Dot5Turbo1106 + GPT3Dot5Turbo0613 Model = openai.GPT3Dot5Turbo0613 + GPT3Dot5Turbo0301 Model = openai.GPT3Dot5Turbo0301 + GPT3Dot5Turbo16K Model = openai.GPT3Dot5Turbo16K + GPT3Dot5Turbo16K0613 Model = openai.GPT3Dot5Turbo16K0613 + GPT3Dot5Turbo Model = openai.GPT3Dot5Turbo + GPT3Dot5TurboInstruct Model = openai.GPT3Dot5TurboInstruct + GPT3Davinci Model = openai.GPT3Davinci + GPT3Davinci002 Model = openai.GPT3Davinci002 + GPT3Curie Model = openai.GPT3Curie + GPT3Curie002 Model = openai.GPT3Curie002 + GPT3Ada Model = openai.GPT3Ada + GPT3Ada002 Model = openai.GPT3Ada002 + GPT3Babbage Model = openai.GPT3Babbage + GPT3Babbage002 Model = openai.GPT3Babbage002 +) + +type UsageCallback func(types.Meta) +type StreamCallback func(string) + +type OpenAI struct { + openAIClient *openai.Client + model Model + temperature float32 + maxTokens int + stop []string + verbose bool + usageCallback UsageCallback + functions map[string]Function + functionsMaxIterations uint + toolChoice *string + calledFunctionName *string + finishReason string + cache *cache.Cache +} + +// WithModel sets the model to use for the OpenAI instance. +func (o *OpenAI) WithModel(model Model) *OpenAI { + o.model = model + return o +} + +// WithTemperature sets the temperature to use for the OpenAI instance. +func (o *OpenAI) WithTemperature(temperature float32) *OpenAI { + o.temperature = temperature + return o +} + +// WithMaxTokens sets the max tokens to use for the OpenAI instance. +func (o *OpenAI) WithMaxTokens(maxTokens int) *OpenAI { + o.maxTokens = maxTokens + return o +} + +// WithUsageCallback sets the usage callback to use for the OpenAI instance. +func (o *OpenAI) WithCallback(callback UsageCallback) *OpenAI { + o.usageCallback = callback + return o +} + +// WithStop sets the stop sequences to use for the OpenAI instance. +func (o *OpenAI) WithStop(stop []string) *OpenAI { + o.stop = stop + return o +} + +// WithClient sets the client to use for the OpenAI instance. +func (o *OpenAI) WithClient(client *openai.Client) *OpenAI { + o.openAIClient = client + return o +} + +// WithVerbose sets the verbose flag to use for the OpenAI instance. +func (o *OpenAI) WithVerbose(verbose bool) *OpenAI { + o.verbose = verbose + return o +} + +// WithCache sets the cache to use for the OpenAI instance. +func (o *OpenAI) WithCompletionCache(cache *cache.Cache) *OpenAI { + o.cache = cache + return o +} + +func (o *OpenAI) WithToolChoice(toolChoice *string) *OpenAI { + o.toolChoice = toolChoice + return o +} diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go new file mode 100644 index 00000000..32fb9c06 --- /dev/null +++ b/llm/openai/formatters.go @@ -0,0 +1,129 @@ +package openai + +import ( + "github.com/henomis/lingoose/thread" + "github.com/sashabaranov/go-openai" +) + +func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMessage { + chatCompletionMessages := make([]openai.ChatCompletionMessage, len(t.Messages)) + for i, message := range t.Messages { + chatCompletionMessages[i] = openai.ChatCompletionMessage{ + Role: threadRoleToOpenAIRole[message.Role], + } + + if len(message.Contents) > 1 { + chatCompletionMessages[i].MultiContent = threadContentsToChatMessageParts(message) + continue + } + + switch message.Role { + case thread.RoleUser: + if data, ok := message.Contents[0].Data.(string); ok { + chatCompletionMessages[i].Content = data + } else { + continue + } + case thread.RoleAssistant: + if data, ok := message.Contents[0].Data.(string); ok { + chatCompletionMessages[i].Content = data + } else if data, ok := message.Contents[0].Data.([]*thread.ToolCallData); ok { + var toolCalls []openai.ToolCall + for _, toolCallData := range data { + toolCalls = append(toolCalls, openai.ToolCall{ + ID: toolCallData.ID, + Type: "function", + Function: openai.FunctionCall{ + Name: toolCallData.Function.Name, + Arguments: toolCallData.Function.Arguments, + }, + }) + } + chatCompletionMessages[i].ToolCalls = toolCalls + } else { + continue + } + case thread.RoleTool: + if data, ok := message.Contents[0].Data.(*thread.ToolResponseData); ok { + chatCompletionMessages[i].ToolCallID = data.ID + chatCompletionMessages[i].Name = data.Name + chatCompletionMessages[i].Content = data.Result + } else { + continue + } + } + } + + return chatCompletionMessages +} + +func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePart { + chatMessageParts := make([]openai.ChatMessagePart, len(m.Contents)) + + for i, content := range m.Contents { + var chatMessagePart *openai.ChatMessagePart + + switch content.Type { + case thread.ContentTypeText: + chatMessagePart = &openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeText, + Text: content.Data.(string), + } + case thread.ContentTypeImage: + chatMessagePart = &openai.ChatMessagePart{ + Type: openai.ChatMessagePartTypeImageURL, + ImageURL: &openai.ChatMessageImageURL{ + URL: content.Data.(string), + Detail: openai.ImageURLDetailAuto, + }, + } + default: + continue + } + + chatMessageParts[i] = *chatMessagePart + } + + return chatMessageParts +} + +func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thread.Message { + return thread.NewToolMessage().AddContent( + thread.NewToolResponseContent( + &thread.ToolResponseData{ + ID: toolCall.ID, + Name: toolCall.Function.Name, + Result: result, + }, + ), + ) +} + +func textContentToThreadMessage(content string) *thread.Message { + return thread.NewAssistantMessage().AddContent( + thread.NewTextContent(content), + ) +} + +func toolCallsToToolCallMessage(toolCalls []openai.ToolCall) *thread.Message { + if len(toolCalls) == 0 { + return nil + } + + var toolCallData []*thread.ToolCallData + for _, toolCall := range toolCalls { + toolCallData = append(toolCallData, &thread.ToolCallData{ + ID: toolCall.ID, + Function: thread.ToolCallFunction{ + Name: toolCall.Function.Name, + Arguments: toolCall.Function.Arguments, + }, + }) + } + + return thread.NewAssistantMessage().AddContent( + thread.NewToolCallContent( + toolCallData, + ), + ) +} diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go new file mode 100644 index 00000000..1e6630a0 --- /dev/null +++ b/llm/openai/legacy.go @@ -0,0 +1,386 @@ +// Package openai provides a wrapper around the OpenAI API. +package openai + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/henomis/lingoose/chat" + "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/types" + "github.com/mitchellh/mapstructure" + "github.com/sashabaranov/go-openai" +) + +func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *OpenAI { + openAIKey := os.Getenv("OPENAI_API_KEY") + + return &OpenAI{ + openAIClient: openai.NewClient(openAIKey), + model: model, + temperature: temperature, + maxTokens: maxTokens, + verbose: verbose, + functions: make(map[string]Function), + functionsMaxIterations: DefaultMaxIterations, + } +} + +// CalledFunctionName returns the name of the function that was called. +func (o *OpenAI) CalledFunctionName() *string { + return o.calledFunctionName +} + +// FinishReason returns the LLM finish reason. +func (o *OpenAI) FinishReason() string { + return o.finishReason +} + +func NewCompletion() *OpenAI { + return NewLegacy( + GPT3Dot5TurboInstruct, + DefaultOpenAITemperature, + DefaultOpenAIMaxTokens, + false, + ) +} + +func NewChat() *OpenAI { + return NewLegacy( + GPT3Dot5Turbo, + DefaultOpenAITemperature, + DefaultOpenAIMaxTokens, + false, + ) +} + +// Completion returns a single completion for the given prompt. +func (o *OpenAI) Completion(ctx context.Context, prompt string) (string, error) { + var cacheResult *cache.Result + var err error + + if o.cache != nil { + cacheResult, err = o.cache.Get(ctx, prompt) + if err == nil { + return strings.Join(cacheResult.Answer, "\n"), nil + } else if !errors.Is(err, cache.ErrCacheMiss) { + return "", fmt.Errorf("%w: %w", ErrOpenAICompletion, err) + } + } + + outputs, err := o.BatchCompletion(ctx, []string{prompt}) + if err != nil { + return "", err + } + + if o.cache != nil { + err = o.cache.Set(ctx, cacheResult.Embedding, outputs[0]) + if err != nil { + return "", fmt.Errorf("%w: %w", ErrOpenAICompletion, err) + } + } + + return outputs[0], nil +} + +// BatchCompletion returns multiple completions for the given prompts. +func (o *OpenAI) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { + response, err := o.openAIClient.CreateCompletion( + ctx, + openai.CompletionRequest{ + Model: string(o.model), + Prompt: prompts, + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, + }, + ) + + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrOpenAICompletion, err) + } + + if o.usageCallback != nil { + o.setUsageMetadata(response.Usage) + } + + if len(response.Choices) == 0 { + return nil, fmt.Errorf("%w: no choices returned", ErrOpenAICompletion) + } + + var outputs []string + for _, choice := range response.Choices { + index := choice.Index + outputs = append(outputs, strings.TrimSpace(choice.Text)) + if o.verbose { + debugCompletion(prompts[index], choice.Text) + } + } + + return outputs, nil +} + +// CompletionStream returns a single completion stream for the given prompt. +func (o *OpenAI) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { + return o.BatchCompletionStream(ctx, []StreamCallback{callbackFn}, []string{prompt}) +} + +// BatchCompletionStream returns multiple completion streams for the given prompts. +func (o *OpenAI) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { + stream, err := o.openAIClient.CreateCompletionStream( + ctx, + openai.CompletionRequest{ + Model: string(o.model), + Prompt: prompts, + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, + }, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrOpenAICompletion, err) + } + + defer stream.Close() + + for { + response, errRecv := stream.Recv() + if errors.Is(errRecv, io.EOF) { + break + } + + if errRecv != nil { + return fmt.Errorf("%w: %w", ErrOpenAICompletion, errRecv) + } + + if o.usageCallback != nil { + o.setUsageMetadata(response.Usage) + } + + if len(response.Choices) == 0 { + return fmt.Errorf("%w: no choices returned", ErrOpenAICompletion) + } + + for _, choice := range response.Choices { + index := choice.Index + output := choice.Text + if o.verbose { + debugCompletion(prompts[index], output) + } + + callbackFn[index](output) + } + } + + return nil +} + +// Chat returns a single chat completion for the given prompt. +func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { + messages, err := buildMessages(prompt) + if err != nil { + return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + + chatCompletionRequest := openai.ChatCompletionRequest{ + Model: string(o.model), + Messages: messages, + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, + } + + if len(o.functions) > 0 { + chatCompletionRequest.Tools = o.getFunctions() + if o.toolChoice != nil { + chatCompletionRequest.ToolChoice = openai.ToolChoice{ + Type: openai.ToolTypeFunction, + Function: openai.ToolFunction{ + Name: *o.toolChoice, + }, + } + } else { + chatCompletionRequest.ToolChoice = "auto" + } + } + + response, err := o.openAIClient.CreateChatCompletion( + ctx, + chatCompletionRequest, + ) + + if err != nil { + return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + + if o.usageCallback != nil { + o.setUsageMetadata(response.Usage) + } + + if len(response.Choices) == 0 { + return "", fmt.Errorf("%w: no choices returned", ErrOpenAIChat) + } + + content := response.Choices[0].Message.Content + + o.finishReason = string(response.Choices[0].FinishReason) + o.calledFunctionName = nil + if len(response.Choices[0].Message.ToolCalls) > 0 && len(o.functions) > 0 { + if o.verbose { + fmt.Printf("Calling function %s\n", response.Choices[0].Message.ToolCalls[0].Function.Name) + fmt.Printf("Function call arguments: %s\n", response.Choices[0].Message.ToolCalls[0].Function.Arguments) + } + + content, err = o.functionCall(response) + if err != nil { + return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + } + + if o.verbose { + debugChat(prompt, content) + } + + return content, nil +} + +// ChatStream returns a single chat stream for the given prompt. +func (o *OpenAI) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { + messages, err := buildMessages(prompt) + if err != nil { + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + + stream, err := o.openAIClient.CreateChatCompletionStream( + ctx, + openai.ChatCompletionRequest{ + Model: string(o.model), + Messages: messages, + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, + }, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + + for { + response, errRecv := stream.Recv() + if errors.Is(errRecv, io.EOF) { + break + } + + // oops no usage here? + // if o.usageCallback != nil { + // o.setUsageMetadata(response.Usage) + // } + + if len(response.Choices) == 0 { + return fmt.Errorf("%w: no choices returned", ErrOpenAIChat) + } + + content := response.Choices[0].Delta.Content + + if o.verbose { + debugChat(prompt, content) + } + + callbackFn(content) + } + + return nil +} + +// SetStop sets the stop sequences for the completion. +func (o *OpenAI) SetStop(stop []string) { + o.stop = stop +} + +func (o *OpenAI) setUsageMetadata(usage openai.Usage) { + callbackMetadata := make(types.Meta) + + err := mapstructure.Decode(usage, &callbackMetadata) + if err != nil { + return + } + + o.usageCallback(callbackMetadata) +} + +func buildMessages(prompt *chat.Chat) ([]openai.ChatCompletionMessage, error) { + var messages []openai.ChatCompletionMessage + + promptMessages, err := prompt.ToMessages() + if err != nil { + return nil, err + } + + for _, message := range promptMessages { + if message.Type == chat.MessageTypeUser { + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: message.Content, + }) + } else if message.Type == chat.MessageTypeAssistant { + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleAssistant, + Content: message.Content, + }) + } else if message.Type == chat.MessageTypeSystem { + messages = append(messages, openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleSystem, + Content: message.Content, + }) + } else if message.Type == chat.MessageTypeFunction { + fnmessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleFunction, + Content: message.Content, + } + if message.Name != nil { + fnmessage.Name = *message.Name + } + + messages = append(messages, fnmessage) + } + } + + return messages, nil +} + +func debugChat(prompt *chat.Chat, content string) { + promptMessages, err := prompt.ToMessages() + if err != nil { + return + } + + for _, message := range promptMessages { + if message.Type == chat.MessageTypeUser { + fmt.Printf("---USER---\n%s\n", message.Content) + } else if message.Type == chat.MessageTypeAssistant { + fmt.Printf("---AI---\n%s\n", message.Content) + } else if message.Type == chat.MessageTypeSystem { + fmt.Printf("---SYSTEM---\n%s\n", message.Content) + } else if message.Type == chat.MessageTypeFunction { + fmt.Printf("---FUNCTION---\n%s()\n%s\n", *message.Name, message.Content) + } + } + fmt.Printf("---AI---\n%s\n", content) +} + +func debugCompletion(prompt string, content string) { + fmt.Printf("---USER---\n%s\n", prompt) + fmt.Printf("---AI---\n%s\n", content) +} diff --git a/llm/openai/newopenai.go b/llm/openai/newopenai.go deleted file mode 100644 index 55fba736..00000000 --- a/llm/openai/newopenai.go +++ /dev/null @@ -1,204 +0,0 @@ -package openai - -import ( - "context" - "fmt" - - "github.com/henomis/lingoose/thread" - openai "github.com/sashabaranov/go-openai" -) - -var threadRoleToOpenAIRole = map[thread.Role]string{ - thread.RoleUser: "user", - thread.RoleAssistant: "assistant", - thread.RoleTool: "tool", -} - -func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMessage { - chatCompletionMessages := make([]openai.ChatCompletionMessage, len(t.Messages)) - for i, message := range t.Messages { - chatCompletionMessages[i] = openai.ChatCompletionMessage{ - Role: threadRoleToOpenAIRole[message.Role], - } - - if len(message.Contents) > 1 { - chatCompletionMessages[i].MultiContent = threadContentsToChatMessageParts(message) - continue - } - - switch message.Role { - case thread.RoleUser, thread.RoleAssistant: - if data, ok := message.Contents[0].Data.(string); ok { - chatCompletionMessages[i].Content = data - } else { - continue - } - case thread.RoleTool: - if data, ok := message.Contents[0].Data.(thread.ToolData); ok { - chatCompletionMessages[i].ToolCallID = data.ID - chatCompletionMessages[i].Content = data.Result - } else { - continue - } - } - } - - return chatCompletionMessages -} - -func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePart { - chatMessageParts := make([]openai.ChatMessagePart, len(m.Contents)) - - for i, content := range m.Contents { - var chatMessagePart *openai.ChatMessagePart - - switch content.Type { - case thread.ContentTypeText: - chatMessagePart = &openai.ChatMessagePart{ - Type: openai.ChatMessagePartTypeText, - Text: content.Data.(string), - } - case thread.ContentTypeImage: - chatMessagePart = &openai.ChatMessagePart{ - Type: openai.ChatMessagePartTypeImageURL, - ImageURL: &openai.ChatMessageImageURL{ - URL: content.Data.(string), - Detail: openai.ImageURLDetailAuto, - }, - } - default: - continue - } - - chatMessageParts[i] = *chatMessagePart - } - - return chatMessageParts -} - -func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) (*thread.Thread, error) { - if t == nil { - return nil, nil - } - - chatCompletionRequest := openai.ChatCompletionRequest{ - Model: string(o.model), - Messages: threadToChatCompletionMessages(t), - MaxTokens: o.maxTokens, - Temperature: o.temperature, - N: DefaultOpenAINumResults, - TopP: DefaultOpenAITopP, - Stop: o.stop, - } - - if len(o.functions) > 0 { - chatCompletionRequest.Tools = o.getChatCompletionRequestTools() - chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() - } - - response, err := o.openAIClient.CreateChatCompletion( - ctx, - chatCompletionRequest, - ) - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrOpenAIChat, err) - } - - if o.usageCallback != nil { - o.setUsageMetadata(response.Usage) - } - - if len(response.Choices) == 0 { - return nil, fmt.Errorf("%w: no choices returned", ErrOpenAIChat) - } - - var messages []*thread.Message - if response.Choices[0].FinishReason == "tool_calls" { - messages = o.callTools(response) - } else { - if o.verbose { - //TODO - } - messages = []*thread.Message{ - textContentToThreadMessage(response.Choices[0].Message.Content), - } - } - - t.Messages = append(t.Messages, messages...) - - return t, nil -} - -func (o *OpenAI) getChatCompletionRequestTools() []openai.Tool { - return o.getFunctions() -} - -func (o *OpenAI) getChatCompletionRequestToolChoice() any { - if o.toolChoice != nil { - return openai.ToolChoice{ - Type: openai.ToolTypeFunction, - Function: openai.ToolFunction{ - Name: *o.toolChoice, - }, - } - } - - return "auto" -} - -func (o *OpenAI) callTool(toolCall openai.ToolCall) (string, error) { - fn, ok := o.functions[toolCall.Function.Name] - if !ok { - return "", fmt.Errorf("unknown function %s", toolCall.Function.Name) - } - - resultAsJSON, err := callFnWithArgumentAsJSON(fn.Fn, toolCall.Function.Arguments) - if err != nil { - return "", err - } - - o.calledFunctionName = &fn.Name - - return resultAsJSON, nil -} - -func (o *OpenAI) callTools(response openai.ChatCompletionResponse) []*thread.Message { - if len(o.functions) == 0 || len(response.Choices[0].Message.ToolCalls) == 0 { - return nil - } - - var messages []*thread.Message - for _, toolCall := range response.Choices[0].Message.ToolCalls { - if o.verbose { - fmt.Printf("Calling function %s\n", toolCall.Function.Name) - fmt.Printf("Function call arguments: %s\n", toolCall.Function.Arguments) - } - - result, err := o.callTool(toolCall) - if err != nil { - result = fmt.Sprintf("error: %s", err) - } - - messages = append(messages, toolCallResultToThreadMessage(toolCall, result)) - } - - return messages -} - -func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thread.Message { - return thread.NewToolMessage().AddContent( - thread.NewToolContent( - &thread.ToolData{ - ID: toolCall.ID, - Name: toolCall.Function.Name, - Result: result, - }, - ), - ) -} - -func textContentToThreadMessage(content string) *thread.Message { - return thread.NewAssistantMessage().AddContent( - thread.NewTextContent(content), - ) -} diff --git a/llm/openai/openai.go b/llm/openai/openai.go index 43e87e20..1cf40257 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -1,4 +1,3 @@ -// Package openai provides a wrapper around the OpenAI API. package openai import ( @@ -7,333 +6,106 @@ import ( "fmt" "io" "os" - "strings" - "github.com/henomis/lingoose/chat" - "github.com/henomis/lingoose/llm/cache" - "github.com/henomis/lingoose/types" - "github.com/mitchellh/mapstructure" - "github.com/sashabaranov/go-openai" + "github.com/henomis/lingoose/thread" + openai "github.com/sashabaranov/go-openai" ) -var ( - ErrOpenAICompletion = fmt.Errorf("openai completion error") - ErrOpenAIChat = fmt.Errorf("openai chat error") -) - -const ( - DefaultOpenAIMaxTokens = 256 - DefaultOpenAITemperature = 0.7 - DefaultOpenAINumResults = 1 - DefaultOpenAITopP = 1.0 - DefaultMaxIterations = 3 -) - -type Model string - const ( - GPT432K0613 Model = openai.GPT432K0613 - GPT432K0314 Model = openai.GPT432K0314 - GPT432K Model = openai.GPT432K - GPT40613 Model = openai.GPT40613 - GPT40314 Model = openai.GPT40314 - GPT4TurboPreview Model = openai.GPT4TurboPreview - GPT4VisionPreview Model = openai.GPT4VisionPreview - GPT4 Model = openai.GPT4 - GPT3Dot5Turbo1106 Model = openai.GPT3Dot5Turbo1106 - GPT3Dot5Turbo0613 Model = openai.GPT3Dot5Turbo0613 - GPT3Dot5Turbo0301 Model = openai.GPT3Dot5Turbo0301 - GPT3Dot5Turbo16K Model = openai.GPT3Dot5Turbo16K - GPT3Dot5Turbo16K0613 Model = openai.GPT3Dot5Turbo16K0613 - GPT3Dot5Turbo Model = openai.GPT3Dot5Turbo - GPT3Dot5TurboInstruct Model = openai.GPT3Dot5TurboInstruct - GPT3Davinci Model = openai.GPT3Davinci - GPT3Davinci002 Model = openai.GPT3Davinci002 - GPT3Curie Model = openai.GPT3Curie - GPT3Curie002 Model = openai.GPT3Curie002 - GPT3Ada Model = openai.GPT3Ada - GPT3Ada002 Model = openai.GPT3Ada002 - GPT3Babbage Model = openai.GPT3Babbage - GPT3Babbage002 Model = openai.GPT3Babbage002 + EOS = "\x00" ) -type UsageCallback func(types.Meta) -type StreamCallback func(string) - -type OpenAI struct { - openAIClient *openai.Client - model Model - temperature float32 - maxTokens int - stop []string - verbose bool - usageCallback UsageCallback - functions map[string]Function - functionsMaxIterations uint - toolChoice *string - calledFunctionName *string - finishReason string - cache *cache.Cache +var threadRoleToOpenAIRole = map[thread.Role]string{ + thread.RoleUser: "user", + thread.RoleAssistant: "assistant", + thread.RoleTool: "tool", } -func New(model Model, temperature float32, maxTokens int, verbose bool) *OpenAI { +func New() *OpenAI { openAIKey := os.Getenv("OPENAI_API_KEY") return &OpenAI{ openAIClient: openai.NewClient(openAIKey), - model: model, - temperature: temperature, - maxTokens: maxTokens, - verbose: verbose, + model: GPT3Dot5Turbo, + temperature: DefaultOpenAITemperature, + maxTokens: DefaultOpenAIMaxTokens, + verbose: false, functions: make(map[string]Function), functionsMaxIterations: DefaultMaxIterations, } } -// WithModel sets the model to use for the OpenAI instance. -func (o *OpenAI) WithModel(model Model) *OpenAI { - o.model = model - return o -} - -// WithTemperature sets the temperature to use for the OpenAI instance. -func (o *OpenAI) WithTemperature(temperature float32) *OpenAI { - o.temperature = temperature - return o -} - -// WithMaxTokens sets the max tokens to use for the OpenAI instance. -func (o *OpenAI) WithMaxTokens(maxTokens int) *OpenAI { - o.maxTokens = maxTokens - return o -} - -// WithUsageCallback sets the usage callback to use for the OpenAI instance. -func (o *OpenAI) WithCallback(callback UsageCallback) *OpenAI { - o.usageCallback = callback - return o -} - -// WithStop sets the stop sequences to use for the OpenAI instance. -func (o *OpenAI) WithStop(stop []string) *OpenAI { - o.stop = stop - return o -} - -// WithClient sets the client to use for the OpenAI instance. -func (o *OpenAI) WithClient(client *openai.Client) *OpenAI { - o.openAIClient = client - return o -} - -// WithVerbose sets the verbose flag to use for the OpenAI instance. -func (o *OpenAI) WithVerbose(verbose bool) *OpenAI { - o.verbose = verbose - return o -} - -// WithCache sets the cache to use for the OpenAI instance. -func (o *OpenAI) WithCompletionCache(cache *cache.Cache) *OpenAI { - o.cache = cache - return o -} - -func (o *OpenAI) WithToolChoice(toolChoice string) *OpenAI { - o.toolChoice = &toolChoice - return o -} - -// CalledFunctionName returns the name of the function that was called. -func (o *OpenAI) CalledFunctionName() *string { - return o.calledFunctionName -} - -// FinishReason returns the LLM finish reason. -func (o *OpenAI) FinishReason() string { - return o.finishReason -} - -func NewCompletion() *OpenAI { - return New( - GPT3Dot5TurboInstruct, - DefaultOpenAITemperature, - DefaultOpenAIMaxTokens, - false, - ) -} - -func NewChat() *OpenAI { - return New( - GPT3Dot5Turbo, - DefaultOpenAITemperature, - DefaultOpenAIMaxTokens, - false, - ) -} - -// Completion returns a single completion for the given prompt. -func (o *OpenAI) Completion(ctx context.Context, prompt string) (string, error) { - var cacheResult *cache.Result - var err error - - if o.cache != nil { - cacheResult, err = o.cache.Get(ctx, prompt) - if err == nil { - return strings.Join(cacheResult.Answer, "\n"), nil - } else if !errors.Is(err, cache.ErrCacheMiss) { - return "", fmt.Errorf("%w: %w", ErrOpenAICompletion, err) - } +func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn StreamCallback) error { + if t == nil { + return nil } - outputs, err := o.BatchCompletion(ctx, []string{prompt}) - if err != nil { - return "", err - } + chatCompletionRequest := o.buildChatCompletionRequest(t) - if o.cache != nil { - err = o.cache.Set(ctx, cacheResult.Embedding, outputs[0]) - if err != nil { - return "", fmt.Errorf("%w: %w", ErrOpenAICompletion, err) - } - } - - return outputs[0], nil -} - -// BatchCompletion returns multiple completions for the given prompts. -func (o *OpenAI) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { - response, err := o.openAIClient.CreateCompletion( - ctx, - openai.CompletionRequest{ - Model: string(o.model), - Prompt: prompts, - MaxTokens: o.maxTokens, - Temperature: o.temperature, - N: DefaultOpenAINumResults, - TopP: DefaultOpenAITopP, - Stop: o.stop, - }, - ) - - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrOpenAICompletion, err) - } - - if o.usageCallback != nil { - o.setUsageMetadata(response.Usage) - } - - if len(response.Choices) == 0 { - return nil, fmt.Errorf("%w: no choices returned", ErrOpenAICompletion) - } - - var outputs []string - for _, choice := range response.Choices { - index := choice.Index - outputs = append(outputs, strings.TrimSpace(choice.Text)) - if o.verbose { - debugCompletion(prompts[index], choice.Text) - } + if len(o.functions) > 0 { + chatCompletionRequest.Tools = o.getChatCompletionRequestTools() + chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() } - return outputs, nil -} - -// CompletionStream returns a single completion stream for the given prompt. -func (o *OpenAI) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { - return o.BatchCompletionStream(ctx, []StreamCallback{callbackFn}, []string{prompt}) -} - -// BatchCompletionStream returns multiple completion streams for the given prompts. -func (o *OpenAI) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { - stream, err := o.openAIClient.CreateCompletionStream( + stream, err := o.openAIClient.CreateChatCompletionStream( ctx, - openai.CompletionRequest{ - Model: string(o.model), - Prompt: prompts, - MaxTokens: o.maxTokens, - Temperature: o.temperature, - N: DefaultOpenAINumResults, - TopP: DefaultOpenAITopP, - Stop: o.stop, - }, + chatCompletionRequest, ) if err != nil { - return fmt.Errorf("%w: %w", ErrOpenAICompletion, err) + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) } - defer stream.Close() - + var messages []*thread.Message + var content string for { response, errRecv := stream.Recv() if errors.Is(errRecv, io.EOF) { - break - } + callbackFn(EOS) - if errRecv != nil { - return fmt.Errorf("%w: %w", ErrOpenAICompletion, errRecv) - } - - if o.usageCallback != nil { - o.setUsageMetadata(response.Usage) + if len(content) > 0 { + messages = append(messages, thread.NewAssistantMessage().AddContent( + thread.NewTextContent(content), + )) + } + break } if len(response.Choices) == 0 { - return fmt.Errorf("%w: no choices returned", ErrOpenAICompletion) + return fmt.Errorf("%w: no choices returned", ErrOpenAIChat) } - for _, choice := range response.Choices { - index := choice.Index - output := choice.Text - if o.verbose { - debugCompletion(prompts[index], output) - } - - callbackFn[index](output) + if response.Choices[0].FinishReason == "tool_calls" || len(response.Choices[0].Delta.ToolCalls) > 0 { + messages = append(messages, o.callTools(response.Choices[0].Delta.ToolCalls)...) + } else { + content += response.Choices[0].Delta.Content } + + callbackFn(response.Choices[0].Delta.Content) } + t.AddMessages(messages) + return nil } -// Chat returns a single chat completion for the given prompt. -func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { - messages, err := buildMessages(prompt) - if err != nil { - return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) +func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) error { + if t == nil { + return nil } - chatCompletionRequest := openai.ChatCompletionRequest{ - Model: string(o.model), - Messages: messages, - MaxTokens: o.maxTokens, - Temperature: o.temperature, - N: DefaultOpenAINumResults, - TopP: DefaultOpenAITopP, - Stop: o.stop, - } + chatCompletionRequest := o.buildChatCompletionRequest(t) if len(o.functions) > 0 { - chatCompletionRequest.Tools = o.getFunctions() - if o.toolChoice != nil { - chatCompletionRequest.ToolChoice = openai.ToolChoice{ - Type: openai.ToolTypeFunction, - Function: openai.ToolFunction{ - Name: *o.toolChoice, - }, - } - } else { - chatCompletionRequest.ToolChoice = "auto" - } + chatCompletionRequest.Tools = o.getChatCompletionRequestTools() + chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() } response, err := o.openAIClient.CreateChatCompletion( ctx, chatCompletionRequest, ) - if err != nil { - return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) } if o.usageCallback != nil { @@ -341,159 +113,105 @@ func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { } if len(response.Choices) == 0 { - return "", fmt.Errorf("%w: no choices returned", ErrOpenAIChat) + return fmt.Errorf("%w: no choices returned", ErrOpenAIChat) } - content := response.Choices[0].Message.Content - - o.finishReason = string(response.Choices[0].FinishReason) - o.calledFunctionName = nil - if len(response.Choices[0].Message.ToolCalls) > 0 && len(o.functions) > 0 { - if o.verbose { - fmt.Printf("Calling function %s\n", response.Choices[0].Message.ToolCalls[0].Function.Name) - fmt.Printf("Function call arguments: %s\n", response.Choices[0].Message.ToolCalls[0].Function.Arguments) - } - - content, err = o.functionCall(response) - if err != nil { - return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) + var messages []*thread.Message + if response.Choices[0].FinishReason == "tool_calls" || len(response.Choices[0].Message.ToolCalls) > 0 { + messages = append(messages, toolCallsToToolCallMessage(response.Choices[0].Message.ToolCalls)) + messages = append(messages, o.callTools(response.Choices[0].Message.ToolCalls)...) + } else { + messages = []*thread.Message{ + textContentToThreadMessage(response.Choices[0].Message.Content), } } - if o.verbose { - debugChat(prompt, content) - } + t.Messages = append(t.Messages, messages...) - return content, nil + return nil } -// ChatStream returns a single chat stream for the given prompt. -func (o *OpenAI) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { - messages, err := buildMessages(prompt) - if err != nil { - return fmt.Errorf("%w: %w", ErrOpenAIChat, err) - } - - stream, err := o.openAIClient.CreateChatCompletionStream( - ctx, - openai.ChatCompletionRequest{ - Model: string(o.model), - Messages: messages, - MaxTokens: o.maxTokens, - Temperature: o.temperature, - N: DefaultOpenAINumResults, - TopP: DefaultOpenAITopP, - Stop: o.stop, - }, - ) - if err != nil { - return fmt.Errorf("%w: %w", ErrOpenAIChat, err) +func (o *OpenAI) buildChatCompletionRequest(t *thread.Thread) openai.ChatCompletionRequest { + return openai.ChatCompletionRequest{ + Model: string(o.model), + Messages: threadToChatCompletionMessages(t), + MaxTokens: o.maxTokens, + Temperature: o.temperature, + N: DefaultOpenAINumResults, + TopP: DefaultOpenAITopP, + Stop: o.stop, } +} - for { - response, errRecv := stream.Recv() - if errors.Is(errRecv, io.EOF) { - break - } - - // oops no usage here? - // if o.usageCallback != nil { - // o.setUsageMetadata(response.Usage) - // } - - if len(response.Choices) == 0 { - return fmt.Errorf("%w: no choices returned", ErrOpenAIChat) - } - - content := response.Choices[0].Delta.Content +func (o *OpenAI) getChatCompletionRequestTools() []openai.Tool { + tools := []openai.Tool{} - if o.verbose { - debugChat(prompt, content) - } - - callbackFn(content) + for _, function := range o.functions { + tools = append(tools, openai.Tool{ + Type: "function", + Function: openai.FunctionDefinition{ + Name: function.Name, + Description: function.Description, + Parameters: function.Parameters, + }, + }) } - return nil -} - -// SetStop sets the stop sequences for the completion. -func (o *OpenAI) SetStop(stop []string) { - o.stop = stop + return tools } -func (o *OpenAI) setUsageMetadata(usage openai.Usage) { - callbackMetadata := make(types.Meta) +func (o *OpenAI) getChatCompletionRequestToolChoice() any { + if o.toolChoice == nil { + return "none" + } - err := mapstructure.Decode(usage, &callbackMetadata) - if err != nil { - return + if *o.toolChoice == "auto" { + return "auto" } - o.usageCallback(callbackMetadata) + return openai.ToolChoice{ + Type: openai.ToolTypeFunction, + Function: openai.ToolFunction{ + Name: *o.toolChoice, + }, + } } -func buildMessages(prompt *chat.Chat) ([]openai.ChatCompletionMessage, error) { - var messages []openai.ChatCompletionMessage +func (o *OpenAI) callTool(toolCall openai.ToolCall) (string, error) { + fn, ok := o.functions[toolCall.Function.Name] + if !ok { + return "", fmt.Errorf("unknown function %s", toolCall.Function.Name) + } - promptMessages, err := prompt.ToMessages() + resultAsJSON, err := callFnWithArgumentAsJSON(fn.Fn, toolCall.Function.Arguments) if err != nil { - return nil, err + return "", err } - for _, message := range promptMessages { - if message.Type == chat.MessageTypeUser { - messages = append(messages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleUser, - Content: message.Content, - }) - } else if message.Type == chat.MessageTypeAssistant { - messages = append(messages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleAssistant, - Content: message.Content, - }) - } else if message.Type == chat.MessageTypeSystem { - messages = append(messages, openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleSystem, - Content: message.Content, - }) - } else if message.Type == chat.MessageTypeFunction { - fnmessage := openai.ChatCompletionMessage{ - Role: openai.ChatMessageRoleFunction, - Content: message.Content, - } - if message.Name != nil { - fnmessage.Name = *message.Name - } + o.calledFunctionName = &fn.Name - messages = append(messages, fnmessage) - } - } - - return messages, nil + return resultAsJSON, nil } -func debugChat(prompt *chat.Chat, content string) { - promptMessages, err := prompt.ToMessages() - if err != nil { - return +func (o *OpenAI) callTools(toolCalls []openai.ToolCall) []*thread.Message { + if len(o.functions) == 0 || len(toolCalls) == 0 { + return nil } - for _, message := range promptMessages { - if message.Type == chat.MessageTypeUser { - fmt.Printf("---USER---\n%s\n", message.Content) - } else if message.Type == chat.MessageTypeAssistant { - fmt.Printf("---AI---\n%s\n", message.Content) - } else if message.Type == chat.MessageTypeSystem { - fmt.Printf("---SYSTEM---\n%s\n", message.Content) - } else if message.Type == chat.MessageTypeFunction { - fmt.Printf("---FUNCTION---\n%s()\n%s\n", *message.Name, message.Content) + var messages []*thread.Message + for _, toolCall := range toolCalls { + if o.verbose { + fmt.Printf("Calling function %s\n", toolCall.Function.Name) + fmt.Printf("Function call arguments: %s\n", toolCall.Function.Arguments) } + + result, err := o.callTool(toolCall) + if err != nil { + result = fmt.Sprintf("error: %s", err) + } + + messages = append(messages, toolCallResultToThreadMessage(toolCall, result)) } - fmt.Printf("---AI---\n%s\n", content) -} -func debugCompletion(prompt string, content string) { - fmt.Printf("---USER---\n%s\n", prompt) - fmt.Printf("---AI---\n%s\n", content) + return messages } diff --git a/thread/thread.go b/thread/thread.go index 5accce32..356b0280 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -7,11 +7,10 @@ type Thread struct { type ContentType string const ( - ContentTypeText ContentType = "text" - ContentTypeImage ContentType = "image" - // ContentTypeVideo ContentType = "video" - // ContentTypeAudio ContentType = "audio" - ContentTypeTool ContentType = "tool" + ContentTypeText ContentType = "text" + ContentTypeImage ContentType = "image" + ContentTypeToolCall ContentType = "tool_call" + ContentTypeToolResponse ContentType = "tool_response" ) type Content struct { @@ -32,12 +31,22 @@ type Message struct { Contents []*Content } -type ToolData struct { +type ToolResponseData struct { ID string Name string Result string } +type ToolCallData struct { + ID string + Function ToolCallFunction +} + +type ToolCallFunction struct { + Name string + Arguments string +} + type MediaData struct { Raw any URL *string @@ -57,24 +66,17 @@ func NewImageContent(mediaData *MediaData) *Content { } } -// func NewVideoContent(mediaData *MediaData) *Content { -// return &Content{ -// Type: ContentTypeVideo, -// Data: mediaData, -// } -// } - -// func NewAudioContent(mediaData *MediaData) *Content { -// return &Content{ -// Type: ContentTypeAudio, -// Data: mediaData, -// } -// } +func NewToolResponseContent(toolResponseData *ToolResponseData) *Content { + return &Content{ + Type: ContentTypeToolResponse, + Data: toolResponseData, + } +} -func NewToolContent(toolData *ToolData) *Content { +func NewToolCallContent(data []*ToolCallData) *Content { return &Content{ - Type: ContentTypeTool, - Data: toolData, + Type: ContentTypeToolCall, + Data: data, } } @@ -106,6 +108,15 @@ func (t *Thread) AddMessage(message *Message) *Thread { return t } +func (t *Thread) AddMessages(messages []*Message) *Thread { + t.Messages = append(t.Messages, messages...) + return t +} + +func (t *Thread) CountMessages() int { + return len(t.Messages) +} + func NewThread() *Thread { return &Thread{} } @@ -121,9 +132,16 @@ func (t *Thread) String() string { str += "\tText: " + content.Data.(string) + "\n" case ContentTypeImage: str += "\tImage URL: " + *content.Data.(*MediaData).URL + "\n" - case ContentTypeTool: - str += "\tTool Name: " + content.Data.(*ToolData).Name + "\n" - str += "\tTool Result: " + content.Data.(*ToolData).Result + "\n" + case ContentTypeToolCall: + for _, toolCallData := range content.Data.([]*ToolCallData) { + str += "\tTool Call ID: " + toolCallData.ID + "\n" + str += "\tTool Call Function Name: " + toolCallData.Function.Name + "\n" + str += "\tTool Call Function Arguments: " + toolCallData.Function.Arguments + "\n" + } + case ContentTypeToolResponse: + str += "\tTool ID: " + content.Data.(*ToolResponseData).ID + "\n" + str += "\tTool Name: " + content.Data.(*ToolResponseData).Name + "\n" + str += "\tTool Result: " + content.Data.(*ToolResponseData).Result + "\n" } } } From 68af5416c9389e8b3f5f00a92f6b2bd360bacf71 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 09:52:58 +0100 Subject: [PATCH 06/65] fix --- llm/openai/common.go | 17 +++++++++++++++++ llm/openai/legacy.go | 18 ------------------ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/llm/openai/common.go b/llm/openai/common.go index d0ad5b33..b058af64 100644 --- a/llm/openai/common.go +++ b/llm/openai/common.go @@ -5,6 +5,7 @@ import ( "github.com/henomis/lingoose/llm/cache" "github.com/henomis/lingoose/types" + "github.com/mitchellh/mapstructure" "github.com/sashabaranov/go-openai" ) @@ -120,3 +121,19 @@ func (o *OpenAI) WithToolChoice(toolChoice *string) *OpenAI { o.toolChoice = toolChoice return o } + +// SetStop sets the stop sequences for the completion. +func (o *OpenAI) SetStop(stop []string) { + o.stop = stop +} + +func (o *OpenAI) setUsageMetadata(usage openai.Usage) { + callbackMetadata := make(types.Meta) + + err := mapstructure.Decode(usage, &callbackMetadata) + if err != nil { + return + } + + o.usageCallback(callbackMetadata) +} diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go index 1e6630a0..3c634bdc 100644 --- a/llm/openai/legacy.go +++ b/llm/openai/legacy.go @@ -11,8 +11,6 @@ import ( "github.com/henomis/lingoose/chat" "github.com/henomis/lingoose/llm/cache" - "github.com/henomis/lingoose/types" - "github.com/mitchellh/mapstructure" "github.com/sashabaranov/go-openai" ) @@ -304,22 +302,6 @@ func (o *OpenAI) ChatStream(ctx context.Context, callbackFn StreamCallback, prom return nil } -// SetStop sets the stop sequences for the completion. -func (o *OpenAI) SetStop(stop []string) { - o.stop = stop -} - -func (o *OpenAI) setUsageMetadata(usage openai.Usage) { - callbackMetadata := make(types.Meta) - - err := mapstructure.Decode(usage, &callbackMetadata) - if err != nil { - return - } - - o.usageCallback(callbackMetadata) -} - func buildMessages(prompt *chat.Chat) ([]openai.ChatCompletionMessage, error) { var messages []openai.ChatCompletionMessage From e2149f5d8a9869782705d1598099aec217630a43 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 09:58:20 +0100 Subject: [PATCH 07/65] revert legacy code --- llm/openai/function.go | 29 ++++++++++------------------- llm/openai/legacy.go | 18 ++++-------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/llm/openai/function.go b/llm/openai/function.go index 2f1d6c71..608fd573 100644 --- a/llm/openai/function.go +++ b/llm/openai/function.go @@ -49,21 +49,18 @@ func (o *OpenAI) BindFunction( return nil } -func (o *OpenAI) getFunctions() []openai.Tool { - tools := []openai.Tool{} +func (o *OpenAI) getFunctions() []openai.FunctionDefinition { + functions := []openai.FunctionDefinition{} for _, function := range o.functions { - tools = append(tools, openai.Tool{ - Type: "function", - Function: openai.FunctionDefinition{ - Name: function.Name, - Description: function.Description, - Parameters: function.Parameters, - }, + functions = append(functions, openai.FunctionDefinition{ + Name: function.Name, + Description: function.Description, + Parameters: function.Parameters, }) } - return tools + return functions } func extractFunctionParameter(f interface{}) (map[string]interface{}, error) { @@ -173,21 +170,15 @@ func callFnWithArgumentAsJSON(fn interface{}, argumentAsJSON string) (string, er } func (o *OpenAI) functionCall(response openai.ChatCompletionResponse) (string, error) { - fn, ok := o.functions[response.Choices[0].Message.ToolCalls[0].Function.Name] + fn, ok := o.functions[response.Choices[0].Message.FunctionCall.Name] if !ok { - return "", fmt.Errorf( - "%w: unknown function %s", - ErrOpenAIChat, - response.Choices[0].Message.ToolCalls[0].Function.Name, - ) + return "", fmt.Errorf("%w: unknown function %s", ErrOpenAIChat, response.Choices[0].Message.FunctionCall.Name) } - resultAsJSON, err := callFnWithArgumentAsJSON(fn.Fn, response.Choices[0].Message.ToolCalls[0].Function.Arguments) + resultAsJSON, err := callFnWithArgumentAsJSON(fn.Fn, response.Choices[0].Message.FunctionCall.Arguments) if err != nil { return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) } - o.calledFunctionName = &fn.Name - return resultAsJSON, nil } diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go index 3c634bdc..98ab56de 100644 --- a/llm/openai/legacy.go +++ b/llm/openai/legacy.go @@ -199,17 +199,7 @@ func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { } if len(o.functions) > 0 { - chatCompletionRequest.Tools = o.getFunctions() - if o.toolChoice != nil { - chatCompletionRequest.ToolChoice = openai.ToolChoice{ - Type: openai.ToolTypeFunction, - Function: openai.ToolFunction{ - Name: *o.toolChoice, - }, - } - } else { - chatCompletionRequest.ToolChoice = "auto" - } + chatCompletionRequest.Functions = o.getFunctions() } response, err := o.openAIClient.CreateChatCompletion( @@ -233,10 +223,10 @@ func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { o.finishReason = string(response.Choices[0].FinishReason) o.calledFunctionName = nil - if len(response.Choices[0].Message.ToolCalls) > 0 && len(o.functions) > 0 { + if response.Choices[0].FinishReason == "function_call" && len(o.functions) > 0 { if o.verbose { - fmt.Printf("Calling function %s\n", response.Choices[0].Message.ToolCalls[0].Function.Name) - fmt.Printf("Function call arguments: %s\n", response.Choices[0].Message.ToolCalls[0].Function.Arguments) + fmt.Printf("Calling function %s\n", response.Choices[0].Message.FunctionCall.Name) + fmt.Printf("Function call arguments: %s\n", response.Choices[0].Message.FunctionCall.Arguments) } content, err = o.functionCall(response) From 51c54a575741a3c063c5d8ee92db67a3a8507718 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 10:06:06 +0100 Subject: [PATCH 08/65] linting --- examples/chat/functions/main.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/examples/chat/functions/main.go b/examples/chat/functions/main.go index a0124429..92a9b02c 100644 --- a/examples/chat/functions/main.go +++ b/examples/chat/functions/main.go @@ -5,7 +5,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "os" @@ -33,22 +33,34 @@ func main() { }, ) - llmOpenAI := openai.NewLegacy(openai.GPT3Dot5Turbo0613, openai.DefaultOpenAITemperature, openai.DefaultOpenAIMaxTokens, true). + llmOpenAI := openai.NewLegacy( + openai.GPT3Dot5Turbo0613, + openai.DefaultOpenAITemperature, + openai.DefaultOpenAIMaxTokens, + true, + ). WithCallback(func(response types.Meta) { for k, v := range response { if k == "CompletionTokens" { - outputToken += v.(int) + if t, ok := v.(int); ok { + outputToken += t + } } else if k == "PromptTokens" { - inputToken += v.(int) + if t, ok := v.(int); ok { + inputToken += t + } } } }) - llmOpenAI.BindFunction( + err := llmOpenAI.BindFunction( GetNationalitiesForName, "GetNationalitiesForName", "Use this function to get the nationalities for a given name.", ) + if err != nil { + panic(err) + } response, err := llmOpenAI.Chat(context.Background(), llmChat) if err != nil { @@ -80,7 +92,6 @@ func main() { inputPrice := float64(inputToken) / 1000 * 0.0015 outputPrice := float64(outputToken) / 1000 * 0.002 fmt.Printf("You spent $%f\n", inputPrice+outputPrice) - } type Query struct { @@ -99,13 +110,14 @@ type NationalizeResponse struct { func GetNationalitiesForName(query Query) ([]Country, error) { url := fmt.Sprintf("https://api.nationalize.io/?name=%s", query.Name) + //nolint:gosec resp, err := http.Get(url) if err != nil { return nil, err } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } From f0994f3e42d6df8d2b926bbca9590eaf051eb4be Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 10:16:04 +0100 Subject: [PATCH 09/65] refactor: move legacy code --- llm/openai/common.go | 87 ---------------------------------- llm/openai/function.go | 44 +++++++++++++---- llm/openai/legacy.go | 105 ++++++++++++++++++++++++++++++++++++----- llm/openai/openai.go | 89 ++++++++++++++++++++++++++++------ 4 files changed, 204 insertions(+), 121 deletions(-) diff --git a/llm/openai/common.go b/llm/openai/common.go index b058af64..ce37ee04 100644 --- a/llm/openai/common.go +++ b/llm/openai/common.go @@ -3,9 +3,7 @@ package openai import ( "fmt" - "github.com/henomis/lingoose/llm/cache" "github.com/henomis/lingoose/types" - "github.com/mitchellh/mapstructure" "github.com/sashabaranov/go-openai" ) @@ -52,88 +50,3 @@ const ( type UsageCallback func(types.Meta) type StreamCallback func(string) - -type OpenAI struct { - openAIClient *openai.Client - model Model - temperature float32 - maxTokens int - stop []string - verbose bool - usageCallback UsageCallback - functions map[string]Function - functionsMaxIterations uint - toolChoice *string - calledFunctionName *string - finishReason string - cache *cache.Cache -} - -// WithModel sets the model to use for the OpenAI instance. -func (o *OpenAI) WithModel(model Model) *OpenAI { - o.model = model - return o -} - -// WithTemperature sets the temperature to use for the OpenAI instance. -func (o *OpenAI) WithTemperature(temperature float32) *OpenAI { - o.temperature = temperature - return o -} - -// WithMaxTokens sets the max tokens to use for the OpenAI instance. -func (o *OpenAI) WithMaxTokens(maxTokens int) *OpenAI { - o.maxTokens = maxTokens - return o -} - -// WithUsageCallback sets the usage callback to use for the OpenAI instance. -func (o *OpenAI) WithCallback(callback UsageCallback) *OpenAI { - o.usageCallback = callback - return o -} - -// WithStop sets the stop sequences to use for the OpenAI instance. -func (o *OpenAI) WithStop(stop []string) *OpenAI { - o.stop = stop - return o -} - -// WithClient sets the client to use for the OpenAI instance. -func (o *OpenAI) WithClient(client *openai.Client) *OpenAI { - o.openAIClient = client - return o -} - -// WithVerbose sets the verbose flag to use for the OpenAI instance. -func (o *OpenAI) WithVerbose(verbose bool) *OpenAI { - o.verbose = verbose - return o -} - -// WithCache sets the cache to use for the OpenAI instance. -func (o *OpenAI) WithCompletionCache(cache *cache.Cache) *OpenAI { - o.cache = cache - return o -} - -func (o *OpenAI) WithToolChoice(toolChoice *string) *OpenAI { - o.toolChoice = toolChoice - return o -} - -// SetStop sets the stop sequences for the completion. -func (o *OpenAI) SetStop(stop []string) { - o.stop = stop -} - -func (o *OpenAI) setUsageMetadata(usage openai.Usage) { - callbackMetadata := make(types.Meta) - - err := mapstructure.Decode(usage, &callbackMetadata) - if err != nil { - return - } - - o.usageCallback(callbackMetadata) -} diff --git a/llm/openai/function.go b/llm/openai/function.go index 608fd573..d6dc0158 100644 --- a/llm/openai/function.go +++ b/llm/openai/function.go @@ -19,37 +19,65 @@ type Function struct { type FunctionParameterOption func(map[string]interface{}) error -func (o *OpenAI) BindFunction( +func bindFunction( fn interface{}, name string, description string, functionParamenterOptions ...FunctionParameterOption, -) error { +) (*Function, error) { parameter, err := extractFunctionParameter(fn) if err != nil { - return err + return nil, err } for _, option := range functionParamenterOptions { err = option(parameter) if err != nil { - return err + return nil, err } } - function := Function{ + return &Function{ Name: name, Description: description, Parameters: parameter, Fn: fn, + }, nil +} + +func (o *OpenAILegacy) BindFunction( + fn interface{}, + name string, + description string, + functionParamenterOptions ...FunctionParameterOption, +) error { + function, err := bindFunction(fn, name, description, functionParamenterOptions...) + if err != nil { + return err + } + + o.functions[name] = *function + + return nil +} + +func (o *OpenAI) BindFunction( + fn interface{}, + name string, + description string, + functionParamenterOptions ...FunctionParameterOption, +) error { + function, err := bindFunction(fn, name, description, functionParamenterOptions...) + if err != nil { + return err } - o.functions[name] = function + o.functions[name] = *function return nil } -func (o *OpenAI) getFunctions() []openai.FunctionDefinition { +func (o *OpenAILegacy) getFunctions() []openai.FunctionDefinition { functions := []openai.FunctionDefinition{} for _, function := range o.functions { @@ -169,7 +197,7 @@ func callFnWithArgumentAsJSON(fn interface{}, argumentAsJSON string) (string, er return "", nil } -func (o *OpenAI) functionCall(response openai.ChatCompletionResponse) (string, error) { +func (o *OpenAILegacy) functionCall(response openai.ChatCompletionResponse) (string, error) { fn, ok := o.functions[response.Choices[0].Message.FunctionCall.Name] if !ok { return "", fmt.Errorf("%w: unknown function %s", ErrOpenAIChat, response.Choices[0].Message.FunctionCall.Name) diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go index 98ab56de..4fcd8f1e 100644 --- a/llm/openai/legacy.go +++ b/llm/openai/legacy.go @@ -11,13 +11,94 @@ import ( "github.com/henomis/lingoose/chat" "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/types" + "github.com/mitchellh/mapstructure" "github.com/sashabaranov/go-openai" ) -func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *OpenAI { +type OpenAILegacy struct { + openAIClient *openai.Client + model Model + temperature float32 + maxTokens int + stop []string + verbose bool + usageCallback UsageCallback + functions map[string]Function + functionsMaxIterations uint + calledFunctionName *string + finishReason string + cache *cache.Cache +} + +// WithModel sets the model to use for the OpenAI instance. +func (o *OpenAILegacy) WithModel(model Model) *OpenAILegacy { + o.model = model + return o +} + +// WithTemperature sets the temperature to use for the OpenAI instance. +func (o *OpenAILegacy) WithTemperature(temperature float32) *OpenAILegacy { + o.temperature = temperature + return o +} + +// WithMaxTokens sets the max tokens to use for the OpenAI instance. +func (o *OpenAILegacy) WithMaxTokens(maxTokens int) *OpenAILegacy { + o.maxTokens = maxTokens + return o +} + +// WithUsageCallback sets the usage callback to use for the OpenAI instance. +func (o *OpenAILegacy) WithCallback(callback UsageCallback) *OpenAILegacy { + o.usageCallback = callback + return o +} + +// WithStop sets the stop sequences to use for the OpenAI instance. +func (o *OpenAILegacy) WithStop(stop []string) *OpenAILegacy { + o.stop = stop + return o +} + +// WithClient sets the client to use for the OpenAI instance. +func (o *OpenAILegacy) WithClient(client *openai.Client) *OpenAILegacy { + o.openAIClient = client + return o +} + +// WithVerbose sets the verbose flag to use for the OpenAI instance. +func (o *OpenAILegacy) WithVerbose(verbose bool) *OpenAILegacy { + o.verbose = verbose + return o +} + +// WithCache sets the cache to use for the OpenAI instance. +func (o *OpenAILegacy) WithCompletionCache(cache *cache.Cache) *OpenAILegacy { + o.cache = cache + return o +} + +// SetStop sets the stop sequences for the completion. +func (o *OpenAILegacy) SetStop(stop []string) { + o.stop = stop +} + +func (o *OpenAILegacy) setUsageMetadata(usage openai.Usage) { + callbackMetadata := make(types.Meta) + + err := mapstructure.Decode(usage, &callbackMetadata) + if err != nil { + return + } + + o.usageCallback(callbackMetadata) +} + +func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *OpenAILegacy { openAIKey := os.Getenv("OPENAI_API_KEY") - return &OpenAI{ + return &OpenAILegacy{ openAIClient: openai.NewClient(openAIKey), model: model, temperature: temperature, @@ -29,16 +110,16 @@ func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *O } // CalledFunctionName returns the name of the function that was called. -func (o *OpenAI) CalledFunctionName() *string { +func (o *OpenAILegacy) CalledFunctionName() *string { return o.calledFunctionName } // FinishReason returns the LLM finish reason. -func (o *OpenAI) FinishReason() string { +func (o *OpenAILegacy) FinishReason() string { return o.finishReason } -func NewCompletion() *OpenAI { +func NewCompletion() *OpenAILegacy { return NewLegacy( GPT3Dot5TurboInstruct, DefaultOpenAITemperature, @@ -47,7 +128,7 @@ func NewCompletion() *OpenAI { ) } -func NewChat() *OpenAI { +func NewChat() *OpenAILegacy { return NewLegacy( GPT3Dot5Turbo, DefaultOpenAITemperature, @@ -57,7 +138,7 @@ func NewChat() *OpenAI { } // Completion returns a single completion for the given prompt. -func (o *OpenAI) Completion(ctx context.Context, prompt string) (string, error) { +func (o *OpenAILegacy) Completion(ctx context.Context, prompt string) (string, error) { var cacheResult *cache.Result var err error @@ -86,7 +167,7 @@ func (o *OpenAI) Completion(ctx context.Context, prompt string) (string, error) } // BatchCompletion returns multiple completions for the given prompts. -func (o *OpenAI) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { +func (o *OpenAILegacy) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { response, err := o.openAIClient.CreateCompletion( ctx, openai.CompletionRequest{ @@ -125,12 +206,12 @@ func (o *OpenAI) BatchCompletion(ctx context.Context, prompts []string) ([]strin } // CompletionStream returns a single completion stream for the given prompt. -func (o *OpenAI) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { +func (o *OpenAILegacy) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { return o.BatchCompletionStream(ctx, []StreamCallback{callbackFn}, []string{prompt}) } // BatchCompletionStream returns multiple completion streams for the given prompts. -func (o *OpenAI) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { +func (o *OpenAILegacy) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { stream, err := o.openAIClient.CreateCompletionStream( ctx, openai.CompletionRequest{ @@ -182,7 +263,7 @@ func (o *OpenAI) BatchCompletionStream(ctx context.Context, callbackFn []StreamC } // Chat returns a single chat completion for the given prompt. -func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { +func (o *OpenAILegacy) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { messages, err := buildMessages(prompt) if err != nil { return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) @@ -243,7 +324,7 @@ func (o *OpenAI) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { } // ChatStream returns a single chat stream for the given prompt. -func (o *OpenAI) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { +func (o *OpenAILegacy) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { messages, err := buildMessages(prompt) if err != nil { return fmt.Errorf("%w: %w", ErrOpenAIChat, err) diff --git a/llm/openai/openai.go b/llm/openai/openai.go index 1cf40257..fec3c1de 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -8,6 +8,8 @@ import ( "os" "github.com/henomis/lingoose/thread" + "github.com/henomis/lingoose/types" + "github.com/mitchellh/mapstructure" openai "github.com/sashabaranov/go-openai" ) @@ -21,17 +23,83 @@ var threadRoleToOpenAIRole = map[thread.Role]string{ thread.RoleTool: "tool", } +type OpenAI struct { + openAIClient *openai.Client + model Model + temperature float32 + maxTokens int + stop []string + usageCallback UsageCallback + functions map[string]Function + toolChoice *string +} + +// WithModel sets the model to use for the OpenAI instance. +func (o *OpenAI) WithModel(model Model) *OpenAI { + o.model = model + return o +} + +// WithTemperature sets the temperature to use for the OpenAI instance. +func (o *OpenAI) WithTemperature(temperature float32) *OpenAI { + o.temperature = temperature + return o +} + +// WithMaxTokens sets the max tokens to use for the OpenAI instance. +func (o *OpenAI) WithMaxTokens(maxTokens int) *OpenAI { + o.maxTokens = maxTokens + return o +} + +// WithUsageCallback sets the usage callback to use for the OpenAI instance. +func (o *OpenAI) WithCallback(callback UsageCallback) *OpenAI { + o.usageCallback = callback + return o +} + +// WithStop sets the stop sequences to use for the OpenAI instance. +func (o *OpenAI) WithStop(stop []string) *OpenAI { + o.stop = stop + return o +} + +// WithClient sets the client to use for the OpenAI instance. +func (o *OpenAI) WithClient(client *openai.Client) *OpenAI { + o.openAIClient = client + return o +} + +func (o *OpenAI) WithToolChoice(toolChoice *string) *OpenAI { + o.toolChoice = toolChoice + return o +} + +// SetStop sets the stop sequences for the completion. +func (o *OpenAI) SetStop(stop []string) { + o.stop = stop +} + +func (o *OpenAI) setUsageMetadata(usage openai.Usage) { + callbackMetadata := make(types.Meta) + + err := mapstructure.Decode(usage, &callbackMetadata) + if err != nil { + return + } + + o.usageCallback(callbackMetadata) +} + func New() *OpenAI { openAIKey := os.Getenv("OPENAI_API_KEY") return &OpenAI{ - openAIClient: openai.NewClient(openAIKey), - model: GPT3Dot5Turbo, - temperature: DefaultOpenAITemperature, - maxTokens: DefaultOpenAIMaxTokens, - verbose: false, - functions: make(map[string]Function), - functionsMaxIterations: DefaultMaxIterations, + openAIClient: openai.NewClient(openAIKey), + model: GPT3Dot5Turbo, + temperature: DefaultOpenAITemperature, + maxTokens: DefaultOpenAIMaxTokens, + functions: make(map[string]Function), } } @@ -188,8 +256,6 @@ func (o *OpenAI) callTool(toolCall openai.ToolCall) (string, error) { return "", err } - o.calledFunctionName = &fn.Name - return resultAsJSON, nil } @@ -200,11 +266,6 @@ func (o *OpenAI) callTools(toolCalls []openai.ToolCall) []*thread.Message { var messages []*thread.Message for _, toolCall := range toolCalls { - if o.verbose { - fmt.Printf("Calling function %s\n", toolCall.Function.Name) - fmt.Printf("Function call arguments: %s\n", toolCall.Function.Arguments) - } - result, err := o.callTool(toolCall) if err != nil { result = fmt.Sprintf("error: %s", err) From 5d52a23611588c3867eb4c2e4689292678cfde00 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 10:24:11 +0100 Subject: [PATCH 10/65] fix --- examples/llm/openai/thread/main.go | 12 ++++-- llm/openai/openai.go | 64 +++++++++++++++++++----------- 2 files changed, 48 insertions(+), 28 deletions(-) diff --git a/examples/llm/openai/thread/main.go b/examples/llm/openai/thread/main.go index 878b04af..e26f151e 100644 --- a/examples/llm/openai/thread/main.go +++ b/examples/llm/openai/thread/main.go @@ -23,11 +23,14 @@ func newStr(str string) *string { func main() { openaillm := openai.New() openaillm.WithToolChoice(newStr("getPirateAnswer")) - openaillm.BindFunction( + err := openaillm.BindFunction( getAnswer, "getPirateAnswer", "use this function to get the pirate answer", ) + if err != nil { + panic(err) + } t := thread.NewThread().AddMessage( thread.NewUserMessage().AddContent( @@ -43,7 +46,7 @@ func main() { fmt.Println(t) - err := openaillm.Generate(context.Background(), t) + err = openaillm.Generate(context.Background(), t) if err != nil { panic(err) } @@ -55,14 +58,15 @@ func main() { fmt.Println(t) // disable functions openaillm.WithToolChoice(nil) - - err = openaillm.Stream(context.Background(), t, func(a string) { + openaillm.WithStream(true, func(a string) { if a == openai.EOS { fmt.Printf("\n") return } fmt.Printf("%s", a) }) + + err = openaillm.Generate(context.Background(), t) if err != nil { panic(err) } diff --git a/llm/openai/openai.go b/llm/openai/openai.go index fec3c1de..ab6b4122 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -24,14 +24,15 @@ var threadRoleToOpenAIRole = map[thread.Role]string{ } type OpenAI struct { - openAIClient *openai.Client - model Model - temperature float32 - maxTokens int - stop []string - usageCallback UsageCallback - functions map[string]Function - toolChoice *string + openAIClient *openai.Client + model Model + temperature float32 + maxTokens int + stop []string + usageCallback UsageCallback + functions map[string]Function + streamCallbackFn StreamCallback + toolChoice *string } // WithModel sets the model to use for the OpenAI instance. @@ -53,7 +54,7 @@ func (o *OpenAI) WithMaxTokens(maxTokens int) *OpenAI { } // WithUsageCallback sets the usage callback to use for the OpenAI instance. -func (o *OpenAI) WithCallback(callback UsageCallback) *OpenAI { +func (o *OpenAI) WithUsageCallback(callback UsageCallback) *OpenAI { o.usageCallback = callback return o } @@ -75,6 +76,16 @@ func (o *OpenAI) WithToolChoice(toolChoice *string) *OpenAI { return o } +func (o *OpenAI) WithStream(enable bool, callbackFn StreamCallback) *OpenAI { + if !enable { + o.streamCallbackFn = nil + } else { + o.streamCallbackFn = callbackFn + } + + return o +} + // SetStop sets the stop sequences for the completion. func (o *OpenAI) SetStop(stop []string) { o.stop = stop @@ -103,7 +114,7 @@ func New() *OpenAI { } } -func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn StreamCallback) error { +func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) error { if t == nil { return nil } @@ -115,6 +126,18 @@ func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn Stream chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() } + if o.streamCallbackFn != nil { + return o.stream(ctx, t, chatCompletionRequest) + } + + return o.generate(ctx, t, chatCompletionRequest) +} + +func (o *OpenAI) stream( + ctx context.Context, + t *thread.Thread, + chatCompletionRequest openai.ChatCompletionRequest, +) error { stream, err := o.openAIClient.CreateChatCompletionStream( ctx, chatCompletionRequest, @@ -128,7 +151,7 @@ func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn Stream for { response, errRecv := stream.Recv() if errors.Is(errRecv, io.EOF) { - callbackFn(EOS) + o.streamCallbackFn(EOS) if len(content) > 0 { messages = append(messages, thread.NewAssistantMessage().AddContent( @@ -148,7 +171,7 @@ func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn Stream content += response.Choices[0].Delta.Content } - callbackFn(response.Choices[0].Delta.Content) + o.streamCallbackFn(response.Choices[0].Delta.Content) } t.AddMessages(messages) @@ -156,18 +179,11 @@ func (o *OpenAI) Stream(ctx context.Context, t *thread.Thread, callbackFn Stream return nil } -func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) error { - if t == nil { - return nil - } - - chatCompletionRequest := o.buildChatCompletionRequest(t) - - if len(o.functions) > 0 { - chatCompletionRequest.Tools = o.getChatCompletionRequestTools() - chatCompletionRequest.ToolChoice = o.getChatCompletionRequestToolChoice() - } - +func (o *OpenAI) generate( + ctx context.Context, + t *thread.Thread, + chatCompletionRequest openai.ChatCompletionRequest, +) error { response, err := o.openAIClient.CreateChatCompletion( ctx, chatCompletionRequest, From 472462276b513dcc68cac89bf7549fa5bafa287a Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 11:23:50 +0100 Subject: [PATCH 11/65] fix --- llm/openai/formatters.go | 3 +++ llm/openai/function.go | 6 +++--- llm/openai/legacy.go | 46 ++++++++++++++++++++-------------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index 32fb9c06..c1467bde 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -5,6 +5,7 @@ import ( "github.com/sashabaranov/go-openai" ) +//nolint:gocognit func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMessage { chatCompletionMessages := make([]openai.ChatCompletionMessage, len(t.Messages)) for i, message := range t.Messages { @@ -77,6 +78,8 @@ func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePar Detail: openai.ImageURLDetailAuto, }, } + case thread.ContentTypeToolCall, thread.ContentTypeToolResponse: + continue default: continue } diff --git a/llm/openai/function.go b/llm/openai/function.go index d6dc0158..2206896d 100644 --- a/llm/openai/function.go +++ b/llm/openai/function.go @@ -45,7 +45,7 @@ func bindFunction( }, nil } -func (o *OpenAILegacy) BindFunction( +func (o *Legacy) BindFunction( fn interface{}, name string, description string, @@ -77,7 +77,7 @@ func (o *OpenAI) BindFunction( return nil } -func (o *OpenAILegacy) getFunctions() []openai.FunctionDefinition { +func (o *Legacy) getFunctions() []openai.FunctionDefinition { functions := []openai.FunctionDefinition{} for _, function := range o.functions { @@ -197,7 +197,7 @@ func callFnWithArgumentAsJSON(fn interface{}, argumentAsJSON string) (string, er return "", nil } -func (o *OpenAILegacy) functionCall(response openai.ChatCompletionResponse) (string, error) { +func (o *Legacy) functionCall(response openai.ChatCompletionResponse) (string, error) { fn, ok := o.functions[response.Choices[0].Message.FunctionCall.Name] if !ok { return "", fmt.Errorf("%w: unknown function %s", ErrOpenAIChat, response.Choices[0].Message.FunctionCall.Name) diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go index 4fcd8f1e..bdc16399 100644 --- a/llm/openai/legacy.go +++ b/llm/openai/legacy.go @@ -16,7 +16,7 @@ import ( "github.com/sashabaranov/go-openai" ) -type OpenAILegacy struct { +type Legacy struct { openAIClient *openai.Client model Model temperature float32 @@ -32,59 +32,59 @@ type OpenAILegacy struct { } // WithModel sets the model to use for the OpenAI instance. -func (o *OpenAILegacy) WithModel(model Model) *OpenAILegacy { +func (o *Legacy) WithModel(model Model) *Legacy { o.model = model return o } // WithTemperature sets the temperature to use for the OpenAI instance. -func (o *OpenAILegacy) WithTemperature(temperature float32) *OpenAILegacy { +func (o *Legacy) WithTemperature(temperature float32) *Legacy { o.temperature = temperature return o } // WithMaxTokens sets the max tokens to use for the OpenAI instance. -func (o *OpenAILegacy) WithMaxTokens(maxTokens int) *OpenAILegacy { +func (o *Legacy) WithMaxTokens(maxTokens int) *Legacy { o.maxTokens = maxTokens return o } // WithUsageCallback sets the usage callback to use for the OpenAI instance. -func (o *OpenAILegacy) WithCallback(callback UsageCallback) *OpenAILegacy { +func (o *Legacy) WithCallback(callback UsageCallback) *Legacy { o.usageCallback = callback return o } // WithStop sets the stop sequences to use for the OpenAI instance. -func (o *OpenAILegacy) WithStop(stop []string) *OpenAILegacy { +func (o *Legacy) WithStop(stop []string) *Legacy { o.stop = stop return o } // WithClient sets the client to use for the OpenAI instance. -func (o *OpenAILegacy) WithClient(client *openai.Client) *OpenAILegacy { +func (o *Legacy) WithClient(client *openai.Client) *Legacy { o.openAIClient = client return o } // WithVerbose sets the verbose flag to use for the OpenAI instance. -func (o *OpenAILegacy) WithVerbose(verbose bool) *OpenAILegacy { +func (o *Legacy) WithVerbose(verbose bool) *Legacy { o.verbose = verbose return o } // WithCache sets the cache to use for the OpenAI instance. -func (o *OpenAILegacy) WithCompletionCache(cache *cache.Cache) *OpenAILegacy { +func (o *Legacy) WithCompletionCache(cache *cache.Cache) *Legacy { o.cache = cache return o } // SetStop sets the stop sequences for the completion. -func (o *OpenAILegacy) SetStop(stop []string) { +func (o *Legacy) SetStop(stop []string) { o.stop = stop } -func (o *OpenAILegacy) setUsageMetadata(usage openai.Usage) { +func (o *Legacy) setUsageMetadata(usage openai.Usage) { callbackMetadata := make(types.Meta) err := mapstructure.Decode(usage, &callbackMetadata) @@ -95,10 +95,10 @@ func (o *OpenAILegacy) setUsageMetadata(usage openai.Usage) { o.usageCallback(callbackMetadata) } -func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *OpenAILegacy { +func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *Legacy { openAIKey := os.Getenv("OPENAI_API_KEY") - return &OpenAILegacy{ + return &Legacy{ openAIClient: openai.NewClient(openAIKey), model: model, temperature: temperature, @@ -110,16 +110,16 @@ func NewLegacy(model Model, temperature float32, maxTokens int, verbose bool) *O } // CalledFunctionName returns the name of the function that was called. -func (o *OpenAILegacy) CalledFunctionName() *string { +func (o *Legacy) CalledFunctionName() *string { return o.calledFunctionName } // FinishReason returns the LLM finish reason. -func (o *OpenAILegacy) FinishReason() string { +func (o *Legacy) FinishReason() string { return o.finishReason } -func NewCompletion() *OpenAILegacy { +func NewCompletion() *Legacy { return NewLegacy( GPT3Dot5TurboInstruct, DefaultOpenAITemperature, @@ -128,7 +128,7 @@ func NewCompletion() *OpenAILegacy { ) } -func NewChat() *OpenAILegacy { +func NewChat() *Legacy { return NewLegacy( GPT3Dot5Turbo, DefaultOpenAITemperature, @@ -138,7 +138,7 @@ func NewChat() *OpenAILegacy { } // Completion returns a single completion for the given prompt. -func (o *OpenAILegacy) Completion(ctx context.Context, prompt string) (string, error) { +func (o *Legacy) Completion(ctx context.Context, prompt string) (string, error) { var cacheResult *cache.Result var err error @@ -167,7 +167,7 @@ func (o *OpenAILegacy) Completion(ctx context.Context, prompt string) (string, e } // BatchCompletion returns multiple completions for the given prompts. -func (o *OpenAILegacy) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { +func (o *Legacy) BatchCompletion(ctx context.Context, prompts []string) ([]string, error) { response, err := o.openAIClient.CreateCompletion( ctx, openai.CompletionRequest{ @@ -206,12 +206,12 @@ func (o *OpenAILegacy) BatchCompletion(ctx context.Context, prompts []string) ([ } // CompletionStream returns a single completion stream for the given prompt. -func (o *OpenAILegacy) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { +func (o *Legacy) CompletionStream(ctx context.Context, callbackFn StreamCallback, prompt string) error { return o.BatchCompletionStream(ctx, []StreamCallback{callbackFn}, []string{prompt}) } // BatchCompletionStream returns multiple completion streams for the given prompts. -func (o *OpenAILegacy) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { +func (o *Legacy) BatchCompletionStream(ctx context.Context, callbackFn []StreamCallback, prompts []string) error { stream, err := o.openAIClient.CreateCompletionStream( ctx, openai.CompletionRequest{ @@ -263,7 +263,7 @@ func (o *OpenAILegacy) BatchCompletionStream(ctx context.Context, callbackFn []S } // Chat returns a single chat completion for the given prompt. -func (o *OpenAILegacy) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { +func (o *Legacy) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { messages, err := buildMessages(prompt) if err != nil { return "", fmt.Errorf("%w: %w", ErrOpenAIChat, err) @@ -324,7 +324,7 @@ func (o *OpenAILegacy) Chat(ctx context.Context, prompt *chat.Chat) (string, err } // ChatStream returns a single chat stream for the given prompt. -func (o *OpenAILegacy) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { +func (o *Legacy) ChatStream(ctx context.Context, callbackFn StreamCallback, prompt *chat.Chat) error { messages, err := buildMessages(prompt) if err != nil { return fmt.Errorf("%w: %w", ErrOpenAIChat, err) From 56da3781652382ae7965e8ae70ef095f64ecb7c6 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 11:27:46 +0100 Subject: [PATCH 12/65] fix --- llm/openai/formatters.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index c1467bde..192f1d83 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -20,15 +20,15 @@ func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMes switch message.Role { case thread.RoleUser: - if data, ok := message.Contents[0].Data.(string); ok { + if data, isUserTextData := message.Contents[0].Data.(string); isUserTextData { chatCompletionMessages[i].Content = data } else { continue } case thread.RoleAssistant: - if data, ok := message.Contents[0].Data.(string); ok { + if data, isAssistantTextData := message.Contents[0].Data.(string); isAssistantTextData { chatCompletionMessages[i].Content = data - } else if data, ok := message.Contents[0].Data.([]*thread.ToolCallData); ok { + } else if data, isTollCallData := message.Contents[0].Data.([]*thread.ToolCallData); isTollCallData { var toolCalls []openai.ToolCall for _, toolCallData := range data { toolCalls = append(toolCalls, openai.ToolCall{ @@ -45,7 +45,7 @@ func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMes continue } case thread.RoleTool: - if data, ok := message.Contents[0].Data.(*thread.ToolResponseData); ok { + if data, isTollResponseData := message.Contents[0].Data.(*thread.ToolResponseData); isTollResponseData { chatCompletionMessages[i].ToolCallID = data.ID chatCompletionMessages[i].Name = data.Name chatCompletionMessages[i].Content = data.Result From a76608697292cacb90b3048884248576b7c8918a Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 11:28:53 +0100 Subject: [PATCH 13/65] fix --- llm/openai/legacy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/llm/openai/legacy.go b/llm/openai/legacy.go index bdc16399..141c6727 100644 --- a/llm/openai/legacy.go +++ b/llm/openai/legacy.go @@ -280,6 +280,7 @@ func (o *Legacy) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { } if len(o.functions) > 0 { + //nolint:staticcheck chatCompletionRequest.Functions = o.getFunctions() } From 284cfd8cbfea9af6e45505b7d653f20c6a94dbe5 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 11:36:58 +0100 Subject: [PATCH 14/65] fix --- llm/openai/formatters.go | 22 ++++++++++------------ thread/thread.go | 24 ++++++++++-------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index 192f1d83..d33823c6 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -28,15 +28,15 @@ func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMes case thread.RoleAssistant: if data, isAssistantTextData := message.Contents[0].Data.(string); isAssistantTextData { chatCompletionMessages[i].Content = data - } else if data, isTollCallData := message.Contents[0].Data.([]*thread.ToolCallData); isTollCallData { + } else if data, isTollCallData := message.Contents[0].Data.([]thread.ToolCallData); isTollCallData { var toolCalls []openai.ToolCall for _, toolCallData := range data { toolCalls = append(toolCalls, openai.ToolCall{ ID: toolCallData.ID, Type: "function", Function: openai.FunctionCall{ - Name: toolCallData.Function.Name, - Arguments: toolCallData.Function.Arguments, + Name: toolCallData.Name, + Arguments: toolCallData.Arguments, }, }) } @@ -45,7 +45,7 @@ func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMes continue } case thread.RoleTool: - if data, isTollResponseData := message.Contents[0].Data.(*thread.ToolResponseData); isTollResponseData { + if data, isTollResponseData := message.Contents[0].Data.(thread.ToolResponseData); isTollResponseData { chatCompletionMessages[i].ToolCallID = data.ID chatCompletionMessages[i].Name = data.Name chatCompletionMessages[i].Content = data.Result @@ -93,7 +93,7 @@ func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePar func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thread.Message { return thread.NewToolMessage().AddContent( thread.NewToolResponseContent( - &thread.ToolResponseData{ + thread.ToolResponseData{ ID: toolCall.ID, Name: toolCall.Function.Name, Result: result, @@ -113,14 +113,12 @@ func toolCallsToToolCallMessage(toolCalls []openai.ToolCall) *thread.Message { return nil } - var toolCallData []*thread.ToolCallData + var toolCallData []thread.ToolCallData for _, toolCall := range toolCalls { - toolCallData = append(toolCallData, &thread.ToolCallData{ - ID: toolCall.ID, - Function: thread.ToolCallFunction{ - Name: toolCall.Function.Name, - Arguments: toolCall.Function.Arguments, - }, + toolCallData = append(toolCallData, thread.ToolCallData{ + ID: toolCall.ID, + Name: toolCall.Function.Name, + Arguments: toolCall.Function.Arguments, }) } diff --git a/thread/thread.go b/thread/thread.go index 356b0280..ca34c9c6 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -38,11 +38,7 @@ type ToolResponseData struct { } type ToolCallData struct { - ID string - Function ToolCallFunction -} - -type ToolCallFunction struct { + ID string Name string Arguments string } @@ -59,21 +55,21 @@ func NewTextContent(text string) *Content { } } -func NewImageContent(mediaData *MediaData) *Content { +func NewImageContent(mediaData MediaData) *Content { return &Content{ Type: ContentTypeImage, Data: mediaData, } } -func NewToolResponseContent(toolResponseData *ToolResponseData) *Content { +func NewToolResponseContent(toolResponseData ToolResponseData) *Content { return &Content{ Type: ContentTypeToolResponse, Data: toolResponseData, } } -func NewToolCallContent(data []*ToolCallData) *Content { +func NewToolCallContent(data []ToolCallData) *Content { return &Content{ Type: ContentTypeToolCall, Data: data, @@ -133,15 +129,15 @@ func (t *Thread) String() string { case ContentTypeImage: str += "\tImage URL: " + *content.Data.(*MediaData).URL + "\n" case ContentTypeToolCall: - for _, toolCallData := range content.Data.([]*ToolCallData) { + for _, toolCallData := range content.Data.([]ToolCallData) { str += "\tTool Call ID: " + toolCallData.ID + "\n" - str += "\tTool Call Function Name: " + toolCallData.Function.Name + "\n" - str += "\tTool Call Function Arguments: " + toolCallData.Function.Arguments + "\n" + str += "\tTool Call Function Name: " + toolCallData.Name + "\n" + str += "\tTool Call Function Arguments: " + toolCallData.Arguments + "\n" } case ContentTypeToolResponse: - str += "\tTool ID: " + content.Data.(*ToolResponseData).ID + "\n" - str += "\tTool Name: " + content.Data.(*ToolResponseData).Name + "\n" - str += "\tTool Result: " + content.Data.(*ToolResponseData).Result + "\n" + str += "\tTool ID: " + content.Data.(ToolResponseData).ID + "\n" + str += "\tTool Name: " + content.Data.(ToolResponseData).Name + "\n" + str += "\tTool Result: " + content.Data.(ToolResponseData).Result + "\n" } } } From 32b8cfb787094f9591caddc97d651210b5b4af6f Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 12:29:48 +0100 Subject: [PATCH 15/65] use cache --- examples/llm/cache/main.go | 24 ++++++--- llm/openai/openai.go | 101 ++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 8 deletions(-) diff --git a/examples/llm/cache/main.go b/examples/llm/cache/main.go index 169b6ade..d0abe2f1 100644 --- a/examples/llm/cache/main.go +++ b/examples/llm/cache/main.go @@ -12,27 +12,39 @@ import ( "github.com/henomis/lingoose/index/vectordb/jsondb" "github.com/henomis/lingoose/llm/cache" "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/thread" ) func main() { embedder := openaiembedder.New(openaiembedder.AdaEmbeddingV2) index := index.New( - jsondb.New().WithPersist("db.json"), + jsondb.New().WithPersist("index.json"), embedder, ) - llm := openai.NewCompletion().WithCompletionCache(cache.New(embedder, index).WithTopK(3)) + llm := openai.New().WithCache(cache.New(embedder, index).WithTopK(3)) - for { - text := askUserInput("What is your question?") + questions := []string{ + "what's github", + "can you explain what GitHub is", + "can you tell me more about GitHub", + "what is the purpose of GitHub", + } + + for _, question := range questions { + t := thread.NewThread().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(question), + ), + ) - response, err := llm.Completion(context.Background(), text) + err := llm.Generate(context.Background(), t) if err != nil { fmt.Println(err) continue } - fmt.Println(response) + fmt.Println(t) } } diff --git a/llm/openai/openai.go b/llm/openai/openai.go index ab6b4122..52ce1c55 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "os" + "strings" + "github.com/henomis/lingoose/llm/cache" "github.com/henomis/lingoose/thread" "github.com/henomis/lingoose/types" "github.com/mitchellh/mapstructure" @@ -33,6 +35,7 @@ type OpenAI struct { functions map[string]Function streamCallbackFn StreamCallback toolChoice *string + cache *cache.Cache } // WithModel sets the model to use for the OpenAI instance. @@ -86,6 +89,11 @@ func (o *OpenAI) WithStream(enable bool, callbackFn StreamCallback) *OpenAI { return o } +func (o *OpenAI) WithCache(cache *cache.Cache) *OpenAI { + o.cache = cache + return o +} + // SetStop sets the stop sequences for the completion. func (o *OpenAI) SetStop(stop []string) { o.stop = stop @@ -114,11 +122,87 @@ func New() *OpenAI { } } +func getCacheableMessages(t *thread.Thread) []string { + userMessages := make([]*thread.Message, 0) + for _, message := range t.Messages { + if message.Role == thread.RoleUser { + userMessages = append(userMessages, message) + } else { + userMessages = make([]*thread.Message, 0) + } + } + + var messages []string + for _, message := range userMessages { + for _, content := range message.Contents { + if content.Type == thread.ContentTypeText { + messages = append(messages, content.Data.(string)) + } else { + messages = make([]string, 0) + break + } + } + } + + return messages +} + +func (o *OpenAI) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { + messages := getCacheableMessages(t) + cacheQuery := strings.Join(messages, "\n") + cacheResult, err := o.cache.Get(ctx, cacheQuery) + if err != nil { + return cacheResult, err + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(strings.Join(cacheResult.Answer, "\n")), + )) + + return cacheResult, nil +} + +func (o *OpenAI) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { + lastMessage := t.Messages[len(t.Messages)-1] + + if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { + return nil + } + + contents := make([]string, 0) + for _, content := range lastMessage.Contents { + if content.Type == thread.ContentTypeText { + contents = append(contents, content.Data.(string)) + } else { + contents = make([]string, 0) + break + } + } + + err := o.cache.Set(ctx, cacheResult.Embedding, strings.Join(contents, "\n")) + if err != nil { + return err + } + + return nil +} + func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) error { if t == nil { return nil } + var err error + var cacheResult *cache.Result + if o.cache != nil { + cacheResult, err = o.getCache(ctx, t) + if err == nil { + return nil + } else if !errors.Is(err, cache.ErrCacheMiss) { + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + } + chatCompletionRequest := o.buildChatCompletionRequest(t) if len(o.functions) > 0 { @@ -127,10 +211,23 @@ func (o *OpenAI) Generate(ctx context.Context, t *thread.Thread) error { } if o.streamCallbackFn != nil { - return o.stream(ctx, t, chatCompletionRequest) + err = o.stream(ctx, t, chatCompletionRequest) + } else { + err = o.generate(ctx, t, chatCompletionRequest) + } + + if err != nil { + return err } - return o.generate(ctx, t, chatCompletionRequest) + if o.cache != nil { + err = o.setCache(ctx, t, cacheResult) + if err != nil { + return fmt.Errorf("%w: %w", ErrOpenAIChat, err) + } + } + + return nil } func (o *OpenAI) stream( From 3ed7f322a0f36073d31818da4df0b731dd3f09ee Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 12:30:06 +0100 Subject: [PATCH 16/65] fix --- examples/llm/cache/main.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/examples/llm/cache/main.go b/examples/llm/cache/main.go index d0abe2f1..cd8675b4 100644 --- a/examples/llm/cache/main.go +++ b/examples/llm/cache/main.go @@ -1,11 +1,8 @@ package main import ( - "bufio" "context" "fmt" - "os" - "strings" openaiembedder "github.com/henomis/lingoose/embedder/openai" "github.com/henomis/lingoose/index" @@ -47,11 +44,3 @@ func main() { fmt.Println(t) } } - -func askUserInput(question string) string { - fmt.Printf("%s > ", question) - reader := bufio.NewReader(os.Stdin) - name, _ := reader.ReadString('\n') - name = strings.TrimSuffix(name, "\n") - return name -} From 290c44c244ffa690ae014e2c012221db519452bc Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 12:32:13 +0100 Subject: [PATCH 17/65] fix --- pipeline/lmm.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pipeline/lmm.go b/pipeline/lmm.go index 353463c5..ae9ac48c 100644 --- a/pipeline/lmm.go +++ b/pipeline/lmm.go @@ -29,5 +29,4 @@ type Prompt interface { type LlmEngine interface { Completion(ctx context.Context, prompt string) (string, error) Chat(ctx context.Context, chat *chat.Chat) (string, error) - // Generate(ctx context.Context, thread *chat.Thread) (*chat.Thread, error) } From f752f78ea05685401bd0db41cbc36d0d8d9c7939 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 21 Dec 2023 17:11:12 +0100 Subject: [PATCH 18/65] fix --- llm/openai/formatters.go | 6 ------ llm/openai/openai.go | 4 +++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index d33823c6..65d6c564 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -102,12 +102,6 @@ func toolCallResultToThreadMessage(toolCall openai.ToolCall, result string) *thr ) } -func textContentToThreadMessage(content string) *thread.Message { - return thread.NewAssistantMessage().AddContent( - thread.NewTextContent(content), - ) -} - func toolCallsToToolCallMessage(toolCalls []openai.ToolCall) *thread.Message { if len(toolCalls) == 0 { return nil diff --git a/llm/openai/openai.go b/llm/openai/openai.go index 52ce1c55..712378a3 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -303,7 +303,9 @@ func (o *OpenAI) generate( messages = append(messages, o.callTools(response.Choices[0].Message.ToolCalls)...) } else { messages = []*thread.Message{ - textContentToThreadMessage(response.Choices[0].Message.Content), + thread.NewAssistantMessage().AddContent( + thread.NewTextContent(response.Choices[0].Message.Content), + ), } } From cbfb6ae6dabec97f8caeb270e67ae262ba2dbea7 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Wed, 27 Dec 2023 16:25:32 +0100 Subject: [PATCH 19/65] add assistant --- assistant/assistant.go | 93 +++++++++++++++++++++ assistant/prompt.go | 7 ++ examples/assistant/main.go | 64 +++++++++++++++ rag/rag.go | 160 +++++++++++++++++++++++++++++++++++++ 4 files changed, 324 insertions(+) create mode 100644 assistant/assistant.go create mode 100644 assistant/prompt.go create mode 100644 examples/assistant/main.go create mode 100644 rag/rag.go diff --git a/assistant/assistant.go b/assistant/assistant.go new file mode 100644 index 00000000..9fc0552b --- /dev/null +++ b/assistant/assistant.go @@ -0,0 +1,93 @@ +package assistant + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/prompt" + "github.com/henomis/lingoose/thread" + "github.com/henomis/lingoose/types" +) + +type Assistant struct { + llm LLM + rag RAG + thread *thread.Thread +} + +type LLM interface { + Generate(context.Context, *thread.Thread) error +} +type RAG interface { + Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) +} + +func New(llm LLM) *Assistant { + assistant := &Assistant{ + llm: llm, + thread: thread.NewThread(), + } + + return assistant +} + +func (a *Assistant) WithThread(thread *thread.Thread) *Assistant { + a.thread = thread + return a +} + +func (a *Assistant) WithRAG(rag RAG) *Assistant { + a.rag = rag + return a +} + +func (a *Assistant) Run(ctx context.Context, message any) error { + var err error + if _, ok := message.(string); !ok { + return fmt.Errorf("message must be a string") + } + + if a.rag != nil { + message, err = a.generateRAGMessage(ctx, message.(string)) + if err != nil { + return err + } + } + + a.thread.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent( + message.(string), + ), + )) + + return a.llm.Generate(ctx, a.thread) +} + +func (a *Assistant) Thread() *thread.Thread { + return a.thread +} + +func (a *Assistant) generateRAGMessage(ctx context.Context, query string) (string, error) { + searchResults, err := a.rag.Retrieve(ctx, query) + if err != nil { + return "", err + } + + context := "" + + for _, searchResult := range searchResults { + context += searchResult.Content() + "\n\n" + } + + ragPrompt := prompt.NewPromptTemplate(baseRAGPrompt) + err = ragPrompt.Format(types.M{ + "question": query, + "context": context, + }) + if err != nil { + return "", err + } + + return ragPrompt.String(), nil +} diff --git a/assistant/prompt.go b/assistant/prompt.go new file mode 100644 index 00000000..6d297f56 --- /dev/null +++ b/assistant/prompt.go @@ -0,0 +1,7 @@ +package assistant + +const ( + baseRAGPrompt = `You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise. + Question: {{.question}} + Context: {{.context}}` +) diff --git a/examples/assistant/main.go b/examples/assistant/main.go new file mode 100644 index 00000000..a87782e1 --- /dev/null +++ b/examples/assistant/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/henomis/lingoose/assistant" + openaiembedder "github.com/henomis/lingoose/embedder/openai" + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/index/vectordb/jsondb" + "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/rag" + "github.com/henomis/lingoose/thread" +) + +// download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt + +func main() { + openaiEmbedder := openaiembedder.New(openaiembedder.AdaEmbeddingV2) + cache := cache.New( + openaiEmbedder, + index.New( + jsondb.New().WithPersist("cache.json"), + openaiEmbedder, + ), + ) + + r := rag.New( + index.New( + jsondb.New().WithPersist("db.json"), + openaiEmbedder, + ), + ) + + _, err := os.Stat("db.json") + if os.IsNotExist(err) { + r.AddFiles(context.Background(), "state_of_the_union.txt") + } + + a := assistant.New( + openai.New().WithCache(cache), + ).WithRAG(r) + + questions := []string{ + "what is NATO?", + "what is the purpose of NATO?", + "what is the purpose of the NATO Alliance?", + "what is the meaning of NATO?", + } + + for _, question := range questions { + a = a.WithThread(thread.NewThread()) + err := a.Run(context.Background(), question) + if err != nil { + panic(err) + } + + fmt.Println("----") + fmt.Println(a.Thread()) + fmt.Println("----") + } +} diff --git a/rag/rag.go b/rag/rag.go new file mode 100644 index 00000000..23471f82 --- /dev/null +++ b/rag/rag.go @@ -0,0 +1,160 @@ +package rag + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/henomis/lingoose/document" + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/index/option" + "github.com/henomis/lingoose/loader" + "github.com/henomis/lingoose/textsplitter" + "github.com/henomis/lingoose/thread" +) + +const ( + defaultChunkSize = 1000 + defaultChunkOverlap = 0 + defaultTopK = 1 +) + +type LLM interface { + Generate(context.Context, *thread.Thread) error +} + +type RAG struct { + index *index.Index + chunkSize uint + chunkOverlap uint + topK uint +} + +type FusionRAG struct { + RAG + llm LLM +} + +func New(index *index.Index) *RAG { + return &RAG{ + index: index, + chunkSize: defaultChunkSize, + chunkOverlap: defaultChunkOverlap, + topK: defaultTopK, + } +} + +func NewFusionRAG(index *index.Index, llm LLM) *FusionRAG { + return &FusionRAG{ + RAG: *New(index), + llm: llm, + } +} + +func (r *RAG) WithChunkSize(chunkSize uint) *RAG { + r.chunkSize = chunkSize + return r +} + +func (r *RAG) WithChunkOverlap(chunkOverlap uint) *RAG { + r.chunkOverlap = chunkOverlap + return r +} + +func (r *RAG) WithTopK(topK uint) *RAG { + r.topK = topK + return r +} + +func (r *RAG) AddFiles(ctx context.Context, filePath ...string) error { + for _, f := range filePath { + documents, err := r.addFile(ctx, f) + if err != nil { + return err + } + + err = r.index.LoadFromDocuments(ctx, documents) + if err != nil { + return err + } + } + + return nil +} + +func (r *RAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { + results, err := r.index.Query(ctx, query, option.WithTopK(int(r.topK))) + return results, err +} + +func (r *FusionRAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { + if r.llm == nil { + return nil, fmt.Errorf("llm is not set") + } + + t := thread.NewThread().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent( + fmt.Sprintf("Based on the provided question, generate 5 similar questions.\n\nQuestion: %s", query), + ), + ), + ) + + err := r.llm.Generate(ctx, t) + if err != nil { + return nil, err + } + + lastMessage := t.Messages[len(t.Messages)-1] + content := lastMessage.Contents[0].Data.(string) + questions := strings.Split(content, "\n") + + var results index.SearchResults + for _, question := range questions { + res, err := r.index.Query(ctx, question, option.WithTopK(int(r.topK))) + if err != nil { + return nil, err + } + + results = append(results, res...) + } + + // TODO: rerank results + + //remove dupliocates + seen := make(map[string]bool) + var uniqueResults index.SearchResults + for _, result := range results { + if _, ok := seen[result.Content()]; !ok { + uniqueResults = append(uniqueResults, result) + seen[result.Content()] = true + } + } + + return results, err +} + +func (r *RAG) addFile(ctx context.Context, filePath string) ([]document.Document, error) { + var documents []document.Document + var err error + switch filepath.Ext(filePath) { + case ".pdf": + documents, err = loader.NewPDFToTextLoader(filePath).Load(ctx) + case ".docx": + documents, err = loader.NewLibreOfficeLoader(filePath).Load(ctx) + case ".txt": + documents, err = loader.NewTextLoader(filePath, nil).Load(ctx) + default: + return nil, fmt.Errorf("unsupported file type") + } + + if err != nil { + return nil, err + } + + return textsplitter.NewRecursiveCharacterTextSplitter( + int(r.chunkSize), + int(r.chunkOverlap), + ).SplitDocuments(documents), nil +} From afa326ddebb6f8c56aa704792f04f90ddccf80eb Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 28 Dec 2023 16:20:50 +0100 Subject: [PATCH 20/65] implement rag fusion --- examples/assistant/main.go | 40 ++++---------- llm/openai/formatters.go | 2 +- llm/openai/openai.go | 3 +- rag/rag.go | 57 +------------------- rag/rag_fusion.go | 105 +++++++++++++++++++++++++++++++++++++ thread/thread.go | 9 +++- 6 files changed, 128 insertions(+), 88 deletions(-) create mode 100644 rag/rag_fusion.go diff --git a/examples/assistant/main.go b/examples/assistant/main.go index a87782e1..3467c48c 100644 --- a/examples/assistant/main.go +++ b/examples/assistant/main.go @@ -9,7 +9,6 @@ import ( openaiembedder "github.com/henomis/lingoose/embedder/openai" "github.com/henomis/lingoose/index" "github.com/henomis/lingoose/index/vectordb/jsondb" - "github.com/henomis/lingoose/llm/cache" "github.com/henomis/lingoose/llm/openai" "github.com/henomis/lingoose/rag" "github.com/henomis/lingoose/thread" @@ -18,20 +17,12 @@ import ( // download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt func main() { - openaiEmbedder := openaiembedder.New(openaiembedder.AdaEmbeddingV2) - cache := cache.New( - openaiEmbedder, - index.New( - jsondb.New().WithPersist("cache.json"), - openaiEmbedder, - ), - ) - - r := rag.New( + r := rag.NewRAGFusion( index.New( jsondb.New().WithPersist("db.json"), - openaiEmbedder, + openaiembedder.New(openaiembedder.AdaEmbeddingV2), ), + openai.New().WithTemperature(0), ) _, err := os.Stat("db.json") @@ -40,25 +31,16 @@ func main() { } a := assistant.New( - openai.New().WithCache(cache), + openai.New().WithTemperature(0), ).WithRAG(r) - questions := []string{ - "what is NATO?", - "what is the purpose of NATO?", - "what is the purpose of the NATO Alliance?", - "what is the meaning of NATO?", + a = a.WithThread(thread.NewThread()) + err = a.Run(context.Background(), "what is the purpose of NATO?") + if err != nil { + panic(err) } - for _, question := range questions { - a = a.WithThread(thread.NewThread()) - err := a.Run(context.Background(), question) - if err != nil { - panic(err) - } - - fmt.Println("----") - fmt.Println(a.Thread()) - fmt.Println("----") - } + fmt.Println("----") + fmt.Println(a.Thread()) + fmt.Println("----") } diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index 65d6c564..b57e12a5 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -19,7 +19,7 @@ func threadToChatCompletionMessages(t *thread.Thread) []openai.ChatCompletionMes } switch message.Role { - case thread.RoleUser: + case thread.RoleUser, thread.RoleSystem: if data, isUserTextData := message.Contents[0].Data.(string); isUserTextData { chatCompletionMessages[i].Content = data } else { diff --git a/llm/openai/openai.go b/llm/openai/openai.go index 712378a3..27e1d896 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -20,6 +20,7 @@ const ( ) var threadRoleToOpenAIRole = map[thread.Role]string{ + thread.RoleSystem: "system", thread.RoleUser: "user", thread.RoleAssistant: "assistant", thread.RoleTool: "tool", @@ -271,7 +272,7 @@ func (o *OpenAI) stream( o.streamCallbackFn(response.Choices[0].Delta.Content) } - t.AddMessages(messages) + t.AddMessages(messages...) return nil } diff --git a/rag/rag.go b/rag/rag.go index 23471f82..19c74191 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "path/filepath" - "strings" "github.com/henomis/lingoose/document" "github.com/henomis/lingoose/index" @@ -31,7 +30,7 @@ type RAG struct { topK uint } -type FusionRAG struct { +type RAGFusion struct { RAG llm LLM } @@ -45,13 +44,6 @@ func New(index *index.Index) *RAG { } } -func NewFusionRAG(index *index.Index, llm LLM) *FusionRAG { - return &FusionRAG{ - RAG: *New(index), - llm: llm, - } -} - func (r *RAG) WithChunkSize(chunkSize uint) *RAG { r.chunkSize = chunkSize return r @@ -88,53 +80,6 @@ func (r *RAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, return results, err } -func (r *FusionRAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { - if r.llm == nil { - return nil, fmt.Errorf("llm is not set") - } - - t := thread.NewThread().AddMessage( - thread.NewUserMessage().AddContent( - thread.NewTextContent( - fmt.Sprintf("Based on the provided question, generate 5 similar questions.\n\nQuestion: %s", query), - ), - ), - ) - - err := r.llm.Generate(ctx, t) - if err != nil { - return nil, err - } - - lastMessage := t.Messages[len(t.Messages)-1] - content := lastMessage.Contents[0].Data.(string) - questions := strings.Split(content, "\n") - - var results index.SearchResults - for _, question := range questions { - res, err := r.index.Query(ctx, question, option.WithTopK(int(r.topK))) - if err != nil { - return nil, err - } - - results = append(results, res...) - } - - // TODO: rerank results - - //remove dupliocates - seen := make(map[string]bool) - var uniqueResults index.SearchResults - for _, result := range results { - if _, ok := seen[result.Content()]; !ok { - uniqueResults = append(uniqueResults, result) - seen[result.Content()] = true - } - } - - return results, err -} - func (r *RAG) addFile(ctx context.Context, filePath string) ([]document.Document, error) { var documents []document.Document var err error diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go new file mode 100644 index 00000000..4e146d28 --- /dev/null +++ b/rag/rag_fusion.go @@ -0,0 +1,105 @@ +package rag + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/index/option" + "github.com/henomis/lingoose/thread" +) + +var ragFusionPrompts = []string{ + "You are a helpful assistant that generates multiple search queries based on a single input query.", + "Generate multiple search queries related to: %s", + "OUTPUT (4 queries):", +} + +func NewRAGFusion(index *index.Index, llm LLM) *RAGFusion { + return &RAGFusion{ + RAG: *New(index), + llm: llm, + } +} + +func (r *RAGFusion) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { + if r.llm == nil { + return nil, fmt.Errorf("llm is not set") + } + + t := thread.NewThread().AddMessages( + thread.NewSystemMessage().AddContent( + thread.NewTextContent( + ragFusionPrompts[0], + ), + ), + thread.NewUserMessage().AddContent( + thread.NewTextContent( + fmt.Sprintf(ragFusionPrompts[1], query), + ), + ), + thread.NewUserMessage().AddContent( + thread.NewTextContent( + ragFusionPrompts[2], + ), + ), + ) + + err := r.llm.Generate(ctx, t) + if err != nil { + return nil, err + } + + fmt.Println(t) + + lastMessage := t.Messages[len(t.Messages)-1] + content := lastMessage.Contents[0].Data.(string) + content = strings.TrimSpace(content) + questions := strings.Split(content, "\n") + + var results index.SearchResults + for _, question := range questions { + res, err := r.index.Query(ctx, question, option.WithTopK(int(r.topK))) + if err != nil { + return nil, err + } + + results = append(results, res...) + } + + return reciprocalRankFusion(results), nil +} + +func reciprocalRankFusion(searchResults index.SearchResults) index.SearchResults { + const k = 60.0 + searchResultsScoreMap := make(map[string]float64) + for _, result := range searchResults { + if _, ok := searchResultsScoreMap[result.ID]; !ok { + searchResultsScoreMap[result.ID] = 0 + } + searchResultsScoreMap[result.ID] += 1 / (result.Score + k) + } + + for i, searchResult := range searchResults { + searchResults[i].Score = searchResultsScoreMap[searchResult.ID] + } + + //remove duplicates + seen := make(map[string]bool) + var uniqueSearchResults index.SearchResults + for _, searchResult := range searchResults { + if _, ok := seen[searchResult.Content()]; !ok { + uniqueSearchResults = append(uniqueSearchResults, searchResult) + seen[searchResult.Content()] = true + } + } + + //sort by score + sort.Slice(uniqueSearchResults, func(i, j int) bool { + return uniqueSearchResults[i].Score > uniqueSearchResults[j].Score + }) + + return uniqueSearchResults +} diff --git a/thread/thread.go b/thread/thread.go index ca34c9c6..6d18df55 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -21,6 +21,7 @@ type Content struct { type Role string const ( + RoleSystem Role = "system" RoleUser Role = "user" RoleAssistant Role = "assistant" RoleTool Role = "tool" @@ -87,6 +88,12 @@ func NewUserMessage() *Message { } } +func NewSystemMessage() *Message { + return &Message{ + Role: RoleSystem, + } +} + func NewAssistantMessage() *Message { return &Message{ Role: RoleAssistant, @@ -104,7 +111,7 @@ func (t *Thread) AddMessage(message *Message) *Thread { return t } -func (t *Thread) AddMessages(messages []*Message) *Thread { +func (t *Thread) AddMessages(messages ...*Message) *Thread { t.Messages = append(t.Messages, messages...) return t } From 02a3b7ce3444e3a78e3c91a38fab8c1c7b89a8f9 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 28 Dec 2023 16:22:46 +0100 Subject: [PATCH 21/65] fix --- examples/assistant/main.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/assistant/main.go b/examples/assistant/main.go index 3467c48c..a907ee0b 100644 --- a/examples/assistant/main.go +++ b/examples/assistant/main.go @@ -11,7 +11,6 @@ import ( "github.com/henomis/lingoose/index/vectordb/jsondb" "github.com/henomis/lingoose/llm/openai" "github.com/henomis/lingoose/rag" - "github.com/henomis/lingoose/thread" ) // download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt @@ -34,7 +33,6 @@ func main() { openai.New().WithTemperature(0), ).WithRAG(r) - a = a.WithThread(thread.NewThread()) err = a.Run(context.Background(), "what is the purpose of NATO?") if err != nil { panic(err) From db933dedae71433c64c2ad5534076f764ee3df2d Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 28 Dec 2023 16:23:19 +0100 Subject: [PATCH 22/65] fix --- examples/assistant/main.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/assistant/main.go b/examples/assistant/main.go index a907ee0b..d1928273 100644 --- a/examples/assistant/main.go +++ b/examples/assistant/main.go @@ -26,7 +26,10 @@ func main() { _, err := os.Stat("db.json") if os.IsNotExist(err) { - r.AddFiles(context.Background(), "state_of_the_union.txt") + err = r.AddFiles(context.Background(), "state_of_the_union.txt") + if err != nil { + panic(err) + } } a := assistant.New( From 30ec2ef647f56233c2b687d305b8cb5da6f387f6 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 28 Dec 2023 16:29:57 +0100 Subject: [PATCH 23/65] fix --- assistant/prompt.go | 1 + examples/assistant/main.go | 2 +- rag/rag.go | 2 +- rag/rag_fusion.go | 14 +++++++------- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/assistant/prompt.go b/assistant/prompt.go index 6d297f56..71df12a7 100644 --- a/assistant/prompt.go +++ b/assistant/prompt.go @@ -1,6 +1,7 @@ package assistant const ( + //nolint:lll baseRAGPrompt = `You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise. Question: {{.question}} Context: {{.context}}` diff --git a/examples/assistant/main.go b/examples/assistant/main.go index d1928273..03f1863f 100644 --- a/examples/assistant/main.go +++ b/examples/assistant/main.go @@ -16,7 +16,7 @@ import ( // download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt func main() { - r := rag.NewRAGFusion( + r := rag.NewFusion( index.New( jsondb.New().WithPersist("db.json"), openaiembedder.New(openaiembedder.AdaEmbeddingV2), diff --git a/rag/rag.go b/rag/rag.go index 19c74191..61606c88 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -30,7 +30,7 @@ type RAG struct { topK uint } -type RAGFusion struct { +type Fusion struct { RAG llm LLM } diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go index 4e146d28..67783d2d 100644 --- a/rag/rag_fusion.go +++ b/rag/rag_fusion.go @@ -17,14 +17,14 @@ var ragFusionPrompts = []string{ "OUTPUT (4 queries):", } -func NewRAGFusion(index *index.Index, llm LLM) *RAGFusion { - return &RAGFusion{ +func NewFusion(index *index.Index, llm LLM) *Fusion { + return &Fusion{ RAG: *New(index), llm: llm, } } -func (r *RAGFusion) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { +func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { if r.llm == nil { return nil, fmt.Errorf("llm is not set") } @@ -55,15 +55,15 @@ func (r *RAGFusion) Retrieve(ctx context.Context, query string) ([]index.SearchR fmt.Println(t) lastMessage := t.Messages[len(t.Messages)-1] - content := lastMessage.Contents[0].Data.(string) + content, _ := lastMessage.Contents[0].Data.(string) content = strings.TrimSpace(content) questions := strings.Split(content, "\n") var results index.SearchResults for _, question := range questions { - res, err := r.index.Query(ctx, question, option.WithTopK(int(r.topK))) - if err != nil { - return nil, err + res, queryErr := r.index.Query(ctx, question, option.WithTopK(int(r.topK))) + if queryErr != nil { + return nil, queryErr } results = append(results, res...) From 864c09b5c11c7542853842a848b808e3485fd685 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 28 Dec 2023 17:19:20 +0100 Subject: [PATCH 24/65] add cohere implementation --- llm/cohere/cohere.go | 125 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 124 insertions(+), 1 deletion(-) diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index ae9c9a22..73afd2cb 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -2,14 +2,18 @@ package cohere import ( "context" + "errors" "fmt" "os" + "strings" coherego "github.com/henomis/cohere-go" "github.com/henomis/cohere-go/model" "github.com/henomis/cohere-go/request" "github.com/henomis/cohere-go/response" "github.com/henomis/lingoose/chat" + "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/thread" ) type Model model.Model @@ -22,7 +26,7 @@ const ( ) const ( - DefaultMaxTokens = 20 + DefaultMaxTokens = 256 DefaultTemperature = 0.75 DefaultModel = ModelCommand ) @@ -34,10 +38,20 @@ type Cohere struct { maxTokens int verbose bool stop []string + cache *cache.Cache +} + +func (c *Cohere) WithCache(cache *cache.Cache) *Cohere { + c.cache = cache + return c } // NewCompletion returns a new completion LLM func NewCompletion() *Cohere { + return New() +} + +func New() *Cohere { return &Cohere{ client: coherego.New(os.Getenv("COHERE_API_KEY")), model: DefaultModel, @@ -120,3 +134,112 @@ func (c *Cohere) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { _ = prompt return "", fmt.Errorf("not implemented") } + +func getCacheableMessages(t *thread.Thread) []string { + userMessages := make([]*thread.Message, 0) + for _, message := range t.Messages { + if message.Role == thread.RoleUser { + userMessages = append(userMessages, message) + } else { + userMessages = make([]*thread.Message, 0) + } + } + + var messages []string + for _, message := range userMessages { + for _, content := range message.Contents { + if content.Type == thread.ContentTypeText { + messages = append(messages, content.Data.(string)) + } else { + messages = make([]string, 0) + break + } + } + } + + return messages +} + +func (o *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { + messages := getCacheableMessages(t) + cacheQuery := strings.Join(messages, "\n") + cacheResult, err := o.cache.Get(ctx, cacheQuery) + if err != nil { + return cacheResult, err + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(strings.Join(cacheResult.Answer, "\n")), + )) + + return cacheResult, nil +} + +func (o *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { + lastMessage := t.Messages[len(t.Messages)-1] + + if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { + return nil + } + + contents := make([]string, 0) + for _, content := range lastMessage.Contents { + if content.Type == thread.ContentTypeText { + contents = append(contents, content.Data.(string)) + } else { + contents = make([]string, 0) + break + } + } + + err := o.cache.Set(ctx, cacheResult.Embedding, strings.Join(contents, "\n")) + if err != nil { + return err + } + + return nil +} + +func (o *Cohere) Generate(ctx context.Context, t *thread.Thread) error { + if t == nil { + return nil + } + + var err error + var cacheResult *cache.Result + if o.cache != nil { + cacheResult, err = o.getCache(ctx, t) + if err == nil { + return nil + } else if !errors.Is(err, cache.ErrCacheMiss) { + return err + } + } + + completionQuery := "" + for _, message := range t.Messages { + for _, content := range message.Contents { + if content.Type == thread.ContentTypeText { + completionQuery += content.Data.(string) + "\n" + } + } + } + + completionResponse, err := o.Completion(ctx, completionQuery) + if err != nil { + return err + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(completionResponse), + )) + + if o.cache != nil { + err = o.setCache(ctx, t, cacheResult) + if err != nil { + return err + } + } + + return nil +} From c0628f21a61612f7030fe3b785e8e2161999c611 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 2 Jan 2024 12:55:54 +0100 Subject: [PATCH 25/65] fix linting --- llm/cohere/cohere.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index 73afd2cb..7009771a 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -160,10 +160,10 @@ func getCacheableMessages(t *thread.Thread) []string { return messages } -func (o *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { +func (c *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { messages := getCacheableMessages(t) cacheQuery := strings.Join(messages, "\n") - cacheResult, err := o.cache.Get(ctx, cacheQuery) + cacheResult, err := c.cache.Get(ctx, cacheQuery) if err != nil { return cacheResult, err } @@ -175,7 +175,7 @@ func (o *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, return cacheResult, nil } -func (o *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { +func (c *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { lastMessage := t.Messages[len(t.Messages)-1] if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { @@ -192,7 +192,7 @@ func (o *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *ca } } - err := o.cache.Set(ctx, cacheResult.Embedding, strings.Join(contents, "\n")) + err := c.cache.Set(ctx, cacheResult.Embedding, strings.Join(contents, "\n")) if err != nil { return err } @@ -200,15 +200,15 @@ func (o *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *ca return nil } -func (o *Cohere) Generate(ctx context.Context, t *thread.Thread) error { +func (c *Cohere) Generate(ctx context.Context, t *thread.Thread) error { if t == nil { return nil } var err error var cacheResult *cache.Result - if o.cache != nil { - cacheResult, err = o.getCache(ctx, t) + if c.cache != nil { + cacheResult, err = c.getCache(ctx, t) if err == nil { return nil } else if !errors.Is(err, cache.ErrCacheMiss) { @@ -225,7 +225,7 @@ func (o *Cohere) Generate(ctx context.Context, t *thread.Thread) error { } } - completionResponse, err := o.Completion(ctx, completionQuery) + completionResponse, err := c.Completion(ctx, completionQuery) if err != nil { return err } @@ -234,8 +234,8 @@ func (o *Cohere) Generate(ctx context.Context, t *thread.Thread) error { thread.NewTextContent(completionResponse), )) - if o.cache != nil { - err = o.setCache(ctx, t, cacheResult) + if c.cache != nil { + err = c.setCache(ctx, t, cacheResult) if err != nil { return err } From e7c8c1c5d2016270ee6ebe24482f4874395b16d3 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 9 Jan 2024 10:05:47 +0100 Subject: [PATCH 26/65] implement ollama --- assistant/assistant.go | 2 +- examples/llm/cache/main.go | 2 +- examples/llm/openai/thread/main.go | 2 +- examples/thread/main.go | 27 ++++ go.mod | 2 +- go.sum | 6 +- llm/ollama/formatter.go | 32 ++++ llm/ollama/models.go | 91 +++++++++++ llm/ollama/ollama.go | 233 +++++++++++++++++++++++++++++ rag/rag_fusion.go | 2 +- thread/thread.go | 2 +- 11 files changed, 393 insertions(+), 8 deletions(-) create mode 100644 examples/thread/main.go create mode 100644 llm/ollama/formatter.go create mode 100644 llm/ollama/models.go create mode 100644 llm/ollama/ollama.go diff --git a/assistant/assistant.go b/assistant/assistant.go index 9fc0552b..23757899 100644 --- a/assistant/assistant.go +++ b/assistant/assistant.go @@ -26,7 +26,7 @@ type RAG interface { func New(llm LLM) *Assistant { assistant := &Assistant{ llm: llm, - thread: thread.NewThread(), + thread: thread.New(), } return assistant diff --git a/examples/llm/cache/main.go b/examples/llm/cache/main.go index cd8675b4..faed97d0 100644 --- a/examples/llm/cache/main.go +++ b/examples/llm/cache/main.go @@ -29,7 +29,7 @@ func main() { } for _, question := range questions { - t := thread.NewThread().AddMessage( + t := thread.New().AddMessage( thread.NewUserMessage().AddContent( thread.NewTextContent(question), ), diff --git a/examples/llm/openai/thread/main.go b/examples/llm/openai/thread/main.go index e26f151e..422c3f9a 100644 --- a/examples/llm/openai/thread/main.go +++ b/examples/llm/openai/thread/main.go @@ -32,7 +32,7 @@ func main() { panic(err) } - t := thread.NewThread().AddMessage( + t := thread.New().AddMessage( thread.NewUserMessage().AddContent( thread.NewTextContent("Hello, I'm a user"), ).AddContent( diff --git a/examples/thread/main.go b/examples/thread/main.go new file mode 100644 index 00000000..e91a0582 --- /dev/null +++ b/examples/thread/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/llm/ollama" + "github.com/henomis/lingoose/thread" +) + +func main() { + t := thread.New() + t.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent("Hello"), + )) + + err := ollama.New().WithEndpoint("http://localhost:11434/api").WithModel("llama2"). + WithStream(true, func(s string) { + fmt.Print(s) + }).Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t.String()) + +} diff --git a/go.mod b/go.mod index 62512320..d4bb826e 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/henomis/milvus-go v0.0.4 github.com/henomis/pinecone-go v1.1.2 github.com/henomis/qdrant-go v1.1.0 + github.com/henomis/restclientgo v1.1.1 github.com/invopop/jsonschema v0.7.0 github.com/pkoukk/tiktoken-go v0.1.1 github.com/sashabaranov/go-openai v1.17.9 @@ -21,6 +22,5 @@ require ( require ( github.com/dlclark/regexp2 v1.8.1 // indirect github.com/gomodule/redigo v1.8.9 // indirect - github.com/henomis/restclientgo v1.1.0 // indirect github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect ) diff --git a/go.sum b/go.sum index d9524525..501d1dc1 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,10 @@ github.com/henomis/pinecone-go v1.1.2 h1:hOEhk/WAT7r3ini12fyyoAIT65nzeqmWG8t4YOb github.com/henomis/pinecone-go v1.1.2/go.mod h1:0u2hta1zssq/aaozgS6Nn61CMKlA95Pg6mVsE8zMNus= github.com/henomis/qdrant-go v1.1.0 h1:GU5R4ZZKeD0JXLZ0yv37FUzI0spGECRr2WlE3prmLIY= github.com/henomis/qdrant-go v1.1.0/go.mod h1:p274sBhQPDnfjCzJentWoNIGFadl5yIcLM4Xnmmwk9k= -github.com/henomis/restclientgo v1.1.0 h1:qNhBpTwYXuwfy6SL2EWjbfTrlUw70PZYmqQ4jewdu5E= -github.com/henomis/restclientgo v1.1.0/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= +github.com/henomis/restclientgo v1.1.1-alpha.2 h1:TqZ8AZoY7mVn95ffYaGNE7pp6LXwP49DTc9LhuUMDv4= +github.com/henomis/restclientgo v1.1.1-alpha.2/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= +github.com/henomis/restclientgo v1.1.1 h1:dHzKj4CD2rJqFkENwlMvXfWLlJtZFL0Yfza9NmTZ5W8= +github.com/henomis/restclientgo v1.1.1/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy770So= diff --git a/llm/ollama/formatter.go b/llm/ollama/formatter.go new file mode 100644 index 00000000..ed04f534 --- /dev/null +++ b/llm/ollama/formatter.go @@ -0,0 +1,32 @@ +package ollama + +import "github.com/henomis/lingoose/thread" + +func (o *Ollama) buildChatCompletionRequest(t *thread.Thread) *request { + return &request{ + Model: o.model, + Messages: threadToChatMessages(t), + } +} + +func threadToChatMessages(t *thread.Thread) []message { + chatMessages := make([]message, len(t.Messages)) + for i, m := range t.Messages { + chatMessages[i] = message{ + Role: threadRoleToOpenAIRole[m.Role], + } + + switch m.Role { + case thread.RoleUser, thread.RoleSystem, thread.RoleAssistant: + for _, content := range m.Contents { + if content.Type == thread.ContentTypeText { + chatMessages[i].Content += content.Data.(string) + "\n" + } + } + case thread.RoleTool: + continue + } + } + + return chatMessages +} diff --git a/llm/ollama/models.go b/llm/ollama/models.go new file mode 100644 index 00000000..ad94c865 --- /dev/null +++ b/llm/ollama/models.go @@ -0,0 +1,91 @@ +package ollama + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/henomis/restclientgo" +) + +type request struct { + Model string `json:"model"` + Messages []message `json:"messages"` + Stream bool `json:"stream"` + Options options `json:"options"` +} + +func (r *request) Path() (string, error) { + return "/chat", nil +} + +func (r *request) Encode() (io.Reader, error) { + jsonBytes, err := json.Marshal(r) + if err != nil { + return nil, err + } + + return bytes.NewReader(jsonBytes), nil +} + +func (r *request) ContentType() string { + return "application/json" +} + +type response struct { + HTTPStatusCode int `json:"-"` + acceptContentType string `json:"-"` + Model string `json:"model"` + CreatedAt string `json:"created_at"` +} + +type chatResponse struct { + response + AssistantMessage assistantMessage `json:"message"` +} + +type chatStreamResponse struct { + response + Done bool `json:"done"` + Message message `json:"message"` +} + +type assistantMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +func (r *response) SetAcceptContentType(contentType string) { + r.acceptContentType = contentType +} + +func (r *response) Decode(body io.Reader) error { + return json.NewDecoder(body).Decode(r) +} + +func (r *response) SetBody(body io.Reader) error { + return nil +} + +func (r *response) AcceptContentType() string { + if r.acceptContentType != "" { + return r.acceptContentType + } + return "application/json" +} + +func (r *response) SetStatusCode(code int) error { + r.HTTPStatusCode = code + return nil +} + +func (r *response) SetHeaders(headers restclientgo.Headers) error { return nil } + +type message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type options struct { + Temperature float64 `json:"temperature"` +} diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go new file mode 100644 index 00000000..21fe8322 --- /dev/null +++ b/llm/ollama/ollama.go @@ -0,0 +1,233 @@ +package ollama + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/henomis/lingoose/llm/cache" + "github.com/henomis/lingoose/thread" + "github.com/henomis/restclientgo" +) + +const ( + defaultModel = "llama2" + ndjsonContentType = "application/x-ndjson" + defaultEndpoint = "http://localhost:11434/api" +) + +var ( + ErrOllamaChat = fmt.Errorf("ollama chat error") +) + +var threadRoleToOpenAIRole = map[thread.Role]string{ + thread.RoleSystem: "system", + thread.RoleUser: "user", + thread.RoleAssistant: "assistant", +} + +type StreamCallbackFn func(string) + +type Ollama struct { + model string + restClient *restclientgo.RestClient + streamCallbackFn StreamCallbackFn + cache *cache.Cache +} + +func New() *Ollama { + return &Ollama{ + restClient: restclientgo.New(defaultEndpoint), + model: defaultModel, + } +} + +func (o *Ollama) WithEndpoint(endpoint string) *Ollama { + o.restClient.SetEndpoint(endpoint) + return o +} + +func (o *Ollama) WithModel(model string) *Ollama { + o.model = model + return o +} + +func (o *Ollama) WithStream(enable bool, callbackFn StreamCallbackFn) *Ollama { + if !enable { + o.streamCallbackFn = nil + } else { + o.streamCallbackFn = callbackFn + } + + return o +} + +func (o *Ollama) WithCache(cache *cache.Cache) *Ollama { + o.cache = cache + return o +} + +func getCacheableMessages(t *thread.Thread) []string { + userMessages := make([]*thread.Message, 0) + for _, message := range t.Messages { + if message.Role == thread.RoleUser { + userMessages = append(userMessages, message) + } else { + userMessages = make([]*thread.Message, 0) + } + } + + var messages []string + for _, message := range userMessages { + for _, content := range message.Contents { + if content.Type == thread.ContentTypeText { + messages = append(messages, content.Data.(string)) + } else { + messages = make([]string, 0) + break + } + } + } + + return messages +} + +func (o *Ollama) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { + messages := getCacheableMessages(t) + cacheQuery := strings.Join(messages, "\n") + cacheResult, err := o.cache.Get(ctx, cacheQuery) + if err != nil { + return cacheResult, err + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(strings.Join(cacheResult.Answer, "\n")), + )) + + return cacheResult, nil +} + +func (o *Ollama) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { + lastMessage := t.Messages[len(t.Messages)-1] + + if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { + return nil + } + + contents := make([]string, 0) + for _, content := range lastMessage.Contents { + if content.Type == thread.ContentTypeText { + contents = append(contents, content.Data.(string)) + } else { + contents = make([]string, 0) + break + } + } + + err := o.cache.Set(ctx, cacheResult.Embedding, strings.Join(contents, "\n")) + if err != nil { + return err + } + + return nil +} + +func (o *Ollama) Generate(ctx context.Context, t *thread.Thread) error { + if t == nil { + return nil + } + + var err error + var cacheResult *cache.Result + if o.cache != nil { + cacheResult, err = o.getCache(ctx, t) + if err == nil { + return nil + } else if !errors.Is(err, cache.ErrCacheMiss) { + return fmt.Errorf("%w: %w", ErrOllamaChat, err) + } + } + + chatRequest := o.buildChatCompletionRequest(t) + + if o.streamCallbackFn != nil { + err = o.stream(ctx, t, chatRequest) + } else { + err = o.generate(ctx, t, chatRequest) + } + + if err != nil { + return err + } + + if o.cache != nil { + err = o.setCache(ctx, t, cacheResult) + if err != nil { + return fmt.Errorf("%w: %w", ErrOllamaChat, err) + } + } + + return nil +} + +func (o *Ollama) generate(ctx context.Context, t *thread.Thread, chatRequest *request) error { + var chatResponse chatResponse + + o.restClient.SetStreamCallback(nil) + + err := o.restClient.Post( + ctx, + chatRequest, + &chatResponse, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrOllamaChat, err) + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(chatResponse.AssistantMessage.Content), + )) + + return nil +} + +func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *request) error { + chatResponse := &chatStreamResponse{} + var assistantMessage string + + chatResponse.SetAcceptContentType(ndjsonContentType) + o.restClient.SetStreamCallback( + func(data []byte) error { + var chatResponse chatStreamResponse + + err := json.Unmarshal(data, &chatResponse) + if err != nil { + return err + } + + assistantMessage += chatResponse.Message.Content + o.streamCallbackFn(chatResponse.Message.Content) + + return nil + }, + ) + + chatRequest.Stream = true + + err := o.restClient.Post( + ctx, + chatRequest, + chatResponse, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrOllamaChat, err) + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(assistantMessage), + )) + + return nil +} diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go index 67783d2d..950725aa 100644 --- a/rag/rag_fusion.go +++ b/rag/rag_fusion.go @@ -29,7 +29,7 @@ func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResu return nil, fmt.Errorf("llm is not set") } - t := thread.NewThread().AddMessages( + t := thread.New().AddMessages( thread.NewSystemMessage().AddContent( thread.NewTextContent( ragFusionPrompts[0], diff --git a/thread/thread.go b/thread/thread.go index 6d18df55..8556ae6d 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -120,7 +120,7 @@ func (t *Thread) CountMessages() int { return len(t.Messages) } -func NewThread() *Thread { +func New() *Thread { return &Thread{} } From a35ebfec911b4cfe4725cefc918bba47794b1fa7 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 9 Jan 2024 10:09:40 +0100 Subject: [PATCH 27/65] fix --- llm/ollama/models.go | 4 ++-- llm/ollama/ollama.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/llm/ollama/models.go b/llm/ollama/models.go index ad94c865..c325d33d 100644 --- a/llm/ollama/models.go +++ b/llm/ollama/models.go @@ -63,7 +63,7 @@ func (r *response) Decode(body io.Reader) error { return json.NewDecoder(body).Decode(r) } -func (r *response) SetBody(body io.Reader) error { +func (r *response) SetBody(_ io.Reader) error { return nil } @@ -79,7 +79,7 @@ func (r *response) SetStatusCode(code int) error { return nil } -func (r *response) SetHeaders(headers restclientgo.Headers) error { return nil } +func (r *response) SetHeaders(_ restclientgo.Headers) error { return nil } type message struct { Role string `json:"role"` diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index 21fe8322..f643167e 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -194,10 +194,10 @@ func (o *Ollama) generate(ctx context.Context, t *thread.Thread, chatRequest *re } func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *request) error { - chatResponse := &chatStreamResponse{} + streamChatResponse := &chatStreamResponse{} var assistantMessage string - chatResponse.SetAcceptContentType(ndjsonContentType) + streamChatResponse.SetAcceptContentType(ndjsonContentType) o.restClient.SetStreamCallback( func(data []byte) error { var chatResponse chatStreamResponse @@ -219,7 +219,7 @@ func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *requ err := o.restClient.Post( ctx, chatRequest, - chatResponse, + streamChatResponse, ) if err != nil { return fmt.Errorf("%w: %w", ErrOllamaChat, err) From 44060efbf97533012096ac78bf426eca59e3ba48 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 9 Jan 2024 10:11:06 +0100 Subject: [PATCH 28/65] fix --- llm/ollama/ollama.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index f643167e..02f23115 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -173,42 +173,42 @@ func (o *Ollama) Generate(ctx context.Context, t *thread.Thread) error { } func (o *Ollama) generate(ctx context.Context, t *thread.Thread, chatRequest *request) error { - var chatResponse chatResponse + var response chatResponse o.restClient.SetStreamCallback(nil) err := o.restClient.Post( ctx, chatRequest, - &chatResponse, + &response, ) if err != nil { return fmt.Errorf("%w: %w", ErrOllamaChat, err) } t.AddMessage(thread.NewAssistantMessage().AddContent( - thread.NewTextContent(chatResponse.AssistantMessage.Content), + thread.NewTextContent(response.AssistantMessage.Content), )) return nil } func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *request) error { - streamChatResponse := &chatStreamResponse{} + response := &chatStreamResponse{} var assistantMessage string - streamChatResponse.SetAcceptContentType(ndjsonContentType) + response.SetAcceptContentType(ndjsonContentType) o.restClient.SetStreamCallback( func(data []byte) error { - var chatResponse chatStreamResponse + var streamResponse chatStreamResponse - err := json.Unmarshal(data, &chatResponse) + err := json.Unmarshal(data, &streamResponse) if err != nil { return err } - assistantMessage += chatResponse.Message.Content - o.streamCallbackFn(chatResponse.Message.Content) + assistantMessage += streamResponse.Message.Content + o.streamCallbackFn(streamResponse.Message.Content) return nil }, @@ -219,7 +219,7 @@ func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *requ err := o.restClient.Post( ctx, chatRequest, - streamChatResponse, + response, ) if err != nil { return fmt.Errorf("%w: %w", ErrOllamaChat, err) From ad2143a240995c5b4d4753f991e62edab375c3e7 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 23 Jan 2024 11:15:25 +0100 Subject: [PATCH 29/65] fix --- rag/rag.go | 4 ++++ rag/rag_fusion.go | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rag/rag.go b/rag/rag.go index 61606c88..2de1e8e6 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -75,6 +75,10 @@ func (r *RAG) AddFiles(ctx context.Context, filePath ...string) error { return nil } +func (r *RAG) AddDocuments(ctx context.Context, documents ...document.Document) error { + return r.index.LoadFromDocuments(ctx, documents) +} + func (r *RAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { results, err := r.index.Query(ctx, query, option.WithTopK(int(r.topK))) return results, err diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go index 950725aa..3c0c71fe 100644 --- a/rag/rag_fusion.go +++ b/rag/rag_fusion.go @@ -52,8 +52,6 @@ func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResu return nil, err } - fmt.Println(t) - lastMessage := t.Messages[len(t.Messages)-1] content, _ := lastMessage.Contents[0].Data.(string) content = strings.TrimSpace(content) From b63864ef8b454468726fc0a012ca975dccf79f16 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 23 Jan 2024 11:39:55 +0100 Subject: [PATCH 30/65] chore: add new Loader Method --- loader/csv.go | 5 +++++ loader/hf_image_to_text.go | 5 +++++ loader/hf_speech_recognition.go | 5 +++++ loader/libreoffice.go | 5 +++++ loader/pdf_to_text.go | 5 +++++ loader/pubmed.go | 5 +++++ loader/tesseract.go | 5 +++++ loader/text.go | 5 +++++ loader/whisper.go | 5 +++++ loader/whispercpp.go | 5 +++++ loader/youtube-dl.go | 5 +++++ 11 files changed, 55 insertions(+) diff --git a/loader/csv.go b/loader/csv.go index b38c972e..842a68ac 100644 --- a/loader/csv.go +++ b/loader/csv.go @@ -58,6 +58,11 @@ func (c *CSVLoader) Load(ctx context.Context) ([]document.Document, error) { return documents, nil } +func (c *CSVLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + c.filename = source + return c.Load(ctx) +} + func (c *CSVLoader) validate() error { fileStat, err := os.Stat(c.filename) if err != nil { diff --git a/loader/hf_image_to_text.go b/loader/hf_image_to_text.go index 133525c5..78a0a39c 100644 --- a/loader/hf_image_to_text.go +++ b/loader/hf_image_to_text.go @@ -95,6 +95,11 @@ func (h *HFImageToText) Load(ctx context.Context) ([]document.Document, error) { return documents, nil } +func (h *HFImageToText) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + h.mediaFile = source + return h.Load(ctx) +} + func hfMediaHTTPCall(ctx context.Context, token, model, mediaFile string) ([]byte, error) { buf, err := os.ReadFile(mediaFile) if err != nil { diff --git a/loader/hf_speech_recognition.go b/loader/hf_speech_recognition.go index 3943795c..cd5efe69 100644 --- a/loader/hf_speech_recognition.go +++ b/loader/hf_speech_recognition.go @@ -84,3 +84,8 @@ func (h *HFSpeechRecognition) Load(ctx context.Context) ([]document.Document, er return documents, nil } + +func (h *HFSpeechRecognition) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + h.mediaFile = source + return h.Load(ctx) +} diff --git a/loader/libreoffice.go b/loader/libreoffice.go index 426305bb..2194e0ef 100644 --- a/loader/libreoffice.go +++ b/loader/libreoffice.go @@ -68,6 +68,11 @@ func (l *LibreOfficeLoader) Load(ctx context.Context) ([]document.Document, erro return documents, nil } +func (l *LibreOfficeLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + l.filename = source + return l.Load(ctx) +} + func (l *LibreOfficeLoader) loadFile(ctx context.Context) ([]document.Document, error) { libreOfficeArgs := append(l.libreOfficeArgs, l.filename) diff --git a/loader/pdf_to_text.go b/loader/pdf_to_text.go index 8a92f8a3..83c31464 100644 --- a/loader/pdf_to_text.go +++ b/loader/pdf_to_text.go @@ -64,6 +64,11 @@ func (p *PDFLoader) Load(ctx context.Context) ([]document.Document, error) { return documents, nil } +func (p *PDFLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + p.path = source + return p.Load(ctx) +} + func (p *PDFLoader) loadFile(ctx context.Context) ([]document.Document, error) { //nolint:gosec out, err := exec.CommandContext(ctx, p.pdfToTextPath, p.path, "-").Output() diff --git a/loader/pubmed.go b/loader/pubmed.go index 99e3b891..51d4b285 100644 --- a/loader/pubmed.go +++ b/loader/pubmed.go @@ -57,6 +57,11 @@ func (p *PubMedLoader) Load(ctx context.Context) ([]document.Document, error) { return documens, nil } +func (p *PubMedLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + p.pubMedIDs = []string{source} + return p.Load(ctx) +} + func (p *PubMedLoader) load(ctx context.Context, pubMedID string) (*document.Document, error) { url := fmt.Sprintf(pubMedBioCURLFormat, pubMedID) diff --git a/loader/tesseract.go b/loader/tesseract.go index 382a34a4..8689d6b6 100644 --- a/loader/tesseract.go +++ b/loader/tesseract.go @@ -68,6 +68,11 @@ func (l *TesseractLoader) Load(ctx context.Context) ([]document.Document, error) return documents, nil } +func (l *TesseractLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + l.filename = source + return l.Load(ctx) +} + func (l *TesseractLoader) loadFile(ctx context.Context) ([]document.Document, error) { tesseractArgs := []string{l.filename, "stdout"} tesseractArgs = append(tesseractArgs, l.tesseractArgs...) diff --git a/loader/text.go b/loader/text.go index 4ef42832..34869079 100644 --- a/loader/text.go +++ b/loader/text.go @@ -54,6 +54,11 @@ func (t *TextLoader) Load(ctx context.Context) ([]document.Document, error) { return documents, nil } +func (t *TextLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + t.filename = source + return t.Load(ctx) +} + func (t *TextLoader) validate() error { if t.metadata == nil { t.metadata = make(types.Meta) diff --git a/loader/whisper.go b/loader/whisper.go index a94bee70..93015ceb 100644 --- a/loader/whisper.go +++ b/loader/whisper.go @@ -61,3 +61,8 @@ func (w *WhisperLoader) Load(ctx context.Context) ([]document.Document, error) { return documents, nil } + +func (w *WhisperLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + w.filename = source + return w.Load(ctx) +} diff --git a/loader/whispercpp.go b/loader/whispercpp.go index f442d01f..92cfc685 100644 --- a/loader/whispercpp.go +++ b/loader/whispercpp.go @@ -97,6 +97,11 @@ func (w *WhisperCppLoader) Load(ctx context.Context) ([]document.Document, error return documents, nil } +func (w *WhisperCppLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + w.filename = source + return w.Load(ctx) +} + func (w *WhisperCppLoader) convertAndTrascribe(ctx context.Context) (string, error) { ffmpegArgs := []string{"-i", w.filename} ffmpegArgs = append(ffmpegArgs, w.ffmpegArgs...) diff --git a/loader/youtube-dl.go b/loader/youtube-dl.go index 1301b21c..fa58d0b0 100644 --- a/loader/youtube-dl.go +++ b/loader/youtube-dl.go @@ -76,6 +76,11 @@ func (y *YoutubeDLLoader) Load(ctx context.Context) ([]document.Document, error) return documents, nil } +func (y *YoutubeDLLoader) LoadFromSource(ctx context.Context, source string) ([]document.Document, error) { + y.path = source + return y.Load(ctx) +} + func (y *YoutubeDLLoader) loadVideo(ctx context.Context) ([]document.Document, error) { tempDir, err := os.MkdirTemp("", "youtube-dl") if err != nil { From bd00054bfccfa63c9b4a5c26c5552e6ea02aa059 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 23 Jan 2024 16:03:09 +0100 Subject: [PATCH 31/65] chore: assistant must run a thread --- assistant/assistant.go | 49 +++++++++++++++++++++++++------------- examples/assistant/main.go | 15 ++++++++---- llm/cohere/cohere.go | 2 +- llm/ollama/ollama.go | 2 +- llm/openai/openai.go | 2 +- rag/rag_fusion.go | 4 +++- 6 files changed, 50 insertions(+), 24 deletions(-) diff --git a/assistant/assistant.go b/assistant/assistant.go index 23757899..7d398759 100644 --- a/assistant/assistant.go +++ b/assistant/assistant.go @@ -2,7 +2,6 @@ package assistant import ( "context" - "fmt" "github.com/henomis/lingoose/index" "github.com/henomis/lingoose/prompt" @@ -42,36 +41,48 @@ func (a *Assistant) WithRAG(rag RAG) *Assistant { return a } -func (a *Assistant) Run(ctx context.Context, message any) error { - var err error - if _, ok := message.(string); !ok { - return fmt.Errorf("message must be a string") +func (a *Assistant) Run(ctx context.Context) error { + if a.thread == nil { + return nil } if a.rag != nil { - message, err = a.generateRAGMessage(ctx, message.(string)) + err := a.generateRAGMessage(ctx) if err != nil { return err } } - a.thread.AddMessage(thread.NewUserMessage().AddContent( - thread.NewTextContent( - message.(string), - ), - )) - return a.llm.Generate(ctx, a.thread) } +func (a *Assistant) RunWithThread(ctx context.Context, thread *thread.Thread) error { + a.thread = thread + return a.Run(ctx) +} + func (a *Assistant) Thread() *thread.Thread { return a.thread } -func (a *Assistant) generateRAGMessage(ctx context.Context, query string) (string, error) { +func (a *Assistant) generateRAGMessage(ctx context.Context) error { + lastMessage := a.thread.LastMessage() + if lastMessage.Role != thread.RoleUser || len(lastMessage.Contents) == 0 { + return nil + } + + query := "" + for _, content := range lastMessage.Contents { + if content.Type == thread.ContentTypeText { + query += content.Data.(string) + "\n" + } else { + continue + } + } + searchResults, err := a.rag.Retrieve(ctx, query) if err != nil { - return "", err + return err } context := "" @@ -86,8 +97,14 @@ func (a *Assistant) generateRAGMessage(ctx context.Context, query string) (strin "context": context, }) if err != nil { - return "", err + return err } - return ragPrompt.String(), nil + a.thread.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent( + ragPrompt.String(), + ), + )) + + return nil } diff --git a/examples/assistant/main.go b/examples/assistant/main.go index 03f1863f..77d6c61e 100644 --- a/examples/assistant/main.go +++ b/examples/assistant/main.go @@ -11,6 +11,7 @@ import ( "github.com/henomis/lingoose/index/vectordb/jsondb" "github.com/henomis/lingoose/llm/openai" "github.com/henomis/lingoose/rag" + "github.com/henomis/lingoose/thread" ) // download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt @@ -22,11 +23,11 @@ func main() { openaiembedder.New(openaiembedder.AdaEmbeddingV2), ), openai.New().WithTemperature(0), - ) + ).WithTopK(3) _, err := os.Stat("db.json") if os.IsNotExist(err) { - err = r.AddFiles(context.Background(), "state_of_the_union.txt") + err = r.AddSources(context.Background(), "state_of_the_union.txt") if err != nil { panic(err) } @@ -34,9 +35,15 @@ func main() { a := assistant.New( openai.New().WithTemperature(0), - ).WithRAG(r) + ).WithRAG(r).WithThread( + thread.New().AddMessages( + thread.NewUserMessage().AddContent( + thread.NewTextContent("what is the purpose of NATO?"), + ), + ), + ) - err = a.Run(context.Background(), "what is the purpose of NATO?") + err = a.Run(context.Background()) if err != nil { panic(err) } diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index 7009771a..c59d5871 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -176,7 +176,7 @@ func (c *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, } func (c *Cohere) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { - lastMessage := t.Messages[len(t.Messages)-1] + lastMessage := t.LastMessage() if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { return nil diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index 02f23115..6ed299fe 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -110,7 +110,7 @@ func (o *Ollama) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, } func (o *Ollama) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { - lastMessage := t.Messages[len(t.Messages)-1] + lastMessage := t.LastMessage() if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { return nil diff --git a/llm/openai/openai.go b/llm/openai/openai.go index 27e1d896..c062236e 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -164,7 +164,7 @@ func (o *OpenAI) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, } func (o *OpenAI) setCache(ctx context.Context, t *thread.Thread, cacheResult *cache.Result) error { - lastMessage := t.Messages[len(t.Messages)-1] + lastMessage := t.LastMessage() if lastMessage.Role != thread.RoleAssistant || len(lastMessage.Contents) == 0 { return nil diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go index 3c0c71fe..cbffdbda 100644 --- a/rag/rag_fusion.go +++ b/rag/rag_fusion.go @@ -52,7 +52,9 @@ func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResu return nil, err } - lastMessage := t.Messages[len(t.Messages)-1] + fmt.Println(t) + + lastMessage := t.LastMessage() content, _ := lastMessage.Contents[0].Data.(string) content = strings.TrimSpace(content) questions := strings.Split(content, "\n") From 14b9be47ead05fd15a0a76433b1c6d04d2414896 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 23 Jan 2024 16:15:25 +0100 Subject: [PATCH 32/65] refactor code --- llm/cohere/cohere.go | 27 +--------------------- llm/ollama/ollama.go | 27 +--------------------- llm/openai/openai.go | 27 +--------------------- rag/rag.go | 54 ++++++++++++++++++++++++++++++-------------- thread/thread.go | 31 +++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 95 deletions(-) diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index c59d5871..a3c1ac57 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -135,33 +135,8 @@ func (c *Cohere) Chat(ctx context.Context, prompt *chat.Chat) (string, error) { return "", fmt.Errorf("not implemented") } -func getCacheableMessages(t *thread.Thread) []string { - userMessages := make([]*thread.Message, 0) - for _, message := range t.Messages { - if message.Role == thread.RoleUser { - userMessages = append(userMessages, message) - } else { - userMessages = make([]*thread.Message, 0) - } - } - - var messages []string - for _, message := range userMessages { - for _, content := range message.Contents { - if content.Type == thread.ContentTypeText { - messages = append(messages, content.Data.(string)) - } else { - messages = make([]string, 0) - break - } - } - } - - return messages -} - func (c *Cohere) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { - messages := getCacheableMessages(t) + messages := t.UserQuery() cacheQuery := strings.Join(messages, "\n") cacheResult, err := c.cache.Get(ctx, cacheQuery) if err != nil { diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index 6ed299fe..598fda6e 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -69,33 +69,8 @@ func (o *Ollama) WithCache(cache *cache.Cache) *Ollama { return o } -func getCacheableMessages(t *thread.Thread) []string { - userMessages := make([]*thread.Message, 0) - for _, message := range t.Messages { - if message.Role == thread.RoleUser { - userMessages = append(userMessages, message) - } else { - userMessages = make([]*thread.Message, 0) - } - } - - var messages []string - for _, message := range userMessages { - for _, content := range message.Contents { - if content.Type == thread.ContentTypeText { - messages = append(messages, content.Data.(string)) - } else { - messages = make([]string, 0) - break - } - } - } - - return messages -} - func (o *Ollama) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { - messages := getCacheableMessages(t) + messages := t.UserQuery() cacheQuery := strings.Join(messages, "\n") cacheResult, err := o.cache.Get(ctx, cacheQuery) if err != nil { diff --git a/llm/openai/openai.go b/llm/openai/openai.go index c062236e..54f2cc97 100644 --- a/llm/openai/openai.go +++ b/llm/openai/openai.go @@ -123,33 +123,8 @@ func New() *OpenAI { } } -func getCacheableMessages(t *thread.Thread) []string { - userMessages := make([]*thread.Message, 0) - for _, message := range t.Messages { - if message.Role == thread.RoleUser { - userMessages = append(userMessages, message) - } else { - userMessages = make([]*thread.Message, 0) - } - } - - var messages []string - for _, message := range userMessages { - for _, content := range message.Contents { - if content.Type == thread.ContentTypeText { - messages = append(messages, content.Data.(string)) - } else { - messages = make([]string, 0) - break - } - } - } - - return messages -} - func (o *OpenAI) getCache(ctx context.Context, t *thread.Thread) (*cache.Result, error) { - messages := getCacheableMessages(t) + messages := t.UserQuery() cacheQuery := strings.Join(messages, "\n") cacheResult, err := o.cache.Get(ctx, cacheQuery) if err != nil { diff --git a/rag/rag.go b/rag/rag.go index 2de1e8e6..56aebd10 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -3,7 +3,7 @@ package rag import ( "context" "fmt" - "path/filepath" + "regexp" "github.com/henomis/lingoose/document" "github.com/henomis/lingoose/index" @@ -23,11 +23,16 @@ type LLM interface { Generate(context.Context, *thread.Thread) error } +type Loader interface { + LoadFromSource(context.Context, string) ([]document.Document, error) +} + type RAG struct { index *index.Index chunkSize uint chunkOverlap uint topK uint + loaders map[*regexp.Regexp]Loader // this map a regexp as string to a loader } type Fusion struct { @@ -36,12 +41,15 @@ type Fusion struct { } func New(index *index.Index) *RAG { - return &RAG{ + rag := &RAG{ index: index, chunkSize: defaultChunkSize, chunkOverlap: defaultChunkOverlap, topK: defaultTopK, + loaders: make(map[*regexp.Regexp]Loader), } + + return rag.withDefaultLoaders() } func (r *RAG) WithChunkSize(chunkSize uint) *RAG { @@ -59,9 +67,22 @@ func (r *RAG) WithTopK(topK uint) *RAG { return r } -func (r *RAG) AddFiles(ctx context.Context, filePath ...string) error { - for _, f := range filePath { - documents, err := r.addFile(ctx, f) +func (r *RAG) withDefaultLoaders() *RAG { + r.loaders[regexp.MustCompile(`.*\.pdf`)] = loader.NewPDFToTextLoader("") + r.loaders[regexp.MustCompile(`.*\.docx`)] = loader.NewLibreOfficeLoader("") + r.loaders[regexp.MustCompile(`.*\.txt`)] = loader.NewTextLoader("", nil) + + return r +} + +func (r *RAG) WithLoader(sourceRegexp *regexp.Regexp, loader Loader) *RAG { + r.loaders[sourceRegexp] = loader + return r +} + +func (r *RAG) AddSources(ctx context.Context, sources ...string) error { + for _, source := range sources { + documents, err := r.addSource(ctx, source) if err != nil { return err } @@ -84,20 +105,19 @@ func (r *RAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, return results, err } -func (r *RAG) addFile(ctx context.Context, filePath string) ([]document.Document, error) { - var documents []document.Document - var err error - switch filepath.Ext(filePath) { - case ".pdf": - documents, err = loader.NewPDFToTextLoader(filePath).Load(ctx) - case ".docx": - documents, err = loader.NewLibreOfficeLoader(filePath).Load(ctx) - case ".txt": - documents, err = loader.NewTextLoader(filePath, nil).Load(ctx) - default: - return nil, fmt.Errorf("unsupported file type") +func (r *RAG) addSource(ctx context.Context, source string) ([]document.Document, error) { + var sourceLoader Loader + for regexpStr, loader := range r.loaders { + if regexpStr.MatchString(source) { + sourceLoader = loader + } + } + + if sourceLoader == nil { + return nil, fmt.Errorf("unsupported source type") } + documents, err := sourceLoader.LoadFromSource(ctx, source) if err != nil { return nil, err } diff --git a/thread/thread.go b/thread/thread.go index 8556ae6d..f1cce97d 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -150,3 +150,34 @@ func (t *Thread) String() string { } return str } + +// LastMessage returns the last message in the thread. +func (t *Thread) LastMessage() *Message { + return t.Messages[len(t.Messages)-1] +} + +// UserQuery returns the last user messages as a slice of strings. +func (t *Thread) UserQuery() []string { + userMessages := make([]*Message, 0) + for _, message := range t.Messages { + if message.Role == RoleUser { + userMessages = append(userMessages, message) + } else { + userMessages = make([]*Message, 0) + } + } + + var messages []string + for _, message := range userMessages { + for _, content := range message.Contents { + if content.Type == ContentTypeText { + messages = append(messages, content.Data.(string)) + } else { + messages = make([]string, 0) + break + } + } + } + + return messages +} From f849e6bf913d450a338dd98b34a5932cf400b795 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 23 Jan 2024 16:31:04 +0100 Subject: [PATCH 33/65] fix --- examples/llm/openai/thread/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/llm/openai/thread/main.go b/examples/llm/openai/thread/main.go index 422c3f9a..cc21df67 100644 --- a/examples/llm/openai/thread/main.go +++ b/examples/llm/openai/thread/main.go @@ -13,7 +13,7 @@ type Answer struct { } func getAnswer(a Answer) string { - return a.Answer + return "🦜 ☠️ " + a.Answer } func newStr(str string) *string { From 32877ee0c13cf4e71a3324dc6a7cbe569111ebe8 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 25 Jan 2024 17:59:49 +0100 Subject: [PATCH 34/65] use pinecone-go v2 --- examples/embeddings/pinecone/main.go | 11 +- go.mod | 2 +- go.sum | 6 +- index/vectordb/pinecone/pinecone.go | 152 +++++++++++++++++---------- 4 files changed, 107 insertions(+), 64 deletions(-) diff --git a/examples/embeddings/pinecone/main.go b/examples/embeddings/pinecone/main.go index 6a51f12e..8073b0a3 100644 --- a/examples/embeddings/pinecone/main.go +++ b/examples/embeddings/pinecone/main.go @@ -17,7 +17,8 @@ import ( // download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt func main() { - + replicas := 1 + shards := 1 index := index.New( pineconedb.New( pineconedb.Options{ @@ -25,9 +26,13 @@ func main() { Namespace: "test-namespace", CreateIndexOptions: &pineconedb.CreateIndexOptions{ Dimension: 1536, - Replicas: 1, Metric: "cosine", - PodType: "p1.x1", + Pod: &pineconedb.Pod{ + Replicas: &replicas, + Shards: &shards, + PodType: "s1.x1", + Environment: "gcp-starter", + }, }, }, ), diff --git a/go.mod b/go.mod index d4bb826e..16bd7b86 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/uuid v1.3.0 github.com/henomis/cohere-go v1.0.1 github.com/henomis/milvus-go v0.0.4 - github.com/henomis/pinecone-go v1.1.2 + github.com/henomis/pinecone-go/v2 v2.0.0 github.com/henomis/qdrant-go v1.1.0 github.com/henomis/restclientgo v1.1.1 github.com/invopop/jsonschema v0.7.0 diff --git a/go.sum b/go.sum index 501d1dc1..7ede42f0 100644 --- a/go.sum +++ b/go.sum @@ -13,12 +13,10 @@ github.com/henomis/cohere-go v1.0.1 h1:a47gIN29tqAl4yBTAT+BzQMjsWG94Fz07u9AE4Md+ github.com/henomis/cohere-go v1.0.1/go.mod h1:F6D33jlWle6pbGdf9Fm2bteaOOQOO1cQtFnlFfj+ZXY= github.com/henomis/milvus-go v0.0.4 h1:ArddXRJx/EGdQ75gB7TyEzD4Z4BqXzW16p8jRcjJOhs= github.com/henomis/milvus-go v0.0.4/go.mod h1:nZ/NvDOLoGl7FQrYSm0JfeefPBVXph9PpE4y3lPpbj4= -github.com/henomis/pinecone-go v1.1.2 h1:hOEhk/WAT7r3ini12fyyoAIT65nzeqmWG8t4YObvOZ8= -github.com/henomis/pinecone-go v1.1.2/go.mod h1:0u2hta1zssq/aaozgS6Nn61CMKlA95Pg6mVsE8zMNus= +github.com/henomis/pinecone-go/v2 v2.0.0 h1:HdAX0nGzBagL6ubn17utIz3FGwXfignBovZesOjXiEI= +github.com/henomis/pinecone-go/v2 v2.0.0/go.mod h1:enUHclL18IBnq6POF20hd7YMIawUQZ9NznJ22NJD0/w= github.com/henomis/qdrant-go v1.1.0 h1:GU5R4ZZKeD0JXLZ0yv37FUzI0spGECRr2WlE3prmLIY= github.com/henomis/qdrant-go v1.1.0/go.mod h1:p274sBhQPDnfjCzJentWoNIGFadl5yIcLM4Xnmmwk9k= -github.com/henomis/restclientgo v1.1.1-alpha.2 h1:TqZ8AZoY7mVn95ffYaGNE7pp6LXwP49DTc9LhuUMDv4= -github.com/henomis/restclientgo v1.1.1-alpha.2/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/henomis/restclientgo v1.1.1 h1:dHzKj4CD2rJqFkENwlMvXfWLlJtZFL0Yfza9NmTZ5W8= github.com/henomis/restclientgo v1.1.1/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= diff --git a/index/vectordb/pinecone/pinecone.go b/index/vectordb/pinecone/pinecone.go index df3d3564..9d0f4059 100644 --- a/index/vectordb/pinecone/pinecone.go +++ b/index/vectordb/pinecone/pinecone.go @@ -9,9 +9,9 @@ import ( "github.com/google/uuid" "github.com/henomis/lingoose/index" "github.com/henomis/lingoose/index/option" - pineconego "github.com/henomis/pinecone-go" - pineconegorequest "github.com/henomis/pinecone-go/request" - pineconegoresponse "github.com/henomis/pinecone-go/response" + pineconego "github.com/henomis/pinecone-go/v2" + pineconegorequest "github.com/henomis/pinecone-go/v2/request" + pineconegoresponse "github.com/henomis/pinecone-go/v2/response" ) var _ index.VectorDB = &DB{} @@ -19,17 +19,44 @@ var _ index.VectorDB = &DB{} type DB struct { pineconeClient *pineconego.PineconeGo indexName string - projectID *string namespace string + indexHost *string createIndexOptions *CreateIndexOptions } type CreateIndexOptions struct { - Dimension int - Replicas int - Metric string - PodType string + Dimension int + Metric string + Serverless *Serverless + Pod *Pod +} + +type Serverless struct { + Cloud ServerlessCloud + Region string +} + +type ServerlessCloud string + +const ( + ServerlessCloudAWS ServerlessCloud = "aws" + ServerlessCloudGCP ServerlessCloud = "gcp" + ServerlessCloudAzure ServerlessCloud = "azure" +) + +type Pod struct { + Environment string + Replicas *int + PodType string + Pods *int + Shards *int + MetadataConfig *MetadataConfig + SourceCollection *string +} + +type MetadataConfig struct { + Indexed []string } type Options struct { @@ -40,9 +67,8 @@ type Options struct { func New(options Options) *DB { apiKey := os.Getenv("PINECONE_API_KEY") - environment := os.Getenv("PINECONE_ENVIRONMENT") - pineconeClient := pineconego.New(environment, apiKey) + pineconeClient := pineconego.New(apiKey) return &DB{ pineconeClient: pineconeClient, @@ -52,25 +78,44 @@ func New(options Options) *DB { } } -func (d *DB) WithAPIKeyAndEnvironment(apiKey, environment string) *DB { - d.pineconeClient = pineconego.New(environment, apiKey) +func (d *DB) WithAPIKey(apiKey string) *DB { + d.pineconeClient = pineconego.New(apiKey) return d } +func (d *DB) getIndexHost(ctx context.Context) error { + if d.indexHost != nil { + return nil + } + + resp := &pineconegoresponse.IndexDescribe{} + + err := d.pineconeClient.IndexDescribe(ctx, &pineconegorequest.IndexDescribe{ + IndexName: d.indexName, + }, resp) + if err != nil { + return err + } + + resp.Host = "https://" + resp.Host + d.indexHost = &resp.Host + + return nil +} + func (d *DB) IsEmpty(ctx context.Context) (bool, error) { err := d.createIndexIfRequired(ctx) if err != nil { return true, fmt.Errorf("%w: %w", index.ErrInternal, err) } - err = d.getProjectID(ctx) + err = d.getIndexHost(ctx) if err != nil { return true, fmt.Errorf("%w: %w", index.ErrInternal, err) } req := &pineconegorequest.VectorDescribeIndexStats{ - IndexName: d.indexName, - ProjectID: *d.projectID, + IndexHost: *d.indexHost, } res := &pineconegoresponse.VectorDescribeIndexStats{} @@ -93,7 +138,7 @@ func (d *DB) IsEmpty(ctx context.Context) (bool, error) { func (d *DB) Search(ctx context.Context, values []float64, options *option.Options) (index.SearchResults, error) { if options.Filter == nil { - options.Filter = map[string]string{} + options.Filter = pineconegorequest.Filter{} } matches, err := d.similaritySearch(ctx, values, options) @@ -116,22 +161,17 @@ func (d *DB) Drop(ctx context.Context) error { } func (d *DB) Delete(ctx context.Context, ids []string) error { - err := d.getProjectID(ctx) + err := d.getIndexHost(ctx) if err != nil { return fmt.Errorf("%w: %w", index.ErrInternal, err) } - for _, id := range ids { - idToDelete := id - - deleteErr := d.pineconeClient.VectorDelete(ctx, &pineconegorequest.VectorDelete{ - IndexName: d.indexName, - ProjectID: *d.projectID, - ID: &idToDelete, - }, &pineconegoresponse.VectorDelete{}) - if deleteErr != nil { - return fmt.Errorf("%w: %w", index.ErrInternal, deleteErr) - } + deleteErr := d.pineconeClient.VectorDelete(ctx, &pineconegorequest.VectorDelete{ + IndexHost: *d.indexHost, + IDs: ids, + }, &pineconegoresponse.VectorDelete{}) + if deleteErr != nil { + return fmt.Errorf("%w: %w", index.ErrInternal, deleteErr) } return nil @@ -147,10 +187,10 @@ func (d *DB) similaritySearch( } if opts.Filter == nil { - opts.Filter = map[string]string{} + opts.Filter = pineconegorequest.Filter{} } - err := d.getProjectID(ctx) + err := d.getIndexHost(ctx) if err != nil { return nil, fmt.Errorf("%w: %w", index.ErrInternal, err) } @@ -161,14 +201,13 @@ func (d *DB) similaritySearch( err = d.pineconeClient.VectorQuery( ctx, &pineconegorequest.VectorQuery{ - IndexName: d.indexName, - ProjectID: *d.projectID, + IndexHost: *d.indexHost, TopK: int32(opts.TopK), Vector: values, IncludeMetadata: &includeMetadata, IncludeValues: &includeValues, Namespace: &d.namespace, - Filter: opts.Filter.(map[string]string), + Filter: opts.Filter.(pineconegorequest.Filter), }, res, ) @@ -179,23 +218,6 @@ func (d *DB) similaritySearch( return res.Matches, nil } -func (d *DB) getProjectID(ctx context.Context) error { - if d.projectID != nil { - return nil - } - - whoamiResp := &pineconegoresponse.Whoami{} - - err := d.pineconeClient.Whoami(ctx, &pineconegorequest.Whoami{}, whoamiResp) - if err != nil { - return err - } - - d.projectID = &whoamiResp.ProjectID - - return nil -} - func (d *DB) createIndexIfRequired(ctx context.Context) error { if d.createIndexOptions == nil { return nil @@ -208,7 +230,7 @@ func (d *DB) createIndexIfRequired(ctx context.Context) error { } for _, index := range resp.Indexes { - if index == d.indexName { + if index.Name == d.indexName { return nil } } @@ -218,9 +240,28 @@ func (d *DB) createIndexIfRequired(ctx context.Context) error { req := &pineconegorequest.IndexCreate{ Name: d.indexName, Dimension: d.createIndexOptions.Dimension, - Replicas: &d.createIndexOptions.Replicas, Metric: &metric, - PodType: &d.createIndexOptions.PodType, + } + + if d.createIndexOptions.Serverless != nil { + req.Spec = pineconegorequest.Spec{ + Serverless: &pineconegorequest.ServerlessSpec{ + Cloud: pineconegorequest.ServerlessSpecCloud(d.createIndexOptions.Serverless.Cloud), + Region: d.createIndexOptions.Serverless.Region, + }, + } + } else if d.createIndexOptions.Pod != nil { + req.Spec = pineconegorequest.Spec{ + Pod: &pineconegorequest.PodSpec{ + Environment: d.createIndexOptions.Pod.Environment, + Replicas: d.createIndexOptions.Pod.Replicas, + PodType: d.createIndexOptions.Pod.PodType, + Pods: d.createIndexOptions.Pod.Pods, + Shards: d.createIndexOptions.Pod.Shards, + MetadataConfig: (*pineconegorequest.MetadataConfig)(d.createIndexOptions.Pod.MetadataConfig), + SourceCollection: d.createIndexOptions.Pod.SourceCollection, + }, + } } err = d.pineconeClient.IndexCreate(ctx, req, &pineconegoresponse.IndexCreate{}) @@ -250,7 +291,7 @@ func (d *DB) createIndexIfRequired(ctx context.Context) error { } func (d *DB) Insert(ctx context.Context, datas []index.Data) error { - err := d.getProjectID(ctx) + err := d.getIndexHost(ctx) if err != nil { return fmt.Errorf("%w: %w", index.ErrInternal, err) } @@ -274,8 +315,7 @@ func (d *DB) Insert(ctx context.Context, datas []index.Data) error { } req := &pineconegorequest.VectorUpsert{ - IndexName: d.indexName, - ProjectID: *d.projectID, + IndexHost: *d.indexHost, Vectors: vectors, Namespace: d.namespace, } From 1ecbea5e05a402b75e7678ec46ea27c4ba3451c2 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 25 Jan 2024 18:01:02 +0100 Subject: [PATCH 35/65] fix --- examples/embeddings/pinecone/main.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/embeddings/pinecone/main.go b/examples/embeddings/pinecone/main.go index 8073b0a3..48e42270 100644 --- a/examples/embeddings/pinecone/main.go +++ b/examples/embeddings/pinecone/main.go @@ -74,7 +74,8 @@ func main() { llmOpenAI := openai.NewCompletion().WithVerbose(true) prompt1 := prompt.NewPromptTemplate( - "Based on the following context answer to the question.\n\nContext:\n{{.context}}\n\nQuestion: {{.query}}").WithInputs( + "Based on the following context answer to the question.\n\n" + + "Context:\n{{.context}}\n\nQuestion: {{.query}}").WithInputs( map[string]string{ "query": query, "context": content, @@ -90,11 +91,9 @@ func main() { if err != nil { panic(err) } - } func ingestData(index *index.Index) error { - documents, err := loader.NewDirectoryLoader(".", ".txt").Load(context.Background()) if err != nil { return err @@ -110,9 +109,7 @@ func ingestData(index *index.Index) error { fmt.Println(doc.Metadata) fmt.Println("----------") fmt.Println() - } return index.LoadFromDocuments(context.Background(), documentChunks) - } From a740032cc9447eda3e6d4238c4cdca603da8c0aa Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Fri, 26 Jan 2024 09:37:16 +0100 Subject: [PATCH 36/65] chore: add liglet --- assistant/assistant.go | 1 + examples/linglet/summarize/main.go | 36 +++++++++++++ linglet/summarize/prompt.go | 16 ++++++ linglet/summarize/summarize.go | 85 ++++++++++++++++++++++++++++++ thread/thread.go | 36 +++++++++++++ 5 files changed, 174 insertions(+) create mode 100644 examples/linglet/summarize/main.go create mode 100644 linglet/summarize/prompt.go create mode 100644 linglet/summarize/summarize.go diff --git a/assistant/assistant.go b/assistant/assistant.go index 7d398759..1089eb5f 100644 --- a/assistant/assistant.go +++ b/assistant/assistant.go @@ -18,6 +18,7 @@ type Assistant struct { type LLM interface { Generate(context.Context, *thread.Thread) error } + type RAG interface { Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) } diff --git a/examples/linglet/summarize/main.go b/examples/linglet/summarize/main.go new file mode 100644 index 00000000..f05a8548 --- /dev/null +++ b/examples/linglet/summarize/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/linglet/summarize" + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/loader" + "github.com/henomis/lingoose/textsplitter" + "github.com/henomis/lingoose/thread" +) + +// download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt + +func main() { + + textLoader := loader.NewTextLoader("state_of_the_union.txt", nil). + WithTextSplitter(textsplitter.NewRecursiveCharacterTextSplitter(4000, 0)) + + summarize := summarize.New( + openai.New().WithMaxTokens(2000).WithTemperature(0).WithModel(openai.GPT3Dot5Turbo16K0613), + textLoader, + ).WithCallback( + func(t *thread.Thread, i, n int) { + fmt.Printf("Progress : %.0f%%\n", float64(i)/float64(n)*100) + }, + ) + + summary, err := summarize.Run(context.Background()) + if err != nil { + panic(err) + } + + println(*summary) +} diff --git a/linglet/summarize/prompt.go b/linglet/summarize/prompt.go new file mode 100644 index 00000000..8e92eca8 --- /dev/null +++ b/linglet/summarize/prompt.go @@ -0,0 +1,16 @@ +package summarize + +const ( + summaryPrompt = "Write a concise summary of the following:\n\n{{.text}}" + refinePrompt = `Your job is to produce a final summary. +We have provided an existing summary up to a certain point: + +{{.output}} + +We have the opportunity to refine the existing summary (only if needed) with some more context below. +------------ +{{.text}} +------------ +Given the new context, refine the original summary. +If the context isn't useful, return the original summary.` +) diff --git a/linglet/summarize/summarize.go b/linglet/summarize/summarize.go new file mode 100644 index 00000000..a03f1b70 --- /dev/null +++ b/linglet/summarize/summarize.go @@ -0,0 +1,85 @@ +package summarize + +import ( + "context" + + "github.com/henomis/lingoose/assistant" + "github.com/henomis/lingoose/document" + "github.com/henomis/lingoose/thread" + "github.com/henomis/lingoose/types" +) + +type CallbackFn func(t *thread.Thread, i, n int) + +type LLM interface { + Generate(context.Context, *thread.Thread) error +} + +type Loader interface { + Load(ctx context.Context) ([]document.Document, error) +} + +type Summarize struct { + assistant *assistant.Assistant + loader Loader + callbackFn CallbackFn +} + +func New(llm LLM, loader Loader) *Summarize { + return &Summarize{ + assistant: assistant.New(llm), + loader: loader, + } +} + +func (s *Summarize) WithCallback(callbackFn CallbackFn) *Summarize { + s.callbackFn = callbackFn + return s +} + +func (s *Summarize) Run(ctx context.Context) (*string, error) { + documents, err := s.loader.Load(ctx) + if err != nil { + return nil, err + } + + nDocuments := len(documents) + summary := "" + + if s.callbackFn != nil { + s.callbackFn(s.assistant.Thread(), 0, nDocuments) + } + + for i, document := range documents { + prompt := refinePrompt + if i == 0 { + prompt = summaryPrompt + } + + s.assistant.Thread().ClearMessages().AddMessage( + thread.NewAssistantMessage().AddContent( + thread.NewTextContent( + prompt, + ).Format( + types.M{ + "text": document.Content, + "output": summary, + }, + ), + ), + ) + + err := s.assistant.Run(ctx) + if err != nil { + return nil, err + } + + if s.callbackFn != nil { + s.callbackFn(s.assistant.Thread(), i+1, nDocuments) + } + + summary = s.assistant.Thread().LastMessage().Contents[0].Data.(string) + } + + return &summary, nil +} diff --git a/thread/thread.go b/thread/thread.go index f1cce97d..745858e1 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -1,5 +1,12 @@ package thread +import ( + "strings" + + "github.com/henomis/lingoose/prompt" + "github.com/henomis/lingoose/types" +) + type Thread struct { Messages []*Message } @@ -181,3 +188,32 @@ func (t *Thread) UserQuery() []string { return messages } + +func (t *Thread) ClearMessages() *Thread { + t.Messages = make([]*Message, 0) + return t +} + +func (m *Message) ClearContents() *Message { + m.Contents = make([]*Content, 0) + return m +} + +func (c *Content) Format(input types.M) *Content { + if c.Type != ContentTypeText || input == nil { + return c + } + + if !strings.Contains(c.Data.(string), "{{") { + return c + } + + prompt := prompt.NewPromptTemplate(c.Data.(string)) + err := prompt.Format(input) + if err != nil { + return c + } + c.Data = prompt.String() + + return c +} From 900942d91f0dedc875e6dece5c7c85973aa1f6c9 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Fri, 26 Jan 2024 09:39:41 +0100 Subject: [PATCH 37/65] fix --- linglet/summarize/summarize.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/linglet/summarize/summarize.go b/linglet/summarize/summarize.go index a03f1b70..75d5a4f7 100644 --- a/linglet/summarize/summarize.go +++ b/linglet/summarize/summarize.go @@ -69,8 +69,8 @@ func (s *Summarize) Run(ctx context.Context) (*string, error) { ), ) - err := s.assistant.Run(ctx) - if err != nil { + assistantErr := s.assistant.Run(ctx) + if assistantErr != nil { return nil, err } @@ -78,7 +78,9 @@ func (s *Summarize) Run(ctx context.Context) (*string, error) { s.callbackFn(s.assistant.Thread(), i+1, nDocuments) } - summary = s.assistant.Thread().LastMessage().Contents[0].Data.(string) + if content, ok := s.assistant.Thread().LastMessage().Contents[0].Data.(string); ok { + summary = content + } } return &summary, nil From f126cfd49b8e688294ca4d867569919dfa6220b5 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Fri, 26 Jan 2024 11:56:40 +0100 Subject: [PATCH 38/65] liglet sql --- examples/linglet/sql/main.go | 34 +++++ linglet/sql/prompt.go | 27 ++++ linglet/sql/sql.go | 284 +++++++++++++++++++++++++++++++++++ linglet/sql/sqlite.go | 32 ++++ 4 files changed, 377 insertions(+) create mode 100644 examples/linglet/sql/main.go create mode 100644 linglet/sql/prompt.go create mode 100644 linglet/sql/sql.go create mode 100644 linglet/sql/sqlite.go diff --git a/examples/linglet/sql/main.go b/examples/linglet/sql/main.go new file mode 100644 index 00000000..d5551a37 --- /dev/null +++ b/examples/linglet/sql/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + + lingletsql "github.com/henomis/lingoose/linglet/sql" + "github.com/henomis/lingoose/llm/openai" + // enable sqlite3 driver + // _ "github.com/mattn/go-sqlite3" +) + +// sqlite https://raw.githubusercontent.com/lerocha/chinook-database/master/ChinookDatabase/DataSources/Chinook_Sqlite.sqlite + +func main() { + db, err := sql.Open("sqlite3", "Chinook_Sqlite.sqlite") + if err != nil { + panic(err) + } + + lingletSQL := lingletsql.New( + openai.New().WithMaxTokens(2000).WithTemperature(0).WithModel(openai.GPT3Dot5Turbo16K0613), + db, + ) + + result, err := lingletSQL.Run(context.Background(), "list the top 3 albums most present in playlists.") + if err != nil { + panic(err) + } + + fmt.Printf("SQL Query\n-----\n%s\n\n", result.SQLQuery) + fmt.Printf("Answer\n-------\n%s\n", result.Answer) +} diff --git a/linglet/sql/prompt.go b/linglet/sql/prompt.go new file mode 100644 index 00000000..59a8359c --- /dev/null +++ b/linglet/sql/prompt.go @@ -0,0 +1,27 @@ +package sql + +const ( + sqlPromptTemplate = ` +Use the following table schema info to create your SQL query: +{{.schema}} + +Question: {{.question}} +SQLQuery: ` + + sqlRefinePromptTemplate = ` +SQLQuery: {{.sql_query}} + +The SQLResult has the following error: "{{.sql_error}}" +The SQLQuery you produced is syntactically incorrect. Please fix. +Answer with the SQL query, do not add any extra information, the query must be usable as-is. +SQLQuery: ` + + sqlFinalPromptTemplate = ` +You are an helpful assistant and your goal is to answer to the user's question. +Your answer must be comprehensible by non-tech users. +You will be provided with additional context information as result of an SQL query. + +Question: {{.question}} +SQL result: {{.sql_result}} +Answer: ` +) diff --git a/linglet/sql/sql.go b/linglet/sql/sql.go new file mode 100644 index 00000000..131720f8 --- /dev/null +++ b/linglet/sql/sql.go @@ -0,0 +1,284 @@ +package sql + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/henomis/lingoose/assistant" + "github.com/henomis/lingoose/thread" + "github.com/henomis/lingoose/types" +) + +type CallbackFn func(t *thread.Thread) + +type LLM interface { + Generate(context.Context, *thread.Thread) error +} + +const ( + defaultTopK = 5 +) + +type SQL struct { + db *sql.DB + topk int + assistant *assistant.Assistant + callbackFn CallbackFn +} + +type Result struct { + SQLQuery string + Answer string +} + +func New(llm LLM, db *sql.DB) *SQL { + return &SQL{ + db: db, + topk: defaultTopK, + assistant: assistant.New(llm), + } +} + +func (s *SQL) WithTopK(topk int) *SQL { + s.topk = topk + return s +} + +func (s *SQL) WithCallback(callbackFn CallbackFn) *SQL { + s.callbackFn = callbackFn + return s +} + +func (s *SQL) schema() (*string, error) { + driverType := fmt.Sprintf("%T", s.db.Driver()) + if strings.Contains(driverType, "sqlite") { + return s.sqliteSchema() + } + + return nil, fmt.Errorf("unsupported database driver %s", driverType) +} + +func (s *SQL) systemPrompt() (*string, error) { + driverType := fmt.Sprintf("%T", s.db.Driver()) + if strings.Contains(driverType, "sqlite") { + return &sqliteSystemPromptTemplate, nil + } + + return nil, fmt.Errorf("unsupported database driver %s", driverType) +} + +func (s *SQL) Run(ctx context.Context, question string) (*Result, error) { + sqlQuery, err := s.generateSQLQuery(ctx, question) + if err != nil { + return nil, err + } + + sqlResult, err := s.executeSQLQuery(*sqlQuery) + if err != nil { + refinedSQLResult, refineErr := s.generateRefinedSQLQuery( + ctx, + question, + *sqlQuery, + err, + ) + if refineErr != nil { + return nil, refineErr + } + + if s.callbackFn != nil { + s.callbackFn(s.assistant.Thread()) + } + + sqlResult, refineErr = s.executeSQLQuery(*refinedSQLResult) + if refineErr != nil { + return nil, refineErr + } + sqlQuery = refinedSQLResult + } + + if s.callbackFn != nil { + s.callbackFn(s.assistant.Thread()) + } + + answer, err := s.generateAnswer(ctx, sqlResult, question) + if err != nil { + return nil, err + } + + if s.callbackFn != nil { + s.callbackFn(s.assistant.Thread()) + } + + return &Result{ + SQLQuery: *sqlQuery, + Answer: *answer, + }, nil +} + +func (s *SQL) generateSQLQuery(ctx context.Context, question string) (*string, error) { + systemPrompt, err := s.systemPrompt() + if err != nil { + return nil, err + } + schema, err := s.schema() + if err != nil { + return nil, err + } + + s.assistant.Thread().ClearMessages().AddMessage( + thread.NewSystemMessage().AddContent( + thread.NewTextContent(*systemPrompt).Format( + types.M{ + "top_k": s.topk, + }, + ), + ), + ).AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(sqlPromptTemplate).Format( + types.M{ + "schema": schema, + "question": question, + }, + ), + ), + ) + + err = s.assistant.Run(ctx) + if err != nil { + return nil, err + } + + if content, ok := s.assistant.Thread().LastMessage().Contents[0].Data.(string); ok { + return &content, nil + } + + return nil, fmt.Errorf("no content") +} + +func (s *SQL) generateRefinedSQLQuery(ctx context.Context, question, sqlQuery string, sqlError error) (*string, error) { + systemPrompt, err := s.systemPrompt() + if err != nil { + return nil, err + } + schema, err := s.schema() + if err != nil { + return nil, err + } + + s.assistant.Thread().ClearMessages().AddMessage( + thread.NewSystemMessage().AddContent( + thread.NewTextContent(*systemPrompt).Format( + types.M{ + "top_k": s.topk, + }, + ), + ), + ).AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(sqlPromptTemplate).Format( + types.M{ + "schema": schema, + "question": question, + }, + ), + ), + ).AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(sqlRefinePromptTemplate).Format( + types.M{ + "sql_query": sqlQuery, + "sql_error": sqlError.Error(), + }, + ), + ), + ) + + err = s.assistant.Run(ctx) + if err != nil { + return nil, err + } + + if content, ok := s.assistant.Thread().LastMessage().Contents[0].Data.(string); ok { + return &content, nil + } + + return nil, fmt.Errorf("no content") +} + +func (s *SQL) generateAnswer(ctx context.Context, sqlResult, question string) (*string, error) { + s.assistant.Thread().ClearMessages().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(sqlFinalPromptTemplate).Format( + types.M{ + "question": question, + "sql_result": sqlResult, + }, + ), + ), + ) + + err := s.assistant.Run(ctx) + if err != nil { + return nil, err + } + + if content, ok := s.assistant.Thread().LastMessage().Contents[0].Data.(string); ok { + return &content, nil + } + + return nil, fmt.Errorf("no content") +} + +func (s *SQL) executeSQLQuery(sqlQuery string) (string, error) { + rows, err := s.db.Query(sqlQuery) + if err != nil { + return "", err + } + if err = rows.Err(); err != nil { + return "", err + } + defer rows.Close() + + content := "" + columns, err := rows.Columns() + if err != nil { + return "", err + } + + for _, col := range columns { + if content != "" { + content += "|" + col + } else { + content += col + } + } + + values := make([]sql.RawBytes, len(columns)) + scanArgs := make([]interface{}, len(values)) + for i := range values { + scanArgs[i] = &values[i] + } + + for rows.Next() { + err = rows.Scan(scanArgs...) + if err != nil { + return "", err + } + + row := "" + for _, col := range values { + if row != "" { + row += "|" + string(col) + } else { + row += string(col) + } + } + + content += "\n" + row + } + + return content, nil +} diff --git a/linglet/sql/sqlite.go b/linglet/sql/sqlite.go new file mode 100644 index 00000000..5e3a9090 --- /dev/null +++ b/linglet/sql/sqlite.go @@ -0,0 +1,32 @@ +package sql + +//nolint:lll +var sqliteSystemPromptTemplate = ` +You are a SQLite expert. Given an input question, create a syntactically correct SQLite query to run. Do not add any extra information to the query. The query must be usable as-is. +Unless the user specifies in the question a specific number of examples to obtain, query for at most {{.top_k}} results using the LIMIT clause as per SQLite. You can order the results to return the most informative data in the database. +Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in double quotes (") to denote them as delimited identifiers. +Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table. +Pay attention to use date('now') function to get the current date, if the question involves "today".` + +func (s *SQL) sqliteSchema() (*string, error) { + rows, err := s.db.Query("SELECT sql FROM sqlite_schema WHERE sql IS NOT NULL") + if err != nil { + return nil, err + } + defer rows.Close() + + schema := "" + for rows.Next() { + var row string + scanErr := rows.Scan(&row) + if scanErr != nil { + return nil, scanErr + } + schema += row + "\n" + } + if err = rows.Err(); err != nil { + return nil, err + } + + return &schema, nil +} From 65e3911e4dc0e767c7a51549fb937710d6938147 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 27 Jan 2024 14:14:07 +0100 Subject: [PATCH 39/65] refactor rag --- assistant/assistant.go | 27 +++++++++------------------ rag/rag.go | 9 +++++++-- rag/rag_fusion.go | 13 ++++++++----- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/assistant/assistant.go b/assistant/assistant.go index 1089eb5f..0dd0e976 100644 --- a/assistant/assistant.go +++ b/assistant/assistant.go @@ -2,9 +2,8 @@ package assistant import ( "context" + "strings" - "github.com/henomis/lingoose/index" - "github.com/henomis/lingoose/prompt" "github.com/henomis/lingoose/thread" "github.com/henomis/lingoose/types" ) @@ -20,7 +19,7 @@ type LLM interface { } type RAG interface { - Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) + Retrieve(ctx context.Context, query string) ([]string, error) } func New(llm LLM) *Assistant { @@ -86,24 +85,16 @@ func (a *Assistant) generateRAGMessage(ctx context.Context) error { return err } - context := "" - - for _, searchResult := range searchResults { - context += searchResult.Content() + "\n\n" - } - - ragPrompt := prompt.NewPromptTemplate(baseRAGPrompt) - err = ragPrompt.Format(types.M{ - "question": query, - "context": context, - }) - if err != nil { - return err - } + context := strings.Join(searchResults, "\n\n") a.thread.AddMessage(thread.NewUserMessage().AddContent( thread.NewTextContent( - ragPrompt.String(), + baseRAGPrompt, + ).Format( + types.M{ + "question": query, + "context": context, + }, ), )) diff --git a/rag/rag.go b/rag/rag.go index 56aebd10..4bb7725d 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -100,9 +100,14 @@ func (r *RAG) AddDocuments(ctx context.Context, documents ...document.Document) return r.index.LoadFromDocuments(ctx, documents) } -func (r *RAG) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { +func (r *RAG) Retrieve(ctx context.Context, query string) ([]string, error) { results, err := r.index.Query(ctx, query, option.WithTopK(int(r.topK))) - return results, err + var resultsAsString []string + for _, result := range results { + resultsAsString = append(resultsAsString, result.Content()) + } + + return resultsAsString, err } func (r *RAG) addSource(ctx context.Context, source string) ([]document.Document, error) { diff --git a/rag/rag_fusion.go b/rag/rag_fusion.go index cbffdbda..e8363c82 100644 --- a/rag/rag_fusion.go +++ b/rag/rag_fusion.go @@ -24,7 +24,7 @@ func NewFusion(index *index.Index, llm LLM) *Fusion { } } -func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResult, error) { +func (r *Fusion) Retrieve(ctx context.Context, query string) ([]string, error) { if r.llm == nil { return nil, fmt.Errorf("llm is not set") } @@ -52,8 +52,6 @@ func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResu return nil, err } - fmt.Println(t) - lastMessage := t.LastMessage() content, _ := lastMessage.Contents[0].Data.(string) content = strings.TrimSpace(content) @@ -72,7 +70,7 @@ func (r *Fusion) Retrieve(ctx context.Context, query string) ([]index.SearchResu return reciprocalRankFusion(results), nil } -func reciprocalRankFusion(searchResults index.SearchResults) index.SearchResults { +func reciprocalRankFusion(searchResults index.SearchResults) []string { const k = 60.0 searchResultsScoreMap := make(map[string]float64) for _, result := range searchResults { @@ -101,5 +99,10 @@ func reciprocalRankFusion(searchResults index.SearchResults) index.SearchResults return uniqueSearchResults[i].Score > uniqueSearchResults[j].Score }) - return uniqueSearchResults + var results []string + for _, searchResult := range uniqueSearchResults { + results = append(results, searchResult.Content()) + } + + return results } From cc168cc480b1e8b2077274722d7fba7e9271c7a2 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 27 Jan 2024 14:28:23 +0100 Subject: [PATCH 40/65] fix --- llm/ollama/{models.go => api.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename llm/ollama/{models.go => api.go} (100%) diff --git a/llm/ollama/models.go b/llm/ollama/api.go similarity index 100% rename from llm/ollama/models.go rename to llm/ollama/api.go From ea54263d7fc2b3579ec28452db797773d24d8e33 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 28 Jan 2024 01:10:14 +0100 Subject: [PATCH 41/65] fix --- go.mod | 4 ++-- go.sum | 12 ++++++++---- llm/ollama/api.go | 10 ++++++++++ llm/ollama/ollama.go | 4 +--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 16bd7b86..8d56dd60 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,11 @@ require github.com/mitchellh/mapstructure v1.5.0 require ( github.com/RediSearch/redisearch-go/v2 v2.1.1 github.com/google/uuid v1.3.0 - github.com/henomis/cohere-go v1.0.1 + github.com/henomis/cohere-go v1.1.2 github.com/henomis/milvus-go v0.0.4 github.com/henomis/pinecone-go/v2 v2.0.0 github.com/henomis/qdrant-go v1.1.0 - github.com/henomis/restclientgo v1.1.1 + github.com/henomis/restclientgo v1.2.0 github.com/invopop/jsonschema v0.7.0 github.com/pkoukk/tiktoken-go v0.1.1 github.com/sashabaranov/go-openai v1.17.9 diff --git a/go.sum b/go.sum index 7ede42f0..d39b0c6a 100644 --- a/go.sum +++ b/go.sum @@ -9,16 +9,20 @@ github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/henomis/cohere-go v1.0.1 h1:a47gIN29tqAl4yBTAT+BzQMjsWG94Fz07u9AE4Md+a8= -github.com/henomis/cohere-go v1.0.1/go.mod h1:F6D33jlWle6pbGdf9Fm2bteaOOQOO1cQtFnlFfj+ZXY= +github.com/henomis/cohere-go v1.1.1-alpha.1 h1:QiJe7xK008pHyHYDaJFieEmvdVddxqA9BjW5zjpAN6Q= +github.com/henomis/cohere-go v1.1.1-alpha.1/go.mod h1:T+d7fIYTYjPWbjWR8A67ze/Fg181YtugJVnfSPkxzH8= +github.com/henomis/cohere-go v1.1.2 h1:rzEA1JRm26RnaQValoVeVQ1y1nTofuhr/z/fPyhUW/4= +github.com/henomis/cohere-go v1.1.2/go.mod h1:z+UIgBbNCnLH5M47FYqg4h3CDWxjUv2VDfEwdTb95MU= github.com/henomis/milvus-go v0.0.4 h1:ArddXRJx/EGdQ75gB7TyEzD4Z4BqXzW16p8jRcjJOhs= github.com/henomis/milvus-go v0.0.4/go.mod h1:nZ/NvDOLoGl7FQrYSm0JfeefPBVXph9PpE4y3lPpbj4= github.com/henomis/pinecone-go/v2 v2.0.0 h1:HdAX0nGzBagL6ubn17utIz3FGwXfignBovZesOjXiEI= github.com/henomis/pinecone-go/v2 v2.0.0/go.mod h1:enUHclL18IBnq6POF20hd7YMIawUQZ9NznJ22NJD0/w= github.com/henomis/qdrant-go v1.1.0 h1:GU5R4ZZKeD0JXLZ0yv37FUzI0spGECRr2WlE3prmLIY= github.com/henomis/qdrant-go v1.1.0/go.mod h1:p274sBhQPDnfjCzJentWoNIGFadl5yIcLM4Xnmmwk9k= -github.com/henomis/restclientgo v1.1.1 h1:dHzKj4CD2rJqFkENwlMvXfWLlJtZFL0Yfza9NmTZ5W8= -github.com/henomis/restclientgo v1.1.1/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= +github.com/henomis/restclientgo v1.2.0-alpha.1 h1:cmffFMiRdUzHDuNEGHVvQ/oSDUM7EuiPIlDxtQ0vzBo= +github.com/henomis/restclientgo v1.2.0-alpha.1/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= +github.com/henomis/restclientgo v1.2.0 h1:KINVh4zW4qAeqgO8qbsI1QhiQcn4xgMv3Px4H7++BCk= +github.com/henomis/restclientgo v1.2.0/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy770So= diff --git a/llm/ollama/api.go b/llm/ollama/api.go index c325d33d..654d04bb 100644 --- a/llm/ollama/api.go +++ b/llm/ollama/api.go @@ -48,6 +48,8 @@ type chatStreamResponse struct { response Done bool `json:"done"` Message message `json:"message"` + + streamCallbackFn restclientgo.StreamCallback } type assistantMessage struct { @@ -81,6 +83,14 @@ func (r *response) SetStatusCode(code int) error { func (r *response) SetHeaders(_ restclientgo.Headers) error { return nil } +func (r *chatStreamResponse) SetStreamCallback(fn restclientgo.StreamCallback) { + r.streamCallbackFn = fn +} + +func (r *chatStreamResponse) StreamCallback() restclientgo.StreamCallback { + return r.streamCallbackFn +} + type message struct { Role string `json:"role"` Content string `json:"content"` diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index 598fda6e..8a167c2d 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -150,8 +150,6 @@ func (o *Ollama) Generate(ctx context.Context, t *thread.Thread) error { func (o *Ollama) generate(ctx context.Context, t *thread.Thread, chatRequest *request) error { var response chatResponse - o.restClient.SetStreamCallback(nil) - err := o.restClient.Post( ctx, chatRequest, @@ -173,7 +171,7 @@ func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *requ var assistantMessage string response.SetAcceptContentType(ndjsonContentType) - o.restClient.SetStreamCallback( + response.SetStreamCallback( func(data []byte) error { var streamResponse chatStreamResponse From 3fc9ee530a3b4710c94481005f5995f1f02570db Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 28 Jan 2024 01:11:58 +0100 Subject: [PATCH 42/65] fix --- embedder/cohere/cohere.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embedder/cohere/cohere.go b/embedder/cohere/cohere.go index 1e05d28c..a4c8cef6 100644 --- a/embedder/cohere/cohere.go +++ b/embedder/cohere/cohere.go @@ -58,7 +58,7 @@ func (e *Embedder) Embed(ctx context.Context, texts []string) ([]embedder.Embedd ctx, &request.Embed{ Texts: texts, - Model: &e.model, + Model: e.model, }, resp, ) From 12caa7f2b67389095e4151cca0a75ca67d53cf06 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 28 Jan 2024 01:44:32 +0100 Subject: [PATCH 43/65] add cohere chat stream --- embedder/cohere/cohere.go | 20 ++++-- examples/thread/cohere/main.go | 29 ++++++++ examples/thread/{ => ollama}/main.go | 2 +- llm/cohere/cohere.go | 103 ++++++++++++++++++++------- llm/cohere/formatter.go | 81 +++++++++++++++++++++ llm/ollama/formatter.go | 2 +- llm/ollama/ollama.go | 11 +-- 7 files changed, 206 insertions(+), 42 deletions(-) create mode 100644 examples/thread/cohere/main.go rename examples/thread/{ => ollama}/main.go (92%) create mode 100644 llm/cohere/formatter.go diff --git a/embedder/cohere/cohere.go b/embedder/cohere/cohere.go index a4c8cef6..464a41c6 100644 --- a/embedder/cohere/cohere.go +++ b/embedder/cohere/cohere.go @@ -16,15 +16,23 @@ type EmbedderModel = model.EmbedModel const ( defaultEmbedderModel EmbedderModel = model.EmbedModelEnglishV20 - EmbedderModelEnglishV20 EmbedderModel = model.EmbedModelEnglishV20 - EmbedderModelEnglishLightV20 EmbedderModel = model.EmbedModelEnglishLightV20 - EmbedderModelMultilingualV20 EmbedderModel = model.EmbedModelMultilingualV20 + EmbedderModelEnglishV20 EmbedderModel = model.EmbedModelEnglishV20 + EmbedderModelEnglishLightV20 EmbedderModel = model.EmbedModelEnglishLightV20 + EmbedderModelMultilingualV20 EmbedderModel = model.EmbedModelMultilingualV20 + EmbedderModelEnglishV30 EmbedderModel = model.EmbedModelEnglishV30 + EmbedderModelEnglishLightV30 EmbedderModel = model.EmbedModelEnglishLightV30 + EmbedderModelMultilingualV30 EmbedderModel = model.EmbedModelMultilingualV30 + EmbedderModelMultilingualLightV30 EmbedderModel = model.EmbedModelMultilingualLightV30 ) var EmbedderModelsSize = map[EmbedderModel]int{ - EmbedderModelEnglishLightV20: 1024, - EmbedderModelEnglishV20: 4096, - EmbedderModelMultilingualV20: 768, + EmbedderModelEnglishV30: 1024, + EmbedderModelEnglishLightV30: 384, + EmbedderModelEnglishV20: 4096, + EmbedderModelEnglishLightV20: 1024, + EmbedderModelMultilingualV30: 1024, + EmbedderModelMultilingualLightV30: 384, + EmbedderModelMultilingualV20: 768, } type Embedder struct { diff --git a/examples/thread/cohere/main.go b/examples/thread/cohere/main.go new file mode 100644 index 00000000..2177bea0 --- /dev/null +++ b/examples/thread/cohere/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/llm/cohere" + "github.com/henomis/lingoose/thread" +) + +func main() { + t := thread.New() + t.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent("Hello"), + )) + + err := cohere.New().WithMaxTokens(1000).WithTemperature(0). + WithStream( + func(s string) { + fmt.Print(s) + }, + ).Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t.String()) + +} diff --git a/examples/thread/main.go b/examples/thread/ollama/main.go similarity index 92% rename from examples/thread/main.go rename to examples/thread/ollama/main.go index e91a0582..7bd168fc 100644 --- a/examples/thread/main.go +++ b/examples/thread/ollama/main.go @@ -15,7 +15,7 @@ func main() { )) err := ollama.New().WithEndpoint("http://localhost:11434/api").WithModel("llama2"). - WithStream(true, func(s string) { + WithStream(func(s string) { fmt.Print(s) }).Generate(context.Background(), t) if err != nil { diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index a3c1ac57..214d4e3c 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -16,13 +16,17 @@ import ( "github.com/henomis/lingoose/thread" ) -type Model model.Model +var ( + ErrCohereChat = fmt.Errorf("cohere chat error") +) + +type Model = model.Model const ( - ModelCommand Model = Model(model.ModelCommand) - ModelCommandNightly Model = Model(model.ModelCommandNightly) - ModelCommandLight Model = Model(model.ModelCommandLight) - ModelCommandLightNightly Model = Model(model.ModelCommandLightNightly) + ModelCommand Model = model.ModelCommand + ModelCommandNightly Model = model.ModelCommandNightly + ModelCommandLight Model = model.ModelCommandLight + ModelCommandLightNightly Model = model.ModelCommandLightNightly ) const ( @@ -31,14 +35,17 @@ const ( DefaultModel = ModelCommand ) +type StreamCallbackFn func(string) + type Cohere struct { - client *coherego.Client - model Model - temperature float64 - maxTokens int - verbose bool - stop []string - cache *cache.Cache + client *coherego.Client + model Model + temperature float64 + maxTokens int + verbose bool + stop []string + cache *cache.Cache + streamCallbackFn StreamCallbackFn } func (c *Cohere) WithCache(cache *cache.Cache) *Cohere { @@ -96,6 +103,11 @@ func (c *Cohere) WithStop(stop []string) *Cohere { return c } +func (c *Cohere) WithStream(callbackFn StreamCallbackFn) *Cohere { + c.streamCallbackFn = callbackFn + return c +} + // Completion returns the completion for the given prompt func (c *Cohere) Completion(ctx context.Context, prompt string) (string, error) { resp := &response.Generate{} @@ -187,34 +199,73 @@ func (c *Cohere) Generate(ctx context.Context, t *thread.Thread) error { if err == nil { return nil } else if !errors.Is(err, cache.ErrCacheMiss) { - return err + return fmt.Errorf("%w: %w", ErrCohereChat, err) } } - completionQuery := "" - for _, message := range t.Messages { - for _, content := range message.Contents { - if content.Type == thread.ContentTypeText { - completionQuery += content.Data.(string) + "\n" - } - } + chatRequest := c.buildChatCompletionRequest(t) + + if c.streamCallbackFn != nil { + err = c.stream(ctx, t, chatRequest) + } else { + err = c.generate(ctx, t, chatRequest) } - completionResponse, err := c.Completion(ctx, completionQuery) if err != nil { return err } - t.AddMessage(thread.NewAssistantMessage().AddContent( - thread.NewTextContent(completionResponse), - )) - if c.cache != nil { err = c.setCache(ctx, t, cacheResult) if err != nil { - return err + return fmt.Errorf("%w: %w", ErrCohereChat, err) } } return nil } + +func (c *Cohere) generate(ctx context.Context, t *thread.Thread, chatRequest *request.Chat) error { + var response response.Chat + + err := c.client.Chat( + ctx, + chatRequest, + &response, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrCohereChat, err) + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(response.Text), + )) + + return nil +} + +func (c *Cohere) stream(ctx context.Context, t *thread.Thread, chatRequest *request.Chat) error { + chatResponse := &response.Chat{} + var assistantMessage string + + err := c.client.ChatStream( + ctx, + chatRequest, + chatResponse, + func(r *response.Chat) { + if r.Text != "" { + c.streamCallbackFn(r.Text) + assistantMessage += r.Text + } + }, + ) + if err != nil { + return fmt.Errorf("%w: %w", ErrCohereChat, err) + } + + t.AddMessage(thread.NewAssistantMessage().AddContent( + thread.NewTextContent(assistantMessage), + )) + + return nil +} diff --git a/llm/cohere/formatter.go b/llm/cohere/formatter.go new file mode 100644 index 00000000..67b3360d --- /dev/null +++ b/llm/cohere/formatter.go @@ -0,0 +1,81 @@ +package cohere + +import ( + "github.com/henomis/cohere-go/model" + "github.com/henomis/cohere-go/request" + "github.com/henomis/lingoose/thread" +) + +var threadRoleToCohereRole = map[thread.Role]model.ChatMessageRole{ + thread.RoleSystem: model.ChatMessageRoleChatbot, + thread.RoleUser: model.ChatMessageRoleUser, + thread.RoleAssistant: model.ChatMessageRoleChatbot, +} + +func (c *Cohere) buildChatCompletionRequest(t *thread.Thread) *request.Chat { + message, history := threadToChatMessages(t) + + return &request.Chat{ + Model: c.model, + ChatHistory: history, + Message: message, + } +} + +func threadToChatMessages(t *thread.Thread) (string, []model.ChatMessage) { + var history []model.ChatMessage + var message string + + for _, m := range t.Messages { + chatMessage := model.ChatMessage{ + Role: threadRoleToCohereRole[m.Role], + } + + switch m.Role { + case thread.RoleUser, thread.RoleSystem, thread.RoleAssistant: + for _, content := range m.Contents { + if content.Type == thread.ContentTypeText { + chatMessage.Message += content.Data.(string) + "\n" + } + } + case thread.RoleTool: + continue + } + + history = append(history, chatMessage) + } + + lastMessage := t.LastMessage() + if lastMessage.Role == thread.RoleUser { + for _, content := range lastMessage.Contents { + if content.Type == thread.ContentTypeText { + message += content.Data.(string) + "\n" + } + } + + history = history[:len(history)-1] + } + + return message, history +} + +// chatMessages := make([]message, len(t.Messages)) +// for i, m := range t.Messages { +// chatMessages[i] = message{ +// Role: threadRoleToOpenAIRole[m.Role], +// } + +// switch m.Role { +// case thread.RoleUser, thread.RoleSystem, thread.RoleAssistant: +// for _, content := range m.Contents { +// if content.Type == thread.ContentTypeText { +// chatMessages[i].Content += content.Data.(string) + "\n" +// } +// } +// case thread.RoleTool: +// continue +// } +// } + +// return chatMessages +// } diff --git a/llm/ollama/formatter.go b/llm/ollama/formatter.go index ed04f534..5f4c01c5 100644 --- a/llm/ollama/formatter.go +++ b/llm/ollama/formatter.go @@ -13,7 +13,7 @@ func threadToChatMessages(t *thread.Thread) []message { chatMessages := make([]message, len(t.Messages)) for i, m := range t.Messages { chatMessages[i] = message{ - Role: threadRoleToOpenAIRole[m.Role], + Role: threadRoleToOllamaRole[m.Role], } switch m.Role { diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index 8a167c2d..f9ff005b 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -22,7 +22,7 @@ var ( ErrOllamaChat = fmt.Errorf("ollama chat error") ) -var threadRoleToOpenAIRole = map[thread.Role]string{ +var threadRoleToOllamaRole = map[thread.Role]string{ thread.RoleSystem: "system", thread.RoleUser: "user", thread.RoleAssistant: "assistant", @@ -54,13 +54,8 @@ func (o *Ollama) WithModel(model string) *Ollama { return o } -func (o *Ollama) WithStream(enable bool, callbackFn StreamCallbackFn) *Ollama { - if !enable { - o.streamCallbackFn = nil - } else { - o.streamCallbackFn = callbackFn - } - +func (o *Ollama) WithStream(callbackFn StreamCallbackFn) *Ollama { + o.streamCallbackFn = callbackFn return o } From 5bfb5a804798e48d5171fbecfcd4aabcbd755492 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 28 Jan 2024 01:45:09 +0100 Subject: [PATCH 44/65] fix --- llm/cohere/cohere.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llm/cohere/cohere.go b/llm/cohere/cohere.go index 214d4e3c..b2f2dbbb 100644 --- a/llm/cohere/cohere.go +++ b/llm/cohere/cohere.go @@ -117,7 +117,7 @@ func (c *Cohere) Completion(ctx context.Context, prompt string) (string, error) Prompt: prompt, Temperature: &c.temperature, MaxTokens: &c.maxTokens, - Model: (*model.Model)(&c.model), + Model: &c.model, StopSequences: c.stop, }, resp, From c560ddb436f5c059091b0270e5093c9d0842b6a1 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 30 Jan 2024 10:38:46 +0100 Subject: [PATCH 45/65] ollama embedder --- embedder/ollama/api.go | 69 +++++++++++++++++++ embedder/ollama/ollama.go | 67 +++++++++++++++++++ examples/embeddings/ollama/main.go | 102 +++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 embedder/ollama/api.go create mode 100644 embedder/ollama/ollama.go create mode 100644 examples/embeddings/ollama/main.go diff --git a/embedder/ollama/api.go b/embedder/ollama/api.go new file mode 100644 index 00000000..b0233112 --- /dev/null +++ b/embedder/ollama/api.go @@ -0,0 +1,69 @@ +package ollamaembedder + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/henomis/restclientgo" +) + +type request struct { + Model string `json:"model"` + Prompt string `json:"prompt"` + Options options `json:"options"` +} + +func (r *request) Path() (string, error) { + return "/embeddings", nil +} + +func (r *request) Encode() (io.Reader, error) { + jsonBytes, err := json.Marshal(r) + if err != nil { + return nil, err + } + + return bytes.NewReader(jsonBytes), nil +} + +func (r *request) ContentType() string { + return "application/json" +} + +type response struct { + HTTPStatusCode int `json:"-"` + acceptContentType string `json:"-"` + Embedding []float64 `json:"embedding"` + CreatedAt string `json:"created_at"` +} + +func (r *response) SetAcceptContentType(contentType string) { + r.acceptContentType = contentType +} + +func (r *response) Decode(body io.Reader) error { + return json.NewDecoder(body).Decode(r) +} + +func (r *response) SetBody(_ io.Reader) error { + return nil +} + +func (r *response) AcceptContentType() string { + if r.acceptContentType != "" { + return r.acceptContentType + } + return "application/json" +} + +func (r *response) SetStatusCode(code int) error { + r.HTTPStatusCode = code + return nil +} + +func (r *response) SetHeaders(_ restclientgo.Headers) error { return nil } + +type options struct { + Temperature float64 `json:"temperature"` +} diff --git a/embedder/ollama/ollama.go b/embedder/ollama/ollama.go new file mode 100644 index 00000000..b56c67f5 --- /dev/null +++ b/embedder/ollama/ollama.go @@ -0,0 +1,67 @@ +package ollamaembedder + +import ( + "context" + + "github.com/henomis/lingoose/embedder" + "github.com/henomis/restclientgo" +) + +const ( + defaultModel = "llama2" + defaultEndpoint = "http://localhost:11434/api" +) + +type Embedder struct { + model string + restClient *restclientgo.RestClient +} + +func New() *Embedder { + return &Embedder{ + restClient: restclientgo.New(defaultEndpoint), + model: defaultModel, + } +} + +func (o *Embedder) WithEndpoint(endpoint string) *Embedder { + o.restClient.SetEndpoint(endpoint) + return o +} + +func (o *Embedder) WithModel(model string) *Embedder { + o.model = model + return o +} + +// Embed returns the embeddings for the given texts +func (e *Embedder) Embed(ctx context.Context, texts []string) ([]embedder.Embedding, error) { + embeddings := make([]embedder.Embedding, len(texts)) + for i, text := range texts { + embedding, err := e.embed(ctx, text) + if err != nil { + return nil, err + } + embeddings[i] = embedding + } + + return embeddings, nil +} + +// Embed returns the embeddings for the given texts +func (e *Embedder) embed(ctx context.Context, text string) (embedder.Embedding, error) { + resp := &response{} + err := e.restClient.Post( + ctx, + &request{ + Prompt: text, + Model: e.model, + }, + resp, + ) + if err != nil { + return nil, err + } + + return resp.Embedding, nil +} diff --git a/examples/embeddings/ollama/main.go b/examples/embeddings/ollama/main.go new file mode 100644 index 00000000..eb11012c --- /dev/null +++ b/examples/embeddings/ollama/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "fmt" + + ollamaembedder "github.com/henomis/lingoose/embedder/ollama" + "github.com/henomis/lingoose/index" + indexoption "github.com/henomis/lingoose/index/option" + "github.com/henomis/lingoose/index/vectordb/jsondb" + "github.com/henomis/lingoose/llm/ollama" + "github.com/henomis/lingoose/loader" + "github.com/henomis/lingoose/textsplitter" + "github.com/henomis/lingoose/thread" + "github.com/henomis/lingoose/types" +) + +// download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt + +func main() { + + index := index.New( + jsondb.New().WithPersist("db.json"), + ollamaembedder.New(), + ).WithIncludeContents(true).WithAddDataCallback(func(data *index.Data) error { + data.Metadata["contentLen"] = len(data.Metadata["content"].(string)) + return nil + }) + + indexIsEmpty, _ := index.IsEmpty(context.Background()) + + if indexIsEmpty { + err := ingestData(index) + if err != nil { + panic(err) + } + } + + query := "What is the purpose of the NATO Alliance?" + similarities, err := index.Query( + context.Background(), + query, + indexoption.WithTopK(1), + ) + if err != nil { + panic(err) + } + + for _, similarity := range similarities { + fmt.Printf("Similarity: %f\n", similarity.Score) + fmt.Printf("Document: %s\n", similarity.Content()) + fmt.Println("Metadata: ", similarity.Metadata) + fmt.Println("----------") + } + + documentContext := "" + for _, similarity := range similarities { + documentContext += similarity.Content() + "\n\n" + } + + ollamaLLM := ollama.New() + t := thread.New() + t.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent("Based on the following context answer to the" + + "question.\n\nContext:\n{{.context}}\n\nQuestion: {{.query}}").Format( + types.M{ + "query": query, + "context": documentContext, + }, + ), + )) + + err = ollamaLLM.Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t) +} + +func ingestData(index *index.Index) error { + + fmt.Printf("Ingesting data...") + + documents, err := loader.NewDirectoryLoader(".", ".txt").Load(context.Background()) + if err != nil { + return err + } + + textSplitter := textsplitter.NewRecursiveCharacterTextSplitter(1000, 20) + + documentChunks := textSplitter.SplitDocuments(documents) + + err = index.LoadFromDocuments(context.Background(), documentChunks) + if err != nil { + return err + } + + fmt.Printf("Done!\n") + + return nil +} From ea72117a4786f6f424be4c01081818a192a0a832 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 30 Jan 2024 10:57:57 +0100 Subject: [PATCH 46/65] fix --- embedder/ollama/ollama.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/embedder/ollama/ollama.go b/embedder/ollama/ollama.go index b56c67f5..25d800ac 100644 --- a/embedder/ollama/ollama.go +++ b/embedder/ollama/ollama.go @@ -24,14 +24,14 @@ func New() *Embedder { } } -func (o *Embedder) WithEndpoint(endpoint string) *Embedder { - o.restClient.SetEndpoint(endpoint) - return o +func (e *Embedder) WithEndpoint(endpoint string) *Embedder { + e.restClient.SetEndpoint(endpoint) + return e } -func (o *Embedder) WithModel(model string) *Embedder { - o.model = model - return o +func (e *Embedder) WithModel(model string) *Embedder { + e.model = model + return e } // Embed returns the embeddings for the given texts From dab6909562e185623d2603216d58731330c19fac Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Tue, 30 Jan 2024 16:19:19 +0100 Subject: [PATCH 47/65] fix --- examples/embeddings/ollama/main.go | 6 +++--- llm/ollama/api.go | 34 +++++++++++------------------- llm/ollama/ollama.go | 16 +++++++------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/examples/embeddings/ollama/main.go b/examples/embeddings/ollama/main.go index eb11012c..a8ef80d5 100644 --- a/examples/embeddings/ollama/main.go +++ b/examples/embeddings/ollama/main.go @@ -21,7 +21,7 @@ func main() { index := index.New( jsondb.New().WithPersist("db.json"), - ollamaembedder.New(), + ollamaembedder.New().WithModel("mistral"), ).WithIncludeContents(true).WithAddDataCallback(func(data *index.Data) error { data.Metadata["contentLen"] = len(data.Metadata["content"].(string)) return nil @@ -40,7 +40,7 @@ func main() { similarities, err := index.Query( context.Background(), query, - indexoption.WithTopK(1), + indexoption.WithTopK(3), ) if err != nil { panic(err) @@ -58,7 +58,7 @@ func main() { documentContext += similarity.Content() + "\n\n" } - ollamaLLM := ollama.New() + ollamaLLM := ollama.New().WithModel("mistral") t := thread.New() t.AddMessage(thread.NewUserMessage().AddContent( thread.NewTextContent("Based on the following context answer to the" + diff --git a/llm/ollama/api.go b/llm/ollama/api.go index 654d04bb..53dfde39 100644 --- a/llm/ollama/api.go +++ b/llm/ollama/api.go @@ -32,24 +32,14 @@ func (r *request) ContentType() string { return "application/json" } -type response struct { +type response[T any] struct { HTTPStatusCode int `json:"-"` acceptContentType string `json:"-"` Model string `json:"model"` CreatedAt string `json:"created_at"` -} - -type chatResponse struct { - response - AssistantMessage assistantMessage `json:"message"` -} - -type chatStreamResponse struct { - response - Done bool `json:"done"` - Message message `json:"message"` - - streamCallbackFn restclientgo.StreamCallback + Message T `json:"message"` + Done bool `json:"done"` + streamCallbackFn restclientgo.StreamCallback } type assistantMessage struct { @@ -57,37 +47,37 @@ type assistantMessage struct { Content string `json:"content"` } -func (r *response) SetAcceptContentType(contentType string) { +func (r *response[T]) SetAcceptContentType(contentType string) { r.acceptContentType = contentType } -func (r *response) Decode(body io.Reader) error { +func (r *response[T]) Decode(body io.Reader) error { return json.NewDecoder(body).Decode(r) } -func (r *response) SetBody(_ io.Reader) error { +func (r *response[T]) SetBody(_ io.Reader) error { return nil } -func (r *response) AcceptContentType() string { +func (r *response[T]) AcceptContentType() string { if r.acceptContentType != "" { return r.acceptContentType } return "application/json" } -func (r *response) SetStatusCode(code int) error { +func (r *response[T]) SetStatusCode(code int) error { r.HTTPStatusCode = code return nil } -func (r *response) SetHeaders(_ restclientgo.Headers) error { return nil } +func (r *response[T]) SetHeaders(_ restclientgo.Headers) error { return nil } -func (r *chatStreamResponse) SetStreamCallback(fn restclientgo.StreamCallback) { +func (r *response[T]) SetStreamCallback(fn restclientgo.StreamCallback) { r.streamCallbackFn = fn } -func (r *chatStreamResponse) StreamCallback() restclientgo.StreamCallback { +func (r *response[T]) StreamCallback() restclientgo.StreamCallback { return r.streamCallbackFn } diff --git a/llm/ollama/ollama.go b/llm/ollama/ollama.go index f9ff005b..3fa0831e 100644 --- a/llm/ollama/ollama.go +++ b/llm/ollama/ollama.go @@ -143,32 +143,32 @@ func (o *Ollama) Generate(ctx context.Context, t *thread.Thread) error { } func (o *Ollama) generate(ctx context.Context, t *thread.Thread, chatRequest *request) error { - var response chatResponse + var resp response[assistantMessage] err := o.restClient.Post( ctx, chatRequest, - &response, + &resp, ) if err != nil { return fmt.Errorf("%w: %w", ErrOllamaChat, err) } t.AddMessage(thread.NewAssistantMessage().AddContent( - thread.NewTextContent(response.AssistantMessage.Content), + thread.NewTextContent(resp.Message.Content), )) return nil } func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *request) error { - response := &chatStreamResponse{} + var resp response[message] var assistantMessage string - response.SetAcceptContentType(ndjsonContentType) - response.SetStreamCallback( + resp.SetAcceptContentType(ndjsonContentType) + resp.SetStreamCallback( func(data []byte) error { - var streamResponse chatStreamResponse + var streamResponse response[message] err := json.Unmarshal(data, &streamResponse) if err != nil { @@ -187,7 +187,7 @@ func (o *Ollama) stream(ctx context.Context, t *thread.Thread, chatRequest *requ err := o.restClient.Post( ctx, chatRequest, - response, + &resp, ) if err != nil { return fmt.Errorf("%w: %w", ErrOllamaChat, err) From 88d039fb96d08794a722ccbf817a5e14ed7f5155 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 1 Feb 2024 09:48:18 +0100 Subject: [PATCH 48/65] upgrade golang openai lib and models --- go.mod | 2 +- go.sum | 8 ++------ llm/openai/common.go | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 8d56dd60..78b7b148 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/henomis/restclientgo v1.2.0 github.com/invopop/jsonschema v0.7.0 github.com/pkoukk/tiktoken-go v0.1.1 - github.com/sashabaranov/go-openai v1.17.9 + github.com/sashabaranov/go-openai v1.19.2 ) require ( diff --git a/go.sum b/go.sum index d39b0c6a..b248f958 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/henomis/cohere-go v1.1.1-alpha.1 h1:QiJe7xK008pHyHYDaJFieEmvdVddxqA9BjW5zjpAN6Q= -github.com/henomis/cohere-go v1.1.1-alpha.1/go.mod h1:T+d7fIYTYjPWbjWR8A67ze/Fg181YtugJVnfSPkxzH8= github.com/henomis/cohere-go v1.1.2 h1:rzEA1JRm26RnaQValoVeVQ1y1nTofuhr/z/fPyhUW/4= github.com/henomis/cohere-go v1.1.2/go.mod h1:z+UIgBbNCnLH5M47FYqg4h3CDWxjUv2VDfEwdTb95MU= github.com/henomis/milvus-go v0.0.4 h1:ArddXRJx/EGdQ75gB7TyEzD4Z4BqXzW16p8jRcjJOhs= @@ -19,8 +17,6 @@ github.com/henomis/pinecone-go/v2 v2.0.0 h1:HdAX0nGzBagL6ubn17utIz3FGwXfignBovZe github.com/henomis/pinecone-go/v2 v2.0.0/go.mod h1:enUHclL18IBnq6POF20hd7YMIawUQZ9NznJ22NJD0/w= github.com/henomis/qdrant-go v1.1.0 h1:GU5R4ZZKeD0JXLZ0yv37FUzI0spGECRr2WlE3prmLIY= github.com/henomis/qdrant-go v1.1.0/go.mod h1:p274sBhQPDnfjCzJentWoNIGFadl5yIcLM4Xnmmwk9k= -github.com/henomis/restclientgo v1.2.0-alpha.1 h1:cmffFMiRdUzHDuNEGHVvQ/oSDUM7EuiPIlDxtQ0vzBo= -github.com/henomis/restclientgo v1.2.0-alpha.1/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/henomis/restclientgo v1.2.0 h1:KINVh4zW4qAeqgO8qbsI1QhiQcn4xgMv3Px4H7++BCk= github.com/henomis/restclientgo v1.2.0/go.mod h1:xIeTCu2ZstvRn0fCukNpzXLN3m/kRTU0i0RwAbv7Zug= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 h1:i462o439ZjprVSFSZLZxcsoAe592sZB1rci2Z8j4wdk= @@ -33,8 +29,8 @@ github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6 github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sashabaranov/go-openai v1.17.9 h1:QEoBiGKWW68W79YIfXWEFZ7l5cEgZBV4/Ow3uy+5hNY= -github.com/sashabaranov/go-openai v1.17.9/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= +github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/llm/openai/common.go b/llm/openai/common.go index ce37ee04..5d7b3e65 100644 --- a/llm/openai/common.go +++ b/llm/openai/common.go @@ -28,6 +28,8 @@ const ( GPT432K Model = openai.GPT432K GPT40613 Model = openai.GPT40613 GPT40314 Model = openai.GPT40314 + GPT4Turbo0125 Model = openai.GPT4Turbo0125 + GPT4Turbo1106 Model = openai.GPT4Turbo1106 GPT4TurboPreview Model = openai.GPT4TurboPreview GPT4VisionPreview Model = openai.GPT4VisionPreview GPT4 Model = openai.GPT4 From 35a02420ca9230207ec34ad69a9daee053247ab9 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 1 Feb 2024 10:34:31 +0100 Subject: [PATCH 49/65] update tiktoken --- embedder/cohere/math.go | 45 ++++++++++++++++++++++++++++++++++ embedder/openai/openai.go | 51 +++++---------------------------------- embedder/openai/token.go | 6 ++--- go.mod | 4 +-- go.sum | 8 +++--- 5 files changed, 60 insertions(+), 54 deletions(-) create mode 100644 embedder/cohere/math.go diff --git a/embedder/cohere/math.go b/embedder/cohere/math.go new file mode 100644 index 00000000..cd967797 --- /dev/null +++ b/embedder/cohere/math.go @@ -0,0 +1,45 @@ +package cohereembedder + +import ( + "math" + + "github.com/henomis/lingoose/embedder" +) + +func normalizeEmbeddings(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { + chunkAvgEmbeddings := average(embeddings, lens) + norm := norm(chunkAvgEmbeddings) + + for i := range chunkAvgEmbeddings { + chunkAvgEmbeddings[i] = chunkAvgEmbeddings[i] / norm + } + + return chunkAvgEmbeddings +} + +func average(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { + average := make(embedder.Embedding, len(embeddings[0])) + totalWeight := 0.0 + + for i, embedding := range embeddings { + weight := lens[i] + totalWeight += weight + for j, v := range embedding { + average[j] += v * weight + } + } + + for i, v := range average { + average[i] = v / totalWeight + } + + return average +} + +func norm(a embedder.Embedding) float64 { + var sum float64 + for _, v := range a { + sum += math.Pow(v, 2) + } + return math.Sqrt(sum) +} diff --git a/embedder/openai/openai.go b/embedder/openai/openai.go index 27eade2d..8966e145 100644 --- a/embedder/openai/openai.go +++ b/embedder/openai/openai.go @@ -10,53 +10,14 @@ import ( "github.com/sashabaranov/go-openai" ) -type Model int +type Model = openai.EmbeddingModel const ( - Unknown Model = iota - AdaSimilarity - BabbageSimilarity - CurieSimilarity - DavinciSimilarity - AdaSearchDocument - AdaSearchQuery - BabbageSearchDocument - BabbageSearchQuery - CurieSearchDocument - CurieSearchQuery - DavinciSearchDocument - DavinciSearchQuery - AdaCodeSearchCode - AdaCodeSearchText - BabbageCodeSearchCode - BabbageCodeSearchText - AdaEmbeddingV2 + AdaEmbeddingV2 Model = openai.AdaEmbeddingV2 + SmallEmbedding3 Model = openai.SmallEmbedding3 + LargeEmbedding3 Model = openai.LargeEmbedding3 ) -func (m Model) String() string { - return modelToString[m] -} - -var modelToString = map[Model]string{ - AdaSimilarity: "text-similarity-ada-001", - BabbageSimilarity: "text-similarity-babbage-001", - CurieSimilarity: "text-similarity-curie-001", - DavinciSimilarity: "text-similarity-davinci-001", - AdaSearchDocument: "text-search-ada-doc-001", - AdaSearchQuery: "text-search-ada-query-001", - BabbageSearchDocument: "text-search-babbage-doc-001", - BabbageSearchQuery: "text-search-babbage-query-001", - CurieSearchDocument: "text-search-curie-doc-001", - CurieSearchQuery: "text-search-curie-query-001", - DavinciSearchDocument: "text-search-davinci-doc-001", - DavinciSearchQuery: "text-search-davinci-query-001", - AdaCodeSearchCode: "code-search-ada-code-001", - AdaCodeSearchText: "code-search-ada-text-001", - BabbageCodeSearchCode: "code-search-babbage-code-001", - BabbageCodeSearchText: "code-search-babbage-text-001", - AdaEmbeddingV2: "text-embedding-ada-002", -} - type OpenAIEmbedder struct { openAIClient *openai.Client model Model @@ -143,7 +104,7 @@ func (o *OpenAIEmbedder) concurrentEmbed( func (o *OpenAIEmbedder) safeEmbed(ctx context.Context, text string, maxTokens int) (embedder.Embedding, error) { sanitizedText := text - if strings.HasSuffix(o.model.String(), "001") { + if strings.HasSuffix(string(o.model), "001") { sanitizedText = strings.ReplaceAll(text, "\n", " ") } @@ -207,7 +168,7 @@ func (o *OpenAIEmbedder) openAICreateEmebeddings(ctx context.Context, texts []st ctx, openai.EmbeddingRequest{ Input: texts, - Model: openai.EmbeddingModel(o.model), + Model: o.model, }, ) if err != nil { diff --git a/embedder/openai/token.go b/embedder/openai/token.go index 0b00b39f..ad5120de 100644 --- a/embedder/openai/token.go +++ b/embedder/openai/token.go @@ -5,7 +5,7 @@ import ( ) func (o *OpenAIEmbedder) textToTokens(text string) ([]int, error) { - tokenizer, err := tiktoken.EncodingForModel(o.model.String()) + tokenizer, err := tiktoken.EncodingForModel(string(o.model)) if err != nil { return nil, err } @@ -14,7 +14,7 @@ func (o *OpenAIEmbedder) textToTokens(text string) ([]int, error) { } func (o *OpenAIEmbedder) getMaxTokens() int { - if tiktoken.MODEL_TO_ENCODING[o.model.String()] == "cl100k_base" { + if tiktoken.MODEL_TO_ENCODING[string(o.model)] == "cl100k_base" { return 8191 } @@ -22,7 +22,7 @@ func (o *OpenAIEmbedder) getMaxTokens() int { } func (o *OpenAIEmbedder) tokensToText(tokens []int) (string, error) { - tokenizer, err := tiktoken.EncodingForModel(o.model.String()) + tokenizer, err := tiktoken.EncodingForModel(string(o.model)) if err != nil { return "", err } diff --git a/go.mod b/go.mod index 78b7b148..10df8755 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,12 @@ require ( github.com/henomis/qdrant-go v1.1.0 github.com/henomis/restclientgo v1.2.0 github.com/invopop/jsonschema v0.7.0 - github.com/pkoukk/tiktoken-go v0.1.1 + github.com/pkoukk/tiktoken-go v0.1.5 github.com/sashabaranov/go-openai v1.19.2 ) require ( - github.com/dlclark/regexp2 v1.8.1 // indirect + github.com/dlclark/regexp2 v1.10.0 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect ) diff --git a/go.sum b/go.sum index b248f958..53236990 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/RediSearch/redisearch-go/v2 v2.1.1/go.mod h1:Uw93Wi97QqAsw1DwbQrhVd88 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= -github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= +github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -25,8 +25,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pkoukk/tiktoken-go v0.1.1 h1:jtkYlIECjyM9OW1w4rjPmTohK4arORP9V25y6TM6nXo= -github.com/pkoukk/tiktoken-go v0.1.1/go.mod h1:boMWvk9pQCOTx11pgu0DrIdrAKgQzzJKUP6vLXaz7Rw= +github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= +github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= From b65bda0b7b61ea58abe289a537826ef887f675ea Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 1 Feb 2024 10:45:18 +0100 Subject: [PATCH 50/65] remove tiktoken --- embedder/cohere/math.go | 45 ------------ embedder/openai/math.go | 45 ------------ embedder/openai/math_test.go | 125 --------------------------------- embedder/openai/openai.go | 124 +------------------------------- embedder/openai/openai_test.go | 98 -------------------------- embedder/openai/token.go | 31 -------- go.mod | 3 +- go.sum | 5 +- 8 files changed, 3 insertions(+), 473 deletions(-) delete mode 100644 embedder/cohere/math.go delete mode 100644 embedder/openai/math.go delete mode 100644 embedder/openai/math_test.go delete mode 100644 embedder/openai/openai_test.go delete mode 100644 embedder/openai/token.go diff --git a/embedder/cohere/math.go b/embedder/cohere/math.go deleted file mode 100644 index cd967797..00000000 --- a/embedder/cohere/math.go +++ /dev/null @@ -1,45 +0,0 @@ -package cohereembedder - -import ( - "math" - - "github.com/henomis/lingoose/embedder" -) - -func normalizeEmbeddings(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { - chunkAvgEmbeddings := average(embeddings, lens) - norm := norm(chunkAvgEmbeddings) - - for i := range chunkAvgEmbeddings { - chunkAvgEmbeddings[i] = chunkAvgEmbeddings[i] / norm - } - - return chunkAvgEmbeddings -} - -func average(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { - average := make(embedder.Embedding, len(embeddings[0])) - totalWeight := 0.0 - - for i, embedding := range embeddings { - weight := lens[i] - totalWeight += weight - for j, v := range embedding { - average[j] += v * weight - } - } - - for i, v := range average { - average[i] = v / totalWeight - } - - return average -} - -func norm(a embedder.Embedding) float64 { - var sum float64 - for _, v := range a { - sum += math.Pow(v, 2) - } - return math.Sqrt(sum) -} diff --git a/embedder/openai/math.go b/embedder/openai/math.go deleted file mode 100644 index 12d60a15..00000000 --- a/embedder/openai/math.go +++ /dev/null @@ -1,45 +0,0 @@ -package openaiembedder - -import ( - "math" - - "github.com/henomis/lingoose/embedder" -) - -func normalizeEmbeddings(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { - chunkAvgEmbeddings := average(embeddings, lens) - norm := norm(chunkAvgEmbeddings) - - for i := range chunkAvgEmbeddings { - chunkAvgEmbeddings[i] = chunkAvgEmbeddings[i] / norm - } - - return chunkAvgEmbeddings -} - -func average(embeddings []embedder.Embedding, lens []float64) embedder.Embedding { - average := make(embedder.Embedding, len(embeddings[0])) - totalWeight := 0.0 - - for i, embedding := range embeddings { - weight := lens[i] - totalWeight += weight - for j, v := range embedding { - average[j] += v * weight - } - } - - for i, v := range average { - average[i] = v / totalWeight - } - - return average -} - -func norm(a embedder.Embedding) float64 { - var sum float64 - for _, v := range a { - sum += math.Pow(v, 2) - } - return math.Sqrt(sum) -} diff --git a/embedder/openai/math_test.go b/embedder/openai/math_test.go deleted file mode 100644 index ac4e80f4..00000000 --- a/embedder/openai/math_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package openaiembedder - -import ( - "reflect" - "testing" - - "github.com/henomis/lingoose/embedder" -) - -func Test_norm(t *testing.T) { - type args struct { - a []float64 - } - tests := []struct { - name string - args args - want float64 - }{ - { - name: "norm 1", - args: args{ - a: []float64{1, 2, 3}, - }, - want: 3.7416573867739413, - }, - { - name: "norm 2", - args: args{ - a: []float64{1}, - }, - want: 1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := norm(tt.args.a); got != tt.want { - t.Errorf("norm() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_average(t *testing.T) { - type args struct { - embeddings []embedder.Embedding - lens []float64 - } - tests := []struct { - name string - args args - want embedder.Embedding - }{ - { - name: "average 1", - args: args{ - embeddings: []embedder.Embedding{ - {1, 2, 3}, - {4, 5, 6}, - }, - lens: []float64{1, 1}, - }, - want: embedder.Embedding{2.5, 3.5, 4.5}, - }, - { - name: "average 2", - args: args{ - embeddings: []embedder.Embedding{ - {1, 2, 3}, - {4, 5, 6}, - }, - lens: []float64{2, 1}, - }, - want: embedder.Embedding{2, 3, 4}, - }, - { - name: "average 3", - args: args{ - embeddings: []embedder.Embedding{ - {1, 2, 3}, - {4, 5, 6}, - }, - lens: []float64{1, 2}, - }, - want: embedder.Embedding{3, 4, 5}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := average(tt.args.embeddings, tt.args.lens); !reflect.DeepEqual(got, tt.want) { - t.Errorf("average() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_normalizeEmbeddings(t *testing.T) { - type args struct { - embeddings []embedder.Embedding - lens []float64 - } - tests := []struct { - name string - args args - want embedder.Embedding - }{ - { - name: "normalize 1", - args: args{ - embeddings: []embedder.Embedding{ - {1, 2, 3}, - {4, 5, 6}, - }, - lens: []float64{1, 1}, - }, - want: embedder.Embedding{0.4016096644512494, 0.5622535302317492, 0.722897396012249}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := normalizeEmbeddings(tt.args.embeddings, tt.args.lens); !reflect.DeepEqual(got, tt.want) { - t.Errorf("normalizeEmbeddings() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/embedder/openai/openai.go b/embedder/openai/openai.go index 8966e145..32a22c0c 100644 --- a/embedder/openai/openai.go +++ b/embedder/openai/openai.go @@ -3,8 +3,6 @@ package openaiembedder import ( "context" "os" - "sort" - "strings" "github.com/henomis/lingoose/embedder" "github.com/sashabaranov/go-openai" @@ -40,127 +38,7 @@ func (o *OpenAIEmbedder) WithClient(client *openai.Client) *OpenAIEmbedder { // Embed returns the embeddings for the given texts func (o *OpenAIEmbedder) Embed(ctx context.Context, texts []string) ([]embedder.Embedding, error) { - maxTokens := o.getMaxTokens() - - embeddings, err := o.concurrentEmbed(ctx, texts, maxTokens) - if err != nil { - return nil, err - } - - return embeddings, nil -} - -func (o *OpenAIEmbedder) concurrentEmbed( - ctx context.Context, - texts []string, - maxTokens int, -) ([]embedder.Embedding, error) { - type indexedEmbeddings struct { - index int - embedding embedder.Embedding - err error - } - - var embeddings []indexedEmbeddings - embeddingsChan := make(chan indexedEmbeddings, len(texts)) - - for i, text := range texts { - go func(ctx context.Context, i int, text string, maxTokens int) { - embedding, err := o.safeEmbed(ctx, text, maxTokens) - - embeddingsChan <- indexedEmbeddings{ - index: i, - embedding: embedding, - err: err, - } - }(ctx, i, text, maxTokens) - } - - var err error - for i := 0; i < len(texts); i++ { - embedding := <-embeddingsChan - if embedding.err != nil { - err = embedding.err - continue - } - embeddings = append(embeddings, embedding) - } - - if err != nil { - return nil, err - } - - sort.Slice(embeddings, func(i, j int) bool { - return embeddings[i].index < embeddings[j].index - }) - - var result []embedder.Embedding - for _, embedding := range embeddings { - result = append(result, embedding.embedding) - } - - return result, nil -} - -func (o *OpenAIEmbedder) safeEmbed(ctx context.Context, text string, maxTokens int) (embedder.Embedding, error) { - sanitizedText := text - if strings.HasSuffix(string(o.model), "001") { - sanitizedText = strings.ReplaceAll(text, "\n", " ") - } - - chunkedText, err := o.chunkText(sanitizedText, maxTokens) - if err != nil { - return nil, err - } - - embeddingsForChunks, chunkLens, err := o.getEmebeddingsForChunks(ctx, chunkedText) - if err != nil { - return nil, err - } - - return normalizeEmbeddings(embeddingsForChunks, chunkLens), nil -} - -func (o *OpenAIEmbedder) chunkText(text string, maxTokens int) ([]string, error) { - tokens, err := o.textToTokens(text) - if err != nil { - return nil, err - } - - var textChunks []string - for i := 0; i < len(tokens); i += maxTokens { - end := i + maxTokens - if end > len(tokens) { - end = len(tokens) - } - - textChunk, errToken := o.tokensToText(tokens[i:end]) - if errToken != nil { - return nil, errToken - } - - textChunks = append(textChunks, textChunk) - } - - return textChunks, nil -} - -func (o *OpenAIEmbedder) getEmebeddingsForChunks( - ctx context.Context, - chunks []string, -) ([]embedder.Embedding, []float64, error) { - chunkLens := []float64{} - - embeddingsForChunks, err := o.openAICreateEmebeddings(ctx, chunks) - if err != nil { - return nil, nil, err - } - - for _, chunk := range chunks { - chunkLens = append(chunkLens, float64(len(chunk))) - } - - return embeddingsForChunks, chunkLens, nil + return o.openAICreateEmebeddings(ctx, texts) } func (o *OpenAIEmbedder) openAICreateEmebeddings(ctx context.Context, texts []string) ([]embedder.Embedding, error) { diff --git a/embedder/openai/openai_test.go b/embedder/openai/openai_test.go deleted file mode 100644 index fde710fd..00000000 --- a/embedder/openai/openai_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package openaiembedder - -import ( - "reflect" - "testing" - - "github.com/sashabaranov/go-openai" -) - -func Test_openAIEmbedder_splitText(t *testing.T) { - type fields struct { - openAIClient *openai.Client - model Model - } - type args struct { - text string - maxTokens int - } - tests := []struct { - name string - fields fields - args args - want []string - wantErr bool - }{ - { - name: "split text 1", - fields: fields{ - openAIClient: nil, - model: AdaEmbeddingV2, - }, - args: args{ - text: "Hello, my name is John", - maxTokens: 5, - }, - want: []string{"Hello, my name is", " John"}, - wantErr: false, - }, - { - name: "split text 2", - fields: fields{ - openAIClient: nil, - model: AdaEmbeddingV2, - }, - args: args{ - text: "Hello, my name is John", - maxTokens: 100, - }, - want: []string{"Hello, my name is John"}, - wantErr: false, - }, - { - name: "split text 2", - fields: fields{ - openAIClient: nil, - model: AdaEmbeddingV2, - }, - args: args{ - text: "Lorem ipsum dolor sit amet, consectetur adipisci elit, sed do eiusmod tempor incidunt" + - " ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullamco " + - "laboriosam, nisi ut aliquid ex ea commodi consequatur. Duis aute irure reprehenderit in voluptate " + - "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, " + - "sunt in culpa qui officia deserunt mollit anim id est laborum.", - maxTokens: 10, - }, - want: []string{ - "Lorem ipsum dolor sit amet, consectetur adipisci elit", - ", sed do eiusmod tempor incidunt ut labore et", - " dolore magna aliqua. Ut enim ad minim veniam,", - " quis nostrum exercitationem ullamco laboriosam", - ", nisi ut aliquid ex ea commodi consequ", - "atur. Duis aute irure reprehenderit in volupt", - "ate velit esse cillum dolore eu fugiat nulla", - " pariatur. Excepteur sint obcaecat", - " cupiditat non proident, sunt in culpa qui", - " officia deserunt mollit anim id est labor", - "um.", - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := &OpenAIEmbedder{ - openAIClient: tt.fields.openAIClient, - model: tt.fields.model, - } - got, err := o.chunkText(tt.args.text, tt.args.maxTokens) - if (err != nil) != tt.wantErr { - t.Errorf("openAIEmbedder.splitText() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("openAIEmbedder.splitText() = %#v, want %#v", got, tt.want) - } - }) - } -} diff --git a/embedder/openai/token.go b/embedder/openai/token.go deleted file mode 100644 index ad5120de..00000000 --- a/embedder/openai/token.go +++ /dev/null @@ -1,31 +0,0 @@ -package openaiembedder - -import ( - "github.com/pkoukk/tiktoken-go" -) - -func (o *OpenAIEmbedder) textToTokens(text string) ([]int, error) { - tokenizer, err := tiktoken.EncodingForModel(string(o.model)) - if err != nil { - return nil, err - } - - return tokenizer.Encode(text, nil, nil), nil -} - -func (o *OpenAIEmbedder) getMaxTokens() int { - if tiktoken.MODEL_TO_ENCODING[string(o.model)] == "cl100k_base" { - return 8191 - } - - return 2046 -} - -func (o *OpenAIEmbedder) tokensToText(tokens []int) (string, error) { - tokenizer, err := tiktoken.EncodingForModel(string(o.model)) - if err != nil { - return "", err - } - - return tokenizer.Decode(tokens), nil -} diff --git a/go.mod b/go.mod index 10df8755..17402fbd 100644 --- a/go.mod +++ b/go.mod @@ -15,12 +15,11 @@ require ( github.com/henomis/qdrant-go v1.1.0 github.com/henomis/restclientgo v1.2.0 github.com/invopop/jsonschema v0.7.0 - github.com/pkoukk/tiktoken-go v0.1.5 github.com/sashabaranov/go-openai v1.19.2 ) require ( - github.com/dlclark/regexp2 v1.10.0 // indirect github.com/gomodule/redigo v1.8.9 // indirect github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect + github.com/stretchr/testify v1.8.2 // indirect ) diff --git a/go.sum b/go.sum index 53236990..ef94d22f 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/RediSearch/redisearch-go/v2 v2.1.1/go.mod h1:Uw93Wi97QqAsw1DwbQrhVd88 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= -github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -25,14 +23,13 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/pkoukk/tiktoken-go v0.1.5 h1:hAlT4dCf6Uk50x8E7HQrddhH3EWMKUN+LArExQQsQx4= -github.com/pkoukk/tiktoken-go v0.1.5/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= From f9a564ad3aed1260ce0148cc238bd78e15ad204e Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Thu, 1 Feb 2024 22:29:23 +0100 Subject: [PATCH 51/65] feat: add nomic embedder --- embedder/nomic/api.go | 85 ++++++++++++++++++++++++ embedder/nomic/nomic.go | 74 +++++++++++++++++++++ examples/embeddings/nomic/main.go | 103 ++++++++++++++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 embedder/nomic/api.go create mode 100644 embedder/nomic/nomic.go create mode 100644 examples/embeddings/nomic/main.go diff --git a/embedder/nomic/api.go b/embedder/nomic/api.go new file mode 100644 index 00000000..8c5565e6 --- /dev/null +++ b/embedder/nomic/api.go @@ -0,0 +1,85 @@ +package nomicembedder + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/henomis/lingoose/embedder" + "github.com/henomis/restclientgo" +) + +type Model string + +const ( + ModelNomicEmbedTextV1 Model = "nomic-embed-text-v1" + ModelAllMiniLML6V2 Model = "all-MiniLM-L6-v2" +) + +type TaskType string + +const ( + TaskTypeSearchQuery TaskType = "search_query" + TaskTypeSearchDocument TaskType = "search_document" + TaskTypeClustering TaskType = "clustering" + TaskTypeClassification TaskType = "classification" +) + +type request struct { + Model string `json:"model"` + Texts []string `json:"texts"` + TaskType TaskType `json:"task_type,omitempty"` +} + +func (r *request) Path() (string, error) { + return "/embedding/text", nil +} + +func (r *request) Encode() (io.Reader, error) { + jsonBytes, err := json.Marshal(r) + if err != nil { + return nil, err + } + + return bytes.NewReader(jsonBytes), nil +} + +func (r *request) ContentType() string { + return "application/json" +} + +type response struct { + HTTPStatusCode int `json:"-"` + Embeddings []embedder.Embedding `json:"embeddings"` + Usage Usage `json:"usage"` + RawBody string `json:"-"` +} + +type Usage struct { + TotalTokens int `json:"total_tokens"` +} + +func (r *response) Decode(body io.Reader) error { + return json.NewDecoder(body).Decode(r) +} + +func (r *response) SetBody(body io.Reader) error { + b, err := io.ReadAll(body) + if err != nil { + return err + } + + r.RawBody = string(b) + return nil +} + +func (r *response) AcceptContentType() string { + return "application/json" +} + +func (r *response) SetStatusCode(code int) error { + r.HTTPStatusCode = code + return nil +} + +func (r *response) SetHeaders(_ restclientgo.Headers) error { return nil } diff --git a/embedder/nomic/nomic.go b/embedder/nomic/nomic.go new file mode 100644 index 00000000..a485e142 --- /dev/null +++ b/embedder/nomic/nomic.go @@ -0,0 +1,74 @@ +package nomicembedder + +import ( + "context" + "net/http" + "os" + + "github.com/henomis/lingoose/embedder" + "github.com/henomis/restclientgo" +) + +const ( + defaultEndpoint = "https://api-atlas.nomic.ai/v1" + defaultModel = ModelNomicEmbedTextV1 +) + +type Embedder struct { + taskType TaskType + model Model + restClient *restclientgo.RestClient +} + +func New() *Embedder { + apiKey := os.Getenv("NOMIC_API_KEY") + + return &Embedder{ + restClient: restclientgo.New(defaultEndpoint).WithRequestModifier( + func(req *http.Request) *http.Request { + req.Header.Set("Authorization", "Bearer "+apiKey) + return req + }, + ), + model: defaultModel, + } +} + +func (e *Embedder) WithAPIKey(apiKey string) *Embedder { + e.restClient = restclientgo.New(defaultEndpoint).WithRequestModifier( + func(req *http.Request) *http.Request { + req.Header.Set("Authorization", "Bearer "+apiKey) + return req + }, + ) + return e +} + +func (e *Embedder) WithTaskType(taskType TaskType) *Embedder { + e.taskType = taskType + return e +} + +func (e *Embedder) WithModel(model Model) *Embedder { + e.model = model + return e +} + +// Embed returns the embeddings for the given texts +func (e *Embedder) Embed(ctx context.Context, texts []string) ([]embedder.Embedding, error) { + var resp response + err := e.restClient.Post( + ctx, + &request{ + Texts: texts, + Model: string(e.model), + TaskType: e.taskType, + }, + &resp, + ) + if err != nil { + return nil, err + } + + return resp.Embeddings, nil +} diff --git a/examples/embeddings/nomic/main.go b/examples/embeddings/nomic/main.go new file mode 100644 index 00000000..b978f0af --- /dev/null +++ b/examples/embeddings/nomic/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "context" + "fmt" + + nomicembedder "github.com/henomis/lingoose/embedder/nomic" + "github.com/henomis/lingoose/index" + indexoption "github.com/henomis/lingoose/index/option" + "github.com/henomis/lingoose/index/vectordb/jsondb" + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/loader" + "github.com/henomis/lingoose/prompt" + "github.com/henomis/lingoose/textsplitter" +) + +// download https://raw.githubusercontent.com/hwchase17/chat-your-data/master/state_of_the_union.txt + +func main() { + + index := index.New( + jsondb.New().WithPersist("db.json"), + nomicembedder.New(), + ).WithIncludeContents(true).WithAddDataCallback(func(data *index.Data) error { + data.Metadata["contentLen"] = len(data.Metadata["content"].(string)) + return nil + }) + + indexIsEmpty, _ := index.IsEmpty(context.Background()) + + if indexIsEmpty { + err := ingestData(index) + if err != nil { + panic(err) + } + } + + query := "What is the purpose of the NATO Alliance?" + similarities, err := index.Query( + context.Background(), + query, + indexoption.WithTopK(3), + ) + if err != nil { + panic(err) + } + + for _, similarity := range similarities { + fmt.Printf("Similarity: %f\n", similarity.Score) + fmt.Printf("Document: %s\n", similarity.Content()) + fmt.Println("Metadata: ", similarity.Metadata) + fmt.Println("----------") + } + + documentContext := "" + for _, similarity := range similarities { + documentContext += similarity.Content() + "\n\n" + } + + llmOpenAI := openai.NewCompletion().WithVerbose(true) + prompt1 := prompt.NewPromptTemplate( + "Based on the following context answer to the question.\n\nContext:\n{{.context}}\n\nQuestion: {{.query}}").WithInputs( + map[string]string{ + "query": query, + "context": documentContext, + }, + ) + + err = prompt1.Format(nil) + if err != nil { + panic(err) + } + + output, err := llmOpenAI.Completion(context.Background(), prompt1.String()) + if err != nil { + panic(err) + } + + fmt.Println(output) +} + +func ingestData(index *index.Index) error { + + fmt.Printf("Ingesting data...") + + documents, err := loader.NewDirectoryLoader(".", ".txt").Load(context.Background()) + if err != nil { + return err + } + + textSplitter := textsplitter.NewRecursiveCharacterTextSplitter(1000, 20) + + documentChunks := textSplitter.SplitDocuments(documents) + + err = index.LoadFromDocuments(context.Background(), documentChunks) + if err != nil { + return err + } + + fmt.Printf("Done!\n") + + return nil +} From 83f69b04c1ca3e9d1a286f3f9c783e61c7c81629 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Fri, 2 Feb 2024 10:53:30 +0100 Subject: [PATCH 52/65] add documentation --- README.md | 110 ++-- docs/config.yml | 36 ++ docs/content/_introduction.md | 1 + docs/content/config.md | 21 + docs/content/getting-started.md | 171 ++++++ docs/content/introduction.md | 6 + docs/content/reference/assistant.md | 6 + docs/content/reference/thread.md | 42 ++ docs/layouts/404.html | 5 + docs/layouts/_default/baseof.html | 34 ++ docs/layouts/_default/single.html | 20 + docs/layouts/index.html | 23 + docs/layouts/partials/sidebar.html | 44 ++ docs/layouts/partials/version-banner.html | 14 + docs/layouts/partials/version-switcher.html | 15 + docs/layouts/sitemap.xml | 13 + docs/static/external-link-alt.svg | 5 + docs/static/favicon.png | Bin 0 -> 798 bytes docs/static/lingoose-small.png | Bin 0 -> 29173 bytes docs/static/main.css | 545 ++++++++++++++++++++ docs/static/main.js | 32 ++ docs/static/syntax.css | 57 ++ 22 files changed, 1139 insertions(+), 61 deletions(-) create mode 100644 docs/config.yml create mode 120000 docs/content/_introduction.md create mode 100644 docs/content/config.md create mode 100644 docs/content/getting-started.md create mode 100644 docs/content/introduction.md create mode 100644 docs/content/reference/assistant.md create mode 100644 docs/content/reference/thread.md create mode 100644 docs/layouts/404.html create mode 100644 docs/layouts/_default/baseof.html create mode 100644 docs/layouts/_default/single.html create mode 100644 docs/layouts/index.html create mode 100644 docs/layouts/partials/sidebar.html create mode 100644 docs/layouts/partials/version-banner.html create mode 100644 docs/layouts/partials/version-switcher.html create mode 100644 docs/layouts/sitemap.xml create mode 100644 docs/static/external-link-alt.svg create mode 100644 docs/static/favicon.png create mode 100644 docs/static/lingoose-small.png create mode 100644 docs/static/main.css create mode 100644 docs/static/main.js create mode 100644 docs/static/syntax.css diff --git a/README.md b/README.md index 9093e179..e341ccdc 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,79 @@ -

LINGOOSE

+![lingoose](docs/static/lingoose-small.png) -# 🪿 LinGoose -[![Build Status](https://github.com/henomis/lingoose/actions/workflows/checks.yml/badge.svg)](https://github.com/henomis/lingoose/actions/workflows/checks.yml) [![GoDoc](https://godoc.org/github.com/henomis/lingoose?status.svg)](https://godoc.org/github.com/henomis/lingoose) [![Go Report Card](https://goreportcard.com/badge/github.com/henomis/lingoose)](https://goreportcard.com/report/github.com/henomis/lingoose) [![GitHub release](https://img.shields.io/github/release/henomis/lingoose.svg)](https://github.com/henomis/lingoose/releases) +# 🪿 LinGoose [![Build Status](https://github.com/henomis/lingoose/actions/workflows/checks.yml/badge.svg)](https://github.com/henomis/lingoose/actions/workflows/checks.yml) [![GoDoc](https://godoc.org/github.com/henomis/lingoose?status.svg)](https://godoc.org/github.com/henomis/lingoose) [![Go Report Card](https://goreportcard.com/badge/github.com/henomis/lingoose)](https://goreportcard.com/report/github.com/henomis/lingoose) [![GitHub release](https://img.shields.io/github/release/henomis/lingoose.svg)](https://github.com/henomis/lingoose/releases) -**LinGoose** (_Lingo + Go + Goose_ 🪿) aims to be a complete Go framework for creating LLM apps. 🤖 ⚙️ -> **Did you know?** A goose 🪿 fills its car 🚗 with goose-line ⛽! -

Connect with the Creator

-

- -Follow Simone Vellei - - -Follow on Github - -

+## What is LinGoose? -### Help support this project by giving it a star! ⭐ 🪿 +[LinGoose](https://github.com/henomis/lingoose) is a Go framework for building awesome AI/LLM applications.
-### Start learning LinGoose on Replit [LinGoose course](https://replit.com/@henomis/Building-AI-Applications-with-LinGoose) +- **LinGoose is modular** — You can import only the modules you need to build your application. +- **LinGoose is an abstraction of features** — You can choose your preferred implementation of a feature and/or create your own. +- **LinGoose is a complete solution** — You can use LinGoose to build your AI/LLM application from the ground up. -# Overview -**LinGoose** is a powerful Go framework for developing Large Language Model (LLM) based applications using pipelines. It is designed to be a complete solution and provides multiple components, including Prompts, Templates, Chat, Output Decoders, LLM, Pipelines, and Memory. With **LinGoose**, you can interact with LLM AI through prompts and generate complex templates. Additionally, it includes a chat feature, allowing you to create chatbots. The Output Decoders component enables you to extract specific information from the output of the LLM, while the LLM interface allows you to send prompts to various AI, such as the ones provided by OpenAI. You can chain multiple LLM steps together using Pipelines and store the output of each step in Memory for later retrieval. **LinGoose** also includes a Document component, which is used to store text, and a Loader component, which is used to load Documents from various sources. Finally, it includes TextSplitters, which are used to split text or Documents into multiple parts, Embedders, which are used to embed text or Documents into embeddings, and Indexes, which are used to store embeddings and documents and to perform searches. -# Components +## Quick start +1. [Initialise a new go module](https://golang.org/doc/tutorial/create-module) -**LinGoose** is composed of multiple components, each one with its own purpose. - -| Component | Package | Description | -| ----------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Prompt** | [prompt](prompt/) | Prompts are the way to interact with LLM AI. They can be simple text, or more complex templates. Supports **Prompt Templates** and **[Whisper](https://openai.com) prompt** | -| **Chat Prompt** | [chat](chat/) | Chat is the way to interact with the chat LLM AI. It can be a simple text prompt, or a more complex chatbot. | -| **Decoders** | [decoder](decoder/) | Output decoders are used to decode the output of the LLM. They can be used to extract specific information from the output. Supports **JSONDecoder** and **RegExDecoder** | -| **LLMs** | [llm](llm/) | LLM is an interface to various AI such as the ones provided by OpenAI. It is responsible for sending the prompt to the AI and retrieving the output. Supports **[OpenAI](https://openai.com)**, **[HuggingFace](https://huggingface.co)** and **[Llama.cpp](https://github.com/ggerganov/llama.cpp)**. | -| **Pipelines** | [pipeline](pipeline/) | Pipelines are used to chain multiple LLM steps together. | -| **Memory** | [memory](memory/) | Memory is used to store the output of each step. It can be used to retrieve the output of a previous step. Supports memory in **Ram** | -| **Document** | [document](document/) | Document is used to store a text | -| **Loaders** | [loader](loader/) | Loaders are used to load Documents from various sources. Supports **TextLoader**, **DirectoryLoader**, **PDFToTextLoader** and **PubMedLoader** . | -| **TextSplitters** | [textsplitter](textsplitter/) | TextSplitters are used to split text or Documents into multiple parts. Supports **RecursiveTextSplitter**. | -| **Embedders** | [embedder](embedder/) | Embedders are used to embed text or Documents into embeddings. Supports **[OpenAI](https://openai.com)** | -| **Indexes** | [index](index/) | Indexes are used to store embeddings and documents and to perform searches. Supports **SimpleVectorIndex**, **[Pinecone](https://pinecone.io)** and **[Qdrant](https://qdrant.tech)** | - -# Usage - -Please refer to the documentation at [lingoose.io](https://lingoose.io/docs/) to understand how to use LinGoose. If you prefer the 👉 [examples directory](examples/) contains a lot of examples 🚀. -However, here is a **powerful** example of what **LinGoose** is capable of: +```sh +mkdir example +cd example +go mod init example +``` -_Talk is cheap. Show me the [code](examples/)._ - Linus Torvalds +2. Create your first LinGoose application ```go package main import ( - "context" - - openaiembedder "github.com/henomis/lingoose/embedder/openai" - "github.com/henomis/lingoose/index" - "github.com/henomis/lingoose/index/option" - "github.com/henomis/lingoose/index/vectordb/jsondb" - "github.com/henomis/lingoose/llm/openai" - "github.com/henomis/lingoose/loader" - qapipeline "github.com/henomis/lingoose/pipeline/qa" - "github.com/henomis/lingoose/textsplitter" + "context" + "fmt" + + "github.com/henomis/lingoose/llm/openai" ) func main() { - docs, _ := loader.NewPDFToTextLoader("./kb").WithTextSplitter(textsplitter.NewRecursiveCharacterTextSplitter(2000, 200)).Load(context.Background()) - index := index.New(jsondb.New().WithPersist("db.json"), openaiembedder.New(openaiembedder.AdaEmbeddingV2)).WithIncludeContents(true) - index.LoadFromDocuments(context.Background(), docs) - qapipeline.New(openai.NewChat().WithVerbose(true)).WithIndex(index).Query(context.Background(), "What is the NATO purpose?", option.WithTopK(1)) + + llm := openai.NewCompletion() + + response, err := llm.Completion(context.Background(), "Tell me a joke about geese") + if err != nil { + panic(err) + } + + fmt.Println(response) + } ``` -This is the _famous_ 4-lines **lingoose** knowledge base chatbot. 🤖 +3. Install the Go dependencies +```sh +go mod tidy +``` + +4. Start the example application -# Installation +```sh +export OPENAI_API_KEY=your-api-key -Be sure to have a working Go environment, then run the following command: +go run . -```shell -go get github.com/henomis/lingoose +A goose fills its car with goose-line! ``` -# License +## Reporting Issues + +If you think you've found a bug, or something isn't behaving the way you think it should, please raise an [issue](https://github.com/henomis/lingoose/issues) on GitHub. + +## Contributing + +We welcome contributions, Read our [Contribution Guidelines](https://github.com/henomis/lingoose/blob/master/CONTRIBUTING.md) to learn more about contributing to **LinGoose** + +## License © Simone Vellei, 2023~`time.Now()` -Released under the [MIT License](LICENSE) +Released under the [MIT License](LICENSE) \ No newline at end of file diff --git a/docs/config.yml b/docs/config.yml new file mode 100644 index 00000000..d6982ce8 --- /dev/null +++ b/docs/config.yml @@ -0,0 +1,36 @@ +baseurl: https://lingoose.io/ +metadataformat: yaml +title: lingoose +enableGitInfo: true +pygmentsCodeFences: true +pygmentsUseClasses: true +canonifyURLs: true + +params: + name: lingoose + description: Go framework for building awesome AI/LLM applications + +menu: + main: + - name: Introduction + url: / + weight: -10 + - name: Reference + identifier: reference + weight: 5 + - name: pkg.go.dev → + parent: reference + url: https://pkg.go.dev/github.com/henomis/lingoose + +security: + funcs: + getenv: + - '^HUGO_' + - 'VERSIONS' + - 'CURRENT_VERSION' + +go_initialisms: + replace_defaults: false + initialisms: + - 'CC' + - 'BCC' diff --git a/docs/content/_introduction.md b/docs/content/_introduction.md new file mode 120000 index 00000000..fe840054 --- /dev/null +++ b/docs/content/_introduction.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/docs/content/config.md b/docs/content/config.md new file mode 100644 index 00000000..d48199f0 --- /dev/null +++ b/docs/content/config.md @@ -0,0 +1,21 @@ +--- +linkTitle: Configuration +title: How to configure LinGoose to use APIs +description: How to configure LinGoose to use APIs +menu: main +weight: -5 +--- + +LinGoose implements various APIs to enable you to build AI/LLM applications. For each implementation you need to configure the related API key in your environment. + +## OpenAI +You need to set the `OPENAI_API_KEY` environment variable to your OpenAI API key. To get your API key refer to the [OpenAI website](https://openai.com/). + +## Cohere +You need to set the `COHERE_API_KEY` environment variable to your Cohere API key. To get your API key refer to the [Cohere website](https://cohere.com/). + +## Hugging Face +You need to set the `HUGGING_FACE_HUB_TOKEN` environment variable to your Hugging Face API key. To get your API key refer to the [Hugging Face website](https://huggingface.co/). + +## Nomic +You need to set the `NOMIC_API_KEY` environment variable to your Nomic API key. To get your API key refer to the [Nomic website](https://nomic.ai/). \ No newline at end of file diff --git a/docs/content/getting-started.md b/docs/content/getting-started.md new file mode 100644 index 00000000..c6c17027 --- /dev/null +++ b/docs/content/getting-started.md @@ -0,0 +1,171 @@ +--- +linkTitle: Getting Started +title: Building AI/LLM applications in golang +description: Get started building AI/LLM applications in Golang using LinGoose +menu: main +weight: -7 +--- + +This tutorial will take you through the process of building a powerful AI/LLM application using LinGoose. We will be building a Question Answering system using a knowledge base document and Retrieval Augmented Generation (RAG). + + + +## Set up Project + +Create a directory for your project, and [initialise it as a Go Module](https://golang.org/doc/tutorial/create-module): + +```shell +mkdir lingoose-qa +cd lingoose-qa +go mod init github.com/[username]/lingoose-qa +``` + +Next, create a `main.go` file as follows. We will + +```go +package main + +func main() { + fmt.Println("LinGoose rulez!") +} + +``` + +To automatically add the dependency to your `go.mod` run +```shell +go mod tidy +``` + +By default you'll be using the latest version of LinGoose, but if you want to specify a particular version you can use `go get` (replacing `VERSION` with the particular version desired) +```shell +go get -d github.com/henomis/lingoose@VERSION +``` + + +## Project Implementation + +### Create an AI assistant + +The `assistant` package provides a high-level interface for building AI/LLM applications. It is designed to be easy to use and flexible, allowing you to build a wide range of applications, from simple chatbots to complex AI systems. + +To create an AI assistant, you need to create an instance of `assistant.Assistant`, and then configure it with the necessary components. An AI assistant needs at least one LLM model to be run. + +```go +myAssistant := assistant.New(openai.New().WithTemperature(0)) +``` + +In this example, we create an AI assistant with an LLM model from OpenAI. We also set the temperature to 0, which means that the model will always return the most likely response. + +### Attach a Thread + +The `thread` package provides a simple interface for managing conversations. You can create a new thread, add messages to it, and then run the thread to get the AI assistant's response. + +```go +myAssistant := assistant.New(openai.New().WithTemperature(0)).WithThread( + thread.New().AddMessages( + thread.NewUserMessage().AddContent( + thread.NewTextContent("what is the purpose of NATO?"), + ), + ), +) +``` + +In this example, we create a new thread and add a user message to it. We then attach the thread to the AI assistant, so that it can process the user message and generate a response. + +### Try your first AI response! + +```go +err := myAssistant.Run(context.Background()) +if err != nil { + panic(err) +} + +fmt.Println(myAssistant.Thread()) +``` + +This will print the response generated by the AI assistant. You can now use this response to continue the conversation, or to perform any other action in your application. + +### Attach a RAG + +The `rag` package provides a high-level interface for building Retrieval Augmented Generation (RAG) models. You can create an instance of `rag.RAG`, and then configure it with the necessary components. + +```go +myRAG := rag.New( + index.New( + jsondb.New().WithPersist("db.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), +).WithTopK(3) +``` + +In this example, we create a RAG instance with a JSON index and an OpenAI embedder. We also set the `topK` parameter to 3, which means that the RAG will return the top 3 most relevant documents from the knowledge base. + +Now you can add sources to the RAG to build your knowledge base. + +```go +_, err := os.Stat("db.json") +if os.IsNotExist(err) { + err = myRAG.AddSources(context.Background(), "state_of_the_union.txt") + if err != nil { + panic(err) + } +} +``` + +This will add the contents of the `state_of_the_union.txt` file to the knowledge base and your responses will be based on the content of this file. + +### Full Example + +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/henomis/lingoose/assistant" + openaiembedder "github.com/henomis/lingoose/embedder/openai" + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/index/vectordb/jsondb" + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/rag" + "github.com/henomis/lingoose/thread" +) + +func main() { + myRAG := rag.New( + index.New( + jsondb.New().WithPersist("db.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), + ).WithTopK(3) + + _, err := os.Stat("db.json") + if os.IsNotExist(err) { + err = myRAG.AddSources(context.Background(), "state_of_the_union.txt") + if err != nil { + panic(err) + } + } + + myAssistant := assistant.New( + openai.New().WithTemperature(0), + ).WithRAG(myRAG).WithThread( + thread.New().AddMessages( + thread.NewUserMessage().AddContent( + thread.NewTextContent("what is the purpose of NATO?"), + ), + ), + ) + + err = myRAG.Run(context.Background()) + if err != nil { + panic(err) + } + + fmt.Println("----") + fmt.Println(myAssistant.Thread()) + fmt.Println("----") +} +``` \ No newline at end of file diff --git a/docs/content/introduction.md b/docs/content/introduction.md new file mode 100644 index 00000000..f619918f --- /dev/null +++ b/docs/content/introduction.md @@ -0,0 +1,6 @@ +--- +linkTitle: Introduction +title: Awesome AI/LLM framework for Go +type: homepage +date: 2018-03-17T13:06:47+11:00 +--- diff --git a/docs/content/reference/assistant.md b/docs/content/reference/assistant.md new file mode 100644 index 00000000..f3b5aa4b --- /dev/null +++ b/docs/content/reference/assistant.md @@ -0,0 +1,6 @@ +--- +title: "Your AI assistant" +description: "Your AI assistant" +linkTitle: "Assistant" +menu: { main: { parent: 'reference', weight: -99 } } +--- diff --git a/docs/content/reference/thread.md b/docs/content/reference/thread.md new file mode 100644 index 00000000..88f0247f --- /dev/null +++ b/docs/content/reference/thread.md @@ -0,0 +1,42 @@ +--- +title: "Your conversation history" +description: +linkTitle: "Thread" +menu: { main: { parent: 'reference', weight: -100 } } +--- + +# Thread + +The `thread` package manages communication or interaction in a threaded conversation model. This could be used in a variety of applications, such as chat applications, or any system that requires structured, multi-role communication. + +## Create a Thread + +To create a new thread, you need to create an instance of `thread.Thread` and then add messages to it. You can then run the thread to get the AI assistant's response. + +```go +myThread := thread.New() +``` + +## Add Messages to a Thread + +You can add messages to a thread using the `AddMessages` method. Each message should have a role and a content. + +```go +myThread.AddMessages( + thread.NewSystemMessage().AddContent( + thread.NewTextContent("You are a powerful AI assistant."), + ), + thread.NewUserMessage().AddContent( + thread.NewTextContent("what is the purpose of NATO?"), + ), +) +``` +A Message can have different types of roles such as `System`, `Assistant` or `User`. A Message can have different types of content, such as text, image, or when available tool calls. + +## Your Thread, your history + +Your thread will keep track of all the messages and responses. You can access the thread's history using the `Messages` field. To print the thread's history, you can use the `String` method. + +```go +fmt.Println(myThread) +``` diff --git a/docs/layouts/404.html b/docs/layouts/404.html new file mode 100644 index 00000000..6c298de5 --- /dev/null +++ b/docs/layouts/404.html @@ -0,0 +1,5 @@ +{{ define "main" }} +

Page not found

+ + I'm sorry, but the requested page wasn’t found on the server. +{{ end }} \ No newline at end of file diff --git a/docs/layouts/_default/baseof.html b/docs/layouts/_default/baseof.html new file mode 100644 index 00000000..b4b07912 --- /dev/null +++ b/docs/layouts/_default/baseof.html @@ -0,0 +1,34 @@ + + + + + + {{- if .Params.description }} + {{- else }} + {{- end }} + + {{ if not .IsHome }}{{ .Title }} —{{ end }} {{ .Site.Title }} + + + + + + + + + + + + + + + {{ partial "sidebar" . }} {{ block "main" . }}{{ end }} + + + diff --git a/docs/layouts/_default/single.html b/docs/layouts/_default/single.html new file mode 100644 index 00000000..4b67df02 --- /dev/null +++ b/docs/layouts/_default/single.html @@ -0,0 +1,20 @@ +{{ define "main" }} +
+
+

{{ .LinkTitle }}

+
{{ .Title }}
+ + [edit] + +
+
+ +
+
+ {{partial "version-banner"}} + {{ .Content }} +
+
+{{ end }} diff --git a/docs/layouts/index.html b/docs/layouts/index.html new file mode 100644 index 00000000..c8f0b7bb --- /dev/null +++ b/docs/layouts/index.html @@ -0,0 +1,23 @@ +{{ define "main" }} + {{ range where .Site.Pages "Type" "homepage" }} +
+ {{ .Description }} +
+

{{ .LinkTitle }}

+
{{ .Title }}
+
+
+ +
+
+ {{partial "version-banner"}} + {{ .Content }} + {{.Scratch.Set "intro" (readFile "content/_introduction.md")}} + {{.Scratch.Set "intro" (split (.Scratch.Get "intro") "\n")}} + {{.Scratch.Set "intro" (after 2 (.Scratch.Get "intro"))}} + {{.Scratch.Set "intro" (delimit (.Scratch.Get "intro") "\n")}} + {{.Scratch.Get "intro"|markdownify}} +
+
+ {{ end }} +{{ end }} diff --git a/docs/layouts/partials/sidebar.html b/docs/layouts/partials/sidebar.html new file mode 100644 index 00000000..21910549 --- /dev/null +++ b/docs/layouts/partials/sidebar.html @@ -0,0 +1,44 @@ + + + diff --git a/docs/layouts/partials/version-banner.html b/docs/layouts/partials/version-banner.html new file mode 100644 index 00000000..924c5056 --- /dev/null +++ b/docs/layouts/partials/version-banner.html @@ -0,0 +1,14 @@ +{{ $currentVersion := getenv "CURRENT_VERSION" }} +{{ $versionString := getenv "VERSIONS" }} +{{ $versions := split $versionString "," }} +{{ $latestVersion := index $versions 0 }} + +{{ if (eq $currentVersion "master") }} +
+ You are looking at the docs for the unreleased master branch. The latest version is {{ $latestVersion }}. +
+{{ else if not (eq $latestVersion $currentVersion) }} +
+ You are looking at the docs for an older version ({{ $currentVersion }}). The latest version is {{ $latestVersion }}. +
+{{ end }} diff --git a/docs/layouts/partials/version-switcher.html b/docs/layouts/partials/version-switcher.html new file mode 100644 index 00000000..21c9f879 --- /dev/null +++ b/docs/layouts/partials/version-switcher.html @@ -0,0 +1,15 @@ +{{ $VersionString := getenv "VERSIONS" }} +{{ $Versions := split $VersionString "," }} +{{ $currentVersion := getenv "CURRENT_VERSION" }} + +
+ {{ $currentVersion }} +
+ {{$currentVersion}} + {{ range $i, $version := $Versions }} + {{ if not (eq $currentVersion $version) }} + {{$version}} + {{ end }} + {{ end }} +
+
diff --git a/docs/layouts/sitemap.xml b/docs/layouts/sitemap.xml new file mode 100644 index 00000000..b5a19b01 --- /dev/null +++ b/docs/layouts/sitemap.xml @@ -0,0 +1,13 @@ + + {{ range .Data.Pages }} + {{ if or .Description (eq .Kind "home") }} + + {{ .Permalink }} + {{ if not .Lastmod.IsZero }}{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05-07:00" ) }}{{ end }} + {{ with .Sitemap.ChangeFreq }}{{ . }}{{ end }} + {{ if ge .Sitemap.Priority 0.0 }}{{ .Sitemap.Priority }}{{ end }} + + {{ end }} + {{ end }} + + diff --git a/docs/static/external-link-alt.svg b/docs/static/external-link-alt.svg new file mode 100644 index 00000000..fb0c7ad6 --- /dev/null +++ b/docs/static/external-link-alt.svg @@ -0,0 +1,5 @@ + + diff --git a/docs/static/favicon.png b/docs/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..531857ec2ad12855e13e207baa09ccce5c2da354 GIT binary patch literal 798 zcmV+(1L6FMP)_dRB5E#WQjD1e_MxfZQy9{NK?OmUWg$LP z^nnl=^3g ziUk6|S^KOrBq!7YLP#8k$EgFtWmuYih=|@`JYEnX|A4Bj>DBv}C5cIi5(MO0%vM#Q zyl_DOM&FKz^fCb=E<J~cyZY3ZGUaem?pa@?rme=Do5ILs zTUl9ud$oDWJtO=%`2*tD#WDM{1#4Gj004D_5LTn@87z<&j3!IMeoHq+II_3!$5Il) zClpG>klv(yC*<>dQ4!p_U>Ohq*u{_HE1KI|6ze5ouKM1U$cS*l96HrZS*FOy000m!M*;v~RrmHWCX;S(VDS2?A`wDJLsLWgv@}fsh|~7h z+99bZns6AFHF|xpQC=hy?b#_R%1A9(>R|B;h=}SgbrR+E1^hjIpKmwT7nbGaMjk9J zUUTPkeV(sd^I@K*106RzNXQciA;+U7;+jt*y4@TuSJK(_qCYk^nkp}omop4w&)HEh z>~%OFA>y-;RR(|!hmTg=@6%~?Mw@ZOWHXHR>NHx_5!GD)kc6!A${~b^0U#X!C;%8k c#F=2ce;jOkS89xGNB{r;07*qoM6N<$f)Qt4`v3p{ literal 0 HcmV?d00001 diff --git a/docs/static/lingoose-small.png b/docs/static/lingoose-small.png new file mode 100644 index 0000000000000000000000000000000000000000..90b92d4343c4dbaa9de14145d5b0e003a61d2fa0 GIT binary patch literal 29173 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 CfusWf literal 0 HcmV?d00001 diff --git a/docs/static/main.css b/docs/static/main.css new file mode 100644 index 00000000..a0fa65a1 --- /dev/null +++ b/docs/static/main.css @@ -0,0 +1,545 @@ +:root { + --font-text: 'Roboto', sans-serif; + --font-heading: 'Work Sans', sans-serif; + --font-code: 'Source Code Pro', monospace; + + --main-bg-color: coral; + + --color-link: #336699; + --color-text: #556; + + --color-heading-text: #445; + --color-heading-background: #e9ebed; + + --color-nav-text: #eee; + --color-nav-background: #2c3e50; + --color-nav-active: #1abc9c; + + --color-anchor-default: #DDD; + --color-anchor-hover: #666; + + --color-code-text: #445; + --color-code-background: #f5f9fc; + + --color-blockquote-background: #fffaf3; + --color-blockquote-highlight: rgba(0, 0, 0, 0.1); + + --margin-default: 15px; +} + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +ol, ul { + margin-bottom: var(--margin-default); + list-style: disc; + margin-left: 1.5em; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} + +body { + font-family: var(--font-text); + font-size: 15px; + line-height: 1.55em; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +a { + color: var(--color-link); + text-decoration: none; +} + +img { + max-width: 100%; +} + +a:hover { + text-decoration: underline; +} + +@media (min-width: 768px) { + body { + display: grid; + grid-template: + 'logo header' + 'nav content' + 'nav footer'; + grid-template-columns: 200px 1fr; + grid-template-rows: min-content auto min-content; + } +} + +main { + flex: 1; + padding: 0 20px 20px; + color: var(--color-text); +} + +.content { + position: relative; + grid-area: content; + max-width: 920px; + margin: auto; +} + +main .content { + margin-top: 40px; +} + +header { + grid-area: header; + background: var(--color-heading-background); + padding: 45px 20px; + overflow: hidden; +} + +footer { + padding: 2px; + text-align: center; + font-size: 0.7em; + color: var(--color-heading-text); +} + +h1,h2,h3,h4,h5,h6 { + font-family: var(--font-heading); + color: #445; +} + +h1 { + font-size: 25px; + font-weight: 700; + margin: 15px 0 10px 0; + position: relative; +} + +.description { + font-family: 'Work Sans', sans-serif; + font-size: 18px; + color: var(--color-text); +} + +.header-link { + position: absolute; + top: 0; + right: 0; +} + +h2 { + margin-top: 2em; + margin-bottom: var(--margin-default); + font-size: 19px; + font-weight: 700; +} + +h3 { + margin-top: 1.5em; + margin-bottom: var(--margin-default); + font-size: 16px; + font-weight: 500; +} + +p { + margin-bottom: var(--margin-default); +} + +nav { + grid-area: nav; + color: var(--color-nav-text); + background-color: var(--color-nav-background); + font-family: var(--font-heading); + font-weight: 500; +} + +.menu { + +} + +.menu a { + color: inherit; +} + +.menu a:hover { + text-decoration: none; +} + +.menu-item { + display: block; + padding: 5px 10px; +} + +.submenu .menu-item { + padding: 5px 20px; +} + +.submenu-heading { + margin-top: 15px; +} + +ul.menu { + margin-left:0; + list-style: none; +} + +ul.submenu { + margin-left: 0; + list-style: none; + margin-bottom: 0; +} + +ul.submenu span { + padding: 5px 10px; +} + +ul.menu li.active, +ul.menu a:hover { + background-color: var(--color-nav-active); +} + +.layout--logo { + grid-area: logo; + background-color: var(--color-nav-background); +} + +.logo { + grid-area: logo; + color: #eee; + margin: 15px; + text-align: center; + display: block; +} + +.logo svg { + fill: currentColor; + max-width: 30px; +} + +.logo--name { + vertical-align: top; + height: 100%; + font-size: 30px; +} + +.logo:hover { + text-decoration: none; +} + +code { + font-family: var(--font-code); + font-weight: 500; + color: var(--color-code-text); + background-color: var(--color-code-background); + border-radius: 3px; + display: inline-block; + padding: 0px 5px; + font-size: 13px; + line-height: 1.5; +} + +pre > code { + overflow: auto; + display: block; + padding: 5px 10px; + margin-bottom: var(--margin-default); +} + +strong { + font-weight: 700; +} + +em { + font-style: italic; +} + +.anchor-link { + display: inline-block; +} + +.anchor-link:hover { + text-decoration: none; +} + +.anchor-icon { + fill: var(--color-anchor-default); + display: inline-block; + vertical-align: middle; + padding: 0 5px; + width: 14px; +} + +.anchor-icon:hover { + fill: var(--color-anchor-hover); +} + +@media (min-width: 768px) { + .logo { + margin: 20px 50px; + + } + .logo svg { + max-width: none; + margin: 5px; + } + nav input { + display: none; + } +} + +/* pure css hamburger, adapted from https://codepen.io/erikterwan/pen/EVzeRP */ + +@media (max-width: 767px) { + .layout--logo { + z-index: 2; + } + + nav { + -webkit-user-select: none; + user-select: none; + + } + + .hamburger { + position: absolute; + top: 0px; + left: 0px; + margin: 15px; + z-index: 3; + } + + nav input { + display: block; + width: 70px; + height: 70px; + position: absolute; + top: -7px; + left: -5px; + + cursor: pointer; + + opacity: 0; /* hide this */ + z-index: 4; /* and place it over the hamburger */ + + -webkit-touch-callout: none; + } + + .hamburger span { + display: block; + width: 28px; + height: 4px; + margin: 5px; + position: relative; + background: currentColor; + border-radius: 3px; + z-index: 1; + transform-origin: 0 1.5px; + + transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), + background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0), + opacity 0.55s ease; + } + + nav input:checked ~ .hamburger span { + opacity: 1; + transform: rotate(45deg) translate(-2px, -1px); + } + + nav input:checked ~ .hamburger span:nth-last-child(2) { + opacity: 0; + transform: rotate(0deg) scale(0.2, 0.2); + } + + nav input:checked ~ .hamburger span:nth-last-child(1) { + transform: rotate(-45deg) translate(0, -1px); + } + + .menu { + z-index: 1; + position: absolute; + width: 300px; + height: 100%; + margin: -100px 0 0 -50px; + padding: 150px 0; + + color: var(--color-heading-text); + background-color: var(--color-heading-background); + font-family: var(--font-heading); + + list-style-type: none; + -webkit-font-smoothing: antialiased; + /* to stop flickering of text in safari */ + + transform-origin: 0% 0%; + transform: translate(-100%, 0); + + transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1.0); + } + + ul.menu li.active, + ul.menu a:hover { + background-color: #d3d5d7; + } + + nav input:checked ~ ul { + transform: none; + } + +} + + +blockquote { + background-color: var(--color-blockquote-background); + border-left-color: var(--color-blockquote-highlight); + border-left-width: 9px; + border-left-style: solid; + padding: 1em 20px 1em 11px; + margin-bottom: var(--margin-default); + margin-left: -20px; + margin-right: -20px; +} + +blockquote p { + margin-bottom: 0; +} + +/* Blockquote headings. */ +blockquote p:first-of-type { + font-weight: bold; +} + +blockquote code { + background-color: var(--color-blockquote-highlight); +} + +table { + width: 100%; +} + +td, th { + padding: 0.2em 1em; +} + +tr { + border-bottom: 1px solid var(--color-heading-background); +} + +tr td:first-child, th { + font-weight: bold; +} + +.version-switcher { + position: absolute; + top: 18px; + right: 16px; + display: inline-block; + width: 80px; + z-index: 3; + color: white; +} + + +.version-switcher-options { + display: none; + /*opacity: 40%;*/ + color: var(--color-text); + position: absolute; + top: 0; + right: 0; + background-color: #f9f9f9; + width: 80px; + box-shadow: 0px 3px 7px 0px rgba(0,0,0,0.2); + z-index: 2; +} + +@media (min-width: 768px) { + .version-switcher { + color: var(--color-text); + } +} + +.version-switcher-options a, .version-switcher span { + list-style-type: none; + padding: 3px 10px; + display: block; + cursor: pointer; + font-family: var(--font-code); +} + +.version-switcher-options a { + color: var(--color-text); +} + +.version-switcher-options a:hover { + background: #bbb; + text-decoration: none; +} + +.version-switcher-options a:first-child { + background: #ccc; +} + +.version-switcher:hover .version-switcher-options { + display: block; +} + +.alert-warning { + background-color: rgba(255, 85, 35, 0.84); + padding: 10px; + color: white; + font-weight: bolder; +} + +.alert-warning a { + color: #ff6; +} + +.icon { + display: inline-block; + line-height: .75em; + width: 1em; +} + +.icon svg { + display: block; +} + +.icon svg path { + fill: currentColor; +} + +ul.submenu span.icon { + padding: 0; +} diff --git a/docs/static/main.js b/docs/static/main.js new file mode 100644 index 00000000..189d660a --- /dev/null +++ b/docs/static/main.js @@ -0,0 +1,32 @@ +var anchorForId = function (id) { + var anchor = document.createElement("a"); + anchor.className = "anchor-link"; + anchor.href = "#" + id; + anchor.innerHTML = ' '; + return anchor; +}; + +var linkifyAnchors = function (level, containingElement) { + var headers = containingElement.getElementsByTagName("h" + level); + for (var h = 0; h < headers.length; h++) { + var header = headers[h]; + + if (typeof header.id !== "undefined" && header.id !== "") { + header.appendChild(anchorForId(header.id)); + + } + } +}; + + +document.onreadystatechange = function () { + if (this.readyState === "complete") { + var contentBlock = document.getElementsByTagName("body")[0] + if (!contentBlock) { + return; + } + for (var level = 2; level <= 4; level++) { + linkifyAnchors(level, contentBlock); + } + } +}; diff --git a/docs/static/syntax.css b/docs/static/syntax.css new file mode 100644 index 00000000..4f561d46 --- /dev/null +++ b/docs/static/syntax.css @@ -0,0 +1,57 @@ +.chroma .c { color: #aaaaaa; font-style: italic } /* Comment */ +.chroma .err { color: #F00000; background-color: #F0A0A0 } /* Error */ +.chroma .k { color: #0000aa } /* Keyword */ +.chroma .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ +.chroma .cp { color: #4c8317 } /* Comment.Preproc */ +.chroma .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ +.chroma .cs { color: #0000aa; font-style: italic } /* Comment.Special */ +.chroma .gd { color: #aa0000 } /* Generic.Deleted */ +.chroma .ge { font-style: italic } /* Generic.Emph */ +.chroma .gr { color: #aa0000 } /* Generic.Error */ +.chroma .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.chroma .gi { color: #00aa00 } /* Generic.Inserted */ +.chroma .go { color: #888888 } /* Generic.Output */ +.chroma .gp { color: #555555 } /* Generic.Prompt */ +.chroma .gs { font-weight: bold } /* Generic.Strong */ +.chroma .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.chroma .gt { color: #aa0000 } /* Generic.Traceback */ +.chroma .kc { color: #0000aa } /* Keyword.Constant */ +.chroma .kd { color: #0000aa } /* Keyword.Declaration */ +.chroma .kn { color: #0000aa } /* Keyword.Namespace */ +.chroma .kp { color: #0000aa } /* Keyword.Pseudo */ +.chroma .kr { color: #0000aa } /* Keyword.Reserved */ +.chroma .kt { color: #00aaaa } /* Keyword.Type */ +.chroma .m { color: #009999 } /* Literal.Number */ +.chroma .s { color: #aa5500 } /* Literal.String */ +.chroma .na { color: #1e90ff } /* Name.Attribute */ +.chroma .nb { color: #00aaaa } /* Name.Builtin */ +.chroma .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ +.chroma .no { color: #aa0000 } /* Name.Constant */ +.chroma .nd { color: #888888 } /* Name.Decorator */ +.chroma .ni { color: #800000; font-weight: bold } /* Name.Entity */ +.chroma .nf { color: #00aa00 } /* Name.Function */ +.chroma .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ +.chroma .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ +.chroma .nv { color: #aa0000 } /* Name.Variable */ +.chroma .ow { color: #0000aa } /* Operator.Word */ +.chroma .w { color: #bbbbbb } /* Text.Whitespace */ +.chroma .mf { color: #009999 } /* Literal.Number.Float */ +.chroma .mh { color: #009999 } /* Literal.Number.Hex */ +.chroma .mi { color: #009999 } /* Literal.Number.Integer */ +.chroma .mo { color: #009999 } /* Literal.Number.Oct */ +.chroma .sb { color: #aa5500 } /* Literal.String.Backtick */ +.chroma .sc { color: #aa5500 } /* Literal.String.Char */ +.chroma .sd { color: #aa5500 } /* Literal.String.Doc */ +.chroma .s2 { color: #aa5500 } /* Literal.String.Double */ +.chroma .se { color: #aa5500 } /* Literal.String.Escape */ +.chroma .sh { color: #aa5500 } /* Literal.String.Heredoc */ +.chroma .si { color: #aa5500 } /* Literal.String.Interpol */ +.chroma .sx { color: #aa5500 } /* Literal.String.Other */ +.chroma .sr { color: #009999 } /* Literal.String.Regex */ +.chroma .s1 { color: #aa5500 } /* Literal.String.Single */ +.chroma .ss { color: #0000aa } /* Literal.String.Symbol */ +.chroma .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ +.chroma .vc { color: #aa0000 } /* Name.Variable.Class */ +.chroma .vg { color: #aa0000 } /* Name.Variable.Global */ +.chroma .vi { color: #aa0000 } /* Name.Variable.Instance */ +.chroma .il { color: #009999 } /* Literal.Number.Integer.Long */ From f8ed9b9764e9c8ce2fbc69035048c01b931e008e Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 16:23:08 +0100 Subject: [PATCH 53/65] fix doc --- .gitignore | 4 +- docs/content/reference/llm.md | 117 ++++++++++++++++++++++++++++++++++ thread/thread.go | 21 ++++++ 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 docs/content/reference/llm.md diff --git a/.gitignore b/.gitignore index 5a53df6a..663d0bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,6 @@ bin/ # Dependency directories (remove the comment below to include it) # vendor/ llama.cpp/ -whisper.cpp/ \ No newline at end of file +whisper.cpp/ + +*/.hugo_build.lock \ No newline at end of file diff --git a/docs/content/reference/llm.md b/docs/content/reference/llm.md new file mode 100644 index 00000000..a300dc70 --- /dev/null +++ b/docs/content/reference/llm.md @@ -0,0 +1,117 @@ +--- +title: "Large Language Models" +description: +linkTitle: "LLM" +menu: { main: { parent: 'reference', weight: -99 } } +--- + + +Large Language Models (LLMs) are a type of artificial intelligence model that are trained on a vast amount of text data. They are capable of understanding and generating human-like text, making them incredibly versatile for a wide range of applications. + +Thanks to LLM API providers, we now have access to a variety of solutions. LinGoose defines LLMs as an interface exposing a set of methods to interact with the model. This allows developers to use the same code to interact with different LLMs, regardless of the underlying implementation. + +LinGoose supports the following LLM providers API: +- [OpenAI](https://openai.com) +- [Cohere](https://cohere.ai) +- [Huggingface](https://huggingface.co) +- [Ollama](https://ollama.ai) +- [LocalAI](https://localai.io/) (_via OpenAI API compatibility_) + +## Using LLMs + +To use an LLM, you need to create an instance of your preferrede LLM provider. Here we show how to create an instance of an LLM using the OpenAI API: + +```go +openaiLLM := openai.New() +``` + +A LinGoose LLM implementation may have different methods to configure the behavior of the model, such as setting the temperature, maximum tokens, and other parameters. Here is an example of how to set the temperature, maximum tokens, and model for an OpenAI LLM: + +```go +openaiLLM := openai.New(). + WithTemperature(0.5). + WithMaxTokens(1000). + WithModel(openai.GPT4) +``` + +By default LinGoose LLM will use the API key from the related environment variable (e.g. `OPENAI_API_KEY` for OpenAI). + +## Generating responses + +To generate a response from an LLM, you need to call the `Generate` method and pass a thread containing the user, system or assistant messages. Here is an example of how to generate a response from an OpenAI LLM: + +```go +myThread := thread.New().AddMessage( + thread.NewSystemMessage().AddContent( + thread.NewTextContent("You are a powerful AI assistant."), + ), +).AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Hello, how are you?"), + ), +) + +err = openaiLLM.Generate(context.Background(), myThread) +if err != nil { + panic(err) +} + +fmt.Println(myThread) +``` + +Generating a response from an LLM will update the thread with the AI assistant's response. You can access the thread's history using the `Messages` field. To print the thread's history, you can use the `String` method. + +## OpenAI tools call + +LinGoose supports function binding to OpenAI tools. You can use the `BindFunction` method to add a tool to a the OpenAI LLM instance. Here is an example of how to add a tool call to a message: + +```go +type currentWeatherInput struct { + City string `json:"city" jsonschema:"description=City to get the weather for"` +} + +func getCurrentWeather(input currentWeatherInput) string { + // your code here + return "The forecast for " + input.City + " is sunny." +} + +... + +toolChoice := "auto" +openaiLLM := openai.New().WithToolChoice(&toolChoice) +err := openaiLLM.BindFunction( + getCurrentWeather, + "getCurrentWeather", + "use this function to get the current weather for a city", +) +if err != nil { + panic(err) +} + +myThread := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("I want to ride my bicycle here in Rome, but I don't know the weather."), + ), +) + +err = openaiLLM.Generate(context.Background(), myThread) +if err != nil { + panic(err) +} + +if myThread.LastMessage().Role == thread.RoleTool { + + myThread.LastMessage() + + // last message is a tool call, enrich the thread with the assistant response + // based on the tool call result + err = openaiLLM.Generate(context.Background(), myThread) + if err != nil { + panic(err) + } +} + +fmt.Println(myThread) +``` + +LinGoose allows you to bind a function describing its scope and input's schema. The function will be called by the OpenAI LLM automatically depending on the user's input. Here we force the tool choice to be "auto" to let OpenAI decide which tool to use. If, after an LLM generation, the last message is a tool call, you can enrich the thread with a new LLM generation based on the tool call result. diff --git a/thread/thread.go b/thread/thread.go index 745858e1..74b2a1da 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -217,3 +217,24 @@ func (c *Content) Format(input types.M) *Content { return c } + +func (c *Content) AsString() string { + if contentAsString, ok := c.Data.(string); ok { + return contentAsString + } + return "" +} + +func (c *Content) AsToolResponseData() *ToolResponseData { + if contentAsToolResponseData, ok := c.Data.(ToolResponseData); ok { + return &contentAsToolResponseData + } + return nil +} + +func (c *Content) AsToolCallData() []ToolCallData { + if contentAsToolCallData, ok := c.Data.([]ToolCallData); ok { + return contentAsToolCallData + } + return nil +} From e15869190a355f31c33a103aa7dca599f0e636d0 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 17:15:33 +0100 Subject: [PATCH 54/65] add llms and embedding --- docs/content/reference/embedding.md | 64 +++++++++++++++++++++++++++++ docs/content/reference/llm.md | 31 ++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 docs/content/reference/embedding.md diff --git a/docs/content/reference/embedding.md b/docs/content/reference/embedding.md new file mode 100644 index 00000000..bea30689 --- /dev/null +++ b/docs/content/reference/embedding.md @@ -0,0 +1,64 @@ +--- +title: "LLM Embeddings" +description: +linkTitle: "Embedding" +menu: { main: { parent: 'reference', weight: -99 } } +--- + +Embeddings are a fundamental concept in machine learning and natural language processing. They are vector representations of words, phrases, or documents in a high-dimensional space. These vectors capture semantic and contextual information about the text, allowing algorithms to understand relationships and similarities between words. + +Embeddings are used in various tasks such as language modeling, sentiment analysis, machine translation, and information retrieval. By representing words as vectors, models can perform operations like word similarity, word analogy, and clustering. Embeddings enable algorithms to learn from large amounts of text data and generalize their knowledge to new, unseen examples. + +LinGoose provides an interface for working with embeddings, allowing developers to use the same code to interact with different embedding providers, regardless of the underlying implementation. LinGoose supports the following embedding providers: + +- [OpenAI](https://openai.com) +- [Cohere](https://cohere.ai) +- [Huggingface](https://huggingface.co) +- [Ollama](https://ollama.ai) +- [LocalAI](https://localai.io/) (_via OpenAI API compatibility_) +- [Atlas Nomic](https://atlas.nomic.ai) + +## Using Embeddings + +To use an embedding provider, you need to create an instance of your preferred embedding provider. Here we show how to create an instance of an embedding using the OpenAI API: + +```go +embeddins, err := openaiembedder.New(openaiembedder.AdaEmbeddingV2).Embed( + context.Background(), + []string{"What is the NATO purpose?"}, +) +if err != nil { + panic(err) +} + +fmt.Printf("Embeddings: %v\n", embeddins) +``` + +By default LinGoose embeddings will use the API key from the related environment variable (e.g. `OPENAI_API_KEY` for OpenAI). + +## Private Embeddings + +If you want to run your model or use a private embedding provider, you have many options. + +### Implementing a custom embedding provider +LinGoose Embedder is an interface that can be implemented by any LLM provider. You can create your own Embedder by satisfying the LLM interface. + +### Using a local Embedder +LinGoose allows you to use to use a local Embedder. You can use either LocalAI or Ollama, which are both local providers. +- **LocalAI** is fully compatible with OpenAI API, so you can use it as an OpenAI Embeddings with a custom client configuration (`WithClient(client *openai.Client)`) pointing to your local endpoint. +- **Ollama** is a local Embedding provider that can be used with various models, such as `llama`, `mistral`, and others. + +Here is an example of using Ollama Embedder: + +```go +embeddins, err := ollamaembedder.New(). + WithEndpoint("http://localhost:11434/api"). + WithModel("mistral"). + Embed( + context.Background(), + []string{"What is the NATO purpose?"}, + ) +if err != nil { + panic(err) +} +``` \ No newline at end of file diff --git a/docs/content/reference/llm.md b/docs/content/reference/llm.md index a300dc70..e64c1faa 100644 --- a/docs/content/reference/llm.md +++ b/docs/content/reference/llm.md @@ -115,3 +115,34 @@ fmt.Println(myThread) ``` LinGoose allows you to bind a function describing its scope and input's schema. The function will be called by the OpenAI LLM automatically depending on the user's input. Here we force the tool choice to be "auto" to let OpenAI decide which tool to use. If, after an LLM generation, the last message is a tool call, you can enrich the thread with a new LLM generation based on the tool call result. + + +## Private LLMs +If you want to run your model or use a private LLM provider, you have many options. + +### Implementing a custom LLM provider +LinGoose LLM is an interface that can be implemented by any LLM provider. You can create your own LLM by satisfying the LLM interface. + +### Using a local LLM +LinGoose allows you to use to use a local LLM. You can use either LocalAI or Ollama, which are both local LLM providers. +- **LocalAI** is fully compatible with OpenAI API, so you can use it as an OpenAI LLM with a custom client configuration (`WithClient(client *openai.Client)`) pointing to your local LLM endpoint. +- **Ollama** is a local LLM provider that can be used with various LLMs, such as `llama`, `mistral`, and others. + +Here is an example of how to use Ollama as LLM: + +```go +myThread := thread.New() +myThread.AddMessage(thread.NewUserMessage().AddContent( + thread.NewTextContent("How are you?"), +)) + +err := ollama.New().WithEndpoint("http://localhost:11434/api").WithModel("mistral"). + WithStream(func(s string) { + fmt.Print(s) + }).Generate(context.Background(), myThread) +if err != nil { + panic(err) +} + +fmt.Println(myThread) +``` \ No newline at end of file From de1764778122560e3c16af2804dd42d03829ff02 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 17:46:17 +0100 Subject: [PATCH 55/65] fix --- docs/config.yml | 3 +++ docs/content/legacy/llm.md | 7 ++++++ docs/content/reference/assistant.md | 8 +++--- docs/content/reference/cache.md | 6 +++++ docs/content/reference/document.md | 6 +++++ docs/content/reference/embedding.md | 4 +-- docs/content/reference/indexes.md | 39 +++++++++++++++++++++++++++++ docs/content/reference/linglet.md | 6 +++++ docs/content/reference/loader.md | 6 +++++ docs/content/reference/rag.md | 6 +++++ docs/content/reference/splitter.md | 6 +++++ 11 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 docs/content/legacy/llm.md create mode 100644 docs/content/reference/cache.md create mode 100644 docs/content/reference/document.md create mode 100644 docs/content/reference/indexes.md create mode 100644 docs/content/reference/linglet.md create mode 100644 docs/content/reference/loader.md create mode 100644 docs/content/reference/rag.md create mode 100644 docs/content/reference/splitter.md diff --git a/docs/config.yml b/docs/config.yml index d6982ce8..f420e8bc 100644 --- a/docs/config.yml +++ b/docs/config.yml @@ -18,6 +18,9 @@ menu: - name: Reference identifier: reference weight: 5 + - name: Legacy + identifier: legacy + weight: 6 - name: pkg.go.dev → parent: reference url: https://pkg.go.dev/github.com/henomis/lingoose diff --git a/docs/content/legacy/llm.md b/docs/content/legacy/llm.md new file mode 100644 index 00000000..0adecb5a --- /dev/null +++ b/docs/content/legacy/llm.md @@ -0,0 +1,7 @@ +--- +title: "Legacy components" +description: +linkTitle: "Components" +menu: { main: { parent: 'legacy', weight: -90 } } +--- + diff --git a/docs/content/reference/assistant.md b/docs/content/reference/assistant.md index f3b5aa4b..54d2d24c 100644 --- a/docs/content/reference/assistant.md +++ b/docs/content/reference/assistant.md @@ -1,6 +1,6 @@ --- -title: "Your AI assistant" -description: "Your AI assistant" -linkTitle: "Assistant" -menu: { main: { parent: 'reference', weight: -99 } } +title: "AI Assistant" +description: +linkTitle: "Assistant" +menu: { main: { parent: 'reference', weight: -90 } } --- diff --git a/docs/content/reference/cache.md b/docs/content/reference/cache.md new file mode 100644 index 00000000..a2735654 --- /dev/null +++ b/docs/content/reference/cache.md @@ -0,0 +1,6 @@ +--- +title: "Caching LLM responses" +description: +linkTitle: "Cache" +menu: { main: { parent: 'reference', weight: -92 } } +--- diff --git a/docs/content/reference/document.md b/docs/content/reference/document.md new file mode 100644 index 00000000..5f646082 --- /dev/null +++ b/docs/content/reference/document.md @@ -0,0 +1,6 @@ +--- +title: "A rapresentation of a text content" +description: +linkTitle: "Document" +menu: { main: { parent: 'reference', weight: -96 } } +--- diff --git a/docs/content/reference/embedding.md b/docs/content/reference/embedding.md index bea30689..1016a16c 100644 --- a/docs/content/reference/embedding.md +++ b/docs/content/reference/embedding.md @@ -1,8 +1,8 @@ --- title: "LLM Embeddings" -description: +description: "" linkTitle: "Embedding" -menu: { main: { parent: 'reference', weight: -99 } } +menu: { main: { parent: 'reference', weight: -97 } } --- Embeddings are a fundamental concept in machine learning and natural language processing. They are vector representations of words, phrases, or documents in a high-dimensional space. These vectors capture semantic and contextual information about the text, allowing algorithms to understand relationships and similarities between words. diff --git a/docs/content/reference/indexes.md b/docs/content/reference/indexes.md new file mode 100644 index 00000000..b05e0712 --- /dev/null +++ b/docs/content/reference/indexes.md @@ -0,0 +1,39 @@ +--- +title: "Vector storage" +description: +linkTitle: "Index" +menu: { main: { parent: 'reference', weight: -93 } } +--- + +Vector storage is a component of LinGoose that provides a way to store and retrieve vectors. It is used to store embeddings, which are vector representations of words, phrases, or documents in a high-dimensional space. These vectors capture semantic and contextual information about the text, allowing algorithms to understand relationships and similarities between words. + +LinGoose provides the `Index` interface for working with vector storage, allowing developers to use the same code to interact with different vector storage providers, regardless of the underlying implementation. LinGoose supports the following vector storage providers: + +- JsonDB (it's a simple internal JSON file storage) +- [Pinecone](https://pinecone.io) +- [Qdrant](https://qdrant.tech) +- [Redis](https://redis.io) +- [PostgreSQL](https://www.postgresql.org) +- [Milvus](https://milvus.io) + +## Using Index + +To use an index, you need to create an instance of your preferred index provider. Here we show how to create an instance of an index using the JsonDB provider: + +```go +qdrantIndex := index.New( + qdrantdb.New( + qdrantdb.Options{ + CollectionName: "test", + CreateCollection: &qdrantdb.CreateCollectionOptions{ + Dimension: 1536, + Distance: qdrantdb.DistanceCosine, + }, + }, + ).WithAPIKeyAndEdpoint("", "http://localhost:6333"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), +).WithIncludeContents(true) +``` + +An Index instance requires an Embedder to be passed in. The Embedder is used to convert text into vectors and perform similarity searches. In this example, we use the `openaiembedder` package to create an instance of the OpenAI Embedding service. This examples uses a local Qdrant instance. Every Index provider has its own configuration options, in this case we are creating a collection with a dimension of 1536 and using the cosine distance metric and forcing the index to include the metadata contents. + diff --git a/docs/content/reference/linglet.md b/docs/content/reference/linglet.md new file mode 100644 index 00000000..95c580cb --- /dev/null +++ b/docs/content/reference/linglet.md @@ -0,0 +1,6 @@ +--- +title: "LinGoose Linglets" +description: +linkTitle: "Linglets" +menu: { main: { parent: 'reference', weight: -89 } } +--- diff --git a/docs/content/reference/loader.md b/docs/content/reference/loader.md new file mode 100644 index 00000000..3f2870f5 --- /dev/null +++ b/docs/content/reference/loader.md @@ -0,0 +1,6 @@ +--- +title: "Load a content to a Document" +description: +linkTitle: "Loader" +menu: { main: { parent: 'reference', weight: -95 } } +--- diff --git a/docs/content/reference/rag.md b/docs/content/reference/rag.md new file mode 100644 index 00000000..fa7b4ff8 --- /dev/null +++ b/docs/content/reference/rag.md @@ -0,0 +1,6 @@ +--- +title: "Retrieval Augmented Generation" +description: +linkTitle: "RAG" +menu: { main: { parent: 'reference', weight: -91 } } +--- diff --git a/docs/content/reference/splitter.md b/docs/content/reference/splitter.md new file mode 100644 index 00000000..aa6d144d --- /dev/null +++ b/docs/content/reference/splitter.md @@ -0,0 +1,6 @@ +--- +title: "Splits a Document into smaller parts" +description: +linkTitle: "Splitter" +menu: { main: { parent: 'reference', weight: -94 } } +--- From 6696d78e649e7670661f9f1ce62769ccb73ef1c5 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 18:10:56 +0100 Subject: [PATCH 56/65] update loaders --- loader/csv.go | 6 ++++++ loader/hf_image_to_text.go | 7 +++++++ loader/hf_speech_recognition.go | 7 +++++++ loader/libreoffice.go | 7 +++++++ loader/pdf_to_text.go | 6 ++++++ loader/pubmed.go | 4 ++++ loader/tesseract.go | 7 +++++++ loader/text.go | 9 +++++++++ loader/whisper.go | 8 ++++++++ loader/whispercpp.go | 10 ++++++++++ loader/youtube-dl.go | 8 ++++++++ 11 files changed, 79 insertions(+) diff --git a/loader/csv.go b/loader/csv.go index 842a68ac..46f21f18 100644 --- a/loader/csv.go +++ b/loader/csv.go @@ -27,6 +27,12 @@ func NewCSVLoader(filename string) *CSVLoader { } } +func NewCSV() *CSVLoader { + return &CSVLoader{ + separator: ',', + } +} + func (c *CSVLoader) WithLazyQuotes() *CSVLoader { c.lazyQuotes = true return c diff --git a/loader/hf_image_to_text.go b/loader/hf_image_to_text.go index 78a0a39c..1ffbd816 100644 --- a/loader/hf_image_to_text.go +++ b/loader/hf_image_to_text.go @@ -40,6 +40,13 @@ func NewHFImageToTextLoader(mediaFile string) *HFImageToText { } } +func NewHFImageToText() *HFImageToText { + return &HFImageToText{ + model: hfDefaultImageToTextModel, + token: os.Getenv("HUGGING_FACE_HUB_TOKEN"), + } +} + func (h *HFImageToText) WithToken(token string) *HFImageToText { h.token = token return h diff --git a/loader/hf_speech_recognition.go b/loader/hf_speech_recognition.go index cd5efe69..4ee7ae05 100644 --- a/loader/hf_speech_recognition.go +++ b/loader/hf_speech_recognition.go @@ -35,6 +35,13 @@ func NewHFSpeechRecognitionLoader(mediaFile string) *HFSpeechRecognition { } } +func NewHFSpeechRecognition() *HFSpeechRecognition { + return &HFSpeechRecognition{ + model: hfDefaultSpeechRecognitionModel, + token: os.Getenv("HUGGING_FACE_HUB_TOKEN"), + } +} + func (h *HFSpeechRecognition) WithToken(token string) *HFSpeechRecognition { h.token = token return h diff --git a/loader/libreoffice.go b/loader/libreoffice.go index 2194e0ef..871ee6fa 100644 --- a/loader/libreoffice.go +++ b/loader/libreoffice.go @@ -30,6 +30,13 @@ func NewLibreOfficeLoader(filename string) *LibreOfficeLoader { } } +func NewLibreOffice() *LibreOfficeLoader { + return &LibreOfficeLoader{ + libreOfficePath: defaultLibreOfficePath, + libreOfficeArgs: []string{"--headless", "--convert-to", "txt:Text", "--cat"}, + } +} + func (l *LibreOfficeLoader) WithLibreOfficePath(libreOfficePath string) *LibreOfficeLoader { l.libreOfficePath = libreOfficePath return l diff --git a/loader/pdf_to_text.go b/loader/pdf_to_text.go index 83c31464..dd3c2051 100644 --- a/loader/pdf_to_text.go +++ b/loader/pdf_to_text.go @@ -31,6 +31,12 @@ func NewPDFToTextLoader(path string) *PDFLoader { } } +func NewPDFToText() *PDFLoader { + return &PDFLoader{ + pdfToTextPath: defaultPdfToTextPath, + } +} + func (p *PDFLoader) WithPDFToTextPath(pdfToTextPath string) *PDFLoader { p.pdfToTextPath = pdfToTextPath return p diff --git a/loader/pubmed.go b/loader/pubmed.go index 51d4b285..defc2d50 100644 --- a/loader/pubmed.go +++ b/loader/pubmed.go @@ -33,6 +33,10 @@ func NewPubmedLoader(pubMedIDs []string) *PubMedLoader { } } +func NewPubmed() *PubMedLoader { + return &PubMedLoader{} +} + func (p *PubMedLoader) WithTextSplitter(textSplitter TextSplitter) *PubMedLoader { p.loader.textSplitter = textSplitter return p diff --git a/loader/tesseract.go b/loader/tesseract.go index 8689d6b6..c0a267ca 100644 --- a/loader/tesseract.go +++ b/loader/tesseract.go @@ -30,6 +30,13 @@ func NewTesseractLoader(filename string) *TesseractLoader { } } +func NewTesseract() *TesseractLoader { + return &TesseractLoader{ + tesseractPath: defaultTesseractPath, + tesseractArgs: []string{}, + } +} + func (l *TesseractLoader) WithTesseractPath(tesseractPath string) *TesseractLoader { l.tesseractPath = tesseractPath return l diff --git a/loader/text.go b/loader/text.go index 34869079..bb7a62de 100644 --- a/loader/text.go +++ b/loader/text.go @@ -23,11 +23,20 @@ func NewTextLoader(filename string, metadata types.Meta) *TextLoader { } } +func NewText() *TextLoader { + return &TextLoader{} +} + func (t *TextLoader) WithTextSplitter(textSplitter TextSplitter) *TextLoader { t.loader.textSplitter = textSplitter return t } +func (t *TextLoader) WithMetadata(metadata types.Meta) *TextLoader { + t.metadata = metadata + return t +} + func (t *TextLoader) Load(ctx context.Context) ([]document.Document, error) { _ = ctx err := t.validate() diff --git a/loader/whisper.go b/loader/whisper.go index 93015ceb..b20c9328 100644 --- a/loader/whisper.go +++ b/loader/whisper.go @@ -26,6 +26,14 @@ func NewWhisperLoader(filename string) *WhisperLoader { } } +func NewWhisper() *WhisperLoader { + openAIApiKey := os.Getenv("OPENAI_API_KEY") + + return &WhisperLoader{ + openAIClient: openai.NewClient(openAIApiKey), + } +} + func (w *WhisperLoader) WithClient(client *openai.Client) *WhisperLoader { w.openAIClient = client return w diff --git a/loader/whispercpp.go b/loader/whispercpp.go index 92cfc685..182f7c58 100644 --- a/loader/whispercpp.go +++ b/loader/whispercpp.go @@ -35,6 +35,16 @@ func NewWhisperCppLoader(filename string) *WhisperCppLoader { } } +func NewWhisperCpp() *WhisperCppLoader { + return &WhisperCppLoader{ + ffmpegPath: "/usr/bin/ffmpeg", + ffmpegArgs: []string{"-nostdin", "-f", "wav", "-ar", "16000", "-ac", "1", "-acodec", "pcm_s16le", "-"}, + whisperCppPath: "./whisper.cpp/main", + whisperCppArgs: []string{}, + whisperCppModelPath: "./whisper.cpp/models/ggml-base.bin", + } +} + func (w *WhisperCppLoader) WithTextSplitter(textSplitter TextSplitter) *WhisperCppLoader { w.loader.textSplitter = textSplitter return w diff --git a/loader/youtube-dl.go b/loader/youtube-dl.go index fa58d0b0..fa2d34e6 100644 --- a/loader/youtube-dl.go +++ b/loader/youtube-dl.go @@ -38,6 +38,14 @@ func NewYoutubeDLLoader(url string) *YoutubeDLLoader { } } +func NewYoutubeDL() *YoutubeDLLoader { + return &YoutubeDLLoader{ + youtubeDlPath: defaultYoutubeDLPath, + language: defaultYoutubeDLSubtitleLanguage, + subtitlesMode: defaultYoutubeDLSubtitleMode, + } +} + func (y *YoutubeDLLoader) WithYoutubeDLPath(youtubeDLPath string) *YoutubeDLLoader { y.youtubeDlPath = youtubeDLPath return y From ddaf3ba086327a5b14509adb3d93565313c5b5c8 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 18:12:06 +0100 Subject: [PATCH 57/65] fix rag --- rag/rag.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rag/rag.go b/rag/rag.go index 4bb7725d..c8f3d529 100644 --- a/rag/rag.go +++ b/rag/rag.go @@ -68,9 +68,9 @@ func (r *RAG) WithTopK(topK uint) *RAG { } func (r *RAG) withDefaultLoaders() *RAG { - r.loaders[regexp.MustCompile(`.*\.pdf`)] = loader.NewPDFToTextLoader("") - r.loaders[regexp.MustCompile(`.*\.docx`)] = loader.NewLibreOfficeLoader("") - r.loaders[regexp.MustCompile(`.*\.txt`)] = loader.NewTextLoader("", nil) + r.loaders[regexp.MustCompile(`.*\.pdf`)] = loader.NewPDFToText() + r.loaders[regexp.MustCompile(`.*\.docx`)] = loader.NewLibreOffice() + r.loaders[regexp.MustCompile(`.*\.txt`)] = loader.NewText() return r } From b7a1e1f329dc85893840f7db9b8f4df078607d09 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sat, 3 Feb 2024 18:29:43 +0100 Subject: [PATCH 58/65] add loader --- docs/content/reference/document.md | 6 ----- docs/content/reference/indexes.md | 21 ++++++++++++++++++ docs/content/reference/loader.md | 35 +++++++++++++++++++++++++++++- docs/content/reference/splitter.md | 6 ----- 4 files changed, 55 insertions(+), 13 deletions(-) delete mode 100644 docs/content/reference/document.md delete mode 100644 docs/content/reference/splitter.md diff --git a/docs/content/reference/document.md b/docs/content/reference/document.md deleted file mode 100644 index 5f646082..00000000 --- a/docs/content/reference/document.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "A rapresentation of a text content" -description: -linkTitle: "Document" -menu: { main: { parent: 'reference', weight: -96 } } ---- diff --git a/docs/content/reference/indexes.md b/docs/content/reference/indexes.md index b05e0712..036c1988 100644 --- a/docs/content/reference/indexes.md +++ b/docs/content/reference/indexes.md @@ -37,3 +37,24 @@ qdrantIndex := index.New( An Index instance requires an Embedder to be passed in. The Embedder is used to convert text into vectors and perform similarity searches. In this example, we use the `openaiembedder` package to create an instance of the OpenAI Embedding service. This examples uses a local Qdrant instance. Every Index provider has its own configuration options, in this case we are creating a collection with a dimension of 1536 and using the cosine distance metric and forcing the index to include the metadata contents. +To ingest a document into the index, you can use the `LoadFromDocuments` method: + +```go +err := qdrantIndex.LoadFromDocuments(context.Background(), documents) +``` + +To search for similar documents, you can use the `Search` method: + +```go +query := "What is the purpose of the NATO Alliance?" +similarities, err := index.Query( + context.Background(), + query, + indexoption.WithTopK(3), +) +if err != nil { + panic(err) +} +``` + +The `Query` method returns a list of `SearchResult` objects, which contain the document ID and the similarity score. The `WithTopK` option is used to specify the number of similar documents to return. diff --git a/docs/content/reference/loader.md b/docs/content/reference/loader.md index 3f2870f5..7c2b72e5 100644 --- a/docs/content/reference/loader.md +++ b/docs/content/reference/loader.md @@ -1,6 +1,39 @@ --- -title: "Load a content to a Document" +title: "Load Content into a Document" description: linkTitle: "Loader" menu: { main: { parent: 'reference', weight: -95 } } --- + +The `Loader` interface provides a way to load content into a document. It is used to transform a text content into a document, a structured representation of the content. LinGoose offers loaders for different types of content: + +- Plain text +- CSV +- PDF (via pdftotext) +- Docx, odf, rtf, and other office formats (via LibreOffice) +- OCR (via Tesseract) +- Audio/STT (via OpenAI whisper, whispercpp or Hugging Face) +- Youtube (via youtube-dl) +- Pubmed +- Image to text (via Hugging Face) + +## Using Loader + +To use a loader, you need to create an instance of your preferred loader. Here we show how to create an instance of a loader using the plain text loader: + +```go +pdfLoader := loader.NewPDFToText().WithPDFToTextPath("/opt/homebrew/bin/pdftotext") +kbDocuments := loader.LoadFromSource(context.Background(),"./kb/mydocument.pdf") +``` + +### Splitting documents + +A loader produces a document for each content it loads. However documents may contain a huge amount of text, and it's convenient to split them into smaller parts. + +```go +audioLoader := loader.NewWhisper(). + WithTextSplitter(textsplitter.NewRecursiveCharacterTextSplitter(2000, 200)). + LoadFromSource(context.Background(), "audio.mp3") +``` + +A text splitter is a component that splits a document into documents of a smaller size. The `RecursiveCharacterTextSplitter` accepts as parameters the size of the text chunks and the size of chunk overlap. \ No newline at end of file diff --git a/docs/content/reference/splitter.md b/docs/content/reference/splitter.md deleted file mode 100644 index aa6d144d..00000000 --- a/docs/content/reference/splitter.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "Splits a Document into smaller parts" -description: -linkTitle: "Splitter" -menu: { main: { parent: 'reference', weight: -94 } } ---- From 5bc2d82253a70772d9442c5fc864bfc7d790352a Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 4 Feb 2024 10:06:44 +0100 Subject: [PATCH 59/65] fix --- examples/llm/openai/multimodal/main.go | 30 ++++++++++++++++++++++++++ llm/openai/formatters.go | 14 ++++++++++-- thread/thread.go | 13 +++++------ 3 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 examples/llm/openai/multimodal/main.go diff --git a/examples/llm/openai/multimodal/main.go b/examples/llm/openai/multimodal/main.go new file mode 100644 index 00000000..947d54b1 --- /dev/null +++ b/examples/llm/openai/multimodal/main.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/llm/openai" + "github.com/henomis/lingoose/thread" +) + +func main() { + openaillm := openai.New().WithModel(openai.GPT4VisionPreview) + + t := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent("Can you describe the image?"), + ).AddContent( + thread.NewImageContentFromURL("https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/Anser_anser_1_%28Piotr_Kuczynski%29.jpg/1280px-Anser_anser_1_%28Piotr_Kuczynski%29.jpg"), + ), + ) + + fmt.Println(t) + + err := openaillm.Generate(context.Background(), t) + if err != nil { + panic(err) + } + + fmt.Println(t) +} diff --git a/llm/openai/formatters.go b/llm/openai/formatters.go index b57e12a5..fc9f9bee 100644 --- a/llm/openai/formatters.go +++ b/llm/openai/formatters.go @@ -66,15 +66,25 @@ func threadContentsToChatMessageParts(m *thread.Message) []openai.ChatMessagePar switch content.Type { case thread.ContentTypeText: + contentAsString, ok := content.Data.(string) + if !ok { + continue + } + chatMessagePart = &openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeText, - Text: content.Data.(string), + Text: contentAsString, } case thread.ContentTypeImage: + contentAsString, ok := content.Data.(string) + if !ok { + continue + } + chatMessagePart = &openai.ChatMessagePart{ Type: openai.ChatMessagePartTypeImageURL, ImageURL: &openai.ChatMessageImageURL{ - URL: content.Data.(string), + URL: contentAsString, Detail: openai.ImageURLDetailAuto, }, } diff --git a/thread/thread.go b/thread/thread.go index 74b2a1da..841b7d1a 100644 --- a/thread/thread.go +++ b/thread/thread.go @@ -51,11 +51,6 @@ type ToolCallData struct { Arguments string } -type MediaData struct { - Raw any - URL *string -} - func NewTextContent(text string) *Content { return &Content{ Type: ContentTypeText, @@ -63,10 +58,10 @@ func NewTextContent(text string) *Content { } } -func NewImageContent(mediaData MediaData) *Content { +func NewImageContentFromURL(url string) *Content { return &Content{ Type: ContentTypeImage, - Data: mediaData, + Data: url, } } @@ -141,7 +136,9 @@ func (t *Thread) String() string { case ContentTypeText: str += "\tText: " + content.Data.(string) + "\n" case ContentTypeImage: - str += "\tImage URL: " + *content.Data.(*MediaData).URL + "\n" + if contentAsString, ok := content.Data.(string); ok { + str += "\tImage URL: " + contentAsString + "\n" + } case ContentTypeToolCall: for _, toolCallData := range content.Data.([]ToolCallData) { str += "\tTool Call ID: " + toolCallData.ID + "\n" From b0007186b5f98ab5456bc587b9b2b6f5a4c316e3 Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Sun, 4 Feb 2024 10:27:02 +0100 Subject: [PATCH 60/65] refactor cache --- docs/content/reference/cache.md | 4 ++++ examples/llm/cache/main.go | 12 +++++++----- index/index.go | 4 ++++ llm/cache/cache.go | 12 ++++-------- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/content/reference/cache.md b/docs/content/reference/cache.md index a2735654..226e4733 100644 --- a/docs/content/reference/cache.md +++ b/docs/content/reference/cache.md @@ -4,3 +4,7 @@ description: linkTitle: "Cache" menu: { main: { parent: 'reference', weight: -92 } } --- + +Caching LLM responses can be a good way to improve the performance of your application. This is especially true when you are using an LLM to generate responses to user messages in real-time. By caching the responses, you can avoid making repeated calls to the LLM, which can be slow and expensive. + +LinGoose provides a built-in caching mechanism that you can use to cache LLM responses. The cache needs an Index \ No newline at end of file diff --git a/examples/llm/cache/main.go b/examples/llm/cache/main.go index faed97d0..de233d8a 100644 --- a/examples/llm/cache/main.go +++ b/examples/llm/cache/main.go @@ -14,12 +14,14 @@ import ( func main() { - embedder := openaiembedder.New(openaiembedder.AdaEmbeddingV2) - index := index.New( - jsondb.New().WithPersist("index.json"), - embedder, + llm := openai.New().WithCache( + cache.New( + index.New( + jsondb.New().WithPersist("index.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), + ).WithTopK(3), ) - llm := openai.New().WithCache(cache.New(embedder, index).WithTopK(3)) questions := []string{ "what's github", diff --git a/index/index.go b/index/index.go index a75c75a4..71f13112 100644 --- a/index/index.go +++ b/index/index.go @@ -125,6 +125,10 @@ func (i *Index) Query(ctx context.Context, query string, opts ...option.Option) return i.Search(ctx, embeddings[0], opts...) } +func (i *Index) Embedder() Embedder { + return i.embedder +} + func (i *Index) batchUpsert(ctx context.Context, documents []document.Document) error { for j := 0; j < len(documents); j += i.batchInsertSize { batchEnd := j + i.batchInsertSize diff --git a/llm/cache/cache.go b/llm/cache/cache.go index bfc297a3..3a7493a0 100644 --- a/llm/cache/cache.go +++ b/llm/cache/cache.go @@ -4,19 +4,15 @@ import ( "context" "fmt" - "github.com/henomis/lingoose/embedder" "github.com/henomis/lingoose/index" indexoption "github.com/henomis/lingoose/index/option" "github.com/henomis/lingoose/types" ) -type Embedder interface { - Embed(ctx context.Context, texts []string) ([]embedder.Embedding, error) -} - type Index interface { Search(context.Context, []float64, ...indexoption.Option) (index.SearchResults, error) Add(context.Context, *index.Data) error + Embedder() index.Embedder } var ErrCacheMiss = fmt.Errorf("cache miss") @@ -28,7 +24,7 @@ const ( ) type Cache struct { - embedder Embedder + embedder index.Embedder index Index topK int scoreThreshold float64 @@ -39,9 +35,9 @@ type Result struct { Embedding []float64 } -func New(embedder Embedder, index Index) *Cache { +func New(index Index) *Cache { return &Cache{ - embedder: embedder, + embedder: index.Embedder(), index: index, topK: defaultTopK, scoreThreshold: defaultScoreThreshold, From d4e3412b3bee6b954fe2f8aa13a5a5a772c400ec Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Mon, 5 Feb 2024 08:28:04 +0100 Subject: [PATCH 61/65] add cache doc --- docs/content/reference/cache.md | 48 ++++++++++++++++++++++++++++++++- index/index.go | 4 +++ llm/cache/cache.go | 14 +++++----- 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/docs/content/reference/cache.md b/docs/content/reference/cache.md index 226e4733..3db0fe51 100644 --- a/docs/content/reference/cache.md +++ b/docs/content/reference/cache.md @@ -7,4 +7,50 @@ menu: { main: { parent: 'reference', weight: -92 } } Caching LLM responses can be a good way to improve the performance of your application. This is especially true when you are using an LLM to generate responses to user messages in real-time. By caching the responses, you can avoid making repeated calls to the LLM, which can be slow and expensive. -LinGoose provides a built-in caching mechanism that you can use to cache LLM responses. The cache needs an Index \ No newline at end of file +LinGoose provides a built-in caching mechanism that you can use to cache LLM responses. The cache needs an Index to be created, and it will store the responses in memory. + +## Using the cache + +To use the cache, you need to create an Index and pass it to the LLM instance. Here's an example: + +```go +openAILLM := openai.New().WithCache( + cache.New( + index.New( + jsondb.New().WithPersist("index.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), + ).WithTopK(3), +) +``` + +Here we are creating a new cache with an index that uses a JSON database to persist the data. We are also using the `openaiembedder` to perform the index embedding operations. The `WithTopK` method is used to specify the number of responses to evaluate to get the answer from the cache. + +Once you have created the cache, your LLM instance will use it to store and retrieve responses. The cache will automatically store responses and retrieve them when needed. + +```go +questions := []string{ + "what's github", + "can you explain what GitHub is", + "can you tell me more about GitHub", + "what is the purpose of GitHub", +} + +for _, question := range questions { + t := thread.New().AddMessage( + thread.NewUserMessage().AddContent( + thread.NewTextContent(question), + ), + ) + + err := llm.Generate(context.Background(), t) + if err != nil { + fmt.Println(err) + continue + } + + fmt.Println(t) +} +``` + +In this example, we are using the LLM to generate responses to a list of questions. The cache will store the responses and retrieve them when needed. This can help to improve the performance of your application by avoiding repeated calls to the LLM. \ No newline at end of file diff --git a/index/index.go b/index/index.go index 71f13112..3b2dea07 100644 --- a/index/index.go +++ b/index/index.go @@ -106,6 +106,10 @@ func (i *Index) IsEmpty(ctx context.Context) (bool, error) { return i.vectorDB.IsEmpty(ctx) } +func (i *Index) Drop(ctx context.Context) error { + return i.vectorDB.Drop(ctx) +} + func (i *Index) Search(ctx context.Context, values []float64, opts ...option.Option) (SearchResults, error) { options := &option.Options{ TopK: defaultTopK, diff --git a/llm/cache/cache.go b/llm/cache/cache.go index 3a7493a0..8995b368 100644 --- a/llm/cache/cache.go +++ b/llm/cache/cache.go @@ -9,12 +9,6 @@ import ( "github.com/henomis/lingoose/types" ) -type Index interface { - Search(context.Context, []float64, ...indexoption.Option) (index.SearchResults, error) - Add(context.Context, *index.Data) error - Embedder() index.Embedder -} - var ErrCacheMiss = fmt.Errorf("cache miss") const ( @@ -25,7 +19,7 @@ const ( type Cache struct { embedder index.Embedder - index Index + index *index.Index topK int scoreThreshold float64 } @@ -35,7 +29,7 @@ type Result struct { Embedding []float64 } -func New(index Index) *Cache { +func New(index *index.Index) *Cache { return &Cache{ embedder: index.Embedder(), index: index, @@ -85,6 +79,10 @@ func (c *Cache) Set(ctx context.Context, embedding []float64, answer string) err }) } +func (c *Cache) Clear(ctx context.Context) error { + return c.index.Drop(ctx) +} + func (c *Cache) extractResults(results index.SearchResults) ([]string, bool) { var output []string From 8523b16aed48db43281026427e33493cfe8272ce Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Mon, 5 Feb 2024 08:49:19 +0100 Subject: [PATCH 62/65] docs: add rag --- docs/content/reference/rag.md | 47 +++++++++++++++++++++++++++++++++++ examples/rag/main.go | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 examples/rag/main.go diff --git a/docs/content/reference/rag.md b/docs/content/reference/rag.md index fa7b4ff8..c258bc70 100644 --- a/docs/content/reference/rag.md +++ b/docs/content/reference/rag.md @@ -4,3 +4,50 @@ description: linkTitle: "RAG" menu: { main: { parent: 'reference', weight: -91 } } --- + +Retrieval Augmented Generation (RAG) is a component that combines the strengths of both retrieval-based and generation-based models. It uses a index to find relevant documents and then uses a language model to generate responses based on the retrieved documents. + +LinGoose offers a RAG model that is built on top of the Index component. It uses the index to retrieve relevant documents and then uses a language model to generate responses based on the retrieved documents. The RAG model can be extended to support multiple RAG implementations. + +## Using RAG + +To use RAG, you need to first create an index and then use the index to retrieve relevant documents. + +```go +rag := rag.New( + index.New( + jsondb.New().WithPersist("index.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), +).WithChunkSize(1000).WithChunkOverlap(0) +``` + +Here we create a new RAG model with a JSON database and an OpenAI embedder. We also set the chunk size and overlap for the index. + +```go +rag.AddDocuments( + context.Background(), + document.Document{ + Content: `Augusta Ada King, Countess of Lovelace (née Byron; 10 December 1815 - + 27 November 1852) was an English mathematician and writer, + chiefly known for her work on Charles Babbage's proposed mechanical general-purpose computer, + the Analytical Engine. She was the first to recognise that the machine had applications beyond pure calculation. + `, + Metadata: types.Meta{ + "author": "Wikipedia", + }, + }, +) +``` + +You can add documents to the RAG knowledge using the `AddDocuments` method. You can attach to the RAG one or more Loader specifying the matching pattern for file source of the documents. + +```go +rag = rag.WithLoader(regexp.MustCompile(`.*\.pdf`), loader.NewPDFToText()) +``` + +There are default loader already attached to the RAG that you can override or extend. + +- `.*\.pdf` via `loader.NewPDFToText()` +- `.*\.txt` via `loader.NewText()` +- `.*\.docx` via `loader.NewLibreOffice()` diff --git a/examples/rag/main.go b/examples/rag/main.go new file mode 100644 index 00000000..a1123436 --- /dev/null +++ b/examples/rag/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "fmt" + + "github.com/henomis/lingoose/document" + openaiembedder "github.com/henomis/lingoose/embedder/openai" + "github.com/henomis/lingoose/index" + "github.com/henomis/lingoose/index/vectordb/jsondb" + "github.com/henomis/lingoose/rag" + "github.com/henomis/lingoose/types" +) + +func main() { + + rag := rag.New( + index.New( + jsondb.New().WithPersist("index.json"), + openaiembedder.New(openaiembedder.AdaEmbeddingV2), + ), + ).WithChunkSize(1000).WithChunkOverlap(0) + + rag.AddDocuments( + context.Background(), + document.Document{ + Content: `Augusta Ada King, Countess of Lovelace (née Byron; 10 December 1815 - + 27 November 1852) was an English mathematician and writer, + chiefly known for her work on Charles Babbage's proposed mechanical general-purpose computer, + the Analytical Engine. She was the first to recognise that the machine had applications beyond pure calculation. + `, + Metadata: types.Meta{ + "author": "Wikipedia", + }, + }, + ) + + results, err := rag.Retrieve(context.Background(), "Who was Ada Lovelace?") + if err != nil { + panic(err) + } + + for _, result := range results { + fmt.Println(result) + } +} From 35b4ea6e5236fb4a7480d7da49ba4c4d29f6162d Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Mon, 5 Feb 2024 09:08:57 +0100 Subject: [PATCH 63/65] docs: add assistant and linglet --- docs/content/reference/assistant.md | 28 +++++++++++++ docs/content/reference/examples.md | 10 +++++ docs/content/reference/linglet.md | 62 +++++++++++++++++++++++++++++ examples/linglet/sql/main.go | 5 ++- 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 docs/content/reference/examples.md diff --git a/docs/content/reference/assistant.md b/docs/content/reference/assistant.md index 54d2d24c..a7f95357 100644 --- a/docs/content/reference/assistant.md +++ b/docs/content/reference/assistant.md @@ -4,3 +4,31 @@ description: linkTitle: "Assistant" menu: { main: { parent: 'reference', weight: -90 } } --- + +An AI Assistant is a conversational component that can understand and respond to natural language. It can be used to automate tasks, answer questions, and provide information. LinGoose offers an AI Assistant that is built on top of the `Thread`, `LLM` and `RAG` components. It uses the RAG model to retrieve relevant documents and then uses a language model to generate responses based on the retrieved documents. + +## Using AI Assistant + +LinGoose assistant can optionally be configured with a RAG model and a language model. + +```go +myAssistant := assistant.New( + openai.New().WithTemperature(0), +).WithRAG(myRAG).WithThread( + thread.New().AddMessages( + thread.NewUserMessage().AddContent( + thread.NewTextContent("what is the purpose of NATO?"), + ), + ), +) + + +err = myAssistant.Run(context.Background()) +if err != nil { + panic(err) +} + +fmt.Println(myAssistant.Thread()) +``` + +We can define the LinGoose `Assistant` as a `Thread` runner with an optional `RAG` component that will help to produce the response. \ No newline at end of file diff --git a/docs/content/reference/examples.md b/docs/content/reference/examples.md new file mode 100644 index 00000000..7a48602c --- /dev/null +++ b/docs/content/reference/examples.md @@ -0,0 +1,10 @@ +--- +title: "LinGoose Examples" +description: +linkTitle: "Examples" +menu: { main: { parent: 'reference', weight: -88 } } +--- + +LinGoose provides a number of examples to help you get started with building your own AI app. You can use these examples as a reference to understand how to build your own assistant. + +[LinGoose examples](https://github.com/henomis/lingoose/tree/main/examples) \ No newline at end of file diff --git a/docs/content/reference/linglet.md b/docs/content/reference/linglet.md index 95c580cb..649fbe7e 100644 --- a/docs/content/reference/linglet.md +++ b/docs/content/reference/linglet.md @@ -4,3 +4,65 @@ description: linkTitle: "Linglets" menu: { main: { parent: 'reference', weight: -89 } } --- + +Linglets are pre-built LinGoose Assistants with a specific purpose. They are designed to be used as a starting point for building your own AI app. You can use them as a reference to understand how to build your own assistant. + +There are two Linglets available: + +- `sql` - A Linglet that can understand and respond to SQL queries. +- `summarize` - A Linglet that can summarize text. + +## Using SQL Linglet + +The sql Linglet helps to build SQL queries. It can be used to automate tasks, defining a comlex SQL query, and provide information. + +```go +db, err := sql.Open("sqlite3", "Chinook_Sqlite.sqlite") +if err != nil { + panic(err) +} + +lingletSQL := lingletsql.New( + openai.New().WithMaxTokens(2000).WithTemperature(0).WithModel(openai.GPT3Dot5Turbo16K0613), + db, +) + +result, err := lingletSQL.Run( + context.Background(), + "list the top 3 albums that are most frequently present in playlists.", +) +if err != nil { + panic(err) +} + +fmt.Printf("SQL Query\n-----\n%s\n\n", result.SQLQuery) +fmt.Printf("Answer\n-------\n%s\n", result.Answer) +``` + +## Using Summarize Linglet + +The summarize Linglet helps to summarize text. + +```go + +textLoader := loader.NewTextLoader("state_of_the_union.txt", nil). + WithTextSplitter(textsplitter.NewRecursiveCharacterTextSplitter(4000, 0)) + +summarize := summarize.New( + openai.New().WithMaxTokens(2000).WithTemperature(0).WithModel(openai.GPT3Dot5Turbo16K0613), + textLoader, +).WithCallback( + func(t *thread.Thread, i, n int) { + fmt.Printf("Progress : %.0f%%\n", float64(i)/float64(n)*100) + }, +) + +summary, err := summarize.Run(context.Background()) +if err != nil { + panic(err) +} + +fmt.Println(*summary) +``` + +The summarize linglet chunks the input text into smaller pieces and then iterate over each chunk to summarize the result. It also provides a callback function to track the progress of the summarization process. \ No newline at end of file diff --git a/examples/linglet/sql/main.go b/examples/linglet/sql/main.go index d5551a37..6bcb0820 100644 --- a/examples/linglet/sql/main.go +++ b/examples/linglet/sql/main.go @@ -24,7 +24,10 @@ func main() { db, ) - result, err := lingletSQL.Run(context.Background(), "list the top 3 albums most present in playlists.") + result, err := lingletSQL.Run( + context.Background(), + "list the top 3 albums that are most frequently present in playlists.", + ) if err != nil { panic(err) } From c4ff594332b486b70046bba4e03a54412071222d Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Mon, 5 Feb 2024 09:11:51 +0100 Subject: [PATCH 64/65] fix --- docs/content/legacy/llm.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/content/legacy/llm.md b/docs/content/legacy/llm.md index 0adecb5a..60f097b8 100644 --- a/docs/content/legacy/llm.md +++ b/docs/content/legacy/llm.md @@ -5,3 +5,8 @@ linkTitle: "Components" menu: { main: { parent: 'legacy', weight: -90 } } --- +LinGoose still provides some legacy components that are not recommended for new projects. These components are provided for backward compatibility and will be removed in future releases. To understand the usage of these components, refer to the examples. + +- [Chat](https://github.com/henomis/lingoose/tree/main/examples/chat) +- [Pipeline](https://github.com/henomis/lingoose/tree/main/examples/pipeline) +- [Prompt](https://github.com/henomis/lingoose/tree/main/examples/prompt) \ No newline at end of file From ffbea947d458f2018eb758e1ec63d0752666d6da Mon Sep 17 00:00:00 2001 From: Simone Vellei Date: Mon, 5 Feb 2024 09:20:44 +0100 Subject: [PATCH 65/65] add deploy website --- .github/workflows/hugo.yml | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/hugo.yml diff --git a/.github/workflows/hugo.yml b/.github/workflows/hugo.yml new file mode 100644 index 00000000..cf97732d --- /dev/null +++ b/.github/workflows/hugo.yml @@ -0,0 +1,78 @@ +# Sample workflow for building and deploying a Hugo site to GitHub Pages +name: Deploy Hugo site to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: + - main + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +# Default to bash +defaults: + run: + shell: bash + +jobs: + # Build job + build: + runs-on: ubuntu-latest + env: + HUGO_VERSION: 0.122.0 + steps: + - name: Install Hugo CLI + run: | + wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ + && sudo dpkg -i ${{ runner.temp }}/hugo.deb + - name: Install Dart Sass + run: sudo snap install dart-sass + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup Pages + id: pages + uses: actions/configure-pages@v4 + - name: Install Node.js dependencies + run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" + - name: Build with Hugo + env: + # For maximum backward compatibility with Hugo modules + HUGO_ENVIRONMENT: production + HUGO_ENV: production + run: | + cd docs && hugo \ + --gc \ + --minify \ + --baseURL "${{ steps.pages.outputs.base_url }}/" + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: ./public + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v3 \ No newline at end of file