feat(prompts): load system prompts and defaults from Docker volume; set runtime system prompt; add BACKBEAT standards

This commit is contained in:
anthonyrawlins
2025-09-06 15:42:41 +10:00
parent 1ccb84093e
commit 1806a4fe09
6 changed files with 901 additions and 20 deletions

114
pkg/prompt/loader.go Normal file
View File

@@ -0,0 +1,114 @@
package prompt
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
var (
loadedRoles map[string]RoleDefinition
defaultInstr string
)
// Initialize loads roles and default instructions from the configured directory.
// dir: base directory (e.g., /etc/chorus/prompts)
// defaultPath: optional explicit path to defaults file; if empty, will look for defaults.md or defaults.txt in dir.
func Initialize(dir string, defaultPath string) error {
loadedRoles = make(map[string]RoleDefinition)
// Load roles from all YAML files under dir
if dir != "" {
_ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d == nil || d.IsDir() {
return nil
}
name := d.Name()
if strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") {
_ = loadRolesFile(path)
}
return nil
})
}
// Load default instructions
if defaultPath == "" && dir != "" {
// Try defaults.md then defaults.txt in the directory
tryPaths := []string{
filepath.Join(dir, "defaults.md"),
filepath.Join(dir, "defaults.txt"),
}
for _, p := range tryPaths {
if b, err := os.ReadFile(p); err == nil {
defaultInstr = string(b)
break
}
}
} else if defaultPath != "" {
if b, err := os.ReadFile(defaultPath); err == nil {
defaultInstr = string(b)
}
}
return nil
}
func loadRolesFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
var rf RolesFile
if err := yaml.Unmarshal(data, &rf); err != nil {
return err
}
for id, def := range rf.Roles {
def.ID = id
loadedRoles[id] = def
}
return nil
}
// GetRole returns the role definition by ID if loaded.
func GetRole(id string) (RoleDefinition, bool) {
r, ok := loadedRoles[id]
return r, ok
}
// ListRoles returns IDs of loaded roles.
func ListRoles() []string {
ids := make([]string, 0, len(loadedRoles))
for id := range loadedRoles {
ids = append(ids, id)
}
return ids
}
// GetDefaultInstructions returns the loaded default instructions (may be empty if not present on disk).
func GetDefaultInstructions() string {
return defaultInstr
}
// ComposeSystemPrompt concatenates the role system prompt (S) with default instructions (D).
func ComposeSystemPrompt(roleID string) (string, error) {
r, ok := GetRole(roleID)
if !ok {
return "", errors.New("role not found: " + roleID)
}
s := strings.TrimSpace(r.SystemPrompt)
d := strings.TrimSpace(defaultInstr)
switch {
case s != "" && d != "":
return s + "\n\n" + d, nil
case s != "":
return s, nil
case d != "":
return d, nil
default:
return "", nil
}
}

22
pkg/prompt/types.go Normal file
View File

@@ -0,0 +1,22 @@
package prompt
// RoleDefinition represents a single agent role loaded from YAML.
type RoleDefinition struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
SystemPrompt string `yaml:"system_prompt"`
Defaults struct {
Models []string `yaml:"models"`
Capabilities []string `yaml:"capabilities"`
Expertise []string `yaml:"expertise"`
MaxTasks int `yaml:"max_tasks"`
} `yaml:"defaults"`
}
// RolesFile is the top-level structure for a roles YAML file.
type RolesFile struct {
Roles map[string]RoleDefinition `yaml:"roles"`
}