mirror of
https://github.com/cheikh2shift/godex.git
synced 2026-10-19 01:29:53 -05:00
add initial impl. of scheduler
This commit is contained in:
1 parent
1a7b16d97f
commit
6e1398fd8f
5 files changed
+825
-5
No files matched your search
+36
-2
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/cheikh2shift/godex/internal/mcp"
|
||||
"github.com/cheikh2shift/godex/internal/ml"
|
||||
"github.com/cheikh2shift/godex/internal/providers"
|
||||
"github.com/cheikh2shift/godex/internal/scheduler"
|
||||
"github.com/cheikh2shift/godex/internal/wizard"
|
||||
"github.com/cheikh2shift/godex/modelquery"
|
||||
)
|
||||
@@ -62,7 +63,7 @@ type MCPServer interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
var slashCommands = []string{"/add-path ", "/remove-path ", "/paths", "/tools", "/clear-context", "/commit ", "/commit-pull ", "/commit-merge ", "/commit-search ", "/exit", "/quit", "/q", "/save", "/save-exit", "/kill ", "/killbg", "/bg", "/clear", "/help", "/model", "/model-persist"}
|
||||
var slashCommands = []string{"/add-path ", "/remove-path ", "/paths", "/tools", "/clear-context", "/commit ", "/commit-pull ", "/commit-merge ", "/commit-search ", "/exit", "/quit", "/q", "/save", "/save-exit", "/kill ", "/killbg", "/bg", "/clear", "/help", "/model", "/model-persist", "/schedule"}
|
||||
|
||||
var (
|
||||
greenOrb = lipgloss.NewStyle().Foreground(lipgloss.Color("82")).Render("●")
|
||||
@@ -73,6 +74,7 @@ var (
|
||||
pathPromptMu sync.Mutex
|
||||
hivePendingStop chan struct{}
|
||||
summarisingStart chan struct{}
|
||||
sched *scheduler.Scheduler
|
||||
)
|
||||
|
||||
var thinkingStyle = lipgloss.NewStyle().
|
||||
@@ -319,10 +321,34 @@ func main() {
|
||||
setter.SetStatusChannel(statusCh)
|
||||
}
|
||||
|
||||
sched, err = scheduler.NewDefault()
|
||||
if err != nil {
|
||||
log.Printf("[Scheduler] Failed to initialize: %v", err)
|
||||
} else {
|
||||
sched.SetProviderGetter(&schedulerProviderGetter{provider: provider})
|
||||
schedulerServer := mcp.NewSchedulerServer(sched, &schedulerProviderGetter{provider: provider})
|
||||
servers = append(servers, schedulerServer)
|
||||
for _, tool := range schedulerServer.Tools() {
|
||||
var inputSchema map[string]interface{}
|
||||
if err := json.Unmarshal(tool.InputSchema, &inputSchema); err != nil {
|
||||
inputSchema = map[string]interface{}{}
|
||||
}
|
||||
providerTools = append(providerTools, providers.Tool{
|
||||
Name: tool.Name,
|
||||
Description: tool.Description,
|
||||
InputSchema: inputSchema,
|
||||
})
|
||||
}
|
||||
agent.SetProviderTools(provider, providerTools)
|
||||
if debugMode {
|
||||
fmt.Println(muted.Render("[Scheduler] Initialized"))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
if prompt != "" {
|
||||
err := runSinglePrompt(ctx, provider, prompt, autoConfirm, debugMode, servers)
|
||||
err = runSinglePrompt(ctx, provider, prompt, autoConfirm, debugMode, servers)
|
||||
cleanup(servers)
|
||||
if err != nil {
|
||||
log.Fatalf("prompt failed: %v", err)
|
||||
@@ -852,6 +878,10 @@ promptLoop:
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(input, "/schedule") {
|
||||
handleSchedule(input)
|
||||
continue
|
||||
}
|
||||
|
||||
nativeToolCalls := llmProvider.SupportsNativeToolCalls()
|
||||
var toolsSection string
|
||||
@@ -1234,6 +1264,9 @@ promptLoop:
|
||||
if historyDB != nil {
|
||||
historyDB.Close()
|
||||
}
|
||||
if sched != nil {
|
||||
sched.Close()
|
||||
}
|
||||
fmt.Println("Goodbye!")
|
||||
}
|
||||
|
||||
@@ -1955,6 +1988,7 @@ Commands:
|
||||
/commit-merge <ref> - Merge committed history into current state
|
||||
/commit-search <query> - Search commits by ref or message
|
||||
/clear-context - Reset context counter and LLM client
|
||||
/schedule - List scheduled tasks (see /schedule help for more)
|
||||
/save, /save-exit - Save session and exit
|
||||
/kill <pid> - Kill a background process by PID
|
||||
/killbg - Kill all background processes
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cheikh2shift/godex/internal/agent"
|
||||
"github.com/cheikh2shift/godex/internal/config"
|
||||
"github.com/cheikh2shift/godex/internal/scheduler"
|
||||
)
|
||||
|
||||
type schedulerProviderGetter struct {
|
||||
provider *config.Provider
|
||||
}
|
||||
|
||||
func (s *schedulerProviderGetter) GetProvider(cfg interface{}) (interface{}, error) {
|
||||
cfgMap, ok := cfg.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid config type")
|
||||
}
|
||||
|
||||
providerType, _ := cfgMap["type"].(string)
|
||||
providerName, _ := cfgMap["name"].(string)
|
||||
providerModel, _ := cfgMap["model"].(string)
|
||||
|
||||
if providerType == "unknown" && s.provider != nil {
|
||||
providerType = s.provider.Type
|
||||
providerName = s.provider.Name
|
||||
providerModel = s.provider.Model
|
||||
}
|
||||
|
||||
return agent.GetProviderFromConfig(&agent.ProviderConfig{
|
||||
Type: providerType,
|
||||
Name: providerName,
|
||||
Model: providerModel,
|
||||
})
|
||||
}
|
||||
|
||||
func handleSchedule(input string) {
|
||||
|
||||
if sched == nil {
|
||||
fmt.Println("[Scheduler] Not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Fields(input)
|
||||
if len(parts) == 1 {
|
||||
listScheduledTasks()
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) >= 2 && (parts[1] == "help" || parts[1] == "-h" || parts[1] == "--help") {
|
||||
fmt.Println("Scheduler - Schedule prompts to run at specific times or intervals")
|
||||
fmt.Println("")
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" /schedule - List all scheduled tasks")
|
||||
fmt.Println(" /schedule stop <id> - Stop a running task by 4-char ID")
|
||||
fmt.Println(" /schedule remove <id> - Remove a task from the schedule by 4-char ID")
|
||||
fmt.Println(" /schedule help - Show this help message")
|
||||
fmt.Println("")
|
||||
fmt.Println("LLM Tool (schedule_task):")
|
||||
fmt.Println(" prompt: The prompt to execute")
|
||||
fmt.Println(" interval_sec: Repeating interval in seconds (e.g., 60, 3600)")
|
||||
fmt.Println(" run_at: Time in HH:MM format (e.g., '14:30') or 'now' for immediate execution")
|
||||
fmt.Println("")
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" schedule_task(prompt='Summarize my emails', interval_sec=3600)")
|
||||
fmt.Println(" schedule_task(prompt='Check system status', run_at='09:00')")
|
||||
fmt.Println(" schedule_task(prompt='Generate report', run_at='now')")
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) >= 3 && parts[1] == "stop" {
|
||||
id := strings.ToUpper(parts[2])
|
||||
if len(id) != 4 {
|
||||
fmt.Println("Invalid task ID. Use 4-character identifier (e.g., ABC1)")
|
||||
return
|
||||
}
|
||||
if sched.StopTask(id) {
|
||||
fmt.Printf("Task %s stopped\n", id)
|
||||
} else {
|
||||
fmt.Printf("Task %s not found\n", id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(parts) >= 3 && parts[1] == "remove" {
|
||||
id := strings.ToUpper(parts[2])
|
||||
if len(id) != 4 {
|
||||
fmt.Println("Invalid task ID. Use 4-character identifier (e.g., ABC1)")
|
||||
return
|
||||
}
|
||||
if sched.RemoveTask(id) {
|
||||
fmt.Printf("Task %s removed\n", id)
|
||||
} else {
|
||||
fmt.Printf("Task %s not found\n", id)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" /schedule - List all scheduled tasks")
|
||||
fmt.Println(" /schedule stop <id> - Stop a task by 4-char ID")
|
||||
fmt.Println(" /schedule remove <id> - Remove a task by 4-char ID")
|
||||
}
|
||||
|
||||
func listScheduledTasks() {
|
||||
tasks := sched.ListTasks()
|
||||
if len(tasks) == 0 {
|
||||
fmt.Println("No scheduled tasks")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\n%-6s %-12s %-10s %-8s %-8s %s\n", "ID", "Created", "Interval", "Runs", "Last Run", "Status")
|
||||
fmt.Println(strings.Repeat("-", 80))
|
||||
|
||||
for _, t := range tasks {
|
||||
task, ok := t.(*scheduler.ScheduledTask)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
interval := fmt.Sprintf("%ds", task.IntervalSec)
|
||||
if task.RunAt != "" {
|
||||
interval = task.RunAt
|
||||
}
|
||||
|
||||
lastRun := "Never"
|
||||
if !task.LastRun.IsZero() {
|
||||
lastRun = task.LastRun.Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
status := "OK"
|
||||
if task.LastError != "" {
|
||||
status = truncate(task.LastError, 30)
|
||||
}
|
||||
|
||||
fmt.Printf("%-6s %-12s %-10s %-8d %-8s %s\n",
|
||||
task.ID,
|
||||
task.CreatedAt.Format("2006-01-02"),
|
||||
interval,
|
||||
task.RunCount,
|
||||
lastRun,
|
||||
status,
|
||||
)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -48,7 +48,6 @@ func SendPrompt(ctx context.Context, provider *config.Provider, prompt string) (
|
||||
return strings.TrimSpace(resp), nil
|
||||
}
|
||||
|
||||
|
||||
func CancelPrompt(provider *config.Provider) {
|
||||
if provider == nil {
|
||||
return
|
||||
@@ -71,7 +70,6 @@ func CancelPrompt(provider *config.Provider) {
|
||||
mcpExecutorsMu.Unlock()
|
||||
}
|
||||
|
||||
|
||||
func SetProviderTools(provider *config.Provider, tools []providers.Tool) {
|
||||
p, err := GetProvider(provider)
|
||||
if err != nil {
|
||||
@@ -82,7 +80,6 @@ func SetProviderTools(provider *config.Provider, tools []providers.Tool) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func RegisterMCPExecutor(provider *config.Provider, executor *mcp.MCPToolExecutor) {
|
||||
if provider == nil || executor == nil {
|
||||
return
|
||||
@@ -147,3 +144,21 @@ func GetProvider(cfg *config.Provider) (providers.Provider, error) {
|
||||
func cacheKey(cfg *config.Provider) string {
|
||||
return fmt.Sprintf("%s|%s|%s|%s", cfg.Type, cfg.Name, cfg.Model, cfg.Endpoint)
|
||||
}
|
||||
|
||||
type ProviderConfig struct {
|
||||
Type string
|
||||
Name string
|
||||
Model string
|
||||
}
|
||||
|
||||
func GetProviderFromConfig(cfg *ProviderConfig) (providers.Provider, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("nil config")
|
||||
}
|
||||
provider := &config.Provider{
|
||||
Type: cfg.Type,
|
||||
Name: cfg.Name,
|
||||
Model: cfg.Model,
|
||||
}
|
||||
return GetProvider(provider)
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SchedulerServer struct {
|
||||
scheduler *schedulerExt
|
||||
}
|
||||
|
||||
type schedulerExt struct {
|
||||
scheduler interface {
|
||||
AddTask(prompt string, intervalSec int, runAt string, providerType, providerName, providerModel string) (interface{}, error)
|
||||
RunNow(prompt string, providerType, providerName, providerModel string) (string, error)
|
||||
StopTask(id string) bool
|
||||
RemoveTask(id string) bool
|
||||
ListTasks() []interface{}
|
||||
GetTask(id string) interface{}
|
||||
}
|
||||
mu interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
provider interface {
|
||||
GetProvider(cfg interface{}) (interface{}, error)
|
||||
}
|
||||
providerMu interface {
|
||||
Lock()
|
||||
Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
type TaskInfo struct {
|
||||
ID string `json:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
IntervalSec int `json:"interval_sec"`
|
||||
RunAt string `json:"run_at"`
|
||||
IsRepeating bool `json:"is_repeating"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastRun time.Time `json:"last_run"`
|
||||
RunCount int `json:"run_count"`
|
||||
LastError string `json:"last_error"`
|
||||
}
|
||||
|
||||
func NewSchedulerServer(scheduler interface {
|
||||
AddTask(prompt string, intervalSec int, runAt string, providerType, providerName, providerModel string) (interface{}, error)
|
||||
RunNow(prompt string, providerType, providerName, providerModel string) (string, error)
|
||||
StopTask(id string) bool
|
||||
RemoveTask(id string) bool
|
||||
ListTasks() []interface{}
|
||||
GetTask(id string) interface{}
|
||||
}, providerGetter interface {
|
||||
GetProvider(cfg interface{}) (interface{}, error)
|
||||
}) *SchedulerServer {
|
||||
return &SchedulerServer{
|
||||
scheduler: &schedulerExt{
|
||||
scheduler: scheduler,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) Name() string {
|
||||
return "scheduler"
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) Tools() []Tool {
|
||||
currentTime := time.Now().Format("2006-01-02 15:04:05")
|
||||
return []Tool{
|
||||
{
|
||||
Name: "schedule_task",
|
||||
Description: fmt.Sprintf("Schedule a task to run a prompt at a specific time or at regular intervals. Current time: %s. Use interval_sec for repeating tasks (e.g., 60 for every minute, 3600 for every hour), or run_at for specific times (e.g., \"14:30\" for 2:30 PM daily, or \"now\" to execute immediately).", currentTime),
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "The prompt to execute when the task runs"
|
||||
},
|
||||
"interval_sec": {
|
||||
"type": "integer",
|
||||
"description": "Interval in seconds for repeating tasks (e.g., 60, 300, 3600). Use 0 or omit for one-time task."
|
||||
},
|
||||
"run_at": {
|
||||
"type": "string",
|
||||
"description": "Specific time to run in HH:MM format (e.g., '14:30' for 2:30 PM), or 'now' to execute immediately."
|
||||
}
|
||||
},
|
||||
"required": ["prompt"]
|
||||
}`),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) CallTool(ctx context.Context, name string, args map[string]interface{}) (string, error) {
|
||||
if name != "schedule_task" {
|
||||
return "", fmt.Errorf("unknown tool: %s", name)
|
||||
}
|
||||
|
||||
prompt, ok := args["prompt"].(string)
|
||||
if !ok || strings.TrimSpace(prompt) == "" {
|
||||
return "", fmt.Errorf("prompt is required")
|
||||
}
|
||||
|
||||
intervalSec := 0
|
||||
if iv, ok := args["interval_sec"].(float64); ok {
|
||||
intervalSec = int(iv)
|
||||
}
|
||||
|
||||
runAt := ""
|
||||
if rt, ok := args["run_at"].(string); ok {
|
||||
runAt = strings.TrimSpace(rt)
|
||||
}
|
||||
|
||||
isNow := strings.ToLower(runAt) == "now"
|
||||
|
||||
if intervalSec <= 0 && runAt == "" {
|
||||
return "", fmt.Errorf("must specify either interval_sec or run_at")
|
||||
}
|
||||
|
||||
if isNow {
|
||||
result, err := s.scheduler.scheduler.RunNow(prompt, "unknown", "unknown", "unknown")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error running task: %v", err), nil
|
||||
}
|
||||
return fmt.Sprintf("Task executed immediately.\n\nResult:\n%s", result), nil
|
||||
}
|
||||
|
||||
task, err := s.scheduler.scheduler.AddTask(prompt, intervalSec, runAt, "unknown", "unknown", "unknown")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error scheduling task: %v", err), nil
|
||||
}
|
||||
|
||||
if task == nil {
|
||||
return "Error: task was not created", nil
|
||||
}
|
||||
|
||||
taskInfo, ok := task.(TaskInfo)
|
||||
if !ok {
|
||||
return fmt.Sprintf("Task scheduled successfully but could not get ID"), nil
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Task scheduled successfully with ID: %s\nPrompt: %s\nInterval: %d seconds\nRun at: %s",
|
||||
taskInfo.ID, taskInfo.Prompt, taskInfo.IntervalSec, taskInfo.RunAt), nil
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) AllowedPaths() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) AddPath(ctx context.Context, path string) error {
|
||||
return fmt.Errorf("scheduler server does not support paths")
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) TempAddPath(path string) {
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) AddURL(ctx context.Context, url string) error {
|
||||
return fmt.Errorf("scheduler server does not support URLs")
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) RemovePath(ctx context.Context, path string) error {
|
||||
return fmt.Errorf("scheduler server does not support paths")
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) RemoveURL(ctx context.Context, url string) error {
|
||||
return fmt.Errorf("scheduler server does not support URLs")
|
||||
}
|
||||
|
||||
func (s *SchedulerServer) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type ScheduledTask struct {
|
||||
ID string `json:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
IntervalSec int `json:"interval_sec"`
|
||||
RunAt string `json:"run_at"`
|
||||
IsRepeating bool `json:"is_repeating"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastRun time.Time `json:"last_run"`
|
||||
RunCount int `json:"run_count"`
|
||||
LastError string `json:"last_error"`
|
||||
ProviderType string `json:"provider_type"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ProviderModel string `json:"provider_model"`
|
||||
}
|
||||
|
||||
type Scheduler struct {
|
||||
db *sql.DB
|
||||
tasks map[string]*ScheduledTask
|
||||
taskMu sync.RWMutex
|
||||
running map[string]context.CancelFunc
|
||||
runMu sync.RWMutex
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
provider ProviderGetter
|
||||
}
|
||||
|
||||
type ProviderGetter interface {
|
||||
GetProvider(cfg interface{}) (interface{}, error)
|
||||
}
|
||||
|
||||
func getDefaultDBPath() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("LOCALAPPDATA"), "godex", "scheduler.db")
|
||||
}
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil || homeDir == "" {
|
||||
tmpDir := os.TempDir()
|
||||
return filepath.Join(tmpDir, "godex-scheduler.db")
|
||||
}
|
||||
return filepath.Join(homeDir, ".godex", "scheduler.db")
|
||||
}
|
||||
|
||||
func New(dbPath string) (*Scheduler, error) {
|
||||
if dbPath == "" {
|
||||
dbPath = getDefaultDBPath()
|
||||
}
|
||||
|
||||
dir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", dbPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &Scheduler{
|
||||
db: db,
|
||||
tasks: make(map[string]*ScheduledTask),
|
||||
running: make(map[string]context.CancelFunc),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
if err := s.initSchema(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.loadTasks(); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func NewDefault() (*Scheduler, error) {
|
||||
return New("")
|
||||
}
|
||||
|
||||
func (s *Scheduler) initSchema() error {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS scheduled_tasks (
|
||||
id TEXT PRIMARY KEY,
|
||||
prompt TEXT NOT NULL,
|
||||
interval_sec INTEGER NOT NULL,
|
||||
run_at TEXT NOT NULL,
|
||||
is_repeating INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
last_run TEXT,
|
||||
run_count INTEGER DEFAULT 0,
|
||||
last_error TEXT,
|
||||
provider_type TEXT NOT NULL,
|
||||
provider_name TEXT NOT NULL,
|
||||
provider_model TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_scheduled_tasks_created_at ON scheduled_tasks(created_at);
|
||||
`
|
||||
_, err := s.db.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Scheduler) loadTasks() error {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id, prompt, interval_sec, run_at, is_repeating, created_at,
|
||||
last_run, run_count, last_error, provider_type, provider_name, provider_model
|
||||
FROM scheduled_tasks
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var task ScheduledTask
|
||||
var lastRun, createdAt, runAt sql.NullString
|
||||
err := rows.Scan(
|
||||
&task.ID, &task.Prompt, &task.IntervalSec, &runAt, &task.IsRepeating,
|
||||
&createdAt, &lastRun, &task.RunCount, &task.LastError,
|
||||
&task.ProviderType, &task.ProviderName, &task.ProviderModel,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if createdAt.Valid {
|
||||
task.CreatedAt, _ = time.Parse(time.RFC3339, createdAt.String)
|
||||
}
|
||||
if lastRun.Valid {
|
||||
task.LastRun, _ = time.Parse(time.RFC3339, lastRun.String)
|
||||
}
|
||||
if runAt.Valid {
|
||||
task.RunAt = runAt.String
|
||||
}
|
||||
s.tasks[task.ID] = &task
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
|
||||
func (s *Scheduler) SetProviderGetter(pg ProviderGetter) {
|
||||
s.provider = pg
|
||||
}
|
||||
|
||||
func (s *Scheduler) generateID() (string, error) {
|
||||
charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
bytes := make([]byte, 4)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
result := make([]byte, 4)
|
||||
for i, b := range bytes {
|
||||
result[i] = charset[int(b)%len(charset)]
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
type SchedulerInterface interface {
|
||||
AddTask(prompt string, intervalSec int, runAt string, providerType, providerName, providerModel string) (interface{}, error)
|
||||
RunNow(prompt string, providerType, providerName, providerModel string) (string, error)
|
||||
StopTask(id string) bool
|
||||
RemoveTask(id string) bool
|
||||
ListTasks() []interface{}
|
||||
GetTask(id string) interface{}
|
||||
}
|
||||
|
||||
func (s *Scheduler) AddTask(prompt string, intervalSec int, runAt string, providerType, providerName, providerModel string) (interface{}, error) {
|
||||
if intervalSec <= 0 && runAt == "" {
|
||||
return nil, fmt.Errorf("must specify either interval or run_at time")
|
||||
}
|
||||
|
||||
id, err := s.generateID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := &ScheduledTask{
|
||||
ID: id,
|
||||
Prompt: prompt,
|
||||
IntervalSec: intervalSec,
|
||||
RunAt: runAt,
|
||||
IsRepeating: intervalSec > 0,
|
||||
CreatedAt: time.Now(),
|
||||
RunCount: 0,
|
||||
ProviderType: providerType,
|
||||
ProviderName: providerName,
|
||||
ProviderModel: providerModel,
|
||||
}
|
||||
|
||||
s.taskMu.Lock()
|
||||
s.tasks[id] = task
|
||||
s.taskMu.Unlock()
|
||||
|
||||
if err := s.saveTask(task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.startTask(task)
|
||||
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) RunNow(prompt string, providerType, providerName, providerModel string) (string, error) {
|
||||
if s.provider == nil {
|
||||
return "", fmt.Errorf("no provider getter set")
|
||||
}
|
||||
|
||||
cfg := map[string]interface{}{
|
||||
"type": providerType,
|
||||
"name": providerName,
|
||||
"model": providerModel,
|
||||
}
|
||||
|
||||
prov, err := s.provider.GetProvider(cfg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get provider: %v", err)
|
||||
}
|
||||
|
||||
provInterface, ok := prov.(interface {
|
||||
Send(ctx context.Context, prompt string) (string, error)
|
||||
})
|
||||
if !ok {
|
||||
return "", fmt.Errorf("provider does not support Send method")
|
||||
}
|
||||
|
||||
taskCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
result, err := provInterface.Send(taskCtx, prompt)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("execution error: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Scheduler) saveTask(task *ScheduledTask) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT OR REPLACE INTO scheduled_tasks
|
||||
(id, prompt, interval_sec, run_at, is_repeating, created_at, last_run, run_count, last_error, provider_type, provider_name, provider_model)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, task.ID, task.Prompt, task.IntervalSec, task.RunAt, task.IsRepeating,
|
||||
task.CreatedAt.Format(time.RFC3339), task.LastRun.Format(time.RFC3339),
|
||||
task.RunCount, task.LastError, task.ProviderType, task.ProviderName, task.ProviderModel)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Scheduler) startTask(task *ScheduledTask) {
|
||||
s.runMu.Lock()
|
||||
if _, exists := s.running[task.ID]; exists {
|
||||
s.runMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
s.running[task.ID] = cancel
|
||||
s.runMu.Unlock()
|
||||
|
||||
s.wg.Add(1)
|
||||
go s.runLoop(ctx, task)
|
||||
}
|
||||
|
||||
func (s *Scheduler) runLoop(ctx context.Context, task *ScheduledTask) {
|
||||
defer s.wg.Done()
|
||||
|
||||
sleepDuration := func() time.Duration {
|
||||
if task.IntervalSec > 0 {
|
||||
return time.Duration(task.IntervalSec) * time.Second
|
||||
}
|
||||
if task.RunAt != "" {
|
||||
now := time.Now()
|
||||
target, err := time.Parse("15:04", task.RunAt)
|
||||
if err != nil {
|
||||
return 24 * time.Hour
|
||||
}
|
||||
target = time.Date(now.Year(), now.Month(), now.Day(), target.Hour(), target.Minute(), 0, 0, now.Location())
|
||||
if target.Before(now) {
|
||||
target = target.Add(24 * time.Hour)
|
||||
}
|
||||
return target.Sub(now)
|
||||
}
|
||||
return 24 * time.Hour
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(sleepDuration()):
|
||||
s.executeTask(task)
|
||||
if !task.IsRepeating {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) executeTask(task *ScheduledTask) {
|
||||
if s.provider == nil {
|
||||
task.LastError = "no provider getter set"
|
||||
return
|
||||
}
|
||||
|
||||
cfg := &struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Model string `json:"model"`
|
||||
}{
|
||||
Type: task.ProviderType,
|
||||
Name: task.ProviderName,
|
||||
Model: task.ProviderModel,
|
||||
}
|
||||
|
||||
prov, err := s.provider.GetProvider(cfg)
|
||||
if err != nil {
|
||||
task.LastError = fmt.Sprintf("failed to get provider: %v", err)
|
||||
task.RunCount++
|
||||
s.updateTask(task)
|
||||
return
|
||||
}
|
||||
|
||||
provInterface, ok := prov.(interface {
|
||||
Send(ctx context.Context, prompt string) (string, error)
|
||||
})
|
||||
if !ok {
|
||||
task.LastError = "provider does not support Send method"
|
||||
task.RunCount++
|
||||
s.updateTask(task)
|
||||
return
|
||||
}
|
||||
|
||||
taskCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
result, err := provInterface.Send(taskCtx, task.Prompt)
|
||||
if err != nil {
|
||||
task.LastError = fmt.Sprintf("execution error: %v", err)
|
||||
} else {
|
||||
task.LastError = ""
|
||||
fmt.Printf("[Scheduler %s] Result: %s\n", task.ID, truncate(result, 200))
|
||||
}
|
||||
|
||||
task.LastRun = time.Now()
|
||||
task.RunCount++
|
||||
s.updateTask(task)
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
func (s *Scheduler) updateTask(task *ScheduledTask) {
|
||||
s.taskMu.Lock()
|
||||
s.tasks[task.ID] = task
|
||||
s.taskMu.Unlock()
|
||||
s.saveTask(task)
|
||||
}
|
||||
|
||||
func (s *Scheduler) StopTask(id string) bool {
|
||||
s.runMu.Lock()
|
||||
if cancel, exists := s.running[id]; exists {
|
||||
cancel()
|
||||
delete(s.running, id)
|
||||
s.runMu.Unlock()
|
||||
return true
|
||||
}
|
||||
s.runMu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scheduler) RemoveTask(id string) bool {
|
||||
s.StopTask(id)
|
||||
s.taskMu.Lock()
|
||||
if _, exists := s.tasks[id]; exists {
|
||||
delete(s.tasks, id)
|
||||
s.taskMu.Unlock()
|
||||
_, err := s.db.Exec("DELETE FROM scheduled_tasks WHERE id = ?", id)
|
||||
return err == nil
|
||||
}
|
||||
s.taskMu.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scheduler) ListTasks() []interface{} {
|
||||
s.taskMu.RLock()
|
||||
defer s.taskMu.RUnlock()
|
||||
|
||||
tasks := make([]interface{}, 0, len(s.tasks))
|
||||
for _, task := range s.tasks {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (s *Scheduler) GetTask(id string) interface{} {
|
||||
s.taskMu.RLock()
|
||||
defer s.taskMu.RUnlock()
|
||||
return s.tasks[id]
|
||||
}
|
||||
|
||||
func (s *Scheduler) Close() {
|
||||
close(s.stopCh)
|
||||
s.runMu.Lock()
|
||||
for _, cancel := range s.running {
|
||||
cancel()
|
||||
}
|
||||
s.runMu.Unlock()
|
||||
s.wg.Wait()
|
||||
s.db.Close()
|
||||
}
|
||||
|
||||
type providerConfig struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Model string `json:"model"`
|
||||
}
|
||||
|
||||
func GetProviderFromConfig(cfg *providerConfig) (interface{}, error) {
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("nil config")
|
||||
}
|
||||
|
||||
cfgJSON, _ := json.Marshal(cfg)
|
||||
return string(cfgJSON), nil
|
||||
}
|
||||
Reference in new issue
Block a user