From 3061bf7837ac02061379f4dc08eed509d2d5c447 Mon Sep 17 00:00:00 2001 From: Adrien Date: Sat, 11 May 2024 12:17:09 +0200 Subject: [PATCH] Working user, Keys and chat --- Chat.go | 132 ++++++++---- Request.go | 11 +- RequestMistral.go | 292 +++++++++++++++++++++++++++ RequestOpenai.go | 2 - main.go | 95 ++++++--- static/style.css | 68 +++++++ utils.go | 20 +- views/partials/message-bot.html | 5 +- views/partials/popover-models.html | 20 +- views/partials/popover-settings.html | 13 +- views/partials/popover-usage.html | 6 +- 11 files changed, 576 insertions(+), 88 deletions(-) create mode 100644 RequestMistral.go diff --git a/Chat.go b/Chat.go index 9775dca..ac249a4 100644 --- a/Chat.go +++ b/Chat.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "sort" + "time" "github.com/edgedb/edgedb-go" "github.com/flosch/pongo2" @@ -24,7 +25,7 @@ func ChatPageHandler(c *fiber.Ctx) error { } func LoadModelSelectionHandler(c *fiber.Ctx) error { - openaiExists, anthropicExists := getExistingKeys() + openaiExists, anthropicExists, mistralExists := getExistingKeys() var CompanyInfosAvailable []CompanyInfo @@ -48,6 +49,16 @@ func LoadModelSelectionHandler(c *fiber.Ctx) error { } CompanyInfosAvailable = append(CompanyInfosAvailable, anthropicCompanyInfo) } + if mistralExists { + var mistralCompanyInfo CompanyInfo + for _, info := range CompanyInfos { + if info.ID == "mistral" { + mistralCompanyInfo = info + break + } + } + CompanyInfosAvailable = append(CompanyInfosAvailable, mistralCompanyInfo) + } CheckedModels := []string{"gpt-3.5-turbo"} // Default model out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-models.html")).Execute(pongo2.Context{ @@ -62,45 +73,6 @@ func LoadModelSelectionHandler(c *fiber.Ctx) error { return c.SendString(out) } -func LoadUsageKPIHandler(c *fiber.Ctx) error { - var TotalUsage float32 - // Using the database. Get the sum of all usage.inputCost and outputCost - err := edgeClient.QuerySingle(edgeCtx, ` - WITH - U := ( - SELECT Usage - FILTER .user = global currentUser - ) - SELECT sum(U.input_cost) + sum(U.output_cost) - `, &TotalUsage) - if err != nil { - fmt.Println("Error in edgedb.QuerySingle: in LoadUsageKPIHandler") - log.Fatal(err) - } - - out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-usage.html")).Execute(pongo2.Context{ - "TotalUsage": TotalUsage, - }) - if err != nil { - c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Error rendering template", - }) - } - return c.SendString(out) -} - -func LoadSettingsHandler(c *fiber.Ctx) error { - openaiExists, anthropicExists := getExistingKeys() - - out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-settings.html")).Execute(pongo2.Context{"IsLogin": checkIfLogin(), "OpenaiExists": openaiExists, "AnthropicExists": anthropicExists}) - if err != nil { - c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ - "error": "Error rendering template", - }) - } - return c.SendString(out) -} - func DeleteMessageHandler(c *fiber.Ctx) error { messageId := c.FormValue("id") @@ -307,3 +279,83 @@ func generateEnterKeyChatHTML() string { // Render the HTML template with the messages return htmlString } + +// Popover stuff + +func LoadUsageKPIHandler(c *fiber.Ctx) error { + var TotalUsage float32 + // Using the database. Get the sum of all usage.inputCost and outputCost + err := edgeClient.QuerySingle(edgeCtx, ` + WITH + U := ( + SELECT Usage + FILTER .user = global currentUser + ) + SELECT sum(U.input_cost) + sum(U.output_cost) + `, &TotalUsage) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in LoadUsageKPIHandler") + log.Fatal(err) + } + + now := time.Now() + + var TodayUsage float32 + // Using the database. Get the sum of all usage.inputCost and outputCost + err = edgeClient.QuerySingle(edgeCtx, ` + WITH + U := ( + SELECT Usage + FILTER .user = global currentUser AND .date >= $0 + ) + SELECT sum(U.input_cost) + sum(U.output_cost) + `, &TodayUsage, now.Add(time.Hour*-24)) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in LoadUsageKPIHandler") + log.Fatal(err) + } + + var WeekUsage float32 + // Using the database. Get the sum of all usage.inputCost and outputCost + edgeClient.QuerySingle(edgeCtx, ` + WITH + U := ( + SELECT Usage + FILTER .user = global currentUser AND .date >= $0 + ) + SELECT sum(U.input_cost) + sum(U.output_cost) + `, &WeekUsage, now.Add(time.Hour*-24*7)) + + var MonthUsage float32 + // Using the database. Get the sum of all usage.inputCost and outputCost + edgeClient.QuerySingle(edgeCtx, ` + WITH + U := ( + SELECT Usage + FILTER .user = global currentUser AND .date >= $0 + ) + SELECT sum(U.input_cost) + sum(U.output_cost) + `, &MonthUsage, now.Add(time.Hour*-24*30)) + + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-usage.html")).Execute(pongo2.Context{ + "TotalUsage": TotalUsage, "TodayUsage": TodayUsage, "WeekUsage": WeekUsage, "MonthUsage": MonthUsage, + }) + if err != nil { + c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error rendering template", + }) + } + return c.SendString(out) +} + +func LoadSettingsHandler(c *fiber.Ctx) error { + openaiExists, anthropicExists, mistralExists := getExistingKeys() + + out, err := pongo2.Must(pongo2.FromFile("views/partials/popover-settings.html")).Execute(pongo2.Context{"IsLogin": checkIfLogin(), "OpenaiExists": openaiExists, "AnthropicExists": anthropicExists, "MistralExists": mistralExists}) + if err != nil { + c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": "Error rendering template", + }) + } + return c.SendString(out) +} diff --git a/Request.go b/Request.go index bc22034..f96d611 100644 --- a/Request.go +++ b/Request.go @@ -69,16 +69,23 @@ func GenerateMultipleMessages(c *fiber.Ctx) error { wg.Add(len(lastSelectedModelIds)) for i := range lastSelectedModelIds { + idx := i if model2Icon(lastSelectedModelIds[i]) == "openai" { go func() { defer wg.Done() - response := addOpenaiMessage(lastSelectedModelIds[i], i == 0) + response := addOpenaiMessage(lastSelectedModelIds[idx], idx == 0) InsertedIDs = append(InsertedIDs, response) }() } else if model2Icon(lastSelectedModelIds[i]) == "anthropic" { go func() { defer wg.Done() - response := addAnthropicMessage(lastSelectedModelIds[i], i == 0) + response := addAnthropicMessage(lastSelectedModelIds[idx], idx == 0) + InsertedIDs = append(InsertedIDs, response) + }() + } else if model2Icon(lastSelectedModelIds[i]) == "mistral" { + go func() { + defer wg.Done() + response := addMistralMessage(lastSelectedModelIds[idx], idx == 0) InsertedIDs = append(InsertedIDs, response) }() } diff --git a/RequestMistral.go b/RequestMistral.go new file mode 100644 index 0000000..6238f4e --- /dev/null +++ b/RequestMistral.go @@ -0,0 +1,292 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/edgedb/edgedb-go" +) + +type MistralChatCompletionRequest struct { + Model string `json:"model"` + Messages []MistralMessage `json:"messages"` + Temperature float64 `json:"temperature"` +} + +type MistralMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type MistralChatCompletionResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Usage MistralUsage `json:"usage"` + Choices []MistralChoice `json:"choices"` +} + +type MistralUsage struct { + PromptTokens int32 `json:"prompt_tokens"` + CompletionTokens int32 `json:"completion_tokens"` + TotalTokens int32 `json:"total_tokens"` +} + +type MistralChoice struct { + Message MistralMessage `json:"message"` + FinishReason string `json:"finish_reason"` + Index int `json:"index"` +} + +func init() { + var ModelInfosList = []ModelInfo{} + + modelInfo := ModelInfo{ + ID: "open-mistral-7b", + Name: "Mistral 7b", + Icon: "mistral", + MaxToken: 32000, + InputPrice: 0.25 / 1000000, + OutputPrice: 1.25 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + modelInfo = ModelInfo{ + ID: "open-mixtral-8x7b", + Name: "Mistral 8x7b", + Icon: "mistral", + MaxToken: 32000, + InputPrice: 0.7 / 1000000, + OutputPrice: 0.7 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + modelInfo = ModelInfo{ + ID: "open-mixtral-8x22b", + Name: "Mistral 8x22b", + Icon: "mistral", + MaxToken: 64000, + InputPrice: 2.0 / 1000000, + OutputPrice: 6.0 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + modelInfo = ModelInfo{ + ID: "mistral-small-latest", + Name: "Mistral Small", + Icon: "mistral", + MaxToken: 32000, + InputPrice: 1.0 / 1000000, + OutputPrice: 3.0 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + modelInfo = ModelInfo{ + ID: "mistral-medium-latest", + Name: "Mistral Medium", + Icon: "mistral", + MaxToken: 32000, + InputPrice: 2.7 / 1000000, + OutputPrice: 8.1 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + modelInfo = ModelInfo{ + ID: "mistral-large-latest", + Name: "Mistral Large", + Icon: "mistral", + MaxToken: 32000, + InputPrice: 4.0 / 1000000, + OutputPrice: 12.0 / 1000000, + } + ModelInfosList = append(ModelInfosList, modelInfo) + ModelsInfos = append(ModelsInfos, modelInfo) + + companyInfo := CompanyInfo{ + ID: "mistral", + Name: "Mistral", + Icon: "icons/mistral.png", + ModelInfos: ModelInfosList, + } + CompanyInfos = append(CompanyInfos, companyInfo) +} + +func addMistralMessage(modelID string, selected bool) edgedb.UUID { + Messages := getAllMessages() + + chatCompletion, err := RequestMistral(modelID, Messages, 0.7) + if err != nil { + fmt.Println("Error:", err) + } else if len(chatCompletion.Choices) == 0 { + fmt.Println(chatCompletion) + fmt.Println("No response from Mistral") + id := insertBotMessage("No response from Mistral", selected, modelID) + return id + } else { + Content := chatCompletion.Choices[0].Message.Content + id := insertBotMessage(Content, selected, modelID) + return id + } + return edgedb.UUID{} +} + +func EdgeMessages2MistralMessages(messages []Message) []MistralMessage { + mistralMessages := make([]MistralMessage, len(messages)) + for i, msg := range messages { + var role string + switch msg.Role { + case "user": + role = "user" + case "bot": + role = "assistant" + default: + role = "system" + } + mistralMessages[i] = MistralMessage{ + Role: role, + Content: msg.Content, + } + } + return mistralMessages +} + +func TestMistralKey(apiKey string) bool { + url := "https://api.mistral.ai/v1/chat/completions" + + // Convert messages to Mistral format + mistralMessages := []MistralMessage{ + { + Role: "user", + Content: "Hello", + }, + } + + requestBody := MistralChatCompletionRequest{ + Model: "open-mistral-7b", + Messages: mistralMessages, + Temperature: 0, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + fmt.Println("Error:", err) + return false + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + fmt.Println("Error:", err) + return false + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error:", err) + return false + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error:", err) + return false + } + + var chatCompletionResponse MistralChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + fmt.Println(chatCompletionResponse) + if err != nil { + fmt.Println("Error:", err) + return false + } + if chatCompletionResponse.Usage.CompletionTokens == 0 { + fmt.Println("Error: No response from Mistral") + return false + } + return true +} + +func RequestMistral(model string, messages []Message, temperature float64) (MistralChatCompletionResponse, error) { + var apiKey string + err := edgeClient.QuerySingle(edgeCtx, ` + with + filtered_keys := ( + select Key { + key + } filter .company = $0 + ) + select filtered_keys.key limit 1 + `, &apiKey, "mistral") + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error getting OpenAI API key: %w", err) + } + + url := "https://api.mistral.ai/v1/chat/completions" + + // Convert messages to OpenAI format + mistralMessages := EdgeMessages2MistralMessages(messages) + + requestBody := MistralChatCompletionRequest{ + Model: model, + Messages: mistralMessages, + Temperature: temperature, + } + + jsonBody, err := json.Marshal(requestBody) + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error marshaling JSON: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody)) + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error creating request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+apiKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error reading response body: %w", err) + } + + var chatCompletionResponse MistralChatCompletionResponse + err = json.Unmarshal(body, &chatCompletionResponse) + if err != nil { + return MistralChatCompletionResponse{}, fmt.Errorf("error unmarshaling JSON: %w", err) + } + + var usedModelInfo ModelInfo + for mi := range ModelsInfos { + if ModelsInfos[mi].ID == model { + usedModelInfo = ModelsInfos[mi] + } + } + var inputCost float32 = float32(chatCompletionResponse.Usage.PromptTokens) * usedModelInfo.InputPrice + var outputCost float32 = float32(chatCompletionResponse.Usage.CompletionTokens) * usedModelInfo.OutputPrice + addUsage(inputCost, outputCost, chatCompletionResponse.Usage.PromptTokens, chatCompletionResponse.Usage.CompletionTokens, model) + + return chatCompletionResponse, nil +} diff --git a/RequestOpenai.go b/RequestOpenai.go index 4f242e8..51ab154 100644 --- a/RequestOpenai.go +++ b/RequestOpenai.go @@ -184,8 +184,6 @@ func RequestOpenai(model string, messages []Message, temperature float64) (Opena return OpenaiChatCompletionResponse{}, fmt.Errorf("error getting OpenAI API key: %w", err) } - fmt.Println("OpenAI API key: ", apiKey) - url := "https://api.openai.com/v1/chat/completions" // Convert messages to OpenAI format diff --git a/main.go b/main.go index e39028b..02380b5 100644 --- a/main.go +++ b/main.go @@ -63,6 +63,7 @@ func main() { func addKeys(c *fiber.Ctx) error { openaiKey := c.FormValue("openai_key") anthropicKey := c.FormValue("anthropic_key") + mistralKey := c.FormValue("mistral_key") var Exists bool // Handle OpenAI key @@ -83,11 +84,9 @@ func addKeys(c *fiber.Ctx) error { return c.SendString("") } - fmt.Println("OpenAI API key: ", openaiKey) - if !TestOpenaiKey(openaiKey) { fmt.Println("Invalid OpenAI API Key") - return handleInvalidKey(c, "OpenAI API Key") + return c.SendString("Invalid OpenAI API Key\n") } // Check if the company key already exists @@ -115,8 +114,6 @@ func addKeys(c *fiber.Ctx) error { fmt.Println(err) } } else { - fmt.Println("OpenAI API key: ", openaiKey) - err = edgeClient.Execute(edgeCtx, ` UPDATE global currentUser.setting SET { @@ -155,7 +152,7 @@ func addKeys(c *fiber.Ctx) error { if !TestAnthropicKey(anthropicKey) { fmt.Println("Invalid Anthropic API Key") - return handleInvalidKey(c, "Anthropic API Key") + return c.SendString("Invalid Anthropic API Key\n") } // Check if the company key already exists @@ -171,7 +168,6 @@ func addKeys(c *fiber.Ctx) error { } if Exists { - fmt.Println("Company key already exists") err = edgeClient.Execute(edgeCtx, ` UPDATE Key filter .company = "anthropic" AND .key = $0 SET { @@ -183,8 +179,6 @@ func addKeys(c *fiber.Ctx) error { fmt.Println(err) } } else { - fmt.Println("OpenAI API key: ", openaiKey) - err = edgeClient.Execute(edgeCtx, ` UPDATE global currentUser.setting SET { @@ -203,23 +197,70 @@ func addKeys(c *fiber.Ctx) error { } } + // Handle Mistral key + if mistralKey != "" { + // Check if the OpenAI key already exists + err := edgeClient.QuerySingle(edgeCtx, ` + select exists ( + select global currentUser.setting.keys + filter .company = "mistral" AND .key = $0 + ); + `, &Exists, openaiKey) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addMistralKey: ", err) + return c.SendString("") + } + if Exists { + fmt.Println("Mistral key already exists") + return c.SendString("") + } + + if !TestMistralKey(mistralKey) { + fmt.Println("Invalid Mistral API Key") + return c.SendString("Invalid Mistral API Key\n") + } + + // Check if the company key already exists + err = edgeClient.QuerySingle(edgeCtx, ` + select exists ( + select global currentUser.setting.keys + filter .company = "mistral" + ); + `, &Exists) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addMistralKey") + fmt.Println(err) + } + + if Exists { + err = edgeClient.Execute(edgeCtx, ` + UPDATE Key filter .company = "mistral" AND .key = $0 + SET { + key := $0, + } + `, mistralKey) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addMistralKey") + fmt.Println(err) + } + } else { + err = edgeClient.Execute(edgeCtx, ` + UPDATE global currentUser.setting + SET { + keys += ( + INSERT Key { + company := "mistral", + key := $0, + name := "Mistral API Key", + } + ) + }`, mistralKey) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addMistralKey") + fmt.Println(err) + } + } + } + return c.SendString("") } - -func handleInvalidKey(c *fiber.Ctx, keyType string) error { - NextMessages := []NextMessage{} - nextMsg := NextMessage{ - Icon: "bouvai2", - Content: "
" + markdownToHTML(fmt.Sprintf("Invalid %s", keyType)), - Hidden: false, - Id: "0", - Name: "JADE", - } - NextMessages = append(NextMessages, nextMsg) - - botOut, err := botTmpl.Execute(pongo2.Context{"Messages": NextMessages, "ConversationAreaId": 0}) - if err != nil { - panic(err) - } - return c.SendString(botOut) -} diff --git a/static/style.css b/static/style.css index be2ec5b..2a332d0 100644 --- a/static/style.css +++ b/static/style.css @@ -112,4 +112,72 @@ svg text { fill: #000; stroke: #000; font-size: 240px; +} + +/* Custom Checkbox Styles */ +.custom-checkbox { + display: flex; + align-items: center; + font-size: 1rem; + cursor: pointer; + user-select: none; + /* Prevent text selection */ +} + +.custom-checkbox input { + position: absolute; + opacity: 0; + /* Hide the default checkbox */ + cursor: pointer; +} + +.custom-checkbox .checkmark { + position: relative; + height: 14px; + width: 14px; + background-color: #eee; + /* Light grey background */ + border-radius: 4px; + /* Rounded corners */ + margin-right: 8px; + /* Space between checkmark and label text */ + border: 2px solid #ccc; + /* Grey border */ +} + +/* On mouse-over, add a grey background color */ +.custom-checkbox:hover input~.checkmark { + background-color: #ccc; +} + +/* When the checkbox is checked */ +.custom-checkbox input:checked~.checkmark { + background-color: var(--bulma-primary, #126d0f); + /* Primary color background */ + border-color: var(--bulma-primary, #126d0f); +} + +/* Create the checkmark/indicator (hidden when not checked) */ +.custom-checkbox .checkmark:after { + content: ""; + position: absolute; + display: none; +} + +/* Show the checkmark when checked */ +.custom-checkbox input:checked~.checkmark:after { + display: block; +} + +/* Style the checkmark/indicator */ +.custom-checkbox .checkmark:after { + left: 4px; + top: 1px; + width: 3px; + height: 6px; + border: solid white; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); } \ No newline at end of file diff --git a/utils.go b/utils.go index e47815d..daac131 100644 --- a/utils.go +++ b/utils.go @@ -50,6 +50,9 @@ func model2Icon(model string) string { if strings.Contains(model, "claude-3") { return "anthropic" } + if strings.Contains(model, "mistral") || strings.Contains(model, "mixtral") { + return "mistral" + } return "openai" } @@ -62,13 +65,14 @@ func model2Name(model string) string { return "OpenAI" } -func getExistingKeys() (bool, bool) { +func getExistingKeys() (bool, bool, bool) { if edgeClient == nil { - return false, false + return false, false, false } var openaiExists bool var anthropicExists bool + var mistralExists bool err := edgeClient.QuerySingle(edgeCtx, ` select exists ( @@ -90,5 +94,15 @@ func getExistingKeys() (bool, bool) { fmt.Println("Error in edgedb.QuerySingle: in addOpenaiKey: ", err) } - return openaiExists, anthropicExists + err = edgeClient.QuerySingle(edgeCtx, ` + select exists ( + select global currentUser.setting.keys + filter .company = "mistral" + ); + `, &mistralExists) + if err != nil { + fmt.Println("Error in edgedb.QuerySingle: in addOpenaiKey: ", err) + } + + return openaiExists, anthropicExists, mistralExists } diff --git a/views/partials/message-bot.html b/views/partials/message-bot.html index be49dda..b0d22dd 100644 --- a/views/partials/message-bot.html +++ b/views/partials/message-bot.html @@ -4,8 +4,9 @@