diff --git a/core/agent/agent.go b/core/agent/agent.go index af6b2e4..88d4ffd 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -168,37 +168,6 @@ func (a *Agent) askLLM(ctx context.Context, conversation []openai.ChatCompletion return resp.Choices[0].Message, nil } -func (a *Agent) saveCurrentConversationInMemory() { - if !a.options.enableLongTermMemory && !a.options.enableSummaryMemory { - xlog.Debug("Long term memory is disabled", "agent", a.Character.Name) - return - } - - xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(a.currentConversation)) - - if a.options.enableSummaryMemory && len(a.currentConversation) > 0 { - msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{ - Role: "user", - Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(a.currentConversation).String(), - }}) - if err != nil { - xlog.Error("Error summarizing conversation", "error", err) - } - - if err := a.options.ragdb.Store(msg.Content); err != nil { - xlog.Error("Error storing into memory", "error", err) - } - } else { - for _, message := range a.currentConversation { - if message.Role == "user" { - if err := a.options.ragdb.Store(message.Content); err != nil { - xlog.Error("Error storing into memory", "error", err) - } - } - } - } -} - func (a *Agent) ResetConversation() { a.Lock() defer a.Unlock() @@ -280,59 +249,6 @@ func (a *Agent) runAction(chosenAction Action, params action.ActionParams) (resu return result, nil } -func (a *Agent) knowledgeBaseLookup() { - if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) || - len(a.currentConversation) <= 0 { - xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name) - return - } - - // Walk conversation from bottom to top, and find the first message of the user - // to use it as a query to the KB - var userMessage string - for i := len(a.currentConversation) - 1; i >= 0; i-- { - xlog.Info("[Knowledge Base Lookup] Conversation", "role", a.currentConversation[i].Role, "Content", a.currentConversation[i].Content) - if a.currentConversation[i].Role == "user" { - userMessage = a.currentConversation[i].Content - break - } - } - xlog.Info("[Knowledge Base Lookup] Last user message", "agent", a.Character.Name, "message", userMessage) - - if userMessage == "" { - xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name) - return - } - - results, err := a.options.ragdb.Search(userMessage, a.options.kbResults) - if err != nil { - xlog.Info("Error finding similar strings inside KB:", "error", err) - } - - if len(results) == 0 { - xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name) - return - } - - formatResults := "" - for _, r := range results { - formatResults += fmt.Sprintf("- %s \n", r) - } - xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults) - - // a.currentConversation = append(a.currentConversation, - // openai.ChatCompletionMessage{ - // Role: "system", - // Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults), - // }, - // ) - a.currentConversation = append([]openai.ChatCompletionMessage{ - { - Role: "system", - Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults), - }}, a.currentConversation...) -} - func (a *Agent) consumeJob(job *Job, role string) { a.Lock() paused := a.pause diff --git a/core/agent/knowledgebase.go b/core/agent/knowledgebase.go new file mode 100644 index 0000000..3c9fd94 --- /dev/null +++ b/core/agent/knowledgebase.go @@ -0,0 +1,92 @@ +package agent + +import ( + "fmt" + + "github.com/mudler/LocalAgent/pkg/xlog" + "github.com/sashabaranov/go-openai" +) + +func (a *Agent) knowledgeBaseLookup() { + if (!a.options.enableKB && !a.options.enableLongTermMemory && !a.options.enableSummaryMemory) || + len(a.currentConversation) <= 0 { + xlog.Debug("[Knowledge Base Lookup] Disabled, skipping", "agent", a.Character.Name) + return + } + + // Walk conversation from bottom to top, and find the first message of the user + // to use it as a query to the KB + var userMessage string + for i := len(a.currentConversation) - 1; i >= 0; i-- { + xlog.Info("[Knowledge Base Lookup] Conversation", "role", a.currentConversation[i].Role, "Content", a.currentConversation[i].Content) + if a.currentConversation[i].Role == "user" { + userMessage = a.currentConversation[i].Content + break + } + } + xlog.Info("[Knowledge Base Lookup] Last user message", "agent", a.Character.Name, "message", userMessage) + + if userMessage == "" { + xlog.Info("[Knowledge Base Lookup] No user message found in conversation", "agent", a.Character.Name) + return + } + + results, err := a.options.ragdb.Search(userMessage, a.options.kbResults) + if err != nil { + xlog.Info("Error finding similar strings inside KB:", "error", err) + } + + if len(results) == 0 { + xlog.Info("[Knowledge Base Lookup] No similar strings found in KB", "agent", a.Character.Name) + return + } + + formatResults := "" + for _, r := range results { + formatResults += fmt.Sprintf("- %s \n", r) + } + xlog.Info("[Knowledge Base Lookup] Found similar strings in KB", "agent", a.Character.Name, "results", formatResults) + + // a.currentConversation = append(a.currentConversation, + // openai.ChatCompletionMessage{ + // Role: "system", + // Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults), + // }, + // ) + a.currentConversation = append([]openai.ChatCompletionMessage{ + { + Role: "system", + Content: fmt.Sprintf("Given the user input you have the following in memory:\n%s", formatResults), + }}, a.currentConversation...) +} + +func (a *Agent) saveCurrentConversationInMemory() { + if !a.options.enableLongTermMemory && !a.options.enableSummaryMemory { + xlog.Debug("Long term memory is disabled", "agent", a.Character.Name) + return + } + + xlog.Info("Saving conversation", "agent", a.Character.Name, "conversation size", len(a.currentConversation)) + + if a.options.enableSummaryMemory && len(a.currentConversation) > 0 { + msg, err := a.askLLM(a.context.Context, []openai.ChatCompletionMessage{{ + Role: "user", + Content: "Summarize the conversation below, keep the highlights as a bullet list:\n" + Messages(a.currentConversation).String(), + }}) + if err != nil { + xlog.Error("Error summarizing conversation", "error", err) + } + + if err := a.options.ragdb.Store(msg.Content); err != nil { + xlog.Error("Error storing into memory", "error", err) + } + } else { + for _, message := range a.currentConversation { + if message.Role == "user" { + if err := a.options.ragdb.Store(message.Content); err != nil { + xlog.Error("Error storing into memory", "error", err) + } + } + } + } +} diff --git a/pkg/localrag/client.go b/pkg/localrag/client.go index 0dfd1c9..4034a8f 100644 --- a/pkg/localrag/client.go +++ b/pkg/localrag/client.go @@ -64,8 +64,6 @@ func (c *WrappedClient) Search(s string, similarity int) ([]string, error) { func (c *WrappedClient) Store(s string) error { // the Client API of LocalRAG takes only files at the moment. // So we take the string that we want to store, write it to a file, and then store the file. - - // get current date and time for file name t := time.Now() dateTime := t.Format("2006-01-02-15-04-05") hash := md5.Sum([]byte(s)) diff --git a/webui/app.go b/webui/app.go index 04f86f4..1b5b80e 100644 --- a/webui/app.go +++ b/webui/app.go @@ -1,7 +1,6 @@ package webui import ( - "bytes" "encoding/json" "fmt" "net/http" @@ -15,7 +14,6 @@ import ( "github.com/mudler/LocalAgent/core/state" "github.com/donseba/go-htmx" - "github.com/dslipak/pdf" fiber "github.com/gofiber/fiber/v2" "github.com/gofiber/template/html/v2" ) @@ -79,12 +77,24 @@ func (a *App) Delete(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { if err := pool.Remove(c.Params("name")); err != nil { xlog.Info("Error removing agent", err) - return c.Status(http.StatusInternalServerError).SendString(err.Error()) + return errorJSONMessage(c, err.Error()) } - return c.Redirect("/agents") + return statusJSONMessage(c, "ok") } } +func errorJSONMessage(c *fiber.Ctx, message string) error { + return c.Status(http.StatusInternalServerError).JSON(struct { + Error string `json:"error"` + }{Error: message}) +} + +func statusJSONMessage(c *fiber.Ctx, message string) error { + return c.JSON(struct { + Status string `json:"status"` + }{Status: message}) +} + func (a *App) Pause(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { xlog.Info("Pausing agent", c.Params("name")) @@ -92,7 +102,7 @@ func (a *App) Pause(pool *state.AgentPool) func(c *fiber.Ctx) error { if agent != nil { agent.Pause() } - return c.Redirect("/agents") + return statusJSONMessage(c, "ok") } } @@ -102,7 +112,7 @@ func (a *App) Start(pool *state.AgentPool) func(c *fiber.Ctx) error { if agent != nil { agent.Resume() } - return c.Redirect("/agents") + return statusJSONMessage(c, "ok") } } @@ -110,20 +120,18 @@ func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { config := state.AgentConfig{} if err := c.BodyParser(&config); err != nil { - return err + return errorJSONMessage(c, err.Error()) } fmt.Printf("Agent configuration: %+v\n", config) if config.Name == "" { - c.Status(http.StatusBadRequest).SendString("Name is required") - return nil + return errorJSONMessage(c, "Name is required") } if err := pool.CreateAgent(config.Name, &config); err != nil { - c.Status(http.StatusInternalServerError).SendString(err.Error()) - return nil + return errorJSONMessage(c, err.Error()) } - return c.Redirect("/agents") + return statusJSONMessage(c, "ok") } } @@ -131,7 +139,7 @@ func (a *App) ExportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { agent := pool.GetConfig(c.Params("name")) if agent == nil { - return c.Status(http.StatusNotFound).SendString("Agent not found") + return errorJSONMessage(c, "Agent not found") } c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.json", agent.Name)) @@ -168,15 +176,13 @@ func (a *App) ImportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error { xlog.Info("Importing agent", config.Name) if config.Name == "" { - c.Status(http.StatusBadRequest).SendString("Name is required") - return nil + return errorJSONMessage(c, "Name is required") } if err := pool.CreateAgent(config.Name, &config); err != nil { - c.Status(http.StatusInternalServerError).SendString(err.Error()) - return nil + return errorJSONMessage(c, err.Error()) } - return c.Redirect("/agents") + return statusJSONMessage(c, "ok") } } @@ -237,17 +243,3 @@ func (a *App) Chat(pool *state.AgentPool) func(c *fiber.Ctx) error { return nil } } - -func readPdf(path string) (string, error) { - r, err := pdf.Open(path) - if err != nil { - return "", err - } - var buf bytes.Buffer - b, err := r.GetPlainText() - if err != nil { - return "", err - } - buf.ReadFrom(b) - return buf.String(), nil -} diff --git a/webui/routes.go b/webui/routes.go index d996438..d92a42c 100644 --- a/webui/routes.go +++ b/webui/routes.go @@ -79,7 +79,7 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) { webapp.Get("/notify/:name", app.Notify(pool)) webapp.Post("/chat/:name", app.Chat(pool)) webapp.Post("/create", app.Create(pool)) - webapp.Get("/delete/:name", app.Delete(pool)) + webapp.Delete("/delete/:name", app.Delete(pool)) webapp.Put("/pause/:name", app.Pause(pool)) webapp.Put("/start/:name", app.Start(pool)) diff --git a/webui/views/agents.html b/webui/views/agents.html index f0ba95b..f2dec40 100644 --- a/webui/views/agents.html +++ b/webui/views/agents.html @@ -9,12 +9,83 @@ .button-container { display: flex; justify-content: flex-end; - margin-bottom: 8px; /* Adjusts the spacing to your liking */ + margin-bottom: 8px; + } + .section-box { + background-color: rgba(30, 41, 59, 0.8); + border-radius: 8px; + padding: 20px; + margin-top: 20px; + } + .alert { + padding: 10px 15px; + border-radius: 4px; + margin: 10px 0; + display: none; + } + .alert-success { + background-color: rgba(16, 185, 129, 0.2); + border: 1px solid #10b981; + color: #10b981; + } + .alert-error { + background-color: rgba(239, 68, 68, 0.2); + border: 1px solid #ef4444; + color: #ef4444; + } + .toast { + position: fixed; + top: 20px; + right: 20px; + max-width: 350px; + padding: 15px; + border-radius: 5px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; + } + .toast-success { + background-color: #10b981; + color: white; + } + .toast-error { + background-color: #ef4444; + color: white; + } + .toast-visible { + opacity: 1; + } + .action-btn { + background-color: #374151; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; + } + .start-btn { + background-color: #10b981; + } + .start-btn:hover { + background-color: #059669; + } + .pause-btn { + background-color: #f59e0b; + } + .pause-btn:hover { + background-color: #d97706; } {{template "views/partials/menu"}} + +
+ +
+

Smart Agent List

@@ -55,7 +126,7 @@ {{ $status := .Status }} {{ range .Agents }} - + {{.}} Online: {{ index $status . }} @@ -69,14 +140,24 @@ - + - + @@ -93,19 +174,131 @@
+ + - + \ No newline at end of file diff --git a/webui/views/create.html b/webui/views/create.html index a3ec522..c4a55d1 100644 --- a/webui/views/create.html +++ b/webui/views/create.html @@ -6,13 +6,33 @@ + {{template "views/partials/menu"}}
-

Create New Agent

+

Create New Agent

+ + -
+
@@ -153,11 +173,89 @@
- +
-
+ + +
+ +
+ Agent created successfully! Redirecting to agent list... +
+ + +
+ Error creating agent. +
+
+ + + diff --git a/webui/views/partials/header.html b/webui/views/partials/header.html index 67ca028..95f6d4d 100644 --- a/webui/views/partials/header.html +++ b/webui/views/partials/header.html @@ -220,4 +220,29 @@ select::-webkit-scrollbar-thumb { } - \ No newline at end of file + + + diff --git a/webui/views/settings.html b/webui/views/settings.html index efd41e1..c8b8f9a 100644 --- a/webui/views/settings.html +++ b/webui/views/settings.html @@ -41,46 +41,185 @@ button:hover { background-color: #5a86b8; } + + /* Toast notification styles */ + .toast { + position: fixed; + top: 20px; + right: 20px; + max-width: 350px; + padding: 15px; + border-radius: 5px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; + } + .toast-success { + background-color: #10b981; + color: white; + } + .toast-error { + background-color: #ef4444; + color: white; + } + .toast-visible { + opacity: 1; + } + + /* Action button styles */ + .action-button { + margin-bottom: 10px; + display: inline-block; + width: auto; + margin-right: 10px; + } + + .start-button { + background-color: #10b981; /* Green */ + } + .start-button:hover { + background-color: #059669; + } + + .pause-button { + background-color: #f59e0b; /* Orange */ + } + .pause-button:hover { + background-color: #d97706; + } + + .delete-button { + background-color: #ef4444; /* Red */ + color: white; + padding: 10px 15px; + border-radius: 5px; + text-decoration: none; + display: inline-block; + cursor: pointer; + } + .delete-button:hover { + background-color: #dc2626; + } {{template "views/partials/menu"}} + + +
+ +
+

Agent settings - {{.Name}}

- -
-

Manage Knowledgebase

-

Access and update your knowledgebase to improve agent responses and efficiency.

-
-
- -
-

Export

- Export +

Export

+ Export

Danger section

- - Delete - +
+ - + \ No newline at end of file