From 9a90153dc6abb4216a12ef16d082b94c7930a4f1 Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Sat, 24 May 2025 22:15:33 +0200 Subject: [PATCH] feat(reminders): add reminder system to perform long-term goals in the background (#176) * feat(reminders): add self-ability to set reminders Signed-off-by: Ettore Di Giacinto * feat(reminders): surface reminders result to the user as new conversations Signed-off-by: Ettore Di Giacinto * Fixups * Subscribe all connectors to agents new messages Signed-off-by: Ettore Di Giacinto * Set reminders in the list * fix(telegram): do not always auth Signed-off-by: Ettore Di Giacinto * Small fixups * Improve UX Signed-off-by: Ettore Di Giacinto --------- Signed-off-by: Ettore Di Giacinto --- core/action/reminder.go | 193 ++++++++++++++++++++++++++++++++ core/agent/actions.go | 8 ++ core/agent/agent.go | 119 +++++++++++++------- core/types/state.go | 10 ++ go.mod | 5 +- go.sum | 2 + services/actions.go | 27 +++++ services/connectors/discord.go | 21 ++++ services/connectors/email.go | 33 ++++++ services/connectors/irc.go | 46 ++++++++ services/connectors/matrix.go | 18 +++ services/connectors/telegram.go | 4 +- 12 files changed, 440 insertions(+), 46 deletions(-) create mode 100644 core/action/reminder.go diff --git a/core/action/reminder.go b/core/action/reminder.go new file mode 100644 index 0000000..66ea8bd --- /dev/null +++ b/core/action/reminder.go @@ -0,0 +1,193 @@ +package action + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/mudler/LocalAGI/core/types" + "github.com/robfig/cron/v3" + "github.com/sashabaranov/go-openai/jsonschema" +) + +const ( + ReminderActionName = "set_reminder" + ListRemindersName = "list_reminders" + RemoveReminderName = "remove_reminder" +) + +func NewReminder() *ReminderAction { + return &ReminderAction{} +} + +func NewListReminders() *ListRemindersAction { + return &ListRemindersAction{} +} + +func NewRemoveReminder() *RemoveReminderAction { + return &RemoveReminderAction{} +} + +type ReminderAction struct{} +type ListRemindersAction struct{} +type RemoveReminderAction struct{} + +type RemoveReminderParams struct { + Index int `json:"index"` +} + +func (a *ReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { + result := types.ReminderActionResponse{} + err := params.Unmarshal(&result) + if err != nil { + return types.ActionResult{}, err + } + + // Validate the cron expression + parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + _, err = parser.Parse(result.CronExpr) + if err != nil { + return types.ActionResult{}, err + } + + // Calculate next run time + now := time.Now() + schedule, _ := parser.Parse(result.CronExpr) // We can ignore the error since we validated above + nextRun := schedule.Next(now) + + // Set the reminder details + result.LastRun = now + result.NextRun = nextRun + // IsRecurring is set by the user through the action parameters + + // Store the reminder in the shared state + if sharedState.Reminders == nil { + sharedState.Reminders = make([]types.ReminderActionResponse, 0) + } + sharedState.Reminders = append(sharedState.Reminders, result) + + return types.ActionResult{ + Result: "Reminder set successfully", + Metadata: map[string]interface{}{ + "reminder": result, + }, + }, nil +} + +func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { + if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 { + return types.ActionResult{ + Result: "No reminders set", + }, nil + } + + var result strings.Builder + result.WriteString("Current reminders:\n") + for i, reminder := range sharedState.Reminders { + status := "one-time" + if reminder.IsRecurring { + status = "recurring" + } + result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n", + i+1, + reminder.Message, + reminder.NextRun.Format(time.RFC3339), + status)) + } + + return types.ActionResult{ + Result: result.String(), + Metadata: map[string]interface{}{ + "reminders": sharedState.Reminders, + }, + }, nil +} + +func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) { + var removeParams RemoveReminderParams + err := params.Unmarshal(&removeParams) + if err != nil { + return types.ActionResult{}, err + } + + if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 { + return types.ActionResult{ + Result: "No reminders to remove", + }, nil + } + + // Convert from 1-based index to 0-based + index := removeParams.Index - 1 + if index < 0 || index >= len(sharedState.Reminders) { + return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index) + } + + // Remove the reminder + removed := sharedState.Reminders[index] + sharedState.Reminders = append(sharedState.Reminders[:index], sharedState.Reminders[index+1:]...) + + return types.ActionResult{ + Result: fmt.Sprintf("Removed reminder: %s", removed.Message), + Metadata: map[string]interface{}{ + "removed_reminder": removed, + }, + }, nil +} + +func (a *ReminderAction) Plannable() bool { + return true +} + +func (a *ListRemindersAction) Plannable() bool { + return true +} + +func (a *RemoveReminderAction) Plannable() bool { + return true +} + +func (a *ReminderAction) Definition() types.ActionDefinition { + return types.ActionDefinition{ + Name: ReminderActionName, + Description: "Set a reminder for the agent to wake up and perform a task based on a cron schedule. Examples: '0 0 * * *' (daily at midnight), '0 */2 * * *' (every 2 hours), '0 0 * * 1' (every Monday at midnight)", + Properties: map[string]jsonschema.Definition{ + "message": { + Type: jsonschema.String, + Description: "The message or task to be reminded about", + }, + "cron_expr": { + Type: jsonschema.String, + Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'second minute hour day month weekday'", + }, + "is_recurring": { + Type: jsonschema.Boolean, + Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)", + }, + }, + Required: []string{"message", "cron_expr", "is_recurring"}, + } +} + +func (a *ListRemindersAction) Definition() types.ActionDefinition { + return types.ActionDefinition{ + Name: ListRemindersName, + Description: "List all currently set reminders with their next scheduled run times", + Properties: map[string]jsonschema.Definition{}, + Required: []string{}, + } +} + +func (a *RemoveReminderAction) Definition() types.ActionDefinition { + return types.ActionDefinition{ + Name: RemoveReminderName, + Description: "Remove a reminder by its index number (use list_reminders to see the index)", + Properties: map[string]jsonschema.Definition{ + "index": { + Type: jsonschema.Integer, + Description: "The index number of the reminder to remove (1-based)", + }, + }, + Required: []string{"index"}, + } +} diff --git a/core/agent/actions.go b/core/agent/actions.go index 7b248e7..077bb35 100644 --- a/core/agent/actions.go +++ b/core/agent/actions.go @@ -223,6 +223,14 @@ func (m Messages) IsLastMessageFromRole(role string) bool { } func (a *Agent) generateParameters(job *types.Job, pickTemplate string, act types.Action, c []openai.ChatCompletionMessage, reasoning string, maxAttempts int) (*decisionResult, error) { + + if len(act.Definition().Properties) > 0 { + xlog.Debug("Action has properties", "action", act.Definition().Name, "properties", act.Definition().Properties) + } else { + xlog.Debug("Action has no properties", "action", act.Definition().Name) + return &decisionResult{actionParams: types.ActionParams{}}, nil + } + stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning) if err != nil { return nil, err diff --git a/core/agent/agent.go b/core/agent/agent.go index 62ed1c8..8f9520e 100644 --- a/core/agent/agent.go +++ b/core/agent/agent.go @@ -15,6 +15,7 @@ import ( "github.com/mudler/LocalAGI/core/action" "github.com/mudler/LocalAGI/core/types" "github.com/mudler/LocalAGI/pkg/llm" + "github.com/robfig/cron/v3" "github.com/sashabaranov/go-openai" ) @@ -1026,25 +1027,83 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { xlog.Debug("Agent is running periodically", "agent", a.Character.Name) - // TODO: Would be nice if we have a special action to - // contact the user. This would actually make sure that - // if the agent wants to initiate a conversation, it can do so. - // This would be a special action that would be picked up by the agent - // and would be used to contact the user. + // Check for reminders that need to be triggered + now := time.Now() + var triggeredReminders []types.ReminderActionResponse + var remainingReminders []types.ReminderActionResponse - // if len(conv()) != 0 { - // // Here the LLM could decide to store some part of the conversation too in the memory - // evaluateMemory := NewJob( - // WithText( - // `Evaluate the current conversation and decide if we need to store some relevant informations from it`, - // ), - // WithReasoningCallback(a.options.reasoningCallback), - // WithResultCallback(a.options.resultCallback), - // ) - // a.consumeJob(evaluateMemory, SystemRole) + for _, reminder := range a.sharedState.Reminders { + xlog.Debug("Checking reminder", "reminder", reminder) + if now.After(reminder.NextRun) { + triggeredReminders = append(triggeredReminders, reminder) + xlog.Debug("Reminder triggered", "reminder", reminder) + // Calculate next run time for recurring reminders + if reminder.IsRecurring { + xlog.Debug("Reminder is recurring", "reminder", reminder) + parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow) + schedule, err := parser.Parse(reminder.CronExpr) + if err == nil { + nextRun := schedule.Next(now) + xlog.Debug("Next run time", "reminder", reminder, "nextRun", nextRun) + reminder.LastRun = now + reminder.NextRun = nextRun + remainingReminders = append(remainingReminders, reminder) + } + } + } else { + xlog.Debug("Reminder not triggered", "reminder", reminder) + remainingReminders = append(remainingReminders, reminder) + } + } - // a.ResetConversation() - // } + // Update the reminders list + a.sharedState.Reminders = remainingReminders + + // Handle triggered reminders + for _, reminder := range triggeredReminders { + xlog.Info("Processing triggered reminder", "agent", a.Character.Name, "message", reminder.Message) + + // Create a more natural conversation flow for the reminder + reminderJob := types.NewJob( + types.WithText(fmt.Sprintf("I have a reminder for you: %s", reminder.Message)), + types.WithReasoningCallback(a.options.reasoningCallback), + types.WithResultCallback(a.options.resultCallback), + ) + + // Add the reminder message to the job's metadata + reminderJob.Metadata = map[string]interface{}{ + "message": reminder.Message, + "is_reminder": true, + } + + // Process the reminder as a normal conversation + a.consumeJob(reminderJob, UserRole, a.options.loopDetectionSteps) + + // After the reminder job is complete, ensure the user is notified + if reminderJob.Result != nil && reminderJob.Result.Conversation != nil { + // Get the last assistant message from the conversation + var lastAssistantMsg *openai.ChatCompletionMessage + for i := len(reminderJob.Result.Conversation) - 1; i >= 0; i-- { + if reminderJob.Result.Conversation[i].Role == AssistantRole { + lastAssistantMsg = &reminderJob.Result.Conversation[i] + break + } + } + + if lastAssistantMsg != nil && lastAssistantMsg.Content != "" { + // Send the reminder response to the user + msg := openai.ChatCompletionMessage{ + Role: "assistant", + Content: fmt.Sprintf("Reminder Update: %s\n\n%s", reminder.Message, lastAssistantMsg.Content), + } + + go func(agent *Agent) { + xlog.Info("Sending reminder response to user", "agent", agent.Character.Name, "message", msg.Content) + agent.newConversations <- msg + }(a) + } + } + } if !a.options.standaloneJob { return @@ -1056,7 +1115,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { // - evaluating the result // - asking the agent to do something else based on the result - // whatNext := NewJob(WithText("Decide what to do based on the state")) whatNext := types.NewJob( types.WithText(innerMonologueTemplate), types.WithReasoningCallback(a.options.reasoningCallback), @@ -1065,31 +1123,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) { a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps) xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name) - - // Save results from state - - // a.ResetConversation() - - // doWork := NewJob(WithText("Select the tool to use based on your goal and the current state.")) - // a.consumeJob(doWork, SystemRole) - - // results := []string{} - // for _, v := range doWork.Result.State { - // results = append(results, v.Result) - // } - - // a.ResetConversation() - - // // Here the LLM could decide to do something based on the result of our automatic action - // evaluateAction := NewJob( - // WithText( - // `Evaluate the current situation and decide if we need to execute other tools (for instance to store results into permanent, or short memory). - // We have done the following actions: - // ` + strings.Join(results, "\n"), - // )) - // a.consumeJob(evaluateAction, SystemRole) - - // a.ResetConversation() } func (a *Agent) Run() error { diff --git a/core/types/state.go b/core/types/state.go index b4d7e4d..22f0b0a 100644 --- a/core/types/state.go +++ b/core/types/state.go @@ -29,8 +29,17 @@ const ( DefaultLastMessageDuration = 5 * time.Minute ) +type ReminderActionResponse struct { + Message string `json:"message"` + CronExpr string `json:"cron_expr"` // Cron expression for scheduling + LastRun time.Time `json:"last_run"` // Last time this reminder was triggered + NextRun time.Time `json:"next_run"` // Next scheduled run time + IsRecurring bool `json:"is_recurring"` // Whether this is a recurring reminder +} + type AgentSharedState struct { ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"` + Reminders []ReminderActionResponse `json:"reminders"` } func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState { @@ -39,6 +48,7 @@ func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState { } return &AgentSharedState{ ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration), + Reminders: make([]ReminderActionResponse, 0), } } diff --git a/go.mod b/go.mod index cffe2b1..43cc68d 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,10 @@ require ( mvdan.cc/xurls/v2 v2.6.0 ) -require github.com/JohannesKaufmann/dom v0.2.0 // indirect +require ( + github.com/JohannesKaufmann/dom v0.2.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect +) require ( github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2 diff --git a/go.sum b/go.sum index dbf6ef6..0ee1bee 100644 --- a/go.sum +++ b/go.sum @@ -178,6 +178,8 @@ github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM= github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= diff --git a/services/actions.go b/services/actions.go index 8df64f5..53e522a 100644 --- a/services/actions.go +++ b/services/actions.go @@ -47,6 +47,9 @@ const ( ActionCallAgents = "call_agents" ActionShellcommand = "shell-command" ActionSendTelegramMessage = "send-telegram-message" + ActionSetReminder = "set_reminder" + ActionListReminders = "list_reminders" + ActionRemoveReminder = "remove_reminder" ) var AvailableActions = []string{ @@ -81,6 +84,9 @@ var AvailableActions = []string{ ActionCallAgents, ActionShellcommand, ActionSendTelegramMessage, + ActionSetReminder, + ActionListReminders, + ActionRemoveReminder, } const ( @@ -187,6 +193,12 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL]) case ActionSendTelegramMessage: a = actions.NewSendTelegramMessageRunner(config) + case ActionSetReminder: + a = action.NewReminder() + case ActionListReminders: + a = action.NewListReminders() + case ActionRemoveReminder: + a = action.NewRemoveReminder() default: xlog.Error("Action not found", "name", name) return nil, fmt.Errorf("Action not found") @@ -356,5 +368,20 @@ func ActionsConfigMeta() []config.FieldGroup { Label: "Send Telegram Message", Fields: actions.SendTelegramMessageConfigMeta(), }, + { + Name: "set_reminder", + Label: "Set Reminder", + Fields: []config.Field{}, + }, + { + Name: "list_reminders", + Label: "List Reminders", + Fields: []config.Field{}, + }, + { + Name: "remove_reminder", + Label: "Remove Reminder", + Fields: []config.Field{}, + }, } } diff --git a/services/connectors/discord.go b/services/connectors/discord.go index fd11945..0589f99 100644 --- a/services/connectors/discord.go +++ b/services/connectors/discord.go @@ -83,6 +83,27 @@ func (d *Discord) Start(a *agent.Agent) { dg.StateEnabled = true + if d.defaultChannel != "" { + // handle new conversations + a.AddSubscriber(func(ccm openai.ChatCompletionMessage) { + xlog.Debug("Subscriber(discord)", "message", ccm.Content) + + // Send the message to the default channel + _, err := dg.ChannelMessageSend(d.defaultChannel, ccm.Content) + if err != nil { + xlog.Error(fmt.Sprintf("Error sending message: %v", err)) + } + + a.SharedState().ConversationTracker.AddMessage( + fmt.Sprintf("discord:%s", d.defaultChannel), + openai.ChatCompletionMessage{ + Content: ccm.Content, + Role: "assistant", + }, + ) + }) + } + // Register the messageCreate func as a callback for MessageCreate events. dg.AddHandler(d.messageCreate(a)) diff --git a/services/connectors/email.go b/services/connectors/email.go index 711b530..89126b0 100644 --- a/services/connectors/email.go +++ b/services/connectors/email.go @@ -35,6 +35,7 @@ type Email struct { smtpInsecure bool imapServer string imapInsecure bool + defaultEmail string } func NewEmail(config map[string]string) *Email { @@ -48,6 +49,7 @@ func NewEmail(config map[string]string) *Email { smtpInsecure: config["smtpInsecure"] == "true", imapServer: config["imapServer"], imapInsecure: config["imapInsecure"] == "true", + defaultEmail: config["defaultEmail"], } } @@ -105,6 +107,12 @@ func EmailConfigMeta() []config.Field { Required: true, HelpText: "Agent email address", }, + { + Name: "defaultEmail", + Label: "Default Recipient", + Type: config.FieldTypeText, + HelpText: "Default email address to send messages to when the agent wants to initiate a conversation", + }, } } @@ -367,6 +375,31 @@ func imapWorker(done chan bool, e *Email, a *agent.Agent, c *imapclient.Client, func (e *Email) Start(a *agent.Agent) { go func() { + if e.defaultEmail != "" { + // handle new conversations + a.AddSubscriber(func(ccm openai.ChatCompletionMessage) { + xlog.Debug("Subscriber(email)", "message", ccm.Content) + + // Send the message to the default email + e.sendMail( + e.defaultEmail, + "Message from LocalAGI", + ccm.Content, + "", + "", + []string{e.defaultEmail}, + false, + ) + + a.SharedState().ConversationTracker.AddMessage( + fmt.Sprintf("email:%s", e.defaultEmail), + openai.ChatCompletionMessage{ + Content: ccm.Content, + Role: "assistant", + }, + ) + }) + } xlog.Info("Email connector is now running. Press CTRL-C to exit.") // IMAP dial diff --git a/services/connectors/irc.go b/services/connectors/irc.go index dd7a8fb..1d31fd2 100644 --- a/services/connectors/irc.go +++ b/services/connectors/irc.go @@ -70,6 +70,52 @@ func (i *IRC) Start(a *agent.Agent) { return } i.conn.UseTLS = false + + if i.channel != "" { + // handle new conversations + a.AddSubscriber(func(ccm openai.ChatCompletionMessage) { + xlog.Debug("Subscriber(irc)", "message", ccm.Content) + + // Split the response into multiple messages if it's too long + maxLength := 400 // Safe limit for most IRC servers + response := ccm.Content + + // Handle multiline responses + lines := strings.Split(response, "\n") + for _, line := range lines { + if line == "" { + continue + } + + // Split long lines + for len(line) > 0 { + var chunk string + if len(line) > maxLength { + chunk = line[:maxLength] + line = line[maxLength:] + } else { + chunk = line + line = "" + } + + // Send the message to the channel + i.conn.Privmsg(i.channel, chunk) + + // Small delay to prevent flooding + time.Sleep(500 * time.Millisecond) + } + } + + a.SharedState().ConversationTracker.AddMessage( + fmt.Sprintf("irc:%s", i.channel), + openai.ChatCompletionMessage{ + Content: ccm.Content, + Role: "assistant", + }, + ) + }) + } + i.conn.AddCallback("001", func(e *irc.Event) { xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments) i.conn.Join(i.channel) diff --git a/services/connectors/matrix.go b/services/connectors/matrix.go index e2bd5a7..8fadfec 100644 --- a/services/connectors/matrix.go +++ b/services/connectors/matrix.go @@ -223,6 +223,24 @@ func (m *Matrix) Start(a *agent.Agent) { xlog.Info("Matrix client created") m.client = client + if m.roomID != "" { + // handle new conversations + a.AddSubscriber(func(ccm openai.ChatCompletionMessage) { + xlog.Debug("Subscriber(matrix)", "message", ccm.Content) + _, err := m.client.SendText(context.Background(), id.RoomID(m.roomID), ccm.Content) + if err != nil { + xlog.Error(fmt.Sprintf("Error posting message: %v", err)) + } + a.SharedState().ConversationTracker.AddMessage( + fmt.Sprintf("matrix:%s", m.roomID), + openai.ChatCompletionMessage{ + Content: ccm.Content, + Role: "assistant", + }, + ) + }) + } + syncer := client.Syncer.(*mautrix.DefaultSyncer) syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) { xlog.Info("Received message", evt.Content.AsMessage().Body) diff --git a/services/connectors/telegram.go b/services/connectors/telegram.go index 5e4ca22..0884c68 100644 --- a/services/connectors/telegram.go +++ b/services/connectors/telegram.go @@ -225,7 +225,7 @@ func (t *Telegram) handleUpdate(ctx context.Context, b *bot.Bot, a *agent.Agent, }) } if len(t.admins) > 0 && !slices.Contains(t.admins, username) { - xlog.Info("Unauthorized user", "username", username) + xlog.Info("Unauthorized user", "username", username, "admins", t.admins) _, err := b.SendMessage(ctx, &bot.SendMessageParams{ ChatID: update.Message.Chat.ID, Text: "you are not authorized to use this bot!", @@ -444,7 +444,7 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) { admins := []string{} - if _, ok := config["admins"]; ok { + if _, ok := config["admins"]; ok && strings.Contains(config["admins"], ",") { admins = append(admins, strings.Split(config["admins"], ",")...) }