Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ LocalAGI
**/.env
.vscode
volumes/
example/scheduler/scheduler
example/scheduler/example_tasks.json
182 changes: 133 additions & 49 deletions core/action/reminder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,33 @@ import (
"strings"
"time"

"github.com/mudler/LocalAGI/core/scheduler"
"github.com/mudler/LocalAGI/core/types"
"github.com/robfig/cron/v3"
"github.com/sashabaranov/go-openai/jsonschema"
)

const (
RecurringReminderActionName = "set_recurring_reminder"
OneTimeReminderActionName = "set_onetime_reminder"
ListRemindersName = "list_reminders"
RemoveReminderName = "remove_reminder"

// Deprecated: use RecurringReminderActionName or OneTimeReminderActionName
ReminderActionName = "set_reminder"
ListRemindersName = "list_reminders"
RemoveReminderName = "remove_reminder"
)

func NewReminder() *ReminderAction {
return &ReminderAction{}
func NewRecurringReminder() *RecurringReminderAction {
return &RecurringReminderAction{}
}

func NewOneTimeReminder() *OneTimeReminderAction {
return &OneTimeReminderAction{}
}

// NewReminder returns a RecurringReminderAction for backward compatibility.
// Deprecated: use NewRecurringReminder or NewOneTimeReminder.
func NewReminder() *RecurringReminderAction {
return &RecurringReminderAction{}
}

func NewListReminders() *ListRemindersAction {
Expand All @@ -29,77 +43,122 @@ func NewRemoveReminder() *RemoveReminderAction {
return &RemoveReminderAction{}
}

type ReminderAction struct{}
type RecurringReminderAction struct{}
type OneTimeReminderAction 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{}
func (a *RecurringReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := types.RecurringReminderParams{}
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)
task, err := scheduler.NewTask(
sharedState.AgentName,
result.Message,
scheduler.ScheduleTypeCron,
result.CronExpr,
)
if err != nil {
return types.ActionResult{}, err
}

task.Metadata["reminder_type"] = "user_created"

err = sharedState.Scheduler.CreateTask(task)
if err != nil {
return types.ActionResult{}, err
}

return types.ActionResult{
Result: fmt.Sprintf("Recurring reminder set successfully (ID: %s). Next run: %s", task.ID, task.NextRun.Format(time.RFC3339)),
Metadata: map[string]interface{}{
"task_id": task.ID,
"message": result.Message,
"next_run": task.NextRun,
},
}, nil
}

func (a *OneTimeReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
result := types.OneTimeReminderParams{}
err := params.Unmarshal(&result)
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)
// Validate the delay parses correctly before creating the task
_, err = scheduler.ParseDuration(result.Delay)
if err != nil {
return types.ActionResult{}, fmt.Errorf("invalid delay format, expected a duration like '30m', '2h', '1d', '1d12h': %w", err)
}

// Set the reminder details
result.LastRun = now
result.NextRun = nextRun
// IsRecurring is set by the user through the action parameters
task, err := scheduler.NewTask(
sharedState.AgentName,
result.Message,
scheduler.ScheduleTypeOnce,
result.Delay,
)
if err != nil {
return types.ActionResult{}, err
}

task.Metadata["reminder_type"] = "user_created"

// Store the reminder in the shared state
if sharedState.Reminders == nil {
sharedState.Reminders = make([]types.ReminderActionResponse, 0)
err = sharedState.Scheduler.CreateTask(task)
if err != nil {
return types.ActionResult{}, err
}
sharedState.Reminders = append(sharedState.Reminders, result)

return types.ActionResult{
Result: "Reminder set successfully",
Result: fmt.Sprintf("One-time reminder set in %s (at %s, ID: %s)", result.Delay, task.NextRun.Format(time.RFC3339), task.ID),
Metadata: map[string]interface{}{
"reminder": result,
"task_id": task.ID,
"message": result.Message,
"next_run": task.NextRun,
},
}, 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 {
tasks, err := sharedState.Scheduler.GetAllTasks()
if err != nil {
return types.ActionResult{}, err
}

if len(tasks) == 0 {
return types.ActionResult{
Result: "No reminders set",
}, nil
}

var result strings.Builder
result.WriteString("Current reminders:\n")
for i, reminder := range sharedState.Reminders {

for i, task := range tasks {
status := "one-time"
if reminder.IsRecurring {
if task.ScheduleType == scheduler.ScheduleTypeCron || task.ScheduleType == scheduler.ScheduleTypeInterval {
status = "recurring"
}
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n",

result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s, ID: %s)\n",
i+1,
reminder.Message,
reminder.NextRun.Format(time.RFC3339),
status))
task.Prompt,
task.NextRun.Format(time.RFC3339),
status,
task.ID))
}

return types.ActionResult{
Result: result.String(),
Metadata: map[string]interface{}{
"reminders": sharedState.Reminders,
"tasks": tasks,
},
}, nil
}
Expand All @@ -111,31 +170,42 @@ func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.Agent
return types.ActionResult{}, err
}

if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
tasks, err := sharedState.Scheduler.GetAllTasks()
if err != nil {
return types.ActionResult{}, err
}

if len(tasks) == 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) {
if index < 0 || index >= len(tasks) {
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:]...)
task := tasks[index]
err = sharedState.Scheduler.DeleteTask(task.ID)
if err != nil {
return types.ActionResult{}, err
}

return types.ActionResult{
Result: fmt.Sprintf("Removed reminder: %s", removed.Message),
Result: fmt.Sprintf("Removed reminder: %s", task.Prompt),
Metadata: map[string]interface{}{
"removed_reminder": removed,
"removed_task_id": task.ID,
},
}, nil
}

func (a *ReminderAction) Plannable() bool {
func (a *RecurringReminderAction) Plannable() bool {
return true
}

func (a *OneTimeReminderAction) Plannable() bool {
return true
}

Expand All @@ -147,25 +217,39 @@ func (a *RemoveReminderAction) Plannable() bool {
return true
}

func (a *ReminderAction) Definition() types.ActionDefinition {
func (a *RecurringReminderAction) 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)",
Name: RecurringReminderActionName,
Description: "Set a recurring reminder for the agent to wake up and perform a task on a cron schedule. The reminder will keep repeating. 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'",
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'minute hour day month weekday'",
},
},
Required: []string{"message", "cron_expr"},
}
}

func (a *OneTimeReminderAction) Definition() types.ActionDefinition {
return types.ActionDefinition{
Name: OneTimeReminderActionName,
Description: "Set a one-time reminder for the agent to wake up and perform a task after a delay. The reminder triggers only once and is then automatically removed. Use this when asked to do something 'in X minutes/hours/days'. Examples: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
Properties: map[string]jsonschema.Definition{
"message": {
Type: jsonschema.String,
Description: "The message or task to be reminded about",
},
"is_recurring": {
Type: jsonschema.Boolean,
Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)",
"delay": {
Type: jsonschema.String,
Description: "How long to wait before triggering. Use Go duration format: '30m' (30 minutes), '2h' (2 hours), '1d' (1 day), '1d12h' (1.5 days), '2h30m' (2.5 hours)",
},
},
Required: []string{"message", "cron_expr", "is_recurring"},
Required: []string{"message", "delay"},
}
}

Expand Down
Loading
Loading