diff --git a/.gitignore b/.gitignore index 1574d82..659f466 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,5 @@ models/ data/ pool uploads/ -volumes/ local-agent-framework -localagent -LocalAgent +localagent \ No newline at end of file diff --git a/core/state/config.go b/core/state/config.go index 64aebcf..e1692c3 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -4,7 +4,6 @@ import ( "encoding/json" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" ) type ConnectorConfig struct { @@ -63,6 +62,5 @@ type AgentConfig struct { type Connector interface { AgentResultCallback() func(state agent.ActionState) AgentReasoningCallback() func(state agent.ActionCurrentState) bool - ConfigForm() metaform.Form Start(a *agent.Agent) } diff --git a/pkg/metaform/metaform.go b/pkg/metaform/metaform.go deleted file mode 100644 index 1f9a66e..0000000 --- a/pkg/metaform/metaform.go +++ /dev/null @@ -1,28 +0,0 @@ -package metaform - -// Option represents a selectable option for FieldOption type -type Option struct { - Value string `json:"value"` - Label string `json:"label"` -} - -type FieldKind string - -const ( - FieldString FieldKind = "string" - FieldNumber FieldKind = "number" - FieldOptions FieldKind = "options" -) - -type Field struct { - Kind FieldKind `json:"kind"` - Name string `json:"name"` - Label string `json:"label"` - Required bool `json:"required"` - Placeholder string `json:"placeholder,omitempty"` - Options []Option `json:"options,omitempty"` -} - -type Form struct { - Fields []Field -} diff --git a/services/connectors/discord.go b/services/connectors/discord.go index c833c25..8365058 100644 --- a/services/connectors/discord.go +++ b/services/connectors/discord.go @@ -5,7 +5,6 @@ import ( "github.com/bwmarrin/discordgo" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" ) @@ -38,27 +37,6 @@ func (d *Discord) AgentReasoningCallback() func(state agent.ActionCurrentState) } } -func (d *Discord) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "token", - Label: "Bot Token", - Required: true, - Placeholder: "Your Discord bot token", - }, - { - Kind: metaform.FieldString, - Name: "defaultChannel", - Label: "Default Channel", - Required: true, - Placeholder: "The default channel for the bot to join", - }, - }, - } -} - func (d *Discord) Start(a *agent.Agent) { Token := d.token diff --git a/services/connectors/githubissue.go b/services/connectors/githubissue.go index 750cbce..bd6506e 100644 --- a/services/connectors/githubissue.go +++ b/services/connectors/githubissue.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-github/v69/github" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/sashabaranov/go-openai" @@ -64,51 +63,6 @@ func (g *GithubIssues) AgentReasoningCallback() func(state agent.ActionCurrentSt } } -func (g *GithubIssues) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "token", - Label: "GitHub Token", - Required: true, - Placeholder: "Your GitHub personal access token", - }, - { - Kind: metaform.FieldString, - Name: "owner", - Label: "Repository Owner", - Required: true, - Placeholder: "username or organization", - }, - { - Kind: metaform.FieldString, - Name: "repository", - Label: "Repository Name", - Required: true, - Placeholder: "repo-name", - }, - { - Kind: metaform.FieldOptions, - Name: "replyIfNoReplies", - Label: "Reply Behavior", - Required: true, - Options: []metaform.Option{ - {Value: "true", Label: "Reply only to issues with no comments"}, - {Value: "false", Label: "Reply to all issues"}, - }, - }, - { - Kind: metaform.FieldString, - Name: "pollInterval", - Label: "Poll Interval", - Required: false, - Placeholder: "10m", - }, - }, - } -} - func (g *GithubIssues) Start(a *agent.Agent) { // Start the connector g.agent = a diff --git a/services/connectors/githubpr.go b/services/connectors/githubpr.go index 97a4c03..3114e01 100644 --- a/services/connectors/githubpr.go +++ b/services/connectors/githubpr.go @@ -7,7 +7,6 @@ import ( "github.com/google/go-github/v69/github" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/sashabaranov/go-openai" @@ -64,51 +63,6 @@ func (g *GithubPRs) AgentReasoningCallback() func(state agent.ActionCurrentState } } -func (g *GithubPRs) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "token", - Label: "GitHub Token", - Required: true, - Placeholder: "Your GitHub personal access token", - }, - { - Kind: metaform.FieldString, - Name: "owner", - Label: "Repository Owner", - Required: true, - Placeholder: "username or organization", - }, - { - Kind: metaform.FieldString, - Name: "repository", - Label: "Repository Name", - Required: true, - Placeholder: "repo-name", - }, - { - Kind: metaform.FieldOptions, - Name: "replyIfNoReplies", - Label: "Reply Behavior", - Required: true, - Options: []metaform.Option{ - {Value: "true", Label: "Reply only to PRs with no comments"}, - {Value: "false", Label: "Reply to all PRs"}, - }, - }, - { - Kind: metaform.FieldString, - Name: "pollInterval", - Label: "Poll Interval", - Required: true, - Placeholder: "10m", - }, - }, - } -} - func (g *GithubPRs) Start(a *agent.Agent) { // Start the connector g.agent = a diff --git a/services/connectors/irc.go b/services/connectors/irc.go index f9dd225..b3c1a06 100644 --- a/services/connectors/irc.go +++ b/services/connectors/irc.go @@ -6,7 +6,6 @@ import ( "time" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/actions" irc "github.com/thoj/go-ircevent" @@ -44,51 +43,6 @@ func (i *IRC) AgentReasoningCallback() func(state agent.ActionCurrentState) bool } } -func (i *IRC) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "server", - Label: "Server", - Required: true, - Placeholder: "chat.freenode.net", - }, - { - Kind: metaform.FieldString, - Name: "port", - Label: "Port", - Required: true, - Placeholder: "6667", - }, - { - Kind: metaform.FieldString, - Name: "nickname", - Label: "Nickname", - Required: true, - Placeholder: "LocalAgentBot", - }, - { - Kind: metaform.FieldString, - Name: "channel", - Label: "Channel", - Required: true, - Placeholder: "#general", - }, - { - Kind: metaform.FieldOptions, - Name: "alwaysReply", - Label: "Always Reply", - Required: true, - Options: []metaform.Option{ - {Value: "false", Label: "Only when mentioned"}, - {Value: "true", Label: "Reply to all messages"}, - }, - }, - }, - } -} - // cleanUpUsernameFromMessage removes the bot's nickname from the message func cleanUpMessage(message string, nickname string) string { cleaned := strings.ReplaceAll(message, nickname+":", "") diff --git a/services/connectors/slack.go b/services/connectors/slack.go index d049b5d..30af50e 100644 --- a/services/connectors/slack.go +++ b/services/connectors/slack.go @@ -9,7 +9,6 @@ import ( "os" "strings" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/actions" "github.com/sashabaranov/go-openai" @@ -53,44 +52,6 @@ func (t *Slack) AgentReasoningCallback() func(state agent.ActionCurrentState) bo } } -func (t *Slack) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "appToken", - Label: "App Token", - Required: true, - Placeholder: "xapp-...", - }, - { - Kind: metaform.FieldString, - Name: "botToken", - Label: "Bot Token", - Required: true, - Placeholder: "xoxb-...", - }, - { - Kind: metaform.FieldString, - Name: "channelID", - Label: "Channel ID", - Required: false, - Placeholder: "C12345678", - }, - { - Kind: metaform.FieldOptions, - Name: "alwaysReply", - Label: "Always Reply", - Required: false, - Options: []metaform.Option{ - {Value: "false", Label: "Only when mentioned"}, - {Value: "true", Label: "Reply to all messages"}, - }, - }, - }, - } -} - func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) string { cleaned := strings.ReplaceAll(message, "<@"+b.UserID+">", "") cleaned = strings.ReplaceAll(cleaned, "<@"+b.BotID+">", "") diff --git a/services/connectors/telegram.go b/services/connectors/telegram.go index 03d484f..37de7dd 100644 --- a/services/connectors/telegram.go +++ b/services/connectors/telegram.go @@ -9,7 +9,6 @@ import ( "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" ) type Telegram struct { @@ -38,20 +37,6 @@ func (t *Telegram) AgentReasoningCallback() func(state agent.ActionCurrentState) } } -func (t *Telegram) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "token", - Label: "Bot Token", - Required: true, - Placeholder: "Your Telegram bot token from @BotFather", - }, - }, - } -} - func (t *Telegram) Start(a *agent.Agent) { ctx, cancel := signal.NotifyContext(a.Context(), os.Interrupt) defer cancel() diff --git a/services/connectors/twitter.go b/services/connectors/twitter.go index 422aeaf..32b9850 100644 --- a/services/connectors/twitter.go +++ b/services/connectors/twitter.go @@ -7,7 +7,6 @@ import ( "os/signal" "github.com/mudler/LocalAgent/core/agent" - "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/connectors/twitter" "github.com/sashabaranov/go-openai" @@ -33,37 +32,6 @@ func (t *Twitter) AgentReasoningCallback() func(state agent.ActionCurrentState) } } -func (t *Twitter) ConfigForm() metaform.Form { - return metaform.Form{ - Fields: []metaform.Field{ - { - Kind: metaform.FieldString, - Name: "token", - Label: "Token", - Required: true, - Placeholder: "elmo-...", - }, - { - Kind: metaform.FieldString, - Name: "botUsername", - Label: "Bot Username", - Required: true, - Placeholder: "Chocolate Pineapple", - }, - { - Kind: metaform.FieldOptions, - Name: "noCharacterLimit", - Label: "No Character Limit", - Required: false, - Options: []metaform.Option{ - {Value: "false", Label: ""}, - {Value: "true", Label: ""}, - }, - }, - }, - } -} - func NewTwitterConnector(config map[string]string) (*Twitter, error) { return &Twitter{ token: config["token"], diff --git a/webui/app.go b/webui/app.go index 1feda91..8f7c47a 100644 --- a/webui/app.go +++ b/webui/app.go @@ -10,13 +10,10 @@ import ( "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/webui/types" - "github.com/mudler/LocalAgent/services" "github.com/mudler/LocalAgent/core/agent" "github.com/mudler/LocalAgent/core/sse" "github.com/mudler/LocalAgent/core/state" - "github.com/mudler/LocalAgent/pkg/metaform" - "github.com/mudler/LocalAgent/services/connectors" "github.com/donseba/go-htmx" fiber "github.com/gofiber/fiber/v2" @@ -353,41 +350,3 @@ func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error { return c.JSON(response) } } - -func (app *App) GetConnectorForm(pool *state.AgentPool) func(c *fiber.Ctx) error { - return func(c *fiber.Ctx) error { - connectorType := c.Params("type") - - // Create a temporary connector to get its form metadata - var form metaform.Form - - switch connectorType { - case services.ConnectorTelegram: - form = (&connectors.Telegram{}).ConfigForm() - case services.ConnectorSlack: - form = (&connectors.Slack{}).ConfigForm() - case services.ConnectorDiscord: - form = (&connectors.Discord{}).ConfigForm() - case services.ConnectorGithubIssues: - form = (&connectors.GithubIssues{}).ConfigForm() - case services.ConnectorGithubPRs: - form = (&connectors.GithubPRs{}).ConfigForm() - case services.ConnectorIRC: - form = (&connectors.IRC{}).ConfigForm() - case services.ConnectorTwitter: - form = (&connectors.Twitter{}).ConfigForm() - default: - return c.Status(404).JSON(fiber.Map{ - "error": "Connector type not found", - }) - } - - // Create a data structure to pass both the form and the connector type - data := fiber.Map{ - "Form": form, - "Type": connectorType, - } - - return c.Render("views/partials/metaform", data) - } -} diff --git a/webui/public/js/agent-form.js b/webui/public/js/agent-form.js index e7a3bb1..8f1c9b8 100644 --- a/webui/public/js/agent-form.js +++ b/webui/public/js/agent-form.js @@ -1,39 +1,15 @@ // Common utility functions for agent forms const AgentFormUtils = { // Add dynamic component based on template - addDynamicComponent: function(sectionId, templateFunction, options = {}) { + addDynamicComponent: function(sectionId, templateFunction, dataItems) { const section = document.getElementById(sectionId); - if (!section) return; + const newIndex = section.getElementsByClassName(dataItems.className).length; - const index = section.getElementsByClassName(options.className || 'dynamic-component').length; - const templateData = { index, ...options }; + // Generate HTML from template function + const newHtml = templateFunction(newIndex, dataItems); - // Create a new element from the template - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = templateFunction(index, templateData); - const newElement = tempDiv.firstElementChild; - - // Add the new element to the section - section.appendChild(newElement); - - // If it's a connector, add event listener for type change - if (options.className === 'connector') { - const newIndex = index; - const connectorType = document.getElementById(`connectorType${newIndex}`); - if (connectorType) { - // Add event listener for future changes - connectorType.addEventListener('change', function() { - loadConnectorForm(newIndex, this.value, null); - }); - - // If a connector type is already selected (default value), load its form immediately - if (connectorType.value) { - loadConnectorForm(newIndex, connectorType.value, null); - } - } - } - - return newElement; + // Add to DOM + section.insertAdjacentHTML('beforeend', newHtml); }, // Process form data into JSON structure @@ -84,57 +60,32 @@ const AgentFormUtils = { const connectorsElements = document.getElementsByClassName('connector'); for (let i = 0; i < connectorsElements.length; i++) { - const typeSelect = document.getElementById(`connectorType${i}`); + const typeField = document.getElementById(`connectorType${i}`); + const configField = document.getElementById(`connectorConfig${i}`); - if (typeSelect) { - const connectorType = typeSelect.value; - const configContainer = document.getElementById(`connectorConfigContainer${i}`); - - // Only process if we have a metaform - if (configContainer && configContainer.querySelector('.metaform')) { - try { - // Get all form fields - const fields = configContainer.querySelectorAll('.connector-field'); - let configObj = {}; - - // Process each field based on its type - fields.forEach(field => { - const fieldName = field.dataset.fieldName; - const fieldType = field.dataset.fieldType; - - // Convert value based on field type - let value = field.value; - if (fieldType === 'number' && value !== '') { - value = parseFloat(value); - } - - configObj[fieldName] = value; - }); - - // Add the connector to the list - connectors.push({ - type: connectorType, - config: JSON.stringify(configObj) - }); - } catch (err) { - console.error(`Error processing connector ${i} form:`, err); - showToast(`Error in connector ${i+1} configuration`, 'error'); - - // If button is provided, restore its state - if (button) { - const originalButtonText = button.getAttribute('data-original-text'); - button.innerHTML = originalButtonText; - button.disabled = false; - } - - return null; // Indicate validation error - } - } else { - // If no form is loaded, create an empty config + 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: connectorType, - config: '{}' + 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'); + + // If button is provided, restore its state + if (button) { + const originalButtonText = button.getAttribute('data-original-text'); + button.innerHTML = originalButtonText; + button.disabled = false; + } + + return null; // Indicate validation error } } } @@ -248,16 +199,14 @@ const AgentFormUtils = { const parsed = JSON.parse(configValue); configElement.value = JSON.stringify(parsed, null, 2); } catch (e) { - // If parsing fails, use the raw string - configElement.value = configValue; + console.warn("Failed to parse config JSON string:", e); + configElement.value = configValue; // Keep as is if parsing fails } - } - // If it's already an object, stringify it - else if (typeof configValue === 'object' && configValue !== null) { + } else if (configValue !== undefined && configValue !== null) { + // Direct object, just stringify with formatting configElement.value = JSON.stringify(configValue, null, 2); - } - // Default to empty object - else { + } else { + // Default to empty object configElement.value = '{}'; } }, @@ -265,90 +214,21 @@ const AgentFormUtils = { // Helper function to set select value (with fallback if option doesn't exist) setSelectValue: function(selectElement, value) { // Check if the option exists - let optionExists = false; - for (let i = 0; i < selectElement.options.length; i++) { - if (selectElement.options[i].value === value) { - optionExists = true; - break; - } - } + const optionExists = Array.from(selectElement.options).some(option => option.value === value); - // Set the value if the option exists if (optionExists) { selectElement.value = value; - } else if (selectElement.options.length > 0) { - // Otherwise select the first option - selectElement.selectedIndex = 0; + } else if (value) { + // If value is provided but option doesn't exist, create a new option + const newOption = document.createElement('option'); + newOption.value = value; + newOption.text = value + ' (custom)'; + selectElement.add(newOption); + selectElement.value = value; } } }; -// Function to load connector form based on type -function loadConnectorForm(index, connectorType, configData) { - if (!connectorType) return; - - const configContainer = document.getElementById(`connectorConfigContainer${index}`); - if (!configContainer) return; - - // Show loading indicator - configContainer.innerHTML = '
Select a connector type to load its configuration form.
-