package shared import ( "fmt" "io/fs" "strings" rootassets "gitea.maximumdirect.net/eric/notarius/assets" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" ) // PromptAssetManifest is the ordered set of assets that make up one prompt. // Module files are addressed in the owning module filesystem. SharedFiles use // names in sharedPromptPaths, while ExternalSharedFiles are explicit // caller-owned descriptors. Both kinds are mounted beneath sharedassets. type PromptAssetManifest struct { ModuleDir string ModuleFiles []promptfs.ModulePromptFile SharedFiles []string ExternalSharedFiles []promptfs.SharedPromptFile } var sharedPromptPaths = map[string]string{ "common-dnd-system.md": "prompts/common-dnd-system.md", "common-dnd-extraction-evidence.md": "prompts/common-dnd-extraction-evidence.md", "common-dnd-identity.md": "prompts/common-dnd-identity.md", "common-dnd-transcript-full.md": "prompts/common-dnd-transcript-full.md", "common-dnd-transcript-chunk.md": "prompts/common-dnd-transcript-chunk.md", "common-dnd-references.md": "prompts/common-dnd-references.md", "common-dnd-npc-registry.md": "prompts/common-dnd-npc-registry.md", } func sharedAssetFS() (fs.FS, error) { assets, err := fs.Sub(rootassets.FS(), "dnd/shared") if err != nil { return nil, fmt.Errorf("scope shared D&D prompt assets: %w", err) } return assets, nil } func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) { sharedFiles, err := manifest.sharedPromptFiles() 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 := manifest.sharedPromptFiles() if err != nil { return "", err } moduleFiles := append([]promptfs.ModulePromptFile(nil), manifest.ModuleFiles...) if _, err := promptfs.ModulePromptFS(manifest.ModuleDir, moduleFS, moduleFiles, sharedFiles...); 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 (manifest PromptAssetManifest) sharedPromptFiles() ([]promptfs.SharedPromptFile, error) { files, err := resolveSharedPromptFiles(manifest.SharedFiles) if err != nil { return nil, err } return append(files, manifest.ExternalSharedFiles...), nil } func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) { assets, err := sharedAssetFS() if err != nil { return nil, err } 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: assets, Path: path, }) } return files, nil }