Skip to content

Commit 57023d6

Browse files
Copilotmudler
andauthored
Integrate task scheduler into agent reminder system (#408)
* Initial plan * Add task scheduler with cron/interval/once support and comprehensive tests Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Add comprehensive documentation for scheduler package Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Add working example demonstrating scheduler usage Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Update .gitignore to exclude example binaries and task files Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Integrate task scheduler into existing reminder system Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Fix scheduler integration and compilation errors Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Remove standalone scheduler example - integrated into agent system Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * Update scheduler README to reflect integration with agent system Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> * replace old reminders logic Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * A task scheduler for each agent Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tests Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * change to duration Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mudler <2420543+mudler@users.noreply.github.com> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 1c3e9b3 commit 57023d6

File tree

15 files changed

+1389
-156
lines changed

15 files changed

+1389
-156
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ LocalAGI
88
**/.env
99
.vscode
1010
volumes/
11+
example/scheduler/scheduler
12+
example/scheduler/example_tasks.json

core/action/reminder.go

Lines changed: 133 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,33 @@ import (
66
"strings"
77
"time"
88

9+
"github.com/mudler/LocalAGI/core/scheduler"
910
"github.com/mudler/LocalAGI/core/types"
10-
"github.com/robfig/cron/v3"
1111
"github.com/sashabaranov/go-openai/jsonschema"
1212
)
1313

1414
const (
15+
RecurringReminderActionName = "set_recurring_reminder"
16+
OneTimeReminderActionName = "set_onetime_reminder"
17+
ListRemindersName = "list_reminders"
18+
RemoveReminderName = "remove_reminder"
19+
20+
// Deprecated: use RecurringReminderActionName or OneTimeReminderActionName
1521
ReminderActionName = "set_reminder"
16-
ListRemindersName = "list_reminders"
17-
RemoveReminderName = "remove_reminder"
1822
)
1923

20-
func NewReminder() *ReminderAction {
21-
return &ReminderAction{}
24+
func NewRecurringReminder() *RecurringReminderAction {
25+
return &RecurringReminderAction{}
26+
}
27+
28+
func NewOneTimeReminder() *OneTimeReminderAction {
29+
return &OneTimeReminderAction{}
30+
}
31+
32+
// NewReminder returns a RecurringReminderAction for backward compatibility.
33+
// Deprecated: use NewRecurringReminder or NewOneTimeReminder.
34+
func NewReminder() *RecurringReminderAction {
35+
return &RecurringReminderAction{}
2236
}
2337

2438
func NewListReminders() *ListRemindersAction {
@@ -29,77 +43,122 @@ func NewRemoveReminder() *RemoveReminderAction {
2943
return &RemoveReminderAction{}
3044
}
3145

32-
type ReminderAction struct{}
46+
type RecurringReminderAction struct{}
47+
type OneTimeReminderAction struct{}
3348
type ListRemindersAction struct{}
3449
type RemoveReminderAction struct{}
3550

3651
type RemoveReminderParams struct {
3752
Index int `json:"index"`
3853
}
3954

40-
func (a *ReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
41-
result := types.ReminderActionResponse{}
55+
func (a *RecurringReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
56+
result := types.RecurringReminderParams{}
4257
err := params.Unmarshal(&result)
4358
if err != nil {
4459
return types.ActionResult{}, err
4560
}
4661

47-
// Validate the cron expression
48-
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
49-
_, err = parser.Parse(result.CronExpr)
62+
task, err := scheduler.NewTask(
63+
sharedState.AgentName,
64+
result.Message,
65+
scheduler.ScheduleTypeCron,
66+
result.CronExpr,
67+
)
68+
if err != nil {
69+
return types.ActionResult{}, err
70+
}
71+
72+
task.Metadata["reminder_type"] = "user_created"
73+
74+
err = sharedState.Scheduler.CreateTask(task)
75+
if err != nil {
76+
return types.ActionResult{}, err
77+
}
78+
79+
return types.ActionResult{
80+
Result: fmt.Sprintf("Recurring reminder set successfully (ID: %s). Next run: %s", task.ID, task.NextRun.Format(time.RFC3339)),
81+
Metadata: map[string]interface{}{
82+
"task_id": task.ID,
83+
"message": result.Message,
84+
"next_run": task.NextRun,
85+
},
86+
}, nil
87+
}
88+
89+
func (a *OneTimeReminderAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
90+
result := types.OneTimeReminderParams{}
91+
err := params.Unmarshal(&result)
5092
if err != nil {
5193
return types.ActionResult{}, err
5294
}
5395

54-
// Calculate next run time
55-
now := time.Now()
56-
schedule, _ := parser.Parse(result.CronExpr) // We can ignore the error since we validated above
57-
nextRun := schedule.Next(now)
96+
// Validate the delay parses correctly before creating the task
97+
_, err = scheduler.ParseDuration(result.Delay)
98+
if err != nil {
99+
return types.ActionResult{}, fmt.Errorf("invalid delay format, expected a duration like '30m', '2h', '1d', '1d12h': %w", err)
100+
}
58101

59-
// Set the reminder details
60-
result.LastRun = now
61-
result.NextRun = nextRun
62-
// IsRecurring is set by the user through the action parameters
102+
task, err := scheduler.NewTask(
103+
sharedState.AgentName,
104+
result.Message,
105+
scheduler.ScheduleTypeOnce,
106+
result.Delay,
107+
)
108+
if err != nil {
109+
return types.ActionResult{}, err
110+
}
111+
112+
task.Metadata["reminder_type"] = "user_created"
63113

64-
// Store the reminder in the shared state
65-
if sharedState.Reminders == nil {
66-
sharedState.Reminders = make([]types.ReminderActionResponse, 0)
114+
err = sharedState.Scheduler.CreateTask(task)
115+
if err != nil {
116+
return types.ActionResult{}, err
67117
}
68-
sharedState.Reminders = append(sharedState.Reminders, result)
69118

70119
return types.ActionResult{
71-
Result: "Reminder set successfully",
120+
Result: fmt.Sprintf("One-time reminder set in %s (at %s, ID: %s)", result.Delay, task.NextRun.Format(time.RFC3339), task.ID),
72121
Metadata: map[string]interface{}{
73-
"reminder": result,
122+
"task_id": task.ID,
123+
"message": result.Message,
124+
"next_run": task.NextRun,
74125
},
75126
}, nil
76127
}
77128

78129
func (a *ListRemindersAction) Run(ctx context.Context, sharedState *types.AgentSharedState, params types.ActionParams) (types.ActionResult, error) {
79-
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
130+
tasks, err := sharedState.Scheduler.GetAllTasks()
131+
if err != nil {
132+
return types.ActionResult{}, err
133+
}
134+
135+
if len(tasks) == 0 {
80136
return types.ActionResult{
81137
Result: "No reminders set",
82138
}, nil
83139
}
84140

85141
var result strings.Builder
86142
result.WriteString("Current reminders:\n")
87-
for i, reminder := range sharedState.Reminders {
143+
144+
for i, task := range tasks {
88145
status := "one-time"
89-
if reminder.IsRecurring {
146+
if task.ScheduleType == scheduler.ScheduleTypeCron || task.ScheduleType == scheduler.ScheduleTypeInterval {
90147
status = "recurring"
91148
}
92-
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s)\n",
149+
150+
result.WriteString(fmt.Sprintf("%d. %s (Next run: %s, Status: %s, ID: %s)\n",
93151
i+1,
94-
reminder.Message,
95-
reminder.NextRun.Format(time.RFC3339),
96-
status))
152+
task.Prompt,
153+
task.NextRun.Format(time.RFC3339),
154+
status,
155+
task.ID))
97156
}
98157

99158
return types.ActionResult{
100159
Result: result.String(),
101160
Metadata: map[string]interface{}{
102-
"reminders": sharedState.Reminders,
161+
"tasks": tasks,
103162
},
104163
}, nil
105164
}
@@ -111,31 +170,42 @@ func (a *RemoveReminderAction) Run(ctx context.Context, sharedState *types.Agent
111170
return types.ActionResult{}, err
112171
}
113172

114-
if sharedState.Reminders == nil || len(sharedState.Reminders) == 0 {
173+
tasks, err := sharedState.Scheduler.GetAllTasks()
174+
if err != nil {
175+
return types.ActionResult{}, err
176+
}
177+
178+
if len(tasks) == 0 {
115179
return types.ActionResult{
116180
Result: "No reminders to remove",
117181
}, nil
118182
}
119183

120184
// Convert from 1-based index to 0-based
121185
index := removeParams.Index - 1
122-
if index < 0 || index >= len(sharedState.Reminders) {
186+
if index < 0 || index >= len(tasks) {
123187
return types.ActionResult{}, fmt.Errorf("invalid reminder index: %d", removeParams.Index)
124188
}
125189

126-
// Remove the reminder
127-
removed := sharedState.Reminders[index]
128-
sharedState.Reminders = append(sharedState.Reminders[:index], sharedState.Reminders[index+1:]...)
190+
task := tasks[index]
191+
err = sharedState.Scheduler.DeleteTask(task.ID)
192+
if err != nil {
193+
return types.ActionResult{}, err
194+
}
129195

130196
return types.ActionResult{
131-
Result: fmt.Sprintf("Removed reminder: %s", removed.Message),
197+
Result: fmt.Sprintf("Removed reminder: %s", task.Prompt),
132198
Metadata: map[string]interface{}{
133-
"removed_reminder": removed,
199+
"removed_task_id": task.ID,
134200
},
135201
}, nil
136202
}
137203

138-
func (a *ReminderAction) Plannable() bool {
204+
func (a *RecurringReminderAction) Plannable() bool {
205+
return true
206+
}
207+
208+
func (a *OneTimeReminderAction) Plannable() bool {
139209
return true
140210
}
141211

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

150-
func (a *ReminderAction) Definition() types.ActionDefinition {
220+
func (a *RecurringReminderAction) Definition() types.ActionDefinition {
151221
return types.ActionDefinition{
152-
Name: ReminderActionName,
153-
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)",
222+
Name: RecurringReminderActionName,
223+
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)",
154224
Properties: map[string]jsonschema.Definition{
155225
"message": {
156226
Type: jsonschema.String,
157227
Description: "The message or task to be reminded about",
158228
},
159229
"cron_expr": {
160230
Type: jsonschema.String,
161-
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'second minute hour day month weekday'",
231+
Description: "Cron expression for scheduling (e.g. '0 0 * * *' for daily at midnight). Format: 'minute hour day month weekday'",
232+
},
233+
},
234+
Required: []string{"message", "cron_expr"},
235+
}
236+
}
237+
238+
func (a *OneTimeReminderAction) Definition() types.ActionDefinition {
239+
return types.ActionDefinition{
240+
Name: OneTimeReminderActionName,
241+
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)",
242+
Properties: map[string]jsonschema.Definition{
243+
"message": {
244+
Type: jsonschema.String,
245+
Description: "The message or task to be reminded about",
162246
},
163-
"is_recurring": {
164-
Type: jsonschema.Boolean,
165-
Description: "Whether this reminder should repeat according to the cron schedule (true) or trigger only once (false)",
247+
"delay": {
248+
Type: jsonschema.String,
249+
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)",
166250
},
167251
},
168-
Required: []string{"message", "cron_expr", "is_recurring"},
252+
Required: []string{"message", "delay"},
169253
}
170254
}
171255

0 commit comments

Comments
 (0)