Make shared prompt filesystem composition generic
This commit is contained in:
@@ -18,7 +18,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
{Name: "dnd.scenes.yaml", Path: "assets/prompts/dnd.scenes.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
}, sharedassets.SharedPromptFiles()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare scene prompt assets: %w", err)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
{Name: "dnd.spells.yaml", Path: "assets/prompts/dnd.spells.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
{Name: "instructions.md", Path: "assets/prompts/instructions.md"},
|
||||
})
|
||||
}, sharedassets.SharedPromptFiles()...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare spell prompt assets: %w", err)
|
||||
}
|
||||
|
||||
@@ -9,10 +9,28 @@ import (
|
||||
//go:embed assets/prompts/*.md
|
||||
var embeddedAssets embed.FS
|
||||
|
||||
var commonDNDPromptFiles = []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
}
|
||||
|
||||
func Register(registry *llm.AssetRegistry) error {
|
||||
return registry.RegisterPromptFS(embeddedAssets, "assets/prompts")
|
||||
}
|
||||
|
||||
func SharedPromptFiles() []SharedPromptFile {
|
||||
files := make([]SharedPromptFile, 0, len(commonDNDPromptFiles))
|
||||
for _, name := range commonDNDPromptFiles {
|
||||
files = append(files, SharedPromptFile{
|
||||
Name: name,
|
||||
FS: embeddedAssets,
|
||||
Path: "assets/prompts/" + name,
|
||||
})
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func CommonHashParts() []llm.AssetHashPart {
|
||||
return []llm.AssetHashPart{
|
||||
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"},
|
||||
|
||||
@@ -18,14 +18,18 @@ type ModulePromptFile struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// ModulePromptFS builds a prompt filesystem for a module directory from
|
||||
// module-owned prompt files plus common D&D shared prompt files under the
|
||||
// module's sharedassets subdirectory.
|
||||
func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile) (fs.FS, error) {
|
||||
return modulePromptFS(moduleDir, moduleFS, files, embeddedAssets)
|
||||
// SharedPromptFile maps a caller-owned shared prompt file into a module's
|
||||
// Scriptorium-visible sharedassets prompt subdirectory.
|
||||
type SharedPromptFile struct {
|
||||
Name string
|
||||
FS fs.FS
|
||||
Path string
|
||||
}
|
||||
|
||||
func modulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile, sharedFS fs.FS) (fs.FS, error) {
|
||||
// ModulePromptFS builds a prompt filesystem for a module directory from
|
||||
// module-owned prompt files plus caller-provided shared prompt files under the
|
||||
// module's sharedassets subdirectory.
|
||||
func ModulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile, sharedFiles ...SharedPromptFile) (fs.FS, error) {
|
||||
cleanModuleDir, err := cleanPromptPath(moduleDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("module prompt directory: %w", err)
|
||||
@@ -33,10 +37,7 @@ func modulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
if moduleFS == nil {
|
||||
return nil, fmt.Errorf("module prompt filesystem must not be nil")
|
||||
}
|
||||
if sharedFS == nil {
|
||||
return nil, fmt.Errorf("shared prompt filesystem must not be nil")
|
||||
}
|
||||
assets := make(promptMapFS, len(files)+len(commonDNDPromptFiles))
|
||||
assets := make(promptMapFS, len(files)+len(sharedFiles))
|
||||
for _, file := range files {
|
||||
name, err := cleanPromptPath(file.Name)
|
||||
if err != nil {
|
||||
@@ -56,22 +57,30 @@ func modulePromptFS(moduleDir string, moduleFS fs.FS, files []ModulePromptFile,
|
||||
assets["assets/prompts/"+cleanModuleDir+"/"+name] = append([]byte(nil), data...)
|
||||
}
|
||||
|
||||
for _, name := range commonDNDPromptFiles {
|
||||
data, err := fs.ReadFile(sharedFS, "assets/prompts/"+name)
|
||||
for _, file := range sharedFiles {
|
||||
name, err := cleanPromptPath(file.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read shared prompt asset %s: %w", name, err)
|
||||
return nil, fmt.Errorf("shared prompt file name %q: %w", file.Name, err)
|
||||
}
|
||||
if strings.Contains(name, "/") {
|
||||
return nil, fmt.Errorf("shared prompt file name %q must not contain path separators", file.Name)
|
||||
}
|
||||
if file.FS == nil {
|
||||
return nil, fmt.Errorf("shared prompt file %q filesystem must not be nil", file.Name)
|
||||
}
|
||||
filePath, err := cleanPromptPath(file.Path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("shared prompt file path %q: %w", file.Path, err)
|
||||
}
|
||||
data, err := fs.ReadFile(file.FS, filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read shared prompt asset %s: %w", filePath, err)
|
||||
}
|
||||
assets["assets/prompts/"+cleanModuleDir+"/sharedassets/"+name] = append([]byte(nil), data...)
|
||||
}
|
||||
return assets, nil
|
||||
}
|
||||
|
||||
var commonDNDPromptFiles = []string{
|
||||
"common-dnd-system.md",
|
||||
"common-dnd-transcript.md",
|
||||
"common-dnd-references.md",
|
||||
}
|
||||
|
||||
type promptMapFS map[string][]byte
|
||||
|
||||
func (m promptMapFS) Open(name string) (fs.File, error) {
|
||||
|
||||
@@ -8,34 +8,40 @@ import (
|
||||
)
|
||||
|
||||
func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) {
|
||||
fsys, err := ModulePromptFS("dnd.test", fstest.MapFS{
|
||||
"assets/prompts/dnd.test.yaml": {Data: []byte("id: dnd.test")},
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
sharedFS := fstest.MapFS{
|
||||
"shared/system.md": {Data: []byte("system")},
|
||||
"shared/reference.md": {Data: []byte("reference")},
|
||||
"nested/transcript.md": {Data: []byte("transcript")},
|
||||
}
|
||||
fsys, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/module.test.yaml": {Data: []byte("id: module.test")},
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "dnd.test.yaml", Path: "assets/prompts/dnd.test.yaml"},
|
||||
{Name: "module.test.yaml", Path: "assets/prompts/module.test.yaml"},
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
})
|
||||
},
|
||||
SharedPromptFile{Name: "system.md", FS: sharedFS, Path: "shared/system.md"},
|
||||
SharedPromptFile{Name: "reference.md", FS: sharedFS, Path: "shared/reference.md"},
|
||||
SharedPromptFile{Name: "transcript.md", FS: sharedFS, Path: "nested/transcript.md"},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nil", err)
|
||||
}
|
||||
|
||||
tests := map[string]string{
|
||||
"assets/prompts/dnd.test/dnd.test.yaml": "id: dnd.test",
|
||||
"assets/prompts/dnd.test/task.md": "task",
|
||||
"assets/prompts/dnd.test/sharedassets/common-dnd-system.md": "",
|
||||
"assets/prompts/dnd.test/sharedassets/common-dnd-transcript.md": "",
|
||||
"assets/prompts/dnd.test/sharedassets/common-dnd-references.md": "",
|
||||
"assets/prompts/module.test/module.test.yaml": "id: module.test",
|
||||
"assets/prompts/module.test/task.md": "task",
|
||||
"assets/prompts/module.test/sharedassets/system.md": "system",
|
||||
"assets/prompts/module.test/sharedassets/reference.md": "reference",
|
||||
"assets/prompts/module.test/sharedassets/transcript.md": "transcript",
|
||||
}
|
||||
for path, want := range tests {
|
||||
data, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%q) error = %v, want nil", path, err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
t.Fatalf("ReadFile(%q) returned empty content", path)
|
||||
}
|
||||
if want != "" && !strings.Contains(string(data), want) {
|
||||
t.Fatalf("ReadFile(%q) = %q, want substring %q", path, data, want)
|
||||
if string(data) != want {
|
||||
t.Fatalf("ReadFile(%q) = %q, want %q", path, data, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,10 +52,17 @@ func TestModulePromptFSCombinesModuleAndSharedPrompts(t *testing.T) {
|
||||
if len(entries) != 1 || entries[0].Name() != "assets" || !entries[0].IsDir() {
|
||||
t.Fatalf("ReadDir(.) = %#v, want assets directory", entries)
|
||||
}
|
||||
entries, err = fs.ReadDir(fsys, "assets/prompts/module.test/sharedassets")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadDir(sharedassets) error = %v, want nil", err)
|
||||
}
|
||||
if len(entries) != 3 || entries[0].Name() != "reference.md" || entries[1].Name() != "system.md" || entries[2].Name() != "transcript.md" {
|
||||
t.Fatalf("ReadDir(sharedassets) = %#v, want sorted shared prompt entries", entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsMissingModuleFile(t *testing.T) {
|
||||
_, err := ModulePromptFS("dnd.test", fstest.MapFS{}, []ModulePromptFile{
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "read module prompt asset assets/prompts/task.md") {
|
||||
@@ -58,18 +71,18 @@ func TestModulePromptFSRejectsMissingModuleFile(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsMissingSharedFile(t *testing.T) {
|
||||
_, err := modulePromptFS("dnd.test", fstest.MapFS{
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
}, fstest.MapFS{})
|
||||
if err == nil || !strings.Contains(err.Error(), "read shared prompt asset common-dnd-system.md") {
|
||||
t.Fatalf("modulePromptFS() error = %v, want missing shared asset context", err)
|
||||
}, SharedPromptFile{Name: "system.md", FS: fstest.MapFS{}, Path: "shared/system.md"})
|
||||
if err == nil || !strings.Contains(err.Error(), "read shared prompt asset shared/system.md") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want missing shared asset context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) {
|
||||
_, err := ModulePromptFS("dnd.test", fstest.MapFS{
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "nested/task.md", Path: "assets/prompts/task.md"},
|
||||
@@ -78,3 +91,33 @@ func TestModulePromptFSRejectsNestedVirtualFileName(t *testing.T) {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nested file name error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsNestedSharedFileName(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", fstest.MapFS{
|
||||
"assets/prompts/task.md": {Data: []byte("task")},
|
||||
}, []ModulePromptFile{
|
||||
{Name: "task.md", Path: "assets/prompts/task.md"},
|
||||
}, SharedPromptFile{Name: "nested/system.md", FS: fstest.MapFS{}, Path: "shared/system.md"})
|
||||
if err == nil || !strings.Contains(err.Error(), "shared prompt file name") || !strings.Contains(err.Error(), "must not contain path separators") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nested shared file name error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsInvalidModuleDir(t *testing.T) {
|
||||
_, err := ModulePromptFS(".", fstest.MapFS{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "module prompt directory") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want module directory context", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulePromptFSRejectsNilFilesystems(t *testing.T) {
|
||||
_, err := ModulePromptFS("module.test", nil, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "module prompt filesystem must not be nil") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nil module filesystem error", err)
|
||||
}
|
||||
|
||||
_, err = ModulePromptFS("module.test", fstest.MapFS{}, nil, SharedPromptFile{Name: "system.md", Path: "shared/system.md"})
|
||||
if err == nil || !strings.Contains(err.Error(), "shared prompt file \"system.md\" filesystem must not be nil") {
|
||||
t.Fatalf("ModulePromptFS() error = %v, want nil shared filesystem error", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user