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 <mudler@localai.io> * feat(reminders): surface reminders result to the user as new conversations Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Fixups * Subscribe all connectors to agents new messages Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Set reminders in the list * fix(telegram): do not always auth Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Small fixups * Improve UX Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
committed by
GitHub
parent
490bf998a4
commit
9a90153dc6
193
core/action/reminder.go
Normal file
193
core/action/reminder.go
Normal file
@@ -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"},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
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)
|
stateHUD, err := renderTemplate(pickTemplate, a.prepareHUD(), a.availableActions(), reasoning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/mudler/LocalAGI/core/action"
|
"github.com/mudler/LocalAGI/core/action"
|
||||||
"github.com/mudler/LocalAGI/core/types"
|
"github.com/mudler/LocalAGI/core/types"
|
||||||
"github.com/mudler/LocalAGI/pkg/llm"
|
"github.com/mudler/LocalAGI/pkg/llm"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/sashabaranov/go-openai"
|
"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)
|
xlog.Debug("Agent is running periodically", "agent", a.Character.Name)
|
||||||
|
|
||||||
// TODO: Would be nice if we have a special action to
|
// Check for reminders that need to be triggered
|
||||||
// contact the user. This would actually make sure that
|
now := time.Now()
|
||||||
// if the agent wants to initiate a conversation, it can do so.
|
var triggeredReminders []types.ReminderActionResponse
|
||||||
// This would be a special action that would be picked up by the agent
|
var remainingReminders []types.ReminderActionResponse
|
||||||
// and would be used to contact the user.
|
|
||||||
|
|
||||||
// if len(conv()) != 0 {
|
for _, reminder := range a.sharedState.Reminders {
|
||||||
// // Here the LLM could decide to store some part of the conversation too in the memory
|
xlog.Debug("Checking reminder", "reminder", reminder)
|
||||||
// evaluateMemory := NewJob(
|
if now.After(reminder.NextRun) {
|
||||||
// WithText(
|
triggeredReminders = append(triggeredReminders, reminder)
|
||||||
// `Evaluate the current conversation and decide if we need to store some relevant informations from it`,
|
xlog.Debug("Reminder triggered", "reminder", reminder)
|
||||||
// ),
|
// Calculate next run time for recurring reminders
|
||||||
// WithReasoningCallback(a.options.reasoningCallback),
|
if reminder.IsRecurring {
|
||||||
// WithResultCallback(a.options.resultCallback),
|
xlog.Debug("Reminder is recurring", "reminder", reminder)
|
||||||
// )
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||||
// a.consumeJob(evaluateMemory, SystemRole)
|
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 {
|
if !a.options.standaloneJob {
|
||||||
return
|
return
|
||||||
@@ -1056,7 +1115,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
// - evaluating the result
|
// - evaluating the result
|
||||||
// - asking the agent to do something else based on 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(
|
whatNext := types.NewJob(
|
||||||
types.WithText(innerMonologueTemplate),
|
types.WithText(innerMonologueTemplate),
|
||||||
types.WithReasoningCallback(a.options.reasoningCallback),
|
types.WithReasoningCallback(a.options.reasoningCallback),
|
||||||
@@ -1065,31 +1123,6 @@ func (a *Agent) periodicallyRun(timer *time.Timer) {
|
|||||||
a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
|
a.consumeJob(whatNext, SystemRole, a.options.loopDetectionSteps)
|
||||||
|
|
||||||
xlog.Info("STOP -- Periodically run is done", "agent", a.Character.Name)
|
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 {
|
func (a *Agent) Run() error {
|
||||||
|
|||||||
@@ -29,8 +29,17 @@ const (
|
|||||||
DefaultLastMessageDuration = 5 * time.Minute
|
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 {
|
type AgentSharedState struct {
|
||||||
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
ConversationTracker *conversations.ConversationTracker[string] `json:"conversation_tracker"`
|
||||||
|
Reminders []ReminderActionResponse `json:"reminders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
||||||
@@ -39,6 +48,7 @@ func NewAgentSharedState(lastMessageDuration time.Duration) *AgentSharedState {
|
|||||||
}
|
}
|
||||||
return &AgentSharedState{
|
return &AgentSharedState{
|
||||||
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
ConversationTracker: conversations.NewConversationTracker[string](lastMessageDuration),
|
||||||
|
Reminders: make([]ReminderActionResponse, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -32,7 +32,10 @@ require (
|
|||||||
mvdan.cc/xurls/v2 v2.6.0
|
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 (
|
require (
|
||||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2
|
github.com/JohannesKaufmann/html-to-markdown/v2 v2.3.2
|
||||||
|
|||||||
2
go.sum
2
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
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 h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
|
||||||
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
|
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=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ const (
|
|||||||
ActionCallAgents = "call_agents"
|
ActionCallAgents = "call_agents"
|
||||||
ActionShellcommand = "shell-command"
|
ActionShellcommand = "shell-command"
|
||||||
ActionSendTelegramMessage = "send-telegram-message"
|
ActionSendTelegramMessage = "send-telegram-message"
|
||||||
|
ActionSetReminder = "set_reminder"
|
||||||
|
ActionListReminders = "list_reminders"
|
||||||
|
ActionRemoveReminder = "remove_reminder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var AvailableActions = []string{
|
var AvailableActions = []string{
|
||||||
@@ -81,6 +84,9 @@ var AvailableActions = []string{
|
|||||||
ActionCallAgents,
|
ActionCallAgents,
|
||||||
ActionShellcommand,
|
ActionShellcommand,
|
||||||
ActionSendTelegramMessage,
|
ActionSendTelegramMessage,
|
||||||
|
ActionSetReminder,
|
||||||
|
ActionListReminders,
|
||||||
|
ActionRemoveReminder,
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -187,6 +193,12 @@ func Action(name, agentName string, config map[string]string, pool *state.AgentP
|
|||||||
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
|
a = actions.NewShell(config, actionsConfigs[ActionConfigSSHBoxURL])
|
||||||
case ActionSendTelegramMessage:
|
case ActionSendTelegramMessage:
|
||||||
a = actions.NewSendTelegramMessageRunner(config)
|
a = actions.NewSendTelegramMessageRunner(config)
|
||||||
|
case ActionSetReminder:
|
||||||
|
a = action.NewReminder()
|
||||||
|
case ActionListReminders:
|
||||||
|
a = action.NewListReminders()
|
||||||
|
case ActionRemoveReminder:
|
||||||
|
a = action.NewRemoveReminder()
|
||||||
default:
|
default:
|
||||||
xlog.Error("Action not found", "name", name)
|
xlog.Error("Action not found", "name", name)
|
||||||
return nil, fmt.Errorf("Action not found")
|
return nil, fmt.Errorf("Action not found")
|
||||||
@@ -356,5 +368,20 @@ func ActionsConfigMeta() []config.FieldGroup {
|
|||||||
Label: "Send Telegram Message",
|
Label: "Send Telegram Message",
|
||||||
Fields: actions.SendTelegramMessageConfigMeta(),
|
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{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,27 @@ func (d *Discord) Start(a *agent.Agent) {
|
|||||||
|
|
||||||
dg.StateEnabled = true
|
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.
|
// Register the messageCreate func as a callback for MessageCreate events.
|
||||||
dg.AddHandler(d.messageCreate(a))
|
dg.AddHandler(d.messageCreate(a))
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type Email struct {
|
|||||||
smtpInsecure bool
|
smtpInsecure bool
|
||||||
imapServer string
|
imapServer string
|
||||||
imapInsecure bool
|
imapInsecure bool
|
||||||
|
defaultEmail string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmail(config map[string]string) *Email {
|
func NewEmail(config map[string]string) *Email {
|
||||||
@@ -48,6 +49,7 @@ func NewEmail(config map[string]string) *Email {
|
|||||||
smtpInsecure: config["smtpInsecure"] == "true",
|
smtpInsecure: config["smtpInsecure"] == "true",
|
||||||
imapServer: config["imapServer"],
|
imapServer: config["imapServer"],
|
||||||
imapInsecure: config["imapInsecure"] == "true",
|
imapInsecure: config["imapInsecure"] == "true",
|
||||||
|
defaultEmail: config["defaultEmail"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +107,12 @@ func EmailConfigMeta() []config.Field {
|
|||||||
Required: true,
|
Required: true,
|
||||||
HelpText: "Agent email address",
|
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) {
|
func (e *Email) Start(a *agent.Agent) {
|
||||||
go func() {
|
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.")
|
xlog.Info("Email connector is now running. Press CTRL-C to exit.")
|
||||||
// IMAP dial
|
// IMAP dial
|
||||||
|
|||||||
@@ -70,6 +70,52 @@ func (i *IRC) Start(a *agent.Agent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
i.conn.UseTLS = false
|
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) {
|
i.conn.AddCallback("001", func(e *irc.Event) {
|
||||||
xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments)
|
xlog.Info("Connected to IRC server", "server", i.server, "arguments", e.Arguments)
|
||||||
i.conn.Join(i.channel)
|
i.conn.Join(i.channel)
|
||||||
|
|||||||
@@ -223,6 +223,24 @@ func (m *Matrix) Start(a *agent.Agent) {
|
|||||||
xlog.Info("Matrix client created")
|
xlog.Info("Matrix client created")
|
||||||
m.client = client
|
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 := client.Syncer.(*mautrix.DefaultSyncer)
|
||||||
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
||||||
xlog.Info("Received message", evt.Content.AsMessage().Body)
|
xlog.Info("Received message", evt.Content.AsMessage().Body)
|
||||||
|
|||||||
@@ -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) {
|
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{
|
_, err := b.SendMessage(ctx, &bot.SendMessageParams{
|
||||||
ChatID: update.Message.Chat.ID,
|
ChatID: update.Message.Chat.ID,
|
||||||
Text: "you are not authorized to use this bot!",
|
Text: "you are not authorized to use this bot!",
|
||||||
@@ -444,7 +444,7 @@ func NewTelegramConnector(config map[string]string) (*Telegram, error) {
|
|||||||
|
|
||||||
admins := []string{}
|
admins := []string{}
|
||||||
|
|
||||||
if _, ok := config["admins"]; ok {
|
if _, ok := config["admins"]; ok && strings.Contains(config["admins"], ",") {
|
||||||
admins = append(admins, strings.Split(config["admins"], ",")...)
|
admins = append(admins, strings.Split(config["admins"], ",")...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user