diff --git a/.dockerignore b/.dockerignore index d4beccb..fd344f1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ data wasm -dbschema \ No newline at end of file +dbschema +.git +TODO.md diff --git a/Chat.go b/Chat.go index 6884cc6..e406527 100644 --- a/Chat.go +++ b/Chat.go @@ -206,8 +206,6 @@ func GetMessageContentHandler(c *fiber.Ctx) error { } func GenerateMessageContentHTML(authCookie string, messageId string, onlyContent string, withDiv bool) string { - fmt.Println("Generating message for:", authCookie) - messageUUID, _ := edgedb.ParseUUID(messageId) var selectedMessage Message @@ -372,7 +370,7 @@ func generateEnterKeyChatHTML() string {

To get a key and learn more about the different LLM providers and their offerings, check out their websites:

Get OpenAI API key @@ -422,15 +420,22 @@ func generateEnterKeyChatHTML() string { > Get Fireworks API key +Get Together AI API key +Get DeepSeek API key +

Note: Key are encrypted and saved on a secure database link only with the app.

` htmlString := "
" NextMessages := []TemplateMessage{} nextMsg := TemplateMessage{ - Icon: "icons/bouvai2.png", // Assuming Icon is a field you want to include from Message + Icon: "icons/bouvai2.png", Content: welcomeMessage, - Hidden: false, // Assuming Hidden is a field you want to include from Message + Hidden: false, Id: "0", Name: "JADE", } @@ -929,7 +934,7 @@ func LoadSettingsHandler(c *fiber.Ctx) error { stripeSubLink := "https://billing.stripe.com/p/login/6oE6sc0PTfvq1Hi288?prefilled_email=" + user.Email - openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists := getExistingKeys(c) + openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists, togetherExists, deepseekExists := getExistingKeysNew(c) isPremium, isBasic := IsCurrentUserSubscribed(c) out, err := settingPopoverTmpl.Execute(pongo2.Context{ @@ -943,7 +948,9 @@ func LoadSettingsHandler(c *fiber.Ctx) error { "NimExists": nimExists, "PerplexityExists": perplexityExists, "FireworksExists": fireworksExists, - "AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists || perplexityExists || nimExists, + "TogetherExists": togetherExists, + "DeepseekExists": deepseekExists, + "AnyExists": fireworksExists || openaiExists || anthropicExists || mistralExists || groqExists || gooseaiExists || googleExists || perplexityExists || nimExists || togetherExists || deepseekExists, "isPremium": isPremium, "isBasic": isBasic, "StripeSubLink": stripeSubLink, diff --git a/MyUtils.go b/MyUtils.go index 0df1792..d8124a5 100644 --- a/MyUtils.go +++ b/MyUtils.go @@ -75,123 +75,73 @@ func addCodeHeader(htmlContent string, languages []string) string { return updatedHTML } -func getExistingKeys(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bool, bool) { +func getExistingKeysNew(c *fiber.Ctx) (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool) { + // TODO: Optimize to use only one query if edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}) == nil { - return false, false, false, false, false, false, false, false, false + return false, false, false, false, false, false, false, false, false, false, false } - var ( - openaiExists bool - anthropicExists bool - mistralExists bool - groqExists bool - gooseaiExists bool - nimExists bool - googleExists bool - perplexityExists bool - fireworksExists bool - ) + var userInfo User err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "openai" - ); - `, &openaiExists) + SELECT global currentUser { + setting: { + keys: { + name, + key, + company: { + name + } + } + } + } + LIMIT 1; + `, &userInfo) if err != nil { - fmt.Println("Error checking if OpenAI key exists") - panic(err) + fmt.Println("Error getting user keys", err) + return false, false, false, false, false, false, false, false, false, false, false } - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "anthropic" - ); - `, &anthropicExists) - if err != nil { - fmt.Println("Error checking if Anthropic key exists") - panic(err) + openaiExists := false + anthropicExists := false + mistralExists := false + groqExists := false + gooseaiExists := false + nimExists := false + googleExists := false + perplexityExists := false + fireworksExists := false + togetherExists := false + deepseekExists := false + + for _, key := range userInfo.Setting.Keys { + switch key.Company.Name { + case "openai": + openaiExists = true + case "anthropic": + anthropicExists = true + case "mistral": + mistralExists = true + case "groq": + groqExists = true + case "gooseai": + gooseaiExists = true + case "nim": + nimExists = true + case "google": + googleExists = true + case "perplexity": + perplexityExists = true + case "fireworks": + fireworksExists = true + case "together": + togetherExists = true + case "deepseek": + deepseekExists = true + } } - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "mistral" - ); - `, &mistralExists) - if err != nil { - fmt.Println("Error checking if Mistral key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "groq" - ); - `, &groqExists) - if err != nil { - fmt.Println("Error checking if Groq key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "gooseai" - ); - `, &gooseaiExists) - if err != nil { - fmt.Println("Error checking if GooseAI key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "google" - ); - `, &googleExists) - if err != nil { - fmt.Println("Error checking if Google key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "perplexity" - ); - `, &perplexityExists) - if err != nil { - fmt.Println("Error checking if Perplexity key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "fireworks" - ); - `, &fireworksExists) - if err != nil { - fmt.Println("Error checking if Fireworks key exists") - panic(err) - } - - err = edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` - select exists ( - select global currentUser.setting.keys - filter .company.name = "nim" - ); - `, &nimExists) - if err != nil { - fmt.Println("Error checking if Fireworks key exists") - panic(err) - } - - return openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists + return openaiExists, anthropicExists, mistralExists, groqExists, gooseaiExists, googleExists, perplexityExists, fireworksExists, nimExists, togetherExists, deepseekExists } func Message2RequestMessage(messages []Message, context string) []RequestMessage { diff --git a/Request.go b/Request.go index 1c577a7..6d4363c 100644 --- a/Request.go +++ b/Request.go @@ -158,6 +158,10 @@ func GenerateMultipleMessagesHandler(c *fiber.Ctx) error { addMessageFunc = addFireworkMessage case "nim": addMessageFunc = addNimMessage + case "together": + addMessageFunc = addTogetherMessage + case "deepseek": + addMessageFunc = addDeepseekMessage } var messageID edgedb.UUID diff --git a/RequestDeepseek.go b/RequestDeepseek.go new file mode 100644 index 0000000..cb57220 --- /dev/null +++ b/RequestDeepseek.go @@ -0,0 +1,203 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/edgedb/edgedb-go" + "github.com/gofiber/fiber/v2" +) + +type DeepseekChatCompletionRequest struct { + Model string `json:"model"` + Messages []RequestMessage `json:"messages"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` +} + +type DeepseekChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Usage DeepseekUsage `json:"usage"` + Choices []DeepseekChoice `json:"choices"` +} + +type DeepseekUsage struct { + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + TotalTokens int32 `json:"total_tokens"` +} + +type DeepseekChoice struct { + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` +} + +func addDeepseekMessage(c *fiber.Ctx, llm LLM, selected bool) edgedb.UUID { + Messages := getAllSelectedMessages(c) + + chatCompletion, err := RequestDeepseek(c, llm.Model.ModelID, Messages, float64(llm.Temperature), llm.Context, int(llm.MaxToken)) + if err != nil { + fmt.Println("Error requesting Deepseek: ", err) + id := insertBotMessage(c, "Error requesting DeepSeek, model may not be available anymore. Better error message in development.", selected, llm.ID) + return id + } else if len(chatCompletion.Choices) == 0 { + fmt.Println("No response from DeepSeek") + id := insertBotMessage(c, "No response from DeepSeek", selected, llm.ID) + return id + } else { + Content := chatCompletion.Choices[0].Message.Content + id := insertBotMessage(c, Content, selected, llm.ID) + return id + } +} + +func TestDeepseekKey(apiKey string) bool { + url := "https://api.deepseek.com/chat/completions" + + // Convert messages to OpenAI format + deepseekMessages := []RequestMessage{ + { + Role: "user", + Content: "Hello", + }, + } + + requestBody := DeepseekChatCompletionRequest{ + Model: "deepseek-chat", + Messages: deepseekMessages, + Temperature: 0, + MaxTokens: 10, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + fmt.Println("Failed to test Deepseek API key - json.Marshal :", err) + return false + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + fmt.Println("Failed to test Deepseek API key - http.NewRequest :", err) + return false + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("Failed to test Deepseek API key - client.Do :", err) + return false + } + defer resp.Body.Close() + + fmt.Println(resp.Status) + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Failed to test Deepseek API key - io.ReadAll :", err) + return false + } + + var chatCompletionResponse DeepseekChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + if err != nil { + fmt.Println("Failed to test Deepseek API key - json.Marshal :", err) + return false + } + + if chatCompletionResponse.Usage.CompletionTokens == 0 { + fmt.Println("Failed to test Deepseek API key - No completion tokens :", err) + return false + } + + return true +} + +func RequestDeepseek(c *fiber.Ctx, model string, messages []Message, temperature float64, context string, maxTokens int) (DeepseekChatCompletionResponse, error) { + var apiKey string + err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .company.name = $0 AND .$0 + LIMIT 1 + `, &usedModelInfo, model) + if err != nil { + return DeepseekChatCompletionResponse{}, fmt.Errorf("error getting model info: %w", err) + } + + var inputCost float32 = float32(chatCompletionResponse.Usage.PromptTokens) * usedModelInfo.InputPrice + var outputCost float32 = float32(chatCompletionResponse.Usage.CompletionTokens) * usedModelInfo.OutputPrice + addUsage(c, inputCost, outputCost, chatCompletionResponse.Usage.PromptTokens, chatCompletionResponse.Usage.CompletionTokens, model) + + return chatCompletionResponse, nil +} diff --git a/RequestTogetherai.go b/RequestTogetherai.go new file mode 100644 index 0000000..151b031 --- /dev/null +++ b/RequestTogetherai.go @@ -0,0 +1,188 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/edgedb/edgedb-go" + "github.com/gofiber/fiber/v2" +) + +type TogetherChatCompletionRequest struct { + Model string `json:"model"` + Messages []RequestMessage `json:"messages"` + MaxTokens int `json:"max_tokens"` + Temperature float64 `json:"temperature"` +} + +type TogetherChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Usage TogetherUsage `json:"usage"` + Choices []TogetherChoice `json:"choices"` +} + +type TogetherUsage struct { + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + TotalTokens int32 `json:"total_tokens"` +} + +type TogetherChoice struct { + Text string `json:"text"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` +} + +func addTogetherMessage(c *fiber.Ctx, llm LLM, selected bool) edgedb.UUID { + Messages := getAllSelectedMessages(c) + + chatCompletion, err := RequestTogether(c, llm.Model.ModelID, Messages, float64(llm.Temperature), llm.Context, int(llm.MaxToken)) + if err != nil { + fmt.Println("Error requesting Together: ", err) + id := insertBotMessage(c, "Error requesting Together, model may not be available anymore. Better error message in development.", selected, llm.ID) + return id + } else if len(chatCompletion.Choices) == 0 { + fmt.Println("No response from Together") + id := insertBotMessage(c, "No response from Together", selected, llm.ID) + return id + } else { + Content := chatCompletion.Choices[0].Text + id := insertBotMessage(c, Content, selected, llm.ID) + return id + } +} + +func TestTogetherKey(apiKey string) bool { + url := "https://api.together.xyz/v1/completions" + + // Convert messages to OpenAI format + togetherMessages := []RequestMessage{ + { + Role: "user", + Content: "Hello", + }, + } + + requestBody := TogetherChatCompletionRequest{ + Model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", + Messages: togetherMessages, + Temperature: 0, + MaxTokens: 10, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return false + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + return false + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false + } + + var chatCompletionResponse TogetherChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + if err != nil { + return false + } + if chatCompletionResponse.Usage.CompletionTokens == 0 { + return false + } + return true +} + +func RequestTogether(c *fiber.Ctx, model string, messages []Message, temperature float64, context string, maxTokens int) (TogetherChatCompletionResponse, error) { + var apiKey string + err := edgeGlobalClient.WithGlobals(map[string]interface{}{"ext::auth::client_token": c.Cookies("jade-edgedb-auth-token")}).QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .company.name = $0 AND .$0 + LIMIT 1 + `, &usedModelInfo, model) + if err != nil { + return TogetherChatCompletionResponse{}, fmt.Errorf("error getting model info: %w", err) + } + + var inputCost float32 = float32(chatCompletionResponse.Usage.PromptTokens) * usedModelInfo.InputPrice + var outputCost float32 = float32(chatCompletionResponse.Usage.CompletionTokens) * usedModelInfo.OutputPrice + addUsage(c, inputCost, outputCost, chatCompletionResponse.Usage.PromptTokens, chatCompletionResponse.Usage.CompletionTokens, model) + + return chatCompletionResponse, nil +} diff --git a/TODO.md b/TODO.md index 573a4d2..c69ce5f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,27 @@ # Bugs -[ ] The SSE event that sometime fails after some times. Reconnect when sending a new message. +[ ] Sometime I can redo or edit but the button is not available +[X] The SSE event that sometime fails after some times. Reconnect when sending a new message. [X] 2 selected messages [X] On first response, code block width are too long [X] Change Terms of service to say that I use one cookie [X] Change the lastSelectedLLMs, 2 users can't use the same time... [X] CTRL + Enter not working +[ ] Add all Together AI models # Features +[X] SSE to WebSocket [X] Add max tokens [ ] Errors messages to know what happend -[ ] Add Deepseek API +[ ] Check the response status and add a message accordingly (Like 402 Payment required) +[X] Add Deepseek API +[X] Add TogetherAI API [X] Add Nvidia NIM +[ ] Host the database on Fly.io +[ ] Add login with email and password +[ ] Add login with other provider +[ ] Better temperature settings. (Anthropic from 0-1, OpenAI from 0-2, DeepSeek from -2-2, ect) + +# Other +[ ] Change the terms of service and enter keys page to an HTML +[ ] Split Chat.go into smaller files +[ ] Create a Request package diff --git a/dbschema/default.esdl b/dbschema/default.esdl index b1f1512..ba9f631 100644 --- a/dbschema/default.esdl +++ b/dbschema/default.esdl @@ -28,7 +28,7 @@ module default { type Key { required name: str; - company: Company; + required company: Company; required key: str; required date: datetime { default := datetime_current(); diff --git a/dbschema/migrations/00054-m1crb36.edgeql b/dbschema/migrations/00054-m1crb36.edgeql new file mode 100644 index 0000000..faa0e36 --- /dev/null +++ b/dbschema/migrations/00054-m1crb36.edgeql @@ -0,0 +1,9 @@ +CREATE MIGRATION m1crb36qoksqdmqtzncwo6b5gqb66jlgdfkziozqenrkgwowoj5vka + ONTO m1eqooh5xjbysafocihnromqeyrleo57w6txsr36tu73rkju2eyfcq +{ + ALTER TYPE default::Key { + ALTER LINK company { + SET REQUIRED USING ({}); + }; + }; +}; diff --git a/main.go b/main.go index ff69c29..566d749 100644 --- a/main.go +++ b/main.go @@ -138,6 +138,8 @@ func addKeys(c *fiber.Ctx) error { "nim": c.FormValue("nim_key"), "perplexity": c.FormValue("perplexity_key"), "fireworks": c.FormValue("fireworks_key"), + "together": c.FormValue("together_key"), + "deepseek": c.FormValue("deepseek_key"), } testFunctions := map[string]func(string) bool{ @@ -150,6 +152,8 @@ func addKeys(c *fiber.Ctx) error { "nim": TestNimKey, "perplexity": TestPerplexityKey, "fireworks": TestFireworkKey, + "together": TestTogetherKey, + "deepseek": TestDeepseekKey, } for company, key := range keys { diff --git a/static/icons/deepseek.png b/static/icons/deepseek.png new file mode 100644 index 0000000..d314ccb Binary files /dev/null and b/static/icons/deepseek.png differ diff --git a/static/icons/together.png b/static/icons/together.png new file mode 100644 index 0000000..6d3249c Binary files /dev/null and b/static/icons/together.png differ diff --git a/views/partials/explain-llm-conv-chat.html b/views/partials/explain-llm-conv-chat.html index 1dfb5ba..c840719 100644 --- a/views/partials/explain-llm-conv-chat.html +++ b/views/partials/explain-llm-conv-chat.html @@ -4,7 +4,7 @@ the settings menu. Once enter you get access to all models from this provider.

Get OpenAI API key Get Fireworks API key +Get Together AI API key +Get DeepSeek API key

Conversations

diff --git a/views/partials/popover-settings.html b/views/partials/popover-settings.html index 2a1412b..979e56a 100644 --- a/views/partials/popover-settings.html +++ b/views/partials/popover-settings.html @@ -115,6 +115,30 @@

+ +