From d451919414833483a8ed41f64cea1478769d4f21 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 11 Mar 2025 22:32:13 +0100 Subject: [PATCH] feat(edit): allow to edit agents (#36) --- core/state/pool.go | 96 +++--- services/connectors/irc.go | 20 +- webui/app.go | 50 +++ webui/routes.go | 13 +- webui/views/create.html | 143 ++++++++- webui/views/settings.html | 614 ++++++++++++++++++++++++++++++++++++- 6 files changed, 869 insertions(+), 67 deletions(-) diff --git a/core/state/pool.go b/core/state/pool.go index 4ea38f6..9adfd6e 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -21,18 +21,18 @@ import ( type AgentPool struct { sync.Mutex - file string - pooldir string - pool AgentPoolData - agents map[string]*Agent - managers map[string]sse.Manager - agentStatus map[string]*Status - apiURL, model, multimodalModel, localRAGAPI, apiKey string - availableActions func(*AgentConfig) func(ctx context.Context) []Action - connectors func(*AgentConfig) []Connector - promptBlocks func(*AgentConfig) []PromptBlock - timeout string - conversationLogs string + file string + pooldir string + pool AgentPoolData + agents map[string]*Agent + managers map[string]sse.Manager + agentStatus map[string]*Status + apiURL, defaultModel, defaultMultimodalModel, localRAGAPI, apiKey string + availableActions func(*AgentConfig) func(ctx context.Context) []Action + connectors func(*AgentConfig) []Connector + promptBlocks func(*AgentConfig) []PromptBlock + timeout string + conversationLogs string } type Status struct { @@ -66,7 +66,7 @@ func loadPoolFromFile(path string) (*AgentPoolData, error) { } func NewAgentPool( - model, multimodalModel, apiURL, apiKey, directory string, + defaultModel, defaultMultimodalModel, apiURL, apiKey, directory string, LocalRAGAPI string, availableActions func(*AgentConfig) func(ctx context.Context) []agent.Action, connectors func(*AgentConfig) []Connector, @@ -87,22 +87,22 @@ func NewAgentPool( if _, err := os.Stat(poolfile); err != nil { // file does not exist, create a new pool return &AgentPool{ - file: poolfile, - pooldir: directory, - apiURL: apiURL, - model: model, - multimodalModel: multimodalModel, - localRAGAPI: LocalRAGAPI, - apiKey: apiKey, - agents: make(map[string]*Agent), - pool: make(map[string]AgentConfig), - agentStatus: make(map[string]*Status), - managers: make(map[string]sse.Manager), - connectors: connectors, - availableActions: availableActions, - promptBlocks: promptBlocks, - timeout: timeout, - conversationLogs: conversationPath, + file: poolfile, + pooldir: directory, + apiURL: apiURL, + defaultModel: defaultModel, + defaultMultimodalModel: defaultMultimodalModel, + localRAGAPI: LocalRAGAPI, + apiKey: apiKey, + agents: make(map[string]*Agent), + pool: make(map[string]AgentConfig), + agentStatus: make(map[string]*Status), + managers: make(map[string]sse.Manager), + connectors: connectors, + availableActions: availableActions, + promptBlocks: promptBlocks, + timeout: timeout, + conversationLogs: conversationPath, }, nil } @@ -111,22 +111,22 @@ func NewAgentPool( return nil, err } return &AgentPool{ - file: poolfile, - apiURL: apiURL, - pooldir: directory, - model: model, - multimodalModel: multimodalModel, - apiKey: apiKey, - agents: make(map[string]*Agent), - managers: make(map[string]sse.Manager), - agentStatus: map[string]*Status{}, - pool: *poolData, - connectors: connectors, - localRAGAPI: LocalRAGAPI, - promptBlocks: promptBlocks, - availableActions: availableActions, - timeout: timeout, - conversationLogs: conversationPath, + file: poolfile, + apiURL: apiURL, + pooldir: directory, + defaultModel: defaultModel, + defaultMultimodalModel: defaultMultimodalModel, + apiKey: apiKey, + agents: make(map[string]*Agent), + managers: make(map[string]sse.Manager), + agentStatus: map[string]*Status{}, + pool: *poolData, + connectors: connectors, + localRAGAPI: LocalRAGAPI, + promptBlocks: promptBlocks, + availableActions: availableActions, + timeout: timeout, + conversationLogs: conversationPath, }, nil } @@ -166,8 +166,8 @@ func (a *AgentPool) GetStatusHistory(name string) *Status { func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error { manager := sse.NewManager(5) ctx := context.Background() - model := a.model - multimodalModel := a.multimodalModel + model := a.defaultModel + multimodalModel := a.defaultMultimodalModel if config.MultimodalModel != "" { multimodalModel = config.MultimodalModel } @@ -340,7 +340,7 @@ func (a *AgentPool) startAgentWithConfig(name string, config *AgentConfig) error go func() { if err := agent.Run(); err != nil { - xlog.Error("Agent stopped", "error", err.Error()) + xlog.Error("Agent stopped", "error", err.Error(), "name", name) panic(err) } }() diff --git a/services/connectors/irc.go b/services/connectors/irc.go index 139b3d8..4eec661 100644 --- a/services/connectors/irc.go +++ b/services/connectors/irc.go @@ -8,7 +8,7 @@ import ( "github.com/mudler/LocalAgent/core/agent" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/actions" - "github.com/thoj/go-ircevent" + irc "github.com/thoj/go-ircevent" ) type IRC struct { @@ -53,21 +53,25 @@ func cleanUpMessage(message string, nickname string) string { // isMentioned checks if the bot is mentioned in the message func isMentioned(message string, nickname string) bool { - return strings.Contains(message, nickname+":") || - strings.Contains(message, nickname+",") || - strings.HasPrefix(message, nickname) + return strings.Contains(message, nickname+":") || + strings.Contains(message, nickname+",") || + strings.HasPrefix(message, nickname) } // Start connects to the IRC server and starts listening for messages func (i *IRC) Start(a *agent.Agent) { i.conn = irc.IRC(i.nickname, i.nickname) - i.conn.UseTLS = false + if i.conn == nil { + xlog.Error("Failed to create IRC client") + return + } + i.conn.UseTLS = false i.conn.AddCallback("001", func(e *irc.Event) { xlog.Info("Connected to IRC server", "server", i.server) i.conn.Join(i.channel) xlog.Info("Joined channel", "channel", i.channel) }) - + i.conn.AddCallback("JOIN", func(e *irc.Event) { if e.Nick == i.nickname { xlog.Info("Bot joined channel", "channel", e.Arguments[0]) @@ -128,10 +132,10 @@ func (i *IRC) Start(a *agent.Agent) { chunk = line line = "" } - + // Send the message to the channel i.conn.Privmsg(channel, chunk) - + // Small delay to prevent flooding time.Sleep(500 * time.Millisecond) } diff --git a/webui/app.go b/webui/app.go index aabe00e..3b2fa65 100644 --- a/webui/app.go +++ b/webui/app.go @@ -136,6 +136,56 @@ func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error { } } +// NEW FUNCTION: Get agent configuration +func (a *App) GetAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + config := pool.GetConfig(c.Params("name")) + if config == nil { + return errorJSONMessage(c, "Agent not found") + } + return c.JSON(config) + } +} + +// UpdateAgentConfig handles updating an agent's configuration +func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + agentName := c.Params("name") + + // First check if agent exists + oldConfig := pool.GetConfig(agentName) + if oldConfig == nil { + return errorJSONMessage(c, "Agent not found") + } + + // Parse the new configuration using the same approach as Create + newConfig := state.AgentConfig{} + if err := c.BodyParser(&newConfig); err != nil { + xlog.Error("Error parsing agent config", "error", err) + return errorJSONMessage(c, err.Error()) + } + + // Ensure the name doesn't change + newConfig.Name = agentName + + // Remove the agent first + if err := pool.Remove(agentName); err != nil { + return errorJSONMessage(c, "Error removing agent: "+err.Error()) + } + + // Create agent with new config + if err := pool.CreateAgent(agentName, &newConfig); err != nil { + // Try to restore the old configuration if update fails + if restoreErr := pool.CreateAgent(agentName, oldConfig); restoreErr != nil { + return errorJSONMessage(c, fmt.Sprintf("Failed to update agent and restore failed: %v, %v", err, restoreErr)) + } + return errorJSONMessage(c, "Error updating agent: "+err.Error()) + } + + return statusJSONMessage(c, "ok") + } +} + func (a *App) ExportAgent(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { agent := pool.GetConfig(c.Params("name")) diff --git a/webui/routes.go b/webui/routes.go index b5b5306..cff70b1 100644 --- a/webui/routes.go +++ b/webui/routes.go @@ -102,11 +102,18 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) { } return c.Render("views/settings", fiber.Map{ - // "Character": agent.Character, - "Name": c.Params("name"), - "Status": status, + "Name": c.Params("name"), + "Status": status, + "Actions": services.AvailableActions, + "Connectors": services.AvailableConnectors, + "PromptBlocks": services.AvailableBlockPrompts, }) }) + + // New API endpoints for getting and updating agent configuration + webapp.Get("/api/agent/:name/config", app.GetAgentConfig(pool)) + webapp.Put("/api/agent/:name/config", app.UpdateAgentConfig(pool)) + webapp.Post("/settings/import", app.ImportAgent(pool)) webapp.Get("/settings/export/:name", app.ExportAgent(pool)) } diff --git a/webui/views/create.html b/webui/views/create.html index 76d5b73..c9d8ae3 100644 --- a/webui/views/create.html +++ b/webui/views/create.html @@ -78,7 +78,7 @@
- +
@@ -278,19 +278,152 @@ form.addEventListener('submit', function(e) { e.preventDefault(); - // Show a loading state + // Show loading state const createButton = document.getElementById('create-button'); const originalButtonText = createButton.innerHTML; createButton.innerHTML = ' Creating...'; createButton.disabled = true; - // Get form data + // Build a structured data object const formData = new FormData(form); + const jsonData = {}; - // Send the form data using fetch API + // Process basic form fields + for (const [key, value] of formData.entries()) { + // Skip the array fields (connectors, actions, promptblocks) as they'll be processed separately + if (!key.includes('[') && !key.includes('].')) { + // Handle checkboxes + if (value === 'on') { + jsonData[key] = true; + } + // Handle numeric fields - specifically kb_results + else if (key === 'kb_results') { + // Convert to integer or default to 3 if empty + jsonData[key] = value ? parseInt(value, 10) : 3; + + // Check if the parse was successful + if (isNaN(jsonData[key])) { + showToast('Knowledge Base Results must be a number', 'error'); + createButton.innerHTML = originalButtonText; + createButton.disabled = false; + return; // Stop form submission + } + } + // Handle other numeric fields if needed + else if (key === 'periodic_runs' && value) { + // Try to parse as number if it looks like one + const numValue = parseInt(value, 10); + if (!isNaN(numValue) && String(numValue) === value) { + jsonData[key] = numValue; + } else { + jsonData[key] = value; + } + } + else { + jsonData[key] = value; + } + } + } + + // Process connectors - KEEP CONFIG AS STRING + const connectors = []; + const connectorsElements = document.getElementsByClassName('connector'); + for (let i = 0; i < connectorsElements.length; i++) { + const typeField = document.getElementById(`connectorType${i}`); + const configField = document.getElementById(`connectorConfig${i}`); + + if (typeField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + connectors.push({ + type: typeField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing connector ${i} config:`, err); + showToast(`Error in connector ${i+1} configuration: Invalid JSON`, 'error'); + + createButton.innerHTML = originalButtonText; + createButton.disabled = false; + return; // Stop form submission + } + } + } + jsonData.connectors = connectors; + + // Process actions - KEEP CONFIG AS STRING + const actions = []; + const actionElements = document.getElementsByClassName('action'); + for (let i = 0; i < actionElements.length; i++) { + const nameField = document.getElementById(`actionsName${i}`); + const configField = document.getElementById(`actionsConfig${i}`); + + if (nameField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + actions.push({ + name: nameField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing action ${i} config:`, err); + showToast(`Error in action ${i+1} configuration: Invalid JSON`, 'error'); + + createButton.innerHTML = originalButtonText; + createButton.disabled = false; + return; // Stop form submission + } + } + } + jsonData.actions = actions; + + // Process prompt blocks - KEEP CONFIG AS STRING + const promptBlocks = []; + const promptBlockElements = document.getElementsByClassName('promptBlock'); + for (let i = 0; i < promptBlockElements.length; i++) { + const nameField = document.getElementById(`promptName${i}`); + const configField = document.getElementById(`promptConfig${i}`); + + if (nameField && configField) { + try { + // Validate JSON but send as string + const configValue = configField.value.trim() || '{}'; + // Parse to validate but don't use the parsed object + JSON.parse(configValue); + + promptBlocks.push({ + name: nameField.value, + config: configValue // Send the raw string, not parsed JSON + }); + } catch (err) { + console.error(`Error parsing prompt block ${i} config:`, err); + showToast(`Error in prompt block ${i+1} configuration: Invalid JSON`, 'error'); + + createButton.innerHTML = originalButtonText; + createButton.disabled = false; + return; // Stop form submission + } + } + } + jsonData.promptblocks = promptBlocks; + + console.log('Sending data:', jsonData); + + // Send the structured data as JSON fetch('/create', { method: 'POST', - body: formData + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(jsonData) }) .then(response => response.json()) .then(data => { diff --git a/webui/views/settings.html b/webui/views/settings.html index 96de604..d89b122 100644 --- a/webui/views/settings.html +++ b/webui/views/settings.html @@ -7,11 +7,11 @@ {{template "views/partials/menu"}} - +
- +

Agent settings - {{.Name}}

@@ -34,6 +34,173 @@
+ +
+

Edit Agent Configuration

+
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+

Export Data

Export your agent configuration for backup or transfer.

@@ -66,7 +233,15 @@
+ + \ No newline at end of file