Use shared DnD prompt helpers in modules
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" {
|
||||
|
||||
Reference in New Issue
Block a user