From f91e6439321ee46c437b0d4c7e288c8e2dd9e79a Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 6 Jul 2026 17:22:21 +0000 Subject: [PATCH] Use shared DnD prompt helpers in modules --- internal/modules/chunk/dnd/scenes/chunker.go | 53 +++---------- .../modules/chunk/dnd/scenes/chunker_test.go | 16 ++-- .../chunk/dnd/scenes/scriptorium_assets.go | 78 ------------------- .../modules/extract/dnd/spells/extractor.go | 53 +++---------- .../extract/dnd/spells/extractor_test.go | 16 ++-- .../extract/dnd/spells/scriptorium_assets.go | 78 ------------------- .../dnd/spells/scriptorium_assets_test.go | 7 +- 7 files changed, 36 insertions(+), 265 deletions(-) diff --git a/internal/modules/chunk/dnd/scenes/chunker.go b/internal/modules/chunk/dnd/scenes/chunker.go index b3e70cc..b360dbd 100644 --- a/internal/modules/chunk/dnd/scenes/chunker.go +++ b/internal/modules/chunk/dnd/scenes/chunker.go @@ -8,6 +8,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" + "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" ) const Key = "dnd/scenes" @@ -21,35 +22,11 @@ var providedCapabilities = []string{ "chunks.scenes", } -var acceptedReferenceMediaTypes = []string{ - "application/json", - "application/x-yaml", - "application/yaml", - "text/markdown", - "text/plain", -} - -var referenceSlots = []contracts.ReferenceSlot{ - { - Name: "glossary", - Description: "Optional campaign glossary reference material used only for scene disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "party", - Description: "Optional party roster reference material used only for scene disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "players", - Description: "Optional player list reference material used only for scene disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "roster", - Description: "Deprecated alias for party roster reference material used only for scene disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, +var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{ + Glossary: "Optional campaign glossary reference material used only for scene disambiguation.", + Party: "Optional party roster reference material used only for scene disambiguation.", + Players: "Optional player list reference material used only for scene disambiguation.", + Roster: "Deprecated alias for party roster reference material used only for scene disambiguation.", } var _ contracts.Chunker = (*Chunker)(nil) @@ -66,7 +43,7 @@ func (c *Chunker) Key() string { } func (c *Chunker) ReferenceSlots() []contracts.ReferenceSlot { - return cloneReferenceSlots(referenceSlots) + return dnd.ReferenceSlots(referenceSlotDescriptions) } func (c *Chunker) ManifestMetadata() map[string]any { @@ -122,7 +99,7 @@ func (c *Chunker) Chunk(ctx context.Context, req contracts.ChunkRequest) (contra PromptVersion: ResponseSchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, - Inputs: promptInputs(req), + Inputs: dnd.PromptInputs(req.SourceInput, req.References), }, &response); err != nil { return contracts.ChunkResult{}, chunkerErrorf("complete structured output: %w", err) } @@ -147,7 +124,7 @@ func ModuleSpec() pipeline.ModuleSpec { Stage: pipeline.StageChunk, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), - ReferenceSlots: cloneReferenceSlots(referenceSlots), + ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions), } } @@ -338,15 +315,3 @@ func cloneMetadata(metadata map[string]any) map[string]any { func chunkerErrorf(format string, args ...any) error { return fmt.Errorf("dnd scenes chunker: "+format, args...) } - -func cloneReferenceSlots(slots []contracts.ReferenceSlot) []contracts.ReferenceSlot { - if len(slots) == 0 { - return nil - } - out := make([]contracts.ReferenceSlot, len(slots)) - for i, slot := range slots { - slot.AcceptedMediaTypes = append([]string(nil), slot.AcceptedMediaTypes...) - out[i] = slot - } - return out -} diff --git a/internal/modules/chunk/dnd/scenes/chunker_test.go b/internal/modules/chunk/dnd/scenes/chunker_test.go index 338acc1..42c198b 100644 --- a/internal/modules/chunk/dnd/scenes/chunker_test.go +++ b/internal/modules/chunk/dnd/scenes/chunker_test.go @@ -11,6 +11,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" + "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" ) func TestNewModuleSpecAndRegister(t *testing.T) { @@ -257,15 +258,12 @@ func TestChunkPassesReferencesAsPromptInputs(t *testing.T) { } func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) { - inputs := promptInputs(contracts.ChunkRequest{ - SourceInput: sceneSourceInput(), - References: contracts.ReferenceSet{ - Slots: map[string]contracts.ResolvedReferenceSlot{ - "roster": { - Slot: contracts.ReferenceSlot{Name: "roster"}, - Items: []contracts.ReferenceItem{ - {SlotName: "roster", Content: []byte("Legacy roster text")}, - }, + inputs := dnd.PromptInputs(sceneSourceInput(), contracts.ReferenceSet{ + Slots: map[string]contracts.ResolvedReferenceSlot{ + "roster": { + Slot: contracts.ReferenceSlot{Name: "roster"}, + Items: []contracts.ReferenceItem{ + {SlotName: "roster", Content: []byte("Legacy roster text")}, }, }, }, diff --git a/internal/modules/chunk/dnd/scenes/scriptorium_assets.go b/internal/modules/chunk/dnd/scenes/scriptorium_assets.go index 070ce17..05a7e7e 100644 --- a/internal/modules/chunk/dnd/scenes/scriptorium_assets.go +++ b/internal/modules/chunk/dnd/scenes/scriptorium_assets.go @@ -1,12 +1,9 @@ package scenes import ( - "bytes" "fmt" - "sort" "sync" - "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" @@ -29,36 +26,6 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error { return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas") } -func promptInputs(req contracts.ChunkRequest) contracts.LLMInputSet { - partySlot := req.References.Slots["party"] - if len(partySlot.Items) == 0 { - partySlot = req.References.Slots["roster"] - } - return contracts.LLMInputSet{ - "transcript": transcriptPromptInput(req.SourceInput), - "players": referencePromptMaterial("players", req.References.Slots["players"]), - "party": referencePromptMaterial("party", partySlot), - "glossary": referencePromptMaterial("glossary", req.References.Slots["glossary"]), - } -} - -func transcriptPromptInput(material contracts.LLMInputMaterial) contracts.LLMInputMaterial { - out := material.Clone() - out.Name = "transcript" - return out -} - -func referencePromptMaterial(name string, slot contracts.ResolvedReferenceSlot) contracts.LLMInputMaterial { - body := referencePromptInput(slot) - digest := "" - originURI := "" - if len(slot.Items) == 1 { - digest = slot.Items[0].Digest - originURI = slot.Items[0].Origin.URI - } - return contracts.NewLLMInputMaterial(name, "text/plain", body, digest, originURI) -} - func scriptoriumPromptMetadata() (string, error) { scriptoriumPromptHashOnce.Do(func() { parts := append([]llm.AssetHashPart{ @@ -71,51 +38,6 @@ func scriptoriumPromptMetadata() (string, error) { return scriptoriumPromptHash, scriptoriumPromptHashErr } -func referencePromptInput(slot contracts.ResolvedReferenceSlot) []byte { - if len(slot.Items) == 0 { - return []byte(" ") - } - items := append([]contracts.ReferenceItem(nil), slot.Items...) - sort.SliceStable(items, func(i, j int) bool { - if items[i].Origin.URI != items[j].Origin.URI { - return items[i].Origin.URI < items[j].Origin.URI - } - if items[i].Digest != items[j].Digest { - return items[i].Digest < items[j].Digest - } - return string(items[i].Content) < string(items[j].Content) - }) - if len(items) == 1 { - return append([]byte(nil), items[0].Content...) - } - - var b bytes.Buffer - for i, item := range items { - if i > 0 { - b.WriteString("\n\n") - } - fmt.Fprintf(&b, "Reference %d\n", i+1) - if item.Origin.Type != "" { - fmt.Fprintf(&b, "Origin-Type: %s\n", item.Origin.Type) - } - if item.Origin.URI != "" { - fmt.Fprintf(&b, "Origin-URI: %s\n", item.Origin.URI) - } - if item.Digest != "" { - fmt.Fprintf(&b, "Digest: %s\n", item.Digest) - } - if item.MediaType != "" { - fmt.Fprintf(&b, "Media-Type: %s\n", item.MediaType) - } - if item.SizeBytes > 0 { - fmt.Fprintf(&b, "Size-Bytes: %d\n", item.SizeBytes) - } - b.WriteString("\n") - b.Write(item.Content) - } - return b.Bytes() -} - var ( scriptoriumPromptHashOnce sync.Once scriptoriumPromptHash string diff --git a/internal/modules/extract/dnd/spells/extractor.go b/internal/modules/extract/dnd/spells/extractor.go index 3defa3b..7009bfd 100644 --- a/internal/modules/extract/dnd/spells/extractor.go +++ b/internal/modules/extract/dnd/spells/extractor.go @@ -10,6 +10,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" + "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" ) const Key = "dnd/spells" @@ -25,35 +26,11 @@ var providedCapabilities = []string{ "dnd.spell_casts", } -var acceptedReferenceMediaTypes = []string{ - "application/json", - "application/x-yaml", - "application/yaml", - "text/markdown", - "text/plain", -} - -var referenceSlots = []contracts.ReferenceSlot{ - { - Name: "glossary", - Description: "Optional campaign glossary reference material used only for disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "party", - Description: "Optional party roster reference material used only for disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "players", - Description: "Optional player list reference material used only for disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, - { - Name: "roster", - Description: "Deprecated alias for party roster reference material used only for disambiguation.", - AcceptedMediaTypes: append([]string(nil), acceptedReferenceMediaTypes...), - }, +var referenceSlotDescriptions = dnd.ReferenceSlotDescriptions{ + Glossary: "Optional campaign glossary reference material used only for disambiguation.", + Party: "Optional party roster reference material used only for disambiguation.", + Players: "Optional player list reference material used only for disambiguation.", + Roster: "Deprecated alias for party roster reference material used only for disambiguation.", } var _ contracts.Extractor = (*Extractor)(nil) @@ -77,7 +54,7 @@ func (e *Extractor) SchemaVersion() string { } func (e *Extractor) ReferenceSlots() []contracts.ReferenceSlot { - return cloneReferenceSlots(referenceSlots) + return dnd.ReferenceSlots(referenceSlotDescriptions) } func (e *Extractor) ManifestMetadata() map[string]any { @@ -137,7 +114,7 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.ExtractionRequest PromptVersion: SchemaVersion, ProfileID: req.LLMProfile, SessionID: req.SessionID, - Inputs: promptInputs(req), + Inputs: dnd.PromptInputs(req.SourceInput, req.References), }, &response); err != nil { return contracts.ExtractionResult{}, extractorErrorf("complete structured output: %w", err) } @@ -168,7 +145,7 @@ func ModuleSpec() pipeline.ModuleSpec { Stage: pipeline.StageExtract, Requires: append([]string(nil), requiredCapabilities...), Provides: append([]string(nil), providedCapabilities...), - ReferenceSlots: cloneReferenceSlots(referenceSlots), + ReferenceSlots: dnd.ReferenceSlots(referenceSlotDescriptions), } } @@ -190,15 +167,3 @@ func spellCastPayload(spellCast spellCastResponse) (json.RawMessage, error) { func extractorErrorf(format string, args ...any) error { return fmt.Errorf("dnd spells extractor: "+format, args...) } - -func cloneReferenceSlots(slots []contracts.ReferenceSlot) []contracts.ReferenceSlot { - if len(slots) == 0 { - return nil - } - out := make([]contracts.ReferenceSlot, len(slots)) - for i, slot := range slots { - slot.AcceptedMediaTypes = append([]string(nil), slot.AcceptedMediaTypes...) - out[i] = slot - } - return out -} diff --git a/internal/modules/extract/dnd/spells/extractor_test.go b/internal/modules/extract/dnd/spells/extractor_test.go index 1782abb..e26c04b 100644 --- a/internal/modules/extract/dnd/spells/extractor_test.go +++ b/internal/modules/extract/dnd/spells/extractor_test.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" + "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" ) func TestExtractReturnsSpellCandidateFromStructuredOutput(t *testing.T) { @@ -156,15 +157,12 @@ func TestExtractPassesReferencesAsPromptInputs(t *testing.T) { } func TestPromptInputsMapLegacyRosterReferenceToParty(t *testing.T) { - inputs := promptInputs(contracts.ExtractionRequest{ - SourceInput: spellSourceInput(), - References: contracts.ReferenceSet{ - Slots: map[string]contracts.ResolvedReferenceSlot{ - "roster": { - Slot: contracts.ReferenceSlot{Name: "roster"}, - Items: []contracts.ReferenceItem{ - {SlotName: "roster", Content: []byte("Legacy roster text")}, - }, + inputs := dnd.PromptInputs(spellSourceInput(), contracts.ReferenceSet{ + Slots: map[string]contracts.ResolvedReferenceSlot{ + "roster": { + Slot: contracts.ReferenceSlot{Name: "roster"}, + Items: []contracts.ReferenceItem{ + {SlotName: "roster", Content: []byte("Legacy roster text")}, }, }, }, diff --git a/internal/modules/extract/dnd/spells/scriptorium_assets.go b/internal/modules/extract/dnd/spells/scriptorium_assets.go index ce497f4..3cd7119 100644 --- a/internal/modules/extract/dnd/spells/scriptorium_assets.go +++ b/internal/modules/extract/dnd/spells/scriptorium_assets.go @@ -1,12 +1,9 @@ package spells import ( - "bytes" "fmt" - "sort" "sync" - "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets" "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" @@ -29,36 +26,6 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error { return registry.RegisterSchemaFS(embeddedAssets, "assets/schemas") } -func promptInputs(req contracts.ExtractionRequest) contracts.LLMInputSet { - partySlot := req.References.Slots["party"] - if len(partySlot.Items) == 0 { - partySlot = req.References.Slots["roster"] - } - return contracts.LLMInputSet{ - "transcript": transcriptPromptInput(req.SourceInput), - "players": referencePromptMaterial("players", req.References.Slots["players"]), - "party": referencePromptMaterial("party", partySlot), - "glossary": referencePromptMaterial("glossary", req.References.Slots["glossary"]), - } -} - -func transcriptPromptInput(material contracts.LLMInputMaterial) contracts.LLMInputMaterial { - out := material.Clone() - out.Name = "transcript" - return out -} - -func referencePromptMaterial(name string, slot contracts.ResolvedReferenceSlot) contracts.LLMInputMaterial { - body := referencePromptInput(slot) - digest := "" - originURI := "" - if len(slot.Items) == 1 { - digest = slot.Items[0].Digest - originURI = slot.Items[0].Origin.URI - } - return contracts.NewLLMInputMaterial(name, "text/plain", body, digest, originURI) -} - func scriptoriumPromptMetadata() (string, error) { scriptoriumPromptHashOnce.Do(func() { parts := append([]llm.AssetHashPart{ @@ -71,51 +38,6 @@ func scriptoriumPromptMetadata() (string, error) { return scriptoriumPromptHash, scriptoriumPromptHashErr } -func referencePromptInput(slot contracts.ResolvedReferenceSlot) []byte { - if len(slot.Items) == 0 { - return []byte(" ") - } - items := append([]contracts.ReferenceItem(nil), slot.Items...) - sort.SliceStable(items, func(i, j int) bool { - if items[i].Origin.URI != items[j].Origin.URI { - return items[i].Origin.URI < items[j].Origin.URI - } - if items[i].Digest != items[j].Digest { - return items[i].Digest < items[j].Digest - } - return string(items[i].Content) < string(items[j].Content) - }) - if len(items) == 1 { - return append([]byte(nil), items[0].Content...) - } - - var b bytes.Buffer - for i, item := range items { - if i > 0 { - b.WriteString("\n\n") - } - fmt.Fprintf(&b, "Reference %d\n", i+1) - if item.Origin.Type != "" { - fmt.Fprintf(&b, "Origin-Type: %s\n", item.Origin.Type) - } - if item.Origin.URI != "" { - fmt.Fprintf(&b, "Origin-URI: %s\n", item.Origin.URI) - } - if item.Digest != "" { - fmt.Fprintf(&b, "Digest: %s\n", item.Digest) - } - if item.MediaType != "" { - fmt.Fprintf(&b, "Media-Type: %s\n", item.MediaType) - } - if item.SizeBytes > 0 { - fmt.Fprintf(&b, "Size-Bytes: %d\n", item.SizeBytes) - } - b.WriteString("\n") - b.Write(item.Content) - } - return b.Bytes() -} - var ( scriptoriumPromptHashOnce sync.Once scriptoriumPromptHash string diff --git a/internal/modules/extract/dnd/spells/scriptorium_assets_test.go b/internal/modules/extract/dnd/spells/scriptorium_assets_test.go index 407bce8..d64935a 100644 --- a/internal/modules/extract/dnd/spells/scriptorium_assets_test.go +++ b/internal/modules/extract/dnd/spells/scriptorium_assets_test.go @@ -9,6 +9,7 @@ import ( "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/llm" + "gitea.maximumdirect.net/eric/notarius/internal/modules/sharedassets/dnd" "gitea.maximumdirect.net/eric/scriptorium" ) @@ -72,8 +73,8 @@ func TestReferencePromptInputRenderingIsDeterministic(t *testing.T) { }, }, } - first := string(referencePromptInput(slot)) - second := string(referencePromptInput(slot)) + first := string(dnd.ReferencePromptInput(slot)) + second := string(dnd.ReferencePromptInput(slot)) if first != second { t.Fatalf("reference rendering was not deterministic:\nfirst=%q\nsecond=%q", first, second) } @@ -86,7 +87,7 @@ func TestReferencePromptInputRenderingIsDeterministic(t *testing.T) { } func TestSingleReferencePromptInputKeepsContentOnly(t *testing.T) { - got := string(referencePromptInput(contracts.ResolvedReferenceSlot{ + got := string(dnd.ReferencePromptInput(contracts.ResolvedReferenceSlot{ Items: []contracts.ReferenceItem{{Content: []byte("single reference")}}, })) if got != "single reference" {