package scheduler import ( "context" "encoding/json" "fmt" "trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/service/task" ) // SchedulerTool permet à l'agent de planifier des tâches pour l'utilisateur : // exécution d'un prompt selon une spécification cron, avec ou sans répétition. type SchedulerTool struct { taskService *task.Service } func New(taskService *task.Service) *SchedulerTool { return &SchedulerTool{taskService: taskService} } func (t *SchedulerTool) Name() string { return "scheduler" } func (t *SchedulerTool) Description() string { return "Schedule tasks that run automatically at specified times, with or without repetition. " + "Use this tool whenever the user asks to do something later or on a recurring basis " + "(e.g. 'every day at 8am', 'every Monday at 9am', 'tomorrow at 6pm', 'every hour'). " + "Actions: 'create' (schedule a new task), 'list' (list scheduled tasks), " + "'update' (modify a task's title, prompt, cron or repetition), " + "'delete' (delete a task by its id)." } func (t *SchedulerTool) Params() []domain.ToolParam { return []domain.ToolParam{} } func (t *SchedulerTool) Definition(ctx context.Context) *domain.ToolDefinition { return &domain.ToolDefinition{ Type: "function", Function: &domain.ToolFunction{ Name: t.Name(), Description: t.Description(), Parameters: map[string]any{ "type": "object", "properties": map[string]any{ "action": map[string]any{ "type": "string", "enum": []string{"create", "list", "update", "delete"}, "description": "The scheduling action to perform.", }, "title": map[string]any{ "type": "string", "description": "Short task title, e.g. 'Météo du matin'. Required for 'create'.", }, "prompt": map[string]any{ "type": "string", "description": "Self-contained prompt to execute at the scheduled time. " + "It will run in a fresh conversation with no memory of this one: " + "include all needed context. Required for 'create'.", }, "cron": map[string]any{ "type": "string", "description": "Cron spec, 5 fields 'minute hour day-of-month month day-of-week' " + "(e.g. '0 8 * * *' every day at 08:00, '0 9 * * 1' every Monday at 09:00, " + "'30 18 1 * *' on the 1st at 18:30) or a descriptor like '@daily', '@hourly', '@weekly'. " + "Required for 'create'.", }, "repeat": map[string]any{ "type": "boolean", "description": "true: the task repeats on schedule. " + "false: it runs only once, at the first time matching the cron spec, then is marked done. " + "Default true. Use false for one-shot requests like 'tomorrow at 8am'.", }, "id": map[string]any{ "type": "string", "description": "Task id, as returned by 'list'. Required for 'update' and 'delete'.", }, }, "required": []string{"action"}, }, }, } } func (t *SchedulerTool) Execute(ctx context.Context, input []byte, configuration string) ([]byte, error) { var params struct { Action string `json:"action"` Title string `json:"title"` Prompt string `json:"prompt"` Cron string `json:"cron"` Repeat *bool `json:"repeat"` ID string `json:"id"` } if err := json.Unmarshal(input, ¶ms); err != nil { return nil, fmt.Errorf("invalid parameters: %w", err) } userID, ok := domain.UserIDFromContext(ctx) if !ok { return nil, fmt.Errorf("scheduler tool: missing user context") } teamID, ok := domain.TeamIDFromContext(ctx) if !ok { return nil, fmt.Errorf("scheduler tool: missing team context") } modelID, _ := domain.ModelIDFromContext(ctx) switch params.Action { case "create": repeat := true if params.Repeat != nil { repeat = *params.Repeat } taskModel, err := t.taskService.CreateTask(userID, teamID, &domain.Task{ ModelID: modelID, Label: params.Title, Prompt: params.Prompt, Cron: params.Cron, Repeat: repeat, }) if err != nil { return nil, err } result, err := json.Marshal(taskModel) if err != nil { return nil, err } return result, nil case "list": tasks, err := t.taskService.ListTasks(userID, teamID) if err != nil { return nil, err } result, err := json.Marshal(tasks) if err != nil { return nil, err } return result, nil case "update": if params.ID == "" { return nil, fmt.Errorf("'id' is required for action 'update'") } current, err := t.taskService.GetTask(userID, teamID, params.ID) if err != nil { return nil, err } // mise à jour partielle : seuls les champs fournis changent if params.Title != "" { current.Label = params.Title } if params.Prompt != "" { current.Prompt = params.Prompt } if params.Cron != "" { current.Cron = params.Cron } if params.Repeat != nil { current.Repeat = *params.Repeat } updated, err := t.taskService.UpdateTask(userID, teamID, current) if err != nil { return nil, err } result, err := json.Marshal(updated) if err != nil { return nil, err } return result, nil case "delete": if params.ID == "" { return nil, fmt.Errorf("'id' is required for action 'delete'") } if err := t.taskService.DeleteTask(userID, teamID, params.ID); err != nil { return nil, err } return []byte("task deleted"), nil default: return nil, fmt.Errorf("unknown action %q", params.Action) } }