Add support for executing recipe steps via Grok API streaming, including parameter defaults from YAML, model selection via flags/config, previous step context, and a final summary prompt. Update runner to handle client and model, and refine loader to apply user params with fallbacks.
103 lines
2.5 KiB
Go
103 lines
2.5 KiB
Go
package recipe
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
var (
|
|
// stepRe finds every "### Step N: Title" heading
|
|
stepRe = regexp.MustCompile(`(?m)^### Step (\d+): (.+)$`)
|
|
|
|
// subRe finds the three labelled sections inside each step (no Perl lookahead)
|
|
subRe = regexp.MustCompile(`(?m)^(\*\*(?:Objective|Instructions|Expected output):\*\*)\s*(.+?)(?:\n\n|\n###|\z)`)
|
|
)
|
|
|
|
// Load reads a recipe from disk and fully parses it.
|
|
// Load reads a recipe from disk and fully parses it.
|
|
func Load(path string, userParams map[string]any) (*Recipe, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
parts := bytes.SplitN(b, []byte("---"), 3)
|
|
if len(parts) < 3 {
|
|
return nil, fmt.Errorf("missing YAML frontmatter")
|
|
}
|
|
|
|
var r Recipe
|
|
if err := yaml.Unmarshal(parts[1], &r); err != nil {
|
|
return nil, fmt.Errorf("yaml parse: %w", err)
|
|
}
|
|
|
|
// Apply defaults from YAML if user didn't supply them
|
|
if r.Parameters == nil {
|
|
r.Parameters = make(map[string]Parameter)
|
|
}
|
|
params := make(map[string]any)
|
|
for name, p := range r.Parameters {
|
|
if v, ok := userParams[name]; ok {
|
|
params[name] = v
|
|
} else if p.Default != nil {
|
|
params[name] = p.Default
|
|
}
|
|
}
|
|
|
|
// render templates with defaults applied
|
|
tpl, err := template.New("recipe").Parse(string(parts[2]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var rendered bytes.Buffer
|
|
if err := tpl.Execute(&rendered, params); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := tpl.Execute(&rendered, params); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
body := rendered.String()
|
|
|
|
// extract steps
|
|
matches := stepRe.FindAllStringSubmatch(body, -1)
|
|
for i, m := range matches {
|
|
stepNum := i + 1
|
|
title := m[2]
|
|
|
|
// crude but effective sub-section extraction
|
|
start := strings.Index(body, m[0])
|
|
end := len(body)
|
|
if i+1 < len(matches) {
|
|
end = strings.Index(body[start:], matches[i+1][0]) + start
|
|
}
|
|
section := body[start:end]
|
|
|
|
step := Step{Number: stepNum, Title: title}
|
|
for _, sub := range subRe.FindAllStringSubmatch(section, -1) {
|
|
switch sub[1] {
|
|
case "**Objective:**":
|
|
step.Objective = strings.TrimSpace(sub[2])
|
|
case "**Instructions:**":
|
|
step.Instructions = strings.TrimSpace(sub[2])
|
|
case "**Expected output:**":
|
|
step.Expected = strings.TrimSpace(sub[2])
|
|
}
|
|
}
|
|
r.Steps = append(r.Steps, step)
|
|
}
|
|
|
|
// final summary is everything after the last step
|
|
lastStepEnd := strings.LastIndex(body, matches[len(matches)-1][0])
|
|
r.FinalSummaryPrompt = strings.TrimSpace(body[lastStepEnd+len(matches[len(matches)-1][0]):])
|
|
|
|
return &r, nil
|
|
}
|