Flatten D&D module prompt asset trees
This commit is contained in:
@@ -2,5 +2,5 @@ package scenes
|
|||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:embed assets/schemas/*.json assets/scriptorium/prompts/*.yaml assets/scriptorium/prompts/dnd/scenes/*.md
|
//go:embed assets/schemas/*.json assets/prompts/*.yaml assets/prompts/*.md
|
||||||
var embeddedAssets embed.FS
|
var embeddedAssets embed.FS
|
||||||
|
|||||||
175
internal/modules/chunk/dnd/scenes/prompt_fs.go
Normal file
175
internal/modules/chunk/dnd/scenes/prompt_fs.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package scenes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modulePromptFS() (fs.FS, error) {
|
||||||
|
return promptMapFSFromEmbedded(map[string]string{
|
||||||
|
"assets/prompts/dnd.scenes.yaml": "assets/prompts/dnd.scenes.yaml",
|
||||||
|
"assets/prompts/dnd/scenes/task.md": "assets/prompts/task.md",
|
||||||
|
"assets/prompts/dnd/scenes/instructions.md": "assets/prompts/instructions.md",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptMapFSFromEmbedded(files map[string]string) (fs.FS, error) {
|
||||||
|
assets := make(promptMapFS, len(files))
|
||||||
|
for virtualPath, embeddedPath := range files {
|
||||||
|
data, err := fs.ReadFile(embeddedAssets, embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read prompt asset %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
assets[virtualPath] = append([]byte(nil), data...)
|
||||||
|
}
|
||||||
|
return assets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptMapFS map[string][]byte
|
||||||
|
|
||||||
|
func (m promptMapFS) Open(name string) (fs.File, error) {
|
||||||
|
cleaned, err := cleanPromptPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
if data, ok := m[cleaned]; ok {
|
||||||
|
return &promptFile{
|
||||||
|
reader: bytes.NewReader(data),
|
||||||
|
info: promptFileInfo{name: path.Base(cleaned), size: int64(len(data))},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
entries := m.dirEntries(cleaned)
|
||||||
|
if entries != nil {
|
||||||
|
return &promptDir{name: path.Base(cleaned), entries: entries}, nil
|
||||||
|
}
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m promptMapFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||||
|
cleaned, err := cleanPromptPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
entries := m.dirEntries(cleaned)
|
||||||
|
if entries == nil {
|
||||||
|
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m promptMapFS) dirEntries(dir string) []fs.DirEntry {
|
||||||
|
children := map[string]promptDirEntry{}
|
||||||
|
prefix := ""
|
||||||
|
if dir != "." {
|
||||||
|
prefix = dir + "/"
|
||||||
|
}
|
||||||
|
for name, data := range m {
|
||||||
|
if !strings.HasPrefix(name, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(name, prefix)
|
||||||
|
if rest == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childName, _, hasSlash := strings.Cut(rest, "/")
|
||||||
|
entry := promptDirEntry{name: childName, dir: hasSlash}
|
||||||
|
if !hasSlash {
|
||||||
|
entry.size = int64(len(data))
|
||||||
|
}
|
||||||
|
children[childName] = entry
|
||||||
|
}
|
||||||
|
if len(children) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(children))
|
||||||
|
for name := range children {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
entries := make([]fs.DirEntry, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
entries = append(entries, children[name])
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPromptPath(name string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(name)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", fmt.Errorf("path must not be empty")
|
||||||
|
}
|
||||||
|
cleaned := path.Clean(strings.TrimPrefix(trimmed, "/"))
|
||||||
|
if cleaned == "." || !fs.ValidPath(cleaned) {
|
||||||
|
return "", fmt.Errorf("invalid path %q", name)
|
||||||
|
}
|
||||||
|
return cleaned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptFile struct {
|
||||||
|
reader *bytes.Reader
|
||||||
|
info promptFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *promptFile) Stat() (fs.FileInfo, error) { return f.info, nil }
|
||||||
|
func (f *promptFile) Read(p []byte) (int, error) { return f.reader.Read(p) }
|
||||||
|
func (f *promptFile) Close() error { return nil }
|
||||||
|
|
||||||
|
type promptDir struct {
|
||||||
|
name string
|
||||||
|
offset int
|
||||||
|
entries []fs.DirEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *promptDir) Stat() (fs.FileInfo, error) { return promptFileInfo{name: d.name, dir: true}, nil }
|
||||||
|
func (d *promptDir) Read([]byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (d *promptDir) Close() error { return nil }
|
||||||
|
func (d *promptDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||||
|
if d.offset >= len(d.entries) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
end := len(d.entries)
|
||||||
|
if n > 0 && d.offset+n < end {
|
||||||
|
end = d.offset + n
|
||||||
|
}
|
||||||
|
out := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
|
||||||
|
d.offset = end
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptDirEntry struct {
|
||||||
|
name string
|
||||||
|
dir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e promptDirEntry) Name() string { return e.name }
|
||||||
|
func (e promptDirEntry) IsDir() bool { return e.dir }
|
||||||
|
func (e promptDirEntry) Type() fs.FileMode { return e.fileInfoMode().Type() }
|
||||||
|
func (e promptDirEntry) Info() (fs.FileInfo, error) {
|
||||||
|
return promptFileInfo{name: e.name, dir: e.dir, size: e.size}, nil
|
||||||
|
}
|
||||||
|
func (e promptDirEntry) fileInfoMode() fs.FileMode {
|
||||||
|
if e.dir {
|
||||||
|
return fs.ModeDir | 0o555
|
||||||
|
}
|
||||||
|
return 0o444
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptFileInfo struct {
|
||||||
|
name string
|
||||||
|
dir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i promptFileInfo) Name() string { return i.name }
|
||||||
|
func (i promptFileInfo) Size() int64 { return i.size }
|
||||||
|
func (i promptFileInfo) Mode() fs.FileMode { return promptDirEntry{dir: i.dir}.fileInfoMode() }
|
||||||
|
func (i promptFileInfo) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (i promptFileInfo) IsDir() bool { return i.dir }
|
||||||
|
func (i promptFileInfo) Sys() any { return nil }
|
||||||
@@ -11,10 +11,14 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scriptoriumPromptRoot = "assets/scriptorium/prompts"
|
const scriptoriumPromptRoot = "assets/prompts"
|
||||||
|
|
||||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||||
if err := registry.RegisterPromptFS(embeddedAssets, scriptoriumPromptRoot); err != nil {
|
promptFS, err := modulePromptFS()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("prepare scene prompt assets: %w", err)
|
||||||
|
}
|
||||||
|
if err := registry.RegisterPromptFS(promptFS, scriptoriumPromptRoot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas")
|
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas")
|
||||||
@@ -48,9 +52,9 @@ func referencePromptMaterial(name string, slot contracts.ResolvedReferenceSlot)
|
|||||||
func scriptoriumPromptMetadata() (string, error) {
|
func scriptoriumPromptMetadata() (string, error) {
|
||||||
scriptoriumPromptHashOnce.Do(func() {
|
scriptoriumPromptHashOnce.Do(func() {
|
||||||
parts := append([]llm.AssetHashPart{
|
parts := append([]llm.AssetHashPart{
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd.scenes.yaml"},
|
{FS: embeddedAssets, Path: "assets/prompts/dnd.scenes.yaml"},
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd/scenes/task.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd/scenes/instructions.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||||
}, append(sharedassets.CommonHashParts(), sharedassets.ReferenceHashParts()...)...)
|
}, append(sharedassets.CommonHashParts(), sharedassets.ReferenceHashParts()...)...)
|
||||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package spells
|
|||||||
|
|
||||||
import "embed"
|
import "embed"
|
||||||
|
|
||||||
//go:embed assets/schemas/*.json assets/scriptorium/prompts/*.yaml assets/scriptorium/prompts/dnd/spells/*.md
|
//go:embed assets/schemas/*.json assets/prompts/*.yaml assets/prompts/*.md
|
||||||
var embeddedAssets embed.FS
|
var embeddedAssets embed.FS
|
||||||
|
|||||||
175
internal/modules/extract/dnd/spells/prompt_fs.go
Normal file
175
internal/modules/extract/dnd/spells/prompt_fs.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package spells
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func modulePromptFS() (fs.FS, error) {
|
||||||
|
return promptMapFSFromEmbedded(map[string]string{
|
||||||
|
"assets/prompts/dnd.spells.yaml": "assets/prompts/dnd.spells.yaml",
|
||||||
|
"assets/prompts/dnd/spells/task.md": "assets/prompts/task.md",
|
||||||
|
"assets/prompts/dnd/spells/instructions.md": "assets/prompts/instructions.md",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptMapFSFromEmbedded(files map[string]string) (fs.FS, error) {
|
||||||
|
assets := make(promptMapFS, len(files))
|
||||||
|
for virtualPath, embeddedPath := range files {
|
||||||
|
data, err := fs.ReadFile(embeddedAssets, embeddedPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read prompt asset %s: %w", embeddedPath, err)
|
||||||
|
}
|
||||||
|
assets[virtualPath] = append([]byte(nil), data...)
|
||||||
|
}
|
||||||
|
return assets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptMapFS map[string][]byte
|
||||||
|
|
||||||
|
func (m promptMapFS) Open(name string) (fs.File, error) {
|
||||||
|
cleaned, err := cleanPromptPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
if data, ok := m[cleaned]; ok {
|
||||||
|
return &promptFile{
|
||||||
|
reader: bytes.NewReader(data),
|
||||||
|
info: promptFileInfo{name: path.Base(cleaned), size: int64(len(data))},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
entries := m.dirEntries(cleaned)
|
||||||
|
if entries != nil {
|
||||||
|
return &promptDir{name: path.Base(cleaned), entries: entries}, nil
|
||||||
|
}
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m promptMapFS) ReadDir(name string) ([]fs.DirEntry, error) {
|
||||||
|
cleaned, err := cleanPromptPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &fs.PathError{Op: "readdir", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
entries := m.dirEntries(cleaned)
|
||||||
|
if entries == nil {
|
||||||
|
return nil, &fs.PathError{Op: "readdir", Path: name, Err: fs.ErrNotExist}
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m promptMapFS) dirEntries(dir string) []fs.DirEntry {
|
||||||
|
children := map[string]promptDirEntry{}
|
||||||
|
prefix := ""
|
||||||
|
if dir != "." {
|
||||||
|
prefix = dir + "/"
|
||||||
|
}
|
||||||
|
for name, data := range m {
|
||||||
|
if !strings.HasPrefix(name, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(name, prefix)
|
||||||
|
if rest == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
childName, _, hasSlash := strings.Cut(rest, "/")
|
||||||
|
entry := promptDirEntry{name: childName, dir: hasSlash}
|
||||||
|
if !hasSlash {
|
||||||
|
entry.size = int64(len(data))
|
||||||
|
}
|
||||||
|
children[childName] = entry
|
||||||
|
}
|
||||||
|
if len(children) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(children))
|
||||||
|
for name := range children {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
entries := make([]fs.DirEntry, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
entries = append(entries, children[name])
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPromptPath(name string) (string, error) {
|
||||||
|
trimmed := strings.TrimSpace(name)
|
||||||
|
if trimmed == "" {
|
||||||
|
return "", fmt.Errorf("path must not be empty")
|
||||||
|
}
|
||||||
|
cleaned := path.Clean(strings.TrimPrefix(trimmed, "/"))
|
||||||
|
if cleaned == "." || !fs.ValidPath(cleaned) {
|
||||||
|
return "", fmt.Errorf("invalid path %q", name)
|
||||||
|
}
|
||||||
|
return cleaned, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptFile struct {
|
||||||
|
reader *bytes.Reader
|
||||||
|
info promptFileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *promptFile) Stat() (fs.FileInfo, error) { return f.info, nil }
|
||||||
|
func (f *promptFile) Read(p []byte) (int, error) { return f.reader.Read(p) }
|
||||||
|
func (f *promptFile) Close() error { return nil }
|
||||||
|
|
||||||
|
type promptDir struct {
|
||||||
|
name string
|
||||||
|
offset int
|
||||||
|
entries []fs.DirEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *promptDir) Stat() (fs.FileInfo, error) { return promptFileInfo{name: d.name, dir: true}, nil }
|
||||||
|
func (d *promptDir) Read([]byte) (int, error) { return 0, io.EOF }
|
||||||
|
func (d *promptDir) Close() error { return nil }
|
||||||
|
func (d *promptDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||||
|
if d.offset >= len(d.entries) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
end := len(d.entries)
|
||||||
|
if n > 0 && d.offset+n < end {
|
||||||
|
end = d.offset + n
|
||||||
|
}
|
||||||
|
out := append([]fs.DirEntry(nil), d.entries[d.offset:end]...)
|
||||||
|
d.offset = end
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptDirEntry struct {
|
||||||
|
name string
|
||||||
|
dir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e promptDirEntry) Name() string { return e.name }
|
||||||
|
func (e promptDirEntry) IsDir() bool { return e.dir }
|
||||||
|
func (e promptDirEntry) Type() fs.FileMode { return e.fileInfoMode().Type() }
|
||||||
|
func (e promptDirEntry) Info() (fs.FileInfo, error) {
|
||||||
|
return promptFileInfo{name: e.name, dir: e.dir, size: e.size}, nil
|
||||||
|
}
|
||||||
|
func (e promptDirEntry) fileInfoMode() fs.FileMode {
|
||||||
|
if e.dir {
|
||||||
|
return fs.ModeDir | 0o555
|
||||||
|
}
|
||||||
|
return 0o444
|
||||||
|
}
|
||||||
|
|
||||||
|
type promptFileInfo struct {
|
||||||
|
name string
|
||||||
|
dir bool
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i promptFileInfo) Name() string { return i.name }
|
||||||
|
func (i promptFileInfo) Size() int64 { return i.size }
|
||||||
|
func (i promptFileInfo) Mode() fs.FileMode { return promptDirEntry{dir: i.dir}.fileInfoMode() }
|
||||||
|
func (i promptFileInfo) ModTime() time.Time { return time.Time{} }
|
||||||
|
func (i promptFileInfo) IsDir() bool { return i.dir }
|
||||||
|
func (i promptFileInfo) Sys() any { return nil }
|
||||||
@@ -11,10 +11,14 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const scriptoriumPromptRoot = "assets/scriptorium/prompts"
|
const scriptoriumPromptRoot = "assets/prompts"
|
||||||
|
|
||||||
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||||
if err := registry.RegisterPromptFS(embeddedAssets, scriptoriumPromptRoot); err != nil {
|
promptFS, err := modulePromptFS()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("prepare spell prompt assets: %w", err)
|
||||||
|
}
|
||||||
|
if err := registry.RegisterPromptFS(promptFS, scriptoriumPromptRoot); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas")
|
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas")
|
||||||
@@ -48,9 +52,9 @@ func referencePromptMaterial(name string, slot contracts.ResolvedReferenceSlot)
|
|||||||
func scriptoriumPromptMetadata() (string, error) {
|
func scriptoriumPromptMetadata() (string, error) {
|
||||||
scriptoriumPromptHashOnce.Do(func() {
|
scriptoriumPromptHashOnce.Do(func() {
|
||||||
parts := append([]llm.AssetHashPart{
|
parts := append([]llm.AssetHashPart{
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd.spells.yaml"},
|
{FS: embeddedAssets, Path: "assets/prompts/dnd.spells.yaml"},
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd/spells/task.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/task.md"},
|
||||||
{FS: embeddedAssets, Path: "assets/scriptorium/prompts/dnd/spells/instructions.md"},
|
{FS: embeddedAssets, Path: "assets/prompts/instructions.md"},
|
||||||
}, append(sharedassets.CommonHashParts(), sharedassets.ReferenceHashParts()...)...)
|
}, append(sharedassets.CommonHashParts(), sharedassets.ReferenceHashParts()...)...)
|
||||||
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
scriptoriumPromptHash, scriptoriumPromptHashErr = llm.HashAssets(parts)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user