package shared import ( "embed" "fmt" "io/fs" "strings" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" ) //go:embed assets/prompts/*.md var embeddedAssets embed.FS // PromptAssetManifest is the ordered set of assets that make up one prompt. // Module files are addressed in the owning module filesystem; shared files use // the names in sharedPromptPaths and are mounted beneath sharedassets. type PromptAssetManifest struct { ModuleDir string ModuleFiles []promptfs.ModulePromptFile SharedFiles []string } var sharedPromptPaths = map[string]string{ "common-dnd-system.md": "assets/prompts/common-dnd-system.md", "common-dnd-extraction-evidence.md": "assets/prompts/common-dnd-extraction-evidence.md", "common-dnd-identity.md": "assets/prompts/common-dnd-identity.md", "common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md", "common-dnd-references.md": "assets/prompts/common-dnd-references.md", "common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md", } func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) { sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles) if err != nil { return nil, err } moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...) return promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...) } func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) { sharedFiles, err := resolveSharedPromptFiles(manifest.SharedFiles) if err != nil { return "", err } parts := make([]llm.AssetHashPart, 0, len(manifest.ModuleFiles)+len(sharedFiles)) for _, file := range manifest.ModuleFiles { parts = append(parts, llm.AssetHashPart{FS: moduleFS, Path: file.Path}) } for _, file := range sharedFiles { parts = append(parts, llm.AssetHashPart{FS: file.FS, Path: file.Path}) } return llm.HashAssets(parts) } func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) { files := make([]promptfs.SharedPromptFile, 0, len(names)) seen := make(map[string]struct{}, len(names)) for _, name := range names { if name == "" { return nil, fmt.Errorf("shared prompt asset name must not be empty") } if strings.ContainsAny(name, `/\\`) { return nil, fmt.Errorf("shared prompt asset name %q must not contain path separators", name) } if _, ok := seen[name]; ok { return nil, fmt.Errorf("duplicate shared prompt asset name %q", name) } path, ok := sharedPromptPaths[name] if !ok { return nil, fmt.Errorf("unknown shared prompt asset name %q", name) } seen[name] = struct{}{} files = append(files, promptfs.SharedPromptFile{ Name: name, FS: embeddedAssets, Path: path, }) } return files, nil }