package cron import ( "context" "fmt" "os" "strings" "github.com/enmanuel/agents/internal/config" coretypes "github.com/enmanuel/agents/pkg/llm" ) const actionKindSendMessage = "send_message" const actionKindLLMPrompt = "llm_prompt" // handler is a function that fires when a schedule triggers. type handler func(ctx context.Context, room string) // buildHandler returns the handler for a schedule, or nil for unsupported kinds. func (s *Scheduler) buildHandler(sc config.ScheduleCfg) handler { switch sc.Action.Kind { case actionKindSendMessage: return s.sendMessageHandler(sc) case actionKindLLMPrompt: return s.llmPromptHandler(sc) default: return nil } } // sendMessageHandler returns a handler that sends a static message to a Matrix room. // The message content is resolved in priority order: Message > Template file. func (s *Scheduler) sendMessageHandler(sc config.ScheduleCfg) handler { return func(ctx context.Context, room string) { content, err := resolveContent(sc.Action.Message, sc.Action.Template) if err != nil { s.logger.Error("send_message: failed to resolve content", "name", sc.Name, "err", err) return } if content == "" { s.logger.Warn("send_message: empty content, skipping", "name", sc.Name) return } s.logger.Info("cron_fire", "name", sc.Name, "kind", actionKindSendMessage, "room", room) if err := s.matrix.SendMarkdown(ctx, room, content); err != nil { s.logger.Error("send_message: matrix send failed", "name", sc.Name, "room", room, "err", err) } } } // llmPromptHandler returns a handler that calls the LLM with a prompt and sends // the response to a Matrix room. func (s *Scheduler) llmPromptHandler(sc config.ScheduleCfg) handler { return func(ctx context.Context, room string) { if s.llm == nil { s.logger.Warn("llm_prompt: no LLM configured, skipping", "name", sc.Name) return } prompt, err := resolveContent(sc.Action.Prompt, sc.Action.Template) if err != nil { s.logger.Error("llm_prompt: failed to resolve prompt", "name", sc.Name, "err", err) return } if prompt == "" { s.logger.Warn("llm_prompt: empty prompt, skipping", "name", sc.Name) return } s.logger.Info("cron_fire", "name", sc.Name, "kind", actionKindLLMPrompt, "room", room) req := coretypes.CompletionRequest{ Model: s.model, Messages: []coretypes.Message{ {Role: coretypes.RoleUser, Content: prompt}, }, } resp, err := s.llm(ctx, req) if err != nil { s.logger.Error("llm_prompt: LLM call failed", "name", sc.Name, "err", err) return } content := strings.TrimSpace(resp.Content) if content == "" { s.logger.Warn("llm_prompt: LLM returned empty response", "name", sc.Name) return } if err := s.matrix.SendMarkdown(ctx, room, content); err != nil { s.logger.Error("llm_prompt: matrix send failed", "name", sc.Name, "room", room, "err", err) } } } // resolveContent returns the inline text if non-empty, otherwise reads the file at templatePath. func resolveContent(inline, templatePath string) (string, error) { if inline != "" { return inline, nil } if templatePath == "" { return "", nil } data, err := os.ReadFile(templatePath) if err != nil { return "", fmt.Errorf("reading template %q: %w", templatePath, err) } return strings.TrimSpace(string(data)), nil }