Add load job configuration
This commit is contained in:
@@ -45,3 +45,12 @@ tools:
|
|||||||
required:
|
required:
|
||||||
- location
|
- location
|
||||||
timeout: "10s"
|
timeout: "10s"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: "daily_summary"
|
||||||
|
schedule: "0 9 * * *"
|
||||||
|
enabled: true
|
||||||
|
system_message: "You are an assistant."
|
||||||
|
task: "Report the uptime on your host."
|
||||||
|
timeout: "3m"
|
||||||
|
max_iterations: 5 # 0 means unlimited
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.chimeric.al/chimerical/odidere/internal/job"
|
||||||
"code.chimeric.al/chimerical/odidere/internal/llm"
|
"code.chimeric.al/chimerical/odidere/internal/llm"
|
||||||
"code.chimeric.al/chimerical/odidere/internal/stt"
|
"code.chimeric.al/chimerical/odidere/internal/stt"
|
||||||
"code.chimeric.al/chimerical/odidere/internal/tool"
|
"code.chimeric.al/chimerical/odidere/internal/tool"
|
||||||
@@ -30,6 +31,8 @@ type Config struct {
|
|||||||
LLM llm.Config `yaml:"llm"`
|
LLM llm.Config `yaml:"llm"`
|
||||||
// STT configures the speech-to-text client.
|
// STT configures the speech-to-text client.
|
||||||
STT stt.Config `yaml:"stt"`
|
STT stt.Config `yaml:"stt"`
|
||||||
|
// Jobs defines scheduled autonomous tasks.
|
||||||
|
Jobs []job.Config `yaml:"jobs"`
|
||||||
// Tools defines external commands available to the LLM.
|
// Tools defines external commands available to the LLM.
|
||||||
Tools []tool.Tool `yaml:"tools"`
|
Tools []tool.Tool `yaml:"tools"`
|
||||||
// TTS configures the text-to-speech client.
|
// TTS configures the text-to-speech client.
|
||||||
@@ -74,6 +77,11 @@ func (cfg *Config) Validate() error {
|
|||||||
cfg.ShutdownTimeout, err,
|
cfg.ShutdownTimeout, err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
for i, jc := range cfg.Jobs {
|
||||||
|
if err := jc.Validate(); err != nil {
|
||||||
|
return fmt.Errorf("jobs[%d] invalid: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, t := range cfg.Tools {
|
for i, t := range cfg.Tools {
|
||||||
if err := t.Validate(); err != nil {
|
if err := t.Validate(); err != nil {
|
||||||
return fmt.Errorf("tools[%d] invalid: %w", i, err)
|
return fmt.Errorf("tools[%d] invalid: %w", i, err)
|
||||||
|
|||||||
@@ -377,3 +377,185 @@ func TestApplyDefaults_NoOverwrite(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoad_Jobs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config string
|
||||||
|
shouldErr bool
|
||||||
|
check func(t *testing.T, cfg *Config)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid job config",
|
||||||
|
config: `
|
||||||
|
llm:
|
||||||
|
model: test-model
|
||||||
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
stt:
|
||||||
|
url: http://localhost:8178
|
||||||
|
|
||||||
|
tts:
|
||||||
|
url: http://localhost:8880
|
||||||
|
voice: af_heart
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: daily_summary
|
||||||
|
schedule: "0 9 * * *"
|
||||||
|
enabled: true
|
||||||
|
system_message: "You are a research assistant."
|
||||||
|
task: "Summarize today's news about AI."
|
||||||
|
timeout: "10m"
|
||||||
|
max_iterations: 20
|
||||||
|
`,
|
||||||
|
check: func(t *testing.T, cfg *Config) {
|
||||||
|
if len(cfg.Jobs) != 1 {
|
||||||
|
t.Fatalf("len(Jobs) = %d, want 1", len(cfg.Jobs))
|
||||||
|
}
|
||||||
|
j := cfg.Jobs[0]
|
||||||
|
if j.Name != "daily_summary" {
|
||||||
|
t.Errorf("Name = %q, want %q", j.Name, "daily_summary")
|
||||||
|
}
|
||||||
|
if j.Task != "Summarize today's news about AI." {
|
||||||
|
t.Errorf("Task = %q, want %q", j.Task, "Summarize today's news about AI.")
|
||||||
|
}
|
||||||
|
if j.Schedule != "0 9 * * *" {
|
||||||
|
t.Errorf("Schedule = %q, want %q", j.Schedule, "0 9 * * *")
|
||||||
|
}
|
||||||
|
timeout, err := time.ParseDuration(j.Timeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseDuration(%q) error = %v", j.Timeout, err)
|
||||||
|
}
|
||||||
|
if timeout.Minutes() != 10 {
|
||||||
|
t.Errorf("Timeout = %v, want 10m", timeout)
|
||||||
|
}
|
||||||
|
if j.MaxIterations != 20 {
|
||||||
|
t.Errorf("MaxIterations = %d, want 20", j.MaxIterations)
|
||||||
|
}
|
||||||
|
if !j.IsEnabled() {
|
||||||
|
t.Error("IsEnabled() = false, want true")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job with minimal config (defaults)",
|
||||||
|
config: `
|
||||||
|
llm:
|
||||||
|
model: test-model
|
||||||
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
stt:
|
||||||
|
url: http://localhost:8178
|
||||||
|
|
||||||
|
tts:
|
||||||
|
url: http://localhost:8880
|
||||||
|
voice: af_heart
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: log_job
|
||||||
|
schedule: "*/5 * * * *"
|
||||||
|
task: "Check system health."
|
||||||
|
`,
|
||||||
|
check: func(t *testing.T, cfg *Config) {
|
||||||
|
if len(cfg.Jobs) != 1 {
|
||||||
|
t.Fatalf("len(Jobs) = %d, want 1", len(cfg.Jobs))
|
||||||
|
}
|
||||||
|
j := cfg.Jobs[0]
|
||||||
|
if j.MaxIterations != 0 {
|
||||||
|
t.Errorf("MaxIterations = %d, want 0 (unlimited)", j.MaxIterations)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "disabled job",
|
||||||
|
config: `
|
||||||
|
llm:
|
||||||
|
model: test-model
|
||||||
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
stt:
|
||||||
|
url: http://localhost:8178
|
||||||
|
|
||||||
|
tts:
|
||||||
|
url: http://localhost:8880
|
||||||
|
voice: af_heart
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: disabled_job
|
||||||
|
schedule: "0 0 * * *"
|
||||||
|
enabled: false
|
||||||
|
task: "This should not run."
|
||||||
|
`,
|
||||||
|
check: func(t *testing.T, cfg *Config) {
|
||||||
|
if len(cfg.Jobs) != 1 {
|
||||||
|
t.Fatalf("len(Jobs) = %d, want 1", len(cfg.Jobs))
|
||||||
|
}
|
||||||
|
if cfg.Jobs[0].IsEnabled() {
|
||||||
|
t.Error("IsEnabled() = true, want false")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job missing name",
|
||||||
|
config: `
|
||||||
|
llm:
|
||||||
|
model: test-model
|
||||||
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
stt:
|
||||||
|
url: http://localhost:8178
|
||||||
|
|
||||||
|
tts:
|
||||||
|
url: http://localhost:8880
|
||||||
|
voice: af_heart
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- schedule: "0 9 * * *"
|
||||||
|
task: "do something"
|
||||||
|
`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "job missing task",
|
||||||
|
config: `
|
||||||
|
llm:
|
||||||
|
model: test-model
|
||||||
|
url: http://localhost:8080
|
||||||
|
|
||||||
|
stt:
|
||||||
|
url: http://localhost:8178
|
||||||
|
|
||||||
|
tts:
|
||||||
|
url: http://localhost:8880
|
||||||
|
voice: af_heart
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- name: no_task
|
||||||
|
schedule: "0 9 * * *"
|
||||||
|
`,
|
||||||
|
shouldErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
path := writeConfig(t, tt.config)
|
||||||
|
cfg, err := Load(path)
|
||||||
|
|
||||||
|
if tt.shouldErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Load() expected error")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.check != nil {
|
||||||
|
tt.check(t, cfg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user