191 lines
5.7 KiB
Go
191 lines
5.7 KiB
Go
package workflows
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/fs"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/workflow/models"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// templateYAML is the YAML-friendly representation of a workflow template.
|
|
type templateYAML struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description"`
|
|
Steps []stepDefYAML `yaml:"steps"`
|
|
}
|
|
|
|
// stepDefYAML is the YAML-friendly representation of a step definition.
|
|
type stepDefYAML struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
Position int `yaml:"position"`
|
|
Color string `yaml:"color"`
|
|
Prompt string `yaml:"prompt,omitempty"`
|
|
IsStartStep bool `yaml:"is_start_step,omitempty"`
|
|
ShowInCommandPanel bool `yaml:"show_in_command_panel,omitempty"`
|
|
AllowManualMove bool `yaml:"allow_manual_move,omitempty"`
|
|
AutoArchiveAfterHours int `yaml:"auto_archive_after_hours,omitempty"`
|
|
Events stepEventsYAML `yaml:"events,omitempty"`
|
|
}
|
|
|
|
// stepEventsYAML is the YAML-friendly representation of step events.
|
|
type stepEventsYAML struct {
|
|
OnEnter []actionYAML `yaml:"on_enter,omitempty"`
|
|
OnTurnStart []actionYAML `yaml:"on_turn_start,omitempty"`
|
|
OnTurnComplete []actionYAML `yaml:"on_turn_complete,omitempty"`
|
|
OnExit []actionYAML `yaml:"on_exit,omitempty"`
|
|
}
|
|
|
|
// actionYAML is the YAML-friendly representation of a step action.
|
|
type actionYAML struct {
|
|
Type string `yaml:"type"`
|
|
Config map[string]any `yaml:"config,omitempty"`
|
|
}
|
|
|
|
// LoadTemplates parses all embedded YAML files and returns workflow templates.
|
|
func LoadTemplates() ([]*models.WorkflowTemplate, error) {
|
|
entries, err := embeddedFS.ReadDir(".")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("workflows: read embedded dir: %w", err)
|
|
}
|
|
|
|
var templates []*models.WorkflowTemplate
|
|
for _, entry := range entries {
|
|
if entry.IsDir() || !isYAML(entry) {
|
|
continue
|
|
}
|
|
tmpl, err := loadTemplate(entry.Name())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("workflows: load %s: %w", entry.Name(), err)
|
|
}
|
|
templates = append(templates, tmpl)
|
|
}
|
|
return templates, nil
|
|
}
|
|
|
|
func isYAML(entry fs.DirEntry) bool {
|
|
name := entry.Name()
|
|
return strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml")
|
|
}
|
|
|
|
func loadTemplate(filename string) (*models.WorkflowTemplate, error) {
|
|
data, err := embeddedFS.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var raw templateYAML
|
|
dec := yaml.NewDecoder(bytes.NewReader(data))
|
|
dec.KnownFields(true)
|
|
if err := dec.Decode(&raw); err != nil {
|
|
return nil, fmt.Errorf("parse yaml: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
tmpl := &models.WorkflowTemplate{
|
|
ID: raw.ID,
|
|
Name: raw.Name,
|
|
Description: raw.Description,
|
|
IsSystem: true,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
|
|
for _, s := range raw.Steps {
|
|
step, err := convertStep(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tmpl.Steps = append(tmpl.Steps, step)
|
|
}
|
|
return tmpl, nil
|
|
}
|
|
|
|
func convertStep(s stepDefYAML) (models.StepDefinition, error) {
|
|
events, err := convertEvents(s.Events)
|
|
if err != nil {
|
|
return models.StepDefinition{}, fmt.Errorf("step %q: %w", s.ID, err)
|
|
}
|
|
return models.StepDefinition{
|
|
ID: s.ID,
|
|
Name: s.Name,
|
|
Position: s.Position,
|
|
Color: s.Color,
|
|
Prompt: strings.TrimSpace(s.Prompt),
|
|
Events: events,
|
|
AllowManualMove: s.AllowManualMove,
|
|
IsStartStep: s.IsStartStep,
|
|
ShowInCommandPanel: s.ShowInCommandPanel,
|
|
AutoArchiveAfterHours: s.AutoArchiveAfterHours,
|
|
}, nil
|
|
}
|
|
|
|
// Valid action types for each event trigger.
|
|
var (
|
|
validOnEnter = map[string]bool{
|
|
string(models.OnEnterEnablePlanMode): true,
|
|
string(models.OnEnterAutoStartAgent): true,
|
|
string(models.OnEnterResetAgentContext): true,
|
|
}
|
|
validOnTurnStart = map[string]bool{
|
|
string(models.OnTurnStartMoveToNext): true,
|
|
string(models.OnTurnStartMoveToPrevious): true,
|
|
string(models.OnTurnStartMoveToStep): true,
|
|
}
|
|
validOnTurnComplete = map[string]bool{
|
|
string(models.OnTurnCompleteMoveToNext): true,
|
|
string(models.OnTurnCompleteMoveToPrevious): true,
|
|
string(models.OnTurnCompleteMoveToStep): true,
|
|
string(models.OnTurnCompleteDisablePlanMode): true,
|
|
}
|
|
validOnExit = map[string]bool{
|
|
string(models.OnExitDisablePlanMode): true,
|
|
}
|
|
)
|
|
|
|
func convertEvents(e stepEventsYAML) (models.StepEvents, error) {
|
|
var events models.StepEvents
|
|
for _, a := range e.OnEnter {
|
|
if !validOnEnter[a.Type] {
|
|
return events, fmt.Errorf("invalid on_enter action type %q", a.Type)
|
|
}
|
|
events.OnEnter = append(events.OnEnter, models.OnEnterAction{
|
|
Type: models.OnEnterActionType(a.Type),
|
|
Config: a.Config,
|
|
})
|
|
}
|
|
for _, a := range e.OnTurnStart {
|
|
if !validOnTurnStart[a.Type] {
|
|
return events, fmt.Errorf("invalid on_turn_start action type %q", a.Type)
|
|
}
|
|
events.OnTurnStart = append(events.OnTurnStart, models.OnTurnStartAction{
|
|
Type: models.OnTurnStartActionType(a.Type),
|
|
Config: a.Config,
|
|
})
|
|
}
|
|
for _, a := range e.OnTurnComplete {
|
|
if !validOnTurnComplete[a.Type] {
|
|
return events, fmt.Errorf("invalid on_turn_complete action type %q", a.Type)
|
|
}
|
|
events.OnTurnComplete = append(events.OnTurnComplete, models.OnTurnCompleteAction{
|
|
Type: models.OnTurnCompleteActionType(a.Type),
|
|
Config: a.Config,
|
|
})
|
|
}
|
|
for _, a := range e.OnExit {
|
|
if !validOnExit[a.Type] {
|
|
return events, fmt.Errorf("invalid on_exit action type %q", a.Type)
|
|
}
|
|
events.OnExit = append(events.OnExit, models.OnExitAction{
|
|
Type: models.OnExitActionType(a.Type),
|
|
Config: a.Config,
|
|
})
|
|
}
|
|
return events, nil
|
|
}
|