Centralize shared D&D LLM assets

This commit is contained in:
2026-08-05 00:37:19 +00:00
parent a57f83e30d
commit 08954f17e2
15 changed files with 69 additions and 34 deletions

View File

@@ -6,7 +6,7 @@ import (
"io/fs" "io/fs"
) )
//go:embed generic //go:embed dnd generic
var embedded embed.FS var embedded embed.FS
// FS returns the embedded read-only asset filesystem. // FS returns the embedded read-only asset filesystem.

View File

@@ -1,14 +1,17 @@
package register package register
import ( import (
"embed" "fmt"
"io/fs"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
) )
//go:embed assets/profiles/*.yaml
var embeddedProfileAssets embed.FS
func registerFallbackProfiles(assets *llm.AssetRegistry) error { func registerFallbackProfiles(assets *llm.AssetRegistry) error {
return assets.RegisterFallbackProfileFS(embeddedProfileAssets, "assets/profiles") profiles, err := fs.Sub(rootassets.FS(), "dnd/profiles")
if err != nil {
return fmt.Errorf("scope D&D fallback profiles: %w", err)
}
return assets.RegisterFallbackProfileFS(profiles, ".")
} }

View File

@@ -1,18 +1,15 @@
package shared package shared
import ( import (
"embed"
"fmt" "fmt"
"io/fs" "io/fs"
"strings" "strings"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
"gitea.maximumdirect.net/eric/notarius/internal/framework/promptfs" "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. // PromptAssetManifest is the ordered set of assets that make up one prompt.
// Module files are addressed in the owning module filesystem; shared files use // Module files are addressed in the owning module filesystem; shared files use
// the names in sharedPromptPaths and are mounted beneath sharedassets. // the names in sharedPromptPaths and are mounted beneath sharedassets.
@@ -23,13 +20,21 @@ type PromptAssetManifest struct {
} }
var sharedPromptPaths = map[string]string{ var sharedPromptPaths = map[string]string{
"common-dnd-system.md": "assets/prompts/common-dnd-system.md", "common-dnd-system.md": "prompts/common-dnd-system.md",
"common-dnd-extraction-evidence.md": "assets/prompts/common-dnd-extraction-evidence.md", "common-dnd-extraction-evidence.md": "prompts/common-dnd-extraction-evidence.md",
"common-dnd-identity.md": "assets/prompts/common-dnd-identity.md", "common-dnd-identity.md": "prompts/common-dnd-identity.md",
"common-dnd-transcript.md": "assets/prompts/common-dnd-transcript.md", "common-dnd-transcript.md": "prompts/common-dnd-transcript.md",
"common-dnd-references.md": "assets/prompts/common-dnd-references.md", "common-dnd-references.md": "prompts/common-dnd-references.md",
"common-dnd-npcs.md": "assets/prompts/common-dnd-npcs.md", "common-dnd-npcs.md": "prompts/common-dnd-npcs.md",
"common-dnd-entity-reconciliation.md": "assets/prompts/common-dnd-entity-reconciliation.md", "common-dnd-entity-reconciliation.md": "prompts/common-dnd-entity-reconciliation.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) { func (manifest PromptAssetManifest) PromptFS(moduleFS fs.FS) (fs.FS, error) {
@@ -57,6 +62,11 @@ func (manifest PromptAssetManifest) Hash(moduleFS fs.FS) (string, error) {
} }
func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) { func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, error) {
assets, err := sharedAssetFS()
if err != nil {
return nil, err
}
files := make([]promptfs.SharedPromptFile, 0, len(names)) files := make([]promptfs.SharedPromptFile, 0, len(names))
seen := make(map[string]struct{}, len(names)) seen := make(map[string]struct{}, len(names))
for _, name := range names { for _, name := range names {
@@ -76,7 +86,7 @@ func resolveSharedPromptFiles(names []string) ([]promptfs.SharedPromptFile, erro
seen[name] = struct{}{} seen[name] = struct{}{}
files = append(files, promptfs.SharedPromptFile{ files = append(files, promptfs.SharedPromptFile{
Name: name, Name: name,
FS: embeddedAssets, FS: assets,
Path: path, Path: path,
}) })
} }

View File

@@ -111,7 +111,7 @@ func TestPromptAssetManifestRejectsMissingModuleFile(t *testing.T) {
func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) { func TestPromptAssetManifestRejectsMissingSharedFile(t *testing.T) {
const name = "missing-for-test.md" const name = "missing-for-test.md"
const path = "assets/prompts/missing-for-test.md" const path = "prompts/missing-for-test.md"
previous, existed := sharedPromptPaths[name] previous, existed := sharedPromptPaths[name]
sharedPromptPaths[name] = path sharedPromptPaths[name] = path
t.Cleanup(func() { t.Cleanup(func() {
@@ -157,11 +157,15 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Hash() error = %v, want nil", err) t.Fatalf("Hash() error = %v, want nil", err)
} }
sharedFS, err := sharedAssetFS()
if err != nil {
t.Fatalf("sharedAssetFS() error = %v, want nil", err)
}
want, err := llm.HashAssets([]llm.AssetHashPart{ want, err := llm.HashAssets([]llm.AssetHashPart{
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
{FS: moduleFS, Path: "assets/prompts/task.md"}, {FS: moduleFS, Path: "assets/prompts/task.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"},
}) })
if err != nil { if err != nil {
t.Fatalf("HashAssets() error = %v, want nil", err) t.Fatalf("HashAssets() error = %v, want nil", err)
@@ -172,9 +176,9 @@ func TestPromptAssetManifestHashMatchesManifestParts(t *testing.T) {
withUnused, err := llm.HashAssets([]llm.AssetHashPart{ withUnused, err := llm.HashAssets([]llm.AssetHashPart{
{FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"}, {FS: moduleFS, Path: "assets/prompts/dnd.test.yaml"},
{FS: moduleFS, Path: "assets/prompts/task.md"}, {FS: moduleFS, Path: "assets/prompts/task.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-transcript.md"}, {FS: sharedFS, Path: "prompts/common-dnd-transcript.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-system.md"}, {FS: sharedFS, Path: "prompts/common-dnd-system.md"},
{FS: embeddedAssets, Path: "assets/prompts/common-dnd-npcs.md"}, {FS: sharedFS, Path: "prompts/common-dnd-npcs.md"},
}) })
if err != nil { if err != nil {
t.Fatalf("HashAssets() with unused asset error = %v, want nil", err) t.Fatalf("HashAssets() with unused asset error = %v, want nil", err)
@@ -195,7 +199,7 @@ func TestSharedPromptDescriptorsReturnFreshCopies(t *testing.T) {
} }
first[0].Name = "changed.md" first[0].Name = "changed.md"
first[0].Path = "changed.md" first[0].Path = "changed.md"
if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "assets/prompts/common-dnd-system.md" { if reflect.DeepEqual(first, second) || second[0].Name != "common-dnd-system.md" || second[0].Path != "prompts/common-dnd-system.md" {
t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second) t.Fatalf("resolveSharedPromptFiles() reused descriptor state: first=%#v second=%#v", first, second)
} }
} }

View File

@@ -1,6 +0,0 @@
package entityreconcile
import "embed"
//go:embed assets/schemas/dnd_entity_reconcile_llm.v1.json
var embeddedAssets embed.FS

View File

@@ -1,6 +1,10 @@
package entityreconcile package entityreconcile
import ( import (
"fmt"
"io/fs"
rootassets "gitea.maximumdirect.net/eric/notarius/assets"
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
) )
@@ -9,13 +13,25 @@ const (
ResponseSchemaID = "notarius.dnd.entity_reconcile.llm" ResponseSchemaID = "notarius.dnd.entity_reconcile.llm"
ResponseSchemaName = "notarius_dnd_entity_reconcile_llm_v1" ResponseSchemaName = "notarius_dnd_entity_reconcile_llm_v1"
SchemaVersion = "v1" SchemaVersion = "v1"
SchemaAssetPath = "assets/schemas/dnd_entity_reconcile_llm.v1.json" SchemaAssetPath = "schemas/dnd_entity_reconcile_llm.v1.json"
) )
func schemaAssetFS() (fs.FS, error) {
assets, err := fs.Sub(rootassets.FS(), "dnd/entity-reconciliation")
if err != nil {
return nil, fmt.Errorf("scope entity reconciliation assets: %w", err)
}
return assets, nil
}
// LoadResponseSchema returns the shared private duplicate-group response // LoadResponseSchema returns the shared private duplicate-group response
// contract. It is intentionally separate from durable artifact schemas. // contract. It is intentionally separate from durable artifact schemas.
func LoadResponseSchema() (llm.ResponseSchema, error) { func LoadResponseSchema() (llm.ResponseSchema, error) {
return llm.LoadResponseSchema(embeddedAssets, llm.ResponseSchemaDefinition{ assets, err := schemaAssetFS()
if err != nil {
return llm.ResponseSchema{}, err
}
return llm.LoadResponseSchema(assets, llm.ResponseSchemaDefinition{
Key: ResponseSchemaKey, Key: ResponseSchemaKey,
ID: ResponseSchemaID, ID: ResponseSchemaID,
Version: SchemaVersion, Version: SchemaVersion,
@@ -27,5 +43,13 @@ func LoadResponseSchema() (llm.ResponseSchema, error) {
// RegisterSchemaAssets makes the shared private response schema available to // RegisterSchemaAssets makes the shared private response schema available to
// prompt preparation. A family registrar can register it once for all consumers. // prompt preparation. A family registrar can register it once for all consumers.
func RegisterSchemaAssets(registry *llm.AssetRegistry) error { func RegisterSchemaAssets(registry *llm.AssetRegistry) error {
return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas") assets, err := schemaAssetFS()
if err != nil {
return err
}
schemas, err := fs.Sub(assets, "schemas")
if err != nil {
return fmt.Errorf("scope entity reconciliation schemas: %w", err)
}
return registry.RegisterSchemaFS(schemas, ".")
} }