Extract prompt sources and rendering internals
This commit is contained in:
484
internal/promptdef/filesystem_repository.go
Normal file
484
internal/promptdef/filesystem_repository.go
Normal file
@@ -0,0 +1,484 @@
|
||||
package promptdef
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPromptDefinitionNotFound = errors.New("prompt definition not found")
|
||||
ErrInvalidYAML = errors.New("invalid YAML format")
|
||||
ErrInvalidPromptDefinition = errors.New("invalid prompt definition configuration")
|
||||
)
|
||||
|
||||
type filesystemRepository struct {
|
||||
dir string
|
||||
}
|
||||
|
||||
type fsRepository struct {
|
||||
fsys fs.FS
|
||||
root string
|
||||
}
|
||||
|
||||
type promptDefinitionFile struct {
|
||||
ID string `yaml:"id"`
|
||||
Version string `yaml:"version"`
|
||||
DefaultProfile *string `yaml:"default_profile"`
|
||||
Description string `yaml:"description"`
|
||||
SessionID string `yaml:"session_id"`
|
||||
Inputs []promptInputFile `yaml:"inputs"`
|
||||
Messages []promptMessageFile `yaml:"messages"`
|
||||
Output promptOutputContractFile `yaml:"output"`
|
||||
}
|
||||
|
||||
type promptInputFile struct {
|
||||
Name string `yaml:"name"`
|
||||
Required bool `yaml:"required"`
|
||||
ContentType string `yaml:"content_type"`
|
||||
Description string `yaml:"description"`
|
||||
}
|
||||
|
||||
type promptMessageFile struct {
|
||||
Role string `yaml:"role"`
|
||||
Content string `yaml:"content"`
|
||||
ContentFile string `yaml:"content_file"`
|
||||
CacheControl *cacheControlFile `yaml:"cache_control"`
|
||||
}
|
||||
|
||||
type cacheControlFile struct {
|
||||
Type string `yaml:"type"`
|
||||
TTL string `yaml:"ttl"`
|
||||
}
|
||||
|
||||
type promptOutputContractFile struct {
|
||||
Format domain.OutputFormat `yaml:"format"`
|
||||
ValidationMode domain.ValidationMode `yaml:"validation_mode"`
|
||||
SchemaPath string `yaml:"schema_path"`
|
||||
RepairAttempts int `yaml:"repair_attempts"`
|
||||
}
|
||||
|
||||
func NewFilesystemRepository(dir string) Repository {
|
||||
return &filesystemRepository{dir: dir}
|
||||
}
|
||||
|
||||
func NewFSRepository(fsys fs.FS, root string) Repository {
|
||||
return &fsRepository{fsys: fsys, root: root}
|
||||
}
|
||||
|
||||
func (r *filesystemRepository) GetPromptDefinition(ctx context.Context, id string, version string) (*domain.PromptDefinition, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidPromptDefinition)
|
||||
}
|
||||
|
||||
files, err := filecatalog.FindYAMLFiles(ctx, r.dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition directory: %w", err)
|
||||
}
|
||||
|
||||
var matches []promptDefinitionMatch
|
||||
for _, fullPath := range files {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
relPath := filecatalog.RelativePath(r.dir, fullPath)
|
||||
fileMatch := filecatalog.Stem(filepath.Base(fullPath)) == id
|
||||
|
||||
raw, err := loadPromptDefinitionFile(fullPath)
|
||||
if err != nil {
|
||||
if fileMatch || promptDefinitionFileHasID(fullPath, id) {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, relPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
def, err := normalizePromptDefinition(raw, fullPath)
|
||||
if err != nil {
|
||||
if fileMatch || strings.TrimSpace(raw.ID) == id {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidPromptDefinition, relPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if def.ID != id {
|
||||
continue
|
||||
}
|
||||
if version != "" && def.Version != version {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, promptDefinitionMatch{
|
||||
def: def,
|
||||
path: relPath,
|
||||
})
|
||||
}
|
||||
|
||||
if len(matches) > 1 {
|
||||
paths := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
paths = append(paths, match.path)
|
||||
}
|
||||
if version != "" {
|
||||
return nil, fmt.Errorf("%w: duplicate prompt definition id %q version %q found in: %s", ErrInvalidPromptDefinition, id, version, strings.Join(paths, ", "))
|
||||
}
|
||||
return nil, fmt.Errorf("%w: duplicate prompt definition id %q found in: %s", ErrInvalidPromptDefinition, id, strings.Join(paths, ", "))
|
||||
}
|
||||
|
||||
if len(matches) == 1 {
|
||||
return matches[0].def, nil
|
||||
}
|
||||
|
||||
return nil, ErrPromptDefinitionNotFound
|
||||
}
|
||||
|
||||
func (r *fsRepository) GetPromptDefinition(ctx context.Context, id string, version string) (*domain.PromptDefinition, error) {
|
||||
return loadPromptDefinition(ctx, r.fsys, r.root, id, version)
|
||||
}
|
||||
|
||||
type promptDefinitionMatch struct {
|
||||
def *domain.PromptDefinition
|
||||
path string
|
||||
}
|
||||
|
||||
func loadPromptDefinitionFile(path string) (*promptDefinitionFile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition file: %w", err)
|
||||
}
|
||||
|
||||
var raw promptDefinitionFile
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
decoder.KnownFields(true)
|
||||
if err := decoder.Decode(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &raw, nil
|
||||
}
|
||||
|
||||
func promptDefinitionFileHasID(path string, id string) bool {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var raw struct {
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
if err := yaml.NewDecoder(bytes.NewReader(data)).Decode(&raw); err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(raw.ID) == id
|
||||
}
|
||||
|
||||
func loadPromptDefinition(ctx context.Context, fsys fs.FS, root string, id string, version string) (*domain.PromptDefinition, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidPromptDefinition)
|
||||
}
|
||||
if fsys == nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition directory: filesystem is nil")
|
||||
}
|
||||
|
||||
files, err := filecatalog.FindFSYAMLFiles(ctx, fsys, root)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition directory: %w", err)
|
||||
}
|
||||
cleanRoot := filecatalog.CleanFSRoot(root)
|
||||
rootInfo, err := fs.Stat(fsys, cleanRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read prompt definition directory: %w", err)
|
||||
}
|
||||
|
||||
var matches []promptDefinitionMatch
|
||||
for _, fullPath := range files {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
relPath := filecatalog.DisplayPath(root, fullPath)
|
||||
fileMatch := filecatalog.Stem(path.Base(fullPath)) == id
|
||||
data, err := fs.ReadFile(fsys, fullPath)
|
||||
if err != nil {
|
||||
if fileMatch {
|
||||
return nil, fmt.Errorf("%w: %s: failed to read prompt definition file: %v", ErrInvalidYAML, relPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
raw, err := decodePromptDefinition(data)
|
||||
if err != nil {
|
||||
if fileMatch || promptDefinitionDataHasID(data, id) {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, relPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
def, err := normalizePromptDefinitionFromFS(raw, fsys, root, fullPath, rootInfo.IsDir())
|
||||
if err != nil {
|
||||
if fileMatch || strings.TrimSpace(raw.ID) == id {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidPromptDefinition, relPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if def.ID != id {
|
||||
continue
|
||||
}
|
||||
if version != "" && def.Version != version {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, promptDefinitionMatch{
|
||||
def: def,
|
||||
path: relPath,
|
||||
})
|
||||
}
|
||||
|
||||
if len(matches) > 1 {
|
||||
paths := make([]string, 0, len(matches))
|
||||
for _, match := range matches {
|
||||
paths = append(paths, match.path)
|
||||
}
|
||||
if version != "" {
|
||||
return nil, fmt.Errorf("%w: duplicate prompt definition id %q version %q found in: %s", ErrInvalidPromptDefinition, id, version, strings.Join(paths, ", "))
|
||||
}
|
||||
return nil, fmt.Errorf("%w: duplicate prompt definition id %q found in: %s", ErrInvalidPromptDefinition, id, strings.Join(paths, ", "))
|
||||
}
|
||||
|
||||
if len(matches) == 1 {
|
||||
return matches[0].def, nil
|
||||
}
|
||||
|
||||
return nil, ErrPromptDefinitionNotFound
|
||||
}
|
||||
|
||||
func decodePromptDefinition(data []byte) (*promptDefinitionFile, error) {
|
||||
var raw promptDefinitionFile
|
||||
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||
decoder.KnownFields(true)
|
||||
if err := decoder.Decode(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &raw, nil
|
||||
}
|
||||
|
||||
func promptDefinitionDataHasID(data []byte, id string) bool {
|
||||
var raw struct {
|
||||
ID string `yaml:"id"`
|
||||
}
|
||||
if err := yaml.NewDecoder(bytes.NewReader(data)).Decode(&raw); err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(raw.ID) == id
|
||||
}
|
||||
|
||||
func normalizePromptDefinition(raw *promptDefinitionFile, sourcePath string) (*domain.PromptDefinition, error) {
|
||||
promptDir := filepath.Dir(sourcePath)
|
||||
return normalizePromptDefinitionWithContent(raw, func(contentFile string) (string, string, error) {
|
||||
resolvedPath := strings.TrimSpace(contentFile)
|
||||
if !filepath.IsAbs(resolvedPath) {
|
||||
resolvedPath = filepath.Join(promptDir, resolvedPath)
|
||||
}
|
||||
resolvedPath = filepath.Clean(resolvedPath)
|
||||
|
||||
body, err := os.ReadFile(resolvedPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(body), resolvedPath, nil
|
||||
})
|
||||
}
|
||||
|
||||
func normalizePromptDefinitionFromFS(raw *promptDefinitionFile, fsys fs.FS, root string, sourcePath string, rootIsDir bool) (*domain.PromptDefinition, error) {
|
||||
promptDir := path.Dir(sourcePath)
|
||||
return normalizePromptDefinitionWithContent(raw, func(contentFile string) (string, string, error) {
|
||||
var resolvedPath string
|
||||
if rootIsDir {
|
||||
var err error
|
||||
resolvedPath, _, err = filecatalog.ResolveFSPath(root, promptDir, contentFile)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else {
|
||||
resolvedPath = strings.TrimSpace(contentFile)
|
||||
if !path.IsAbs(resolvedPath) {
|
||||
resolvedPath = path.Join(promptDir, resolvedPath)
|
||||
}
|
||||
resolvedPath = strings.TrimPrefix(path.Clean(resolvedPath), "/")
|
||||
}
|
||||
|
||||
body, err := fs.ReadFile(fsys, resolvedPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return string(body), resolvedPath, nil
|
||||
})
|
||||
}
|
||||
|
||||
func normalizePromptDefinitionWithContent(raw *promptDefinitionFile, readContentFile func(string) (string, string, error)) (*domain.PromptDefinition, error) {
|
||||
if raw == nil {
|
||||
return nil, errors.New("prompt definition is nil")
|
||||
}
|
||||
|
||||
id := strings.TrimSpace(raw.ID)
|
||||
if id == "" {
|
||||
return nil, errors.New("id is required")
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(raw.Version)
|
||||
if version == "" {
|
||||
return nil, errors.New("version is required")
|
||||
}
|
||||
|
||||
if len(raw.Messages) == 0 {
|
||||
return nil, errors.New("at least one message is required")
|
||||
}
|
||||
|
||||
inputs := make([]domain.PromptInput, 0, len(raw.Inputs))
|
||||
seenInputNames := make(map[string]struct{}, len(raw.Inputs))
|
||||
for i, in := range raw.Inputs {
|
||||
name := strings.TrimSpace(in.Name)
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("input %d has empty name", i)
|
||||
}
|
||||
if _, exists := seenInputNames[name]; exists {
|
||||
return nil, fmt.Errorf("duplicate input name %q", name)
|
||||
}
|
||||
seenInputNames[name] = struct{}{}
|
||||
|
||||
inputs = append(inputs, domain.PromptInput{
|
||||
Name: name,
|
||||
Required: in.Required,
|
||||
ContentType: strings.TrimSpace(in.ContentType),
|
||||
Description: strings.TrimSpace(in.Description),
|
||||
})
|
||||
}
|
||||
|
||||
templates := make([]domain.PromptMessageTemplate, 0, len(raw.Messages))
|
||||
for i, msg := range raw.Messages {
|
||||
role := strings.TrimSpace(msg.Role)
|
||||
if role == "" {
|
||||
return nil, fmt.Errorf("message %d role is required", i)
|
||||
}
|
||||
|
||||
hasContent := strings.TrimSpace(msg.Content) != ""
|
||||
hasContentFile := strings.TrimSpace(msg.ContentFile) != ""
|
||||
if hasContent == hasContentFile {
|
||||
return nil, fmt.Errorf("message %d (%s) must set exactly one of content or content_file", i, role)
|
||||
}
|
||||
|
||||
cacheControl, err := normalizeCacheControl(msg.CacheControl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("message %d (%s) cache_control: %w", i, role, err)
|
||||
}
|
||||
|
||||
templateContent := msg.Content
|
||||
resolvedContentFile := ""
|
||||
if hasContentFile {
|
||||
body, resolvedPath, err := readContentFile(msg.ContentFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prompt %q message %d (%s): failed to read content_file %q: %w", id, i, role, msg.ContentFile, err)
|
||||
}
|
||||
templateContent = body
|
||||
resolvedContentFile = resolvedPath
|
||||
}
|
||||
|
||||
templates = append(templates, domain.PromptMessageTemplate{
|
||||
Role: role,
|
||||
Content: templateContent,
|
||||
ContentFile: resolvedContentFile,
|
||||
CacheControl: cacheControl,
|
||||
})
|
||||
}
|
||||
|
||||
if !isValidOutputFormat(raw.Output.Format) {
|
||||
return nil, fmt.Errorf("invalid output format: %q", raw.Output.Format)
|
||||
}
|
||||
if !isValidValidationMode(raw.Output.ValidationMode) {
|
||||
return nil, fmt.Errorf("invalid validation mode: %q", raw.Output.ValidationMode)
|
||||
}
|
||||
if raw.Output.ValidationMode == domain.ValidationJSONSchema && strings.TrimSpace(raw.Output.SchemaPath) == "" {
|
||||
return nil, errors.New("output.schema_path is required when output.validation_mode is json_schema")
|
||||
}
|
||||
if raw.Output.RepairAttempts < 0 {
|
||||
return nil, errors.New("output.repair_attempts must be greater than or equal to 0")
|
||||
}
|
||||
|
||||
defaultProfile := ""
|
||||
if raw.DefaultProfile != nil {
|
||||
defaultProfile = strings.TrimSpace(*raw.DefaultProfile)
|
||||
if defaultProfile == "" {
|
||||
return nil, errors.New("default_profile must be a non-empty string when set")
|
||||
}
|
||||
}
|
||||
|
||||
return &domain.PromptDefinition{
|
||||
ID: id,
|
||||
Version: version,
|
||||
DefaultProfile: defaultProfile,
|
||||
Description: strings.TrimSpace(raw.Description),
|
||||
SessionID: strings.TrimSpace(raw.SessionID),
|
||||
Inputs: inputs,
|
||||
Templates: templates,
|
||||
OutputFormat: raw.Output.Format,
|
||||
Validation: domain.OutputContract{
|
||||
Format: raw.Output.Format,
|
||||
ValidationMode: raw.Output.ValidationMode,
|
||||
SchemaPath: strings.TrimSpace(raw.Output.SchemaPath),
|
||||
RepairAttempts: raw.Output.RepairAttempts,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeCacheControl(raw *cacheControlFile) (*domain.CacheControl, error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cacheType := strings.TrimSpace(raw.Type)
|
||||
if cacheType == "" {
|
||||
return nil, errors.New("type is required")
|
||||
}
|
||||
if domain.CacheControlType(cacheType) != domain.CacheControlEphemeral {
|
||||
return nil, fmt.Errorf("unsupported type %q", cacheType)
|
||||
}
|
||||
|
||||
ttl := strings.TrimSpace(raw.TTL)
|
||||
if ttl != "" && ttl != "1h" {
|
||||
return nil, fmt.Errorf("unsupported ttl %q", ttl)
|
||||
}
|
||||
|
||||
return &domain.CacheControl{
|
||||
Type: domain.CacheControlType(cacheType),
|
||||
TTL: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isValidOutputFormat(f domain.OutputFormat) bool {
|
||||
switch f {
|
||||
case domain.FormatText, domain.FormatMarkdown, domain.FormatJSON:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isValidValidationMode(m domain.ValidationMode) bool {
|
||||
switch m {
|
||||
case domain.ValidationNone, domain.ValidationBasic, domain.ValidationJSON, domain.ValidationJSONSchema:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
12
internal/promptdef/repository.go
Normal file
12
internal/promptdef/repository.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package promptdef
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
// Repository loads prompt definitions.
|
||||
type Repository interface {
|
||||
GetPromptDefinition(ctx context.Context, id string, version string) (*domain.PromptDefinition, error)
|
||||
}
|
||||
526
internal/promptdef/repository_test.go
Normal file
526
internal/promptdef/repository_test.go
Normal file
@@ -0,0 +1,526 @@
|
||||
package promptdef
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
func TestFilesystemRepository_GetPromptDefinition(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
if err := copyTree("testdata", tmpDir); err != nil {
|
||||
t.Fatalf("failed to copy testdata: %v", err)
|
||||
}
|
||||
|
||||
repo := NewFilesystemRepository(tmpDir)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("valid inline prompt", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "valid-inline", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if p.ID != "valid-inline" {
|
||||
t.Fatalf("unexpected id: %q", p.ID)
|
||||
}
|
||||
if p.Version != "1.0.0" {
|
||||
t.Fatalf("unexpected version: %q", p.Version)
|
||||
}
|
||||
if p.OutputFormat != domain.FormatMarkdown {
|
||||
t.Fatalf("unexpected output format: %q", p.OutputFormat)
|
||||
}
|
||||
if p.Validation.ValidationMode != domain.ValidationBasic {
|
||||
t.Fatalf("unexpected validation mode: %q", p.Validation.ValidationMode)
|
||||
}
|
||||
if len(p.Templates) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
||||
}
|
||||
if len(p.Inputs) != 1 {
|
||||
t.Fatalf("expected 1 input, got %d", len(p.Inputs))
|
||||
}
|
||||
if p.Inputs[0].ContentType != "text/markdown" {
|
||||
t.Fatalf("expected input content_type to be preserved, got %q", p.Inputs[0].ContentType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid file-backed prompt", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "valid-file-backed", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(p.Templates) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
||||
}
|
||||
if !strings.Contains(p.Templates[1].Content, "{{input \"transcript\"}}") {
|
||||
t.Fatalf("expected content_file template body to be loaded, got %q", p.Templates[1].Content)
|
||||
}
|
||||
if p.Templates[1].ContentFile == "" {
|
||||
t.Fatal("expected ContentFile source metadata to be preserved")
|
||||
}
|
||||
if !filepath.IsAbs(p.Templates[1].ContentFile) {
|
||||
t.Fatalf("expected resolved content_file path to be absolute, got %q", p.Templates[1].ContentFile)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid cache control with ttl", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "valid-cache-control-ttl", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(p.Templates) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
||||
}
|
||||
assertCacheControl(t, p.Templates[0].CacheControl, domain.CacheControlEphemeral, "1h")
|
||||
if p.Templates[1].CacheControl != nil {
|
||||
t.Fatalf("expected second message cache control to be nil, got %#v", p.Templates[1].CacheControl)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid cache control without ttl", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "valid-cache-control-without-ttl", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(p.Templates) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(p.Templates))
|
||||
}
|
||||
assertCacheControl(t, p.Templates[0].CacheControl, domain.CacheControlEphemeral, "")
|
||||
if p.Templates[1].CacheControl != nil {
|
||||
t.Fatalf("expected second message cache control to be nil, got %#v", p.Templates[1].CacheControl)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid session id template", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "valid-session-id", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if p.SessionID != "{{ .session_id }}" {
|
||||
t.Fatalf("expected trimmed session_id template, got %q", p.SessionID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid nested file-backed prompt resolves content file relative to nested YAML", func(t *testing.T) {
|
||||
nestedDir := filepath.Join(tmpDir, "dnd", "recap")
|
||||
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "nested_recap.yaml"), `
|
||||
id: nested-recap
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./nested_recap.user.tmpl
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "nested_recap.user.tmpl"), `Nested recap: {{input "transcript"}}`)
|
||||
|
||||
p, err := repo.GetPromptDefinition(ctx, "nested-recap", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(p.Templates) != 1 {
|
||||
t.Fatalf("expected one template, got %d", len(p.Templates))
|
||||
}
|
||||
if !strings.Contains(p.Templates[0].Content, "Nested recap") {
|
||||
t.Fatalf("expected nested content file body, got %q", p.Templates[0].Content)
|
||||
}
|
||||
if !strings.Contains(p.Templates[0].ContentFile, filepath.Join("dnd", "recap", "nested_recap.user.tmpl")) {
|
||||
t.Fatalf("expected nested content file path, got %q", p.Templates[0].ContentFile)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prompt with default_profile", func(t *testing.T) {
|
||||
p, err := repo.GetPromptDefinition(ctx, "with-default-profile", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if p.DefaultProfile != "local-default" {
|
||||
t.Fatalf("unexpected default profile: %q", p.DefaultProfile)
|
||||
}
|
||||
if len(p.Inputs) != 1 {
|
||||
t.Fatalf("expected one input, got %d", len(p.Inputs))
|
||||
}
|
||||
if p.Inputs[0].ContentType != "" {
|
||||
t.Fatalf("expected missing content_type to remain empty, got %q", p.Inputs[0].ContentType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate prompt IDs fail as ambiguous", func(t *testing.T) {
|
||||
writePromptTestFile(t, filepath.Join(tmpDir, "duplicate_a.yaml"), `
|
||||
id: duplicate-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: First duplicate.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
nestedDir := filepath.Join(tmpDir, "nested")
|
||||
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "duplicate_b.yaml"), `
|
||||
id: duplicate-prompt
|
||||
version: "2.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: Second duplicate.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
|
||||
_, err := repo.GetPromptDefinition(ctx, "duplicate-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidPromptDefinition) {
|
||||
t.Fatalf("expected duplicate prompt to return ErrInvalidPromptDefinition, got %v", err)
|
||||
}
|
||||
for _, want := range []string{"duplicate prompt definition id", "duplicate_a.yaml", filepath.Join("nested", "duplicate_b.yaml")} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("expected error to contain %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate prompt ID and requested version fails as ambiguous", func(t *testing.T) {
|
||||
writePromptTestFile(t, filepath.Join(tmpDir, "version_duplicate_a.yaml"), `
|
||||
id: duplicate-version-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: First duplicate version.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
nestedDir := filepath.Join(tmpDir, "versioned")
|
||||
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "version_duplicate_b.yaml"), `
|
||||
id: duplicate-version-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: Second duplicate version.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
|
||||
_, err := repo.GetPromptDefinition(ctx, "duplicate-version-prompt", "1.0.0")
|
||||
if !errors.Is(err, ErrInvalidPromptDefinition) {
|
||||
t.Fatalf("expected duplicate prompt version to return ErrInvalidPromptDefinition, got %v", err)
|
||||
}
|
||||
for _, want := range []string{"duplicate prompt definition id", "version \"1.0.0\"", "version_duplicate_a.yaml", filepath.Join("versioned", "version_duplicate_b.yaml")} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Fatalf("expected error to contain %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-matching malformed nested prompt is ignored for not found lookup", func(t *testing.T) {
|
||||
nestedDir := filepath.Join(tmpDir, "broken")
|
||||
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "unrelated.yaml"), "id: [")
|
||||
|
||||
_, err := repo.GetPromptDefinition(ctx, "does-not-exist-even-with-broken-nested-file", "")
|
||||
if !errors.Is(err, ErrPromptDefinitionNotFound) {
|
||||
t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("strict decode failure in nested prompt matches by YAML ID", func(t *testing.T) {
|
||||
nestedDir := filepath.Join(tmpDir, "strict")
|
||||
if err := os.MkdirAll(nestedDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writePromptTestFile(t, filepath.Join(nestedDir, "not_named_like_id.yaml"), `
|
||||
id: nested-strict-error
|
||||
version: "1.0.0"
|
||||
unknown_field: true
|
||||
messages:
|
||||
- role: user
|
||||
content: Invalid because of unknown field.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)
|
||||
|
||||
_, err := repo.GetPromptDefinition(ctx, "nested-strict-error", "")
|
||||
if !errors.Is(err, ErrInvalidYAML) {
|
||||
t.Fatalf("expected ErrInvalidYAML, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), filepath.Join("strict", "not_named_like_id.yaml")) {
|
||||
t.Fatalf("expected nested path in error, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("version lookup", func(t *testing.T) {
|
||||
_, err := repo.GetPromptDefinition(ctx, "valid-inline", "9.9.9")
|
||||
if !errors.Is(err, ErrPromptDefinitionNotFound) {
|
||||
t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
id string
|
||||
targetErr error
|
||||
errSubstrs []string
|
||||
}{
|
||||
{name: "invalid YAML", id: "invalid_yaml", targetErr: ErrInvalidYAML},
|
||||
{name: "missing id", id: "missing_id", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"id is required"}},
|
||||
{name: "no messages", id: "no_messages", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"at least one message is required"}},
|
||||
{name: "both content and content_file", id: "both_content_and_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}},
|
||||
{name: "neither content nor content_file", id: "neither_content_nor_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"exactly one"}},
|
||||
{name: "missing content_file", id: "missing_content_file", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"failed to read content_file"}},
|
||||
{name: "duplicate input names", id: "duplicate_input_names", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"duplicate input name"}},
|
||||
{name: "invalid validation mode", id: "invalid_validation_mode", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"invalid validation mode"}},
|
||||
{name: "json_schema without schema_path", id: "json_schema_without_schema_path", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"schema_path"}},
|
||||
{name: "unknown input field", id: "unknown_input_field", targetErr: ErrInvalidYAML, errSubstrs: []string{"field unknown_input_setting not found"}},
|
||||
{name: "empty cache control type", id: "empty_cache_control_type", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"cache_control", "type is required"}},
|
||||
{name: "unsupported cache control type", id: "unsupported_cache_control_type", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"cache_control", "unsupported type"}},
|
||||
{name: "unsupported cache control ttl", id: "unsupported_cache_control_ttl", targetErr: ErrInvalidPromptDefinition, errSubstrs: []string{"cache_control", "unsupported ttl"}},
|
||||
{name: "unknown cache control field", id: "unknown_cache_control_field", targetErr: ErrInvalidYAML, errSubstrs: []string{"field unexpected not found"}},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := repo.GetPromptDefinition(ctx, tc.id, "")
|
||||
if !errors.Is(err, tc.targetErr) {
|
||||
t.Fatalf("expected %v, got %v", tc.targetErr, err)
|
||||
}
|
||||
for _, sub := range tc.errSubstrs {
|
||||
if !strings.Contains(err.Error(), sub) {
|
||||
t.Fatalf("expected error to contain %q, got %v", sub, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("prompt definition not found", func(t *testing.T) {
|
||||
_, err := repo.GetPromptDefinition(ctx, "does-not-exist", "")
|
||||
if !errors.Is(err, ErrPromptDefinitionNotFound) {
|
||||
t.Fatalf("expected ErrPromptDefinitionNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFSRepositoryGetPromptDefinition(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"prompts/nested/prompt.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: fs-prompt
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./messages/user.tmpl
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"prompts/nested/messages/user.tmpl": &fstest.MapFile{Data: []byte(`Summarize {{input "transcript"}}.`)},
|
||||
}, "prompts")
|
||||
|
||||
got, err := repo.GetPromptDefinition(context.Background(), "fs-prompt", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if got.ID != "fs-prompt" {
|
||||
t.Fatalf("unexpected prompt id: %q", got.ID)
|
||||
}
|
||||
if len(got.Templates) != 1 || !strings.Contains(got.Templates[0].Content, `{{input "transcript"}}`) {
|
||||
t.Fatalf("expected content_file body to be loaded, got %+v", got.Templates)
|
||||
}
|
||||
if got.Templates[0].ContentFile != "prompts/nested/messages/user.tmpl" {
|
||||
t.Fatalf("unexpected content file path: %q", got.Templates[0].ContentFile)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSRepositoryContentFileContainment(t *testing.T) {
|
||||
t.Run("nested prompt can reference file inside root", func(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"prompts/nested/prompt.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: fs-contained-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ../shared/user.tmpl
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"prompts/shared/user.tmpl": &fstest.MapFile{Data: []byte(`Inside root.`)},
|
||||
}, "prompts")
|
||||
|
||||
got, err := repo.GetPromptDefinition(context.Background(), "fs-contained-prompt", "")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if len(got.Templates) != 1 || got.Templates[0].Content != "Inside root." {
|
||||
t.Fatalf("expected contained content file, got %+v", got.Templates)
|
||||
}
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
contentFile string
|
||||
wantErr string
|
||||
}{
|
||||
{name: "parent escape rejected", contentFile: "../outside.tmpl", wantErr: "escapes source root"},
|
||||
{name: "absolute path rejected", contentFile: "/outside.tmpl", wantErr: "must be relative"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"prompts/prompt.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: fs-escaped-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ` + tc.contentFile + `
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"outside.tmpl": &fstest.MapFile{Data: []byte(`Outside root.`)},
|
||||
}, "prompts")
|
||||
|
||||
_, err := repo.GetPromptDefinition(context.Background(), "fs-escaped-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidPromptDefinition) {
|
||||
t.Fatalf("expected ErrInvalidPromptDefinition, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected error to contain %q, got %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSRepositoryRejectsDuplicatePromptIDs(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"one.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: duplicate-fs-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: First.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"nested/two.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: duplicate-fs-prompt
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: Second.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
}, ".")
|
||||
|
||||
_, err := repo.GetPromptDefinition(context.Background(), "duplicate-fs-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidPromptDefinition) {
|
||||
t.Fatalf("expected ErrInvalidPromptDefinition, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "one.yaml") || !strings.Contains(err.Error(), "nested/two.yaml") {
|
||||
t.Fatalf("expected duplicate paths in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFSRepositoryRejectsUnknownYAMLFields(t *testing.T) {
|
||||
repo := NewFSRepository(fstest.MapFS{
|
||||
"not_named_like_id.yaml": &fstest.MapFile{Data: []byte(`
|
||||
id: strict-fs-prompt
|
||||
version: "1.0.0"
|
||||
unknown: true
|
||||
messages:
|
||||
- role: user
|
||||
content: Invalid.
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
}, ".")
|
||||
|
||||
_, err := repo.GetPromptDefinition(context.Background(), "strict-fs-prompt", "")
|
||||
if !errors.Is(err, ErrInvalidYAML) {
|
||||
t.Fatalf("expected ErrInvalidYAML, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCacheControl(t *testing.T, got *domain.CacheControl, wantType domain.CacheControlType, wantTTL string) {
|
||||
t.Helper()
|
||||
if got == nil {
|
||||
t.Fatal("expected cache control, got nil")
|
||||
}
|
||||
if got.Type != wantType {
|
||||
t.Fatalf("unexpected cache control type: got %q want %q", got.Type, wantType)
|
||||
}
|
||||
if got.TTL != wantTTL {
|
||||
t.Fatalf("unexpected cache control ttl: got %q want %q", got.TTL, wantTTL)
|
||||
}
|
||||
}
|
||||
|
||||
func writePromptTestFile(t *testing.T, path string, content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(strings.TrimLeft(content, "\n")), 0o644); err != nil {
|
||||
t.Fatalf("failed to write prompt test file %q: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func copyTree(src, dst string) error {
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(src, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rel == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
target := filepath.Join(dst, rel)
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0o755)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, 0o644)
|
||||
})
|
||||
}
|
||||
10
internal/promptdef/testdata/both_content_and_content_file.yaml
vendored
Normal file
10
internal/promptdef/testdata/both_content_and_content_file.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
id: both-content-and-content-file
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hi"
|
||||
content_file: ./messages/user_prompt.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
14
internal/promptdef/testdata/duplicate_input_names.yaml
vendored
Normal file
14
internal/promptdef/testdata/duplicate_input_names.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
id: duplicate-input-names
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
- name: transcript
|
||||
required: false
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hi"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
10
internal/promptdef/testdata/empty_cache_control_type.yaml
vendored
Normal file
10
internal/promptdef/testdata/empty_cache_control_type.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
id: empty-cache-control-type
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control: {}
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
9
internal/promptdef/testdata/invalid_validation_mode.yaml
vendored
Normal file
9
internal/promptdef/testdata/invalid_validation_mode.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
id: invalid-validation-mode
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hi"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: nope
|
||||
repair_attempts: 0
|
||||
9
internal/promptdef/testdata/invalid_yaml.yaml
vendored
Normal file
9
internal/promptdef/testdata/invalid_yaml.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
id: invalid-yaml
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: [broken
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
9
internal/promptdef/testdata/json_schema_without_schema_path.yaml
vendored
Normal file
9
internal/promptdef/testdata/json_schema_without_schema_path.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
id: json-schema-without-schema-path
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: "Return JSON"
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
repair_attempts: 0
|
||||
2
internal/promptdef/testdata/messages/user_prompt.tmpl
vendored
Normal file
2
internal/promptdef/testdata/messages/user_prompt.tmpl
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
Use transcript:
|
||||
{{input "transcript"}}
|
||||
9
internal/promptdef/testdata/missing_content_file.yaml
vendored
Normal file
9
internal/promptdef/testdata/missing_content_file.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
id: missing-content-file
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content_file: ./messages/does_not_exist.tmpl
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
8
internal/promptdef/testdata/missing_id.yaml
vendored
Normal file
8
internal/promptdef/testdata/missing_id.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hi"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
8
internal/promptdef/testdata/neither_content_nor_content_file.yaml
vendored
Normal file
8
internal/promptdef/testdata/neither_content_nor_content_file.yaml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
id: neither-content-nor-content-file
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: user
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
6
internal/promptdef/testdata/no_messages.yaml
vendored
Normal file
6
internal/promptdef/testdata/no_messages.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
id: no-messages
|
||||
version: "1.0.0"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
12
internal/promptdef/testdata/unknown_cache_control_field.yaml
vendored
Normal file
12
internal/promptdef/testdata/unknown_cache_control_field.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
id: unknown-cache-control-field
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
unexpected: true
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
13
internal/promptdef/testdata/unknown_input_field.yaml
vendored
Normal file
13
internal/promptdef/testdata/unknown_input_field.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
id: unknown-input-field
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
unknown_input_setting: true
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hi"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
12
internal/promptdef/testdata/unsupported_cache_control_ttl.yaml
vendored
Normal file
12
internal/promptdef/testdata/unsupported_cache_control_ttl.yaml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
id: unsupported-cache-control-ttl
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
ttl: 5m
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
11
internal/promptdef/testdata/unsupported_cache_control_type.yaml
vendored
Normal file
11
internal/promptdef/testdata/unsupported_cache_control_type.yaml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
id: unsupported-cache-control-type
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control:
|
||||
type: persistent
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
14
internal/promptdef/testdata/valid_cache_control_ttl.yaml
vendored
Normal file
14
internal/promptdef/testdata/valid_cache_control_ttl.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
id: valid-cache-control-ttl
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
ttl: 1h
|
||||
- role: user
|
||||
content: "Summarize the input."
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
13
internal/promptdef/testdata/valid_cache_control_without_ttl.yaml
vendored
Normal file
13
internal/promptdef/testdata/valid_cache_control_without_ttl.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
id: valid-cache-control-without-ttl
|
||||
version: "1.0.0"
|
||||
messages:
|
||||
- role: system
|
||||
content: "Use cached instructions."
|
||||
cache_control:
|
||||
type: ephemeral
|
||||
- role: user
|
||||
content: "Summarize the input."
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
14
internal/promptdef/testdata/valid_file_backed.yaml
vendored
Normal file
14
internal/promptdef/testdata/valid_file_backed.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
id: valid-file-backed
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
messages:
|
||||
- role: system
|
||||
content: "Return markdown."
|
||||
- role: user
|
||||
content_file: ./messages/user_prompt.tmpl
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
18
internal/promptdef/testdata/valid_inline.yaml
vendored
Normal file
18
internal/promptdef/testdata/valid_inline.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
id: valid-inline
|
||||
version: "1.0.0"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
content_type: text/markdown
|
||||
description: Transcript content
|
||||
messages:
|
||||
- role: system
|
||||
content: "You are concise."
|
||||
- role: user
|
||||
content: |
|
||||
Summarize:
|
||||
{{input "transcript"}}
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
10
internal/promptdef/testdata/valid_session_id.yaml
vendored
Normal file
10
internal/promptdef/testdata/valid_session_id.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
id: valid-session-id
|
||||
version: "1.0.0"
|
||||
session_id: " {{ .session_id }} "
|
||||
messages:
|
||||
- role: user
|
||||
content: Hello.
|
||||
output:
|
||||
format: markdown
|
||||
validation_mode: basic
|
||||
repair_attempts: 0
|
||||
13
internal/promptdef/testdata/with_default_profile.yaml
vendored
Normal file
13
internal/promptdef/testdata/with_default_profile.yaml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
id: with-default-profile
|
||||
version: "1.0.0"
|
||||
default_profile: local-default
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
messages:
|
||||
- role: user
|
||||
content: "Write output"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
repair_attempts: 0
|
||||
Reference in New Issue
Block a user