Files
notarius/internal/framework/prompt/registry.go

209 lines
5.3 KiB
Go

package prompt
import (
"crypto/sha256"
"embed"
"encoding/hex"
"fmt"
"io/fs"
"path"
"sort"
"strings"
"text/template"
)
//go:embed assets/**
var embeddedAssets embed.FS
const (
SourceBuiltin = "builtin"
VersionV1 = "v1"
TestGenericPromptID = "test.generic"
)
// Metadata describes a registered prompt asset.
type Metadata struct {
PromptID string `json:"prompt_id"`
PromptVersion string `json:"prompt_version"`
PromptSource string `json:"prompt_source"`
EmbeddedPath string `json:"embedded_path"`
SHA256 string `json:"sha256"`
}
// DiagnosticsMap returns prompt metadata without rendered prompt text.
func (m Metadata) DiagnosticsMap() map[string]any {
return map[string]any{
"prompt_id": m.PromptID,
"prompt_version": m.PromptVersion,
"prompt_source": m.PromptSource,
"embedded_path": m.EmbeddedPath,
"sha256": m.SHA256,
}
}
// Definition identifies a caller-owned system/user prompt bundle.
type Definition struct {
PromptID string
Version string
EmbeddedPath string
SystemPath string
UserPath string
}
// Bundle is a compiled system/user prompt pair.
type Bundle struct {
systemTmpl *template.Template
userTmpl *template.Template
metadata Metadata
}
// Metadata returns metadata for the compiled prompt bundle.
func (b *Bundle) Metadata() Metadata {
if b == nil {
return Metadata{}
}
return b.metadata
}
var promptRegistry map[string]*Bundle
var sharedHardening string
func init() {
var err error
sharedHardening, err = readAsset("assets/shared/prompt_hardening.md")
if err != nil {
panic(err)
}
defs := []Definition{
{
PromptID: TestGenericPromptID,
Version: VersionV1,
EmbeddedPath: "assets/test/generic",
SystemPath: "assets/test/generic/system.md",
UserPath: "assets/test/generic/user.md",
},
}
promptRegistry = make(map[string]*Bundle, len(defs))
for _, def := range defs {
compiled, compileErr := LoadBundle(embeddedAssets, def)
if compileErr != nil {
panic(compileErr)
}
promptRegistry[compiled.metadata.PromptID] = compiled
}
}
// LookupMetadata returns metadata for the requested prompt ID.
func LookupMetadata(promptID string) (Metadata, bool) {
compiled, ok := promptRegistry[strings.TrimSpace(promptID)]
if !ok {
return Metadata{}, false
}
return compiled.metadata, true
}
// MustLookupMetadata returns metadata for the requested prompt ID and panics when missing.
func MustLookupMetadata(promptID string) Metadata {
metadata, ok := LookupMetadata(promptID)
if !ok {
panic(fmt.Sprintf("unknown prompt id %q", promptID))
}
return metadata
}
// RegisteredMetadata returns all prompt metadata sorted by prompt ID.
func RegisteredMetadata() []Metadata {
ids := make([]string, 0, len(promptRegistry))
for id := range promptRegistry {
ids = append(ids, id)
}
sort.Strings(ids)
out := make([]Metadata, 0, len(ids))
for _, id := range ids {
out = append(out, promptRegistry[id].metadata)
}
return out
}
// HardeningText returns the shared hardening instructions available to templates.
func HardeningText() string {
return sharedHardening
}
func readAsset(assetPath string) (string, error) {
content, err := embeddedAssets.ReadFile(assetPath)
if err != nil {
return "", fmt.Errorf("read embedded prompt asset %q: %w", assetPath, err)
}
return string(content), nil
}
// LoadBundle compiles a system/user prompt bundle from a caller-owned filesystem.
func LoadBundle(fsys fs.FS, def Definition) (*Bundle, error) {
promptID := strings.TrimSpace(def.PromptID)
version := strings.TrimSpace(def.Version)
embeddedPath := strings.TrimSpace(def.EmbeddedPath)
systemPath := strings.TrimSpace(def.SystemPath)
userPath := strings.TrimSpace(def.UserPath)
if promptID == "" {
return nil, fmt.Errorf("prompt id must not be empty")
}
if version == "" {
return nil, fmt.Errorf("prompt version must not be empty")
}
if embeddedPath == "" {
return nil, fmt.Errorf("prompt embedded path must not be empty")
}
systemSource, err := readPromptAsset(fsys, systemPath)
if err != nil {
return nil, err
}
userSource, err := readPromptAsset(fsys, userPath)
if err != nil {
return nil, err
}
funcs := template.FuncMap{
"hardening": func() string { return sharedHardening },
}
systemTmpl, err := template.New(path.Base(systemPath)).Option("missingkey=error").Funcs(funcs).Parse(systemSource)
if err != nil {
return nil, fmt.Errorf("parse embedded system prompt %q: %w", systemPath, err)
}
userTmpl, err := template.New(path.Base(userPath)).Option("missingkey=error").Funcs(funcs).Parse(userSource)
if err != nil {
return nil, fmt.Errorf("parse embedded user prompt %q: %w", userPath, err)
}
hashInput := systemSource + "\n\n" + userSource
hash := sha256.Sum256([]byte(hashInput))
metadata := Metadata{
PromptID: promptID,
PromptVersion: version,
PromptSource: SourceBuiltin,
EmbeddedPath: embeddedPath,
SHA256: "sha256:" + hex.EncodeToString(hash[:]),
}
return &Bundle{
systemTmpl: systemTmpl,
userTmpl: userTmpl,
metadata: metadata,
}, nil
}
func readPromptAsset(fsys fs.FS, assetPath string) (string, error) {
if strings.TrimSpace(assetPath) == "" {
return "", fmt.Errorf("prompt asset path must not be empty")
}
content, err := fs.ReadFile(fsys, assetPath)
if err != nil {
return "", fmt.Errorf("read embedded prompt asset %q: %w", assetPath, err)
}
return string(content), nil
}