Add NPC registry grounding for spell extraction
This commit is contained in:
89
internal/modules/dnd/extract/spells/npc_registry.go
Normal file
89
internal/modules/dnd/extract/spells/npc_registry.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package spells
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"mime"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
npccodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/npcs"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/npcs/identity"
|
||||
)
|
||||
|
||||
const (
|
||||
NPCRegistryReferenceSlot = "npcs"
|
||||
NPCRegistryMaxBytes = 1048576
|
||||
)
|
||||
|
||||
type npcRegistryPromptInput struct {
|
||||
input contracts.LLMInputMaterial
|
||||
digest string
|
||||
count int
|
||||
bound bool
|
||||
}
|
||||
|
||||
func resolveNPCRegistry(references contracts.ReferenceSet) (npcRegistryPromptInput, error) {
|
||||
slot, ok := references.Slots[NPCRegistryReferenceSlot]
|
||||
if !ok {
|
||||
return npcRegistryPromptInput{
|
||||
input: contracts.NewLLMInputMaterial(
|
||||
NPCRegistryReferenceSlot,
|
||||
"application/json",
|
||||
[]byte(`{"npcs":[]}`),
|
||||
"",
|
||||
"",
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
if len(slot.Items) != 1 {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("reference slot %q must contain exactly one item", NPCRegistryReferenceSlot)
|
||||
}
|
||||
|
||||
item := slot.Items[0]
|
||||
mediaType, _, err := mime.ParseMediaType(item.MediaType)
|
||||
if err != nil {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("reference slot %q item media type %q is invalid: %w", NPCRegistryReferenceSlot, item.MediaType, err)
|
||||
}
|
||||
if !strings.EqualFold(mediaType, "application/json") {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("reference slot %q item media type %q must be application/json", NPCRegistryReferenceSlot, item.MediaType)
|
||||
}
|
||||
if len(item.Content) > NPCRegistryMaxBytes {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("reference slot %q item is %d bytes, limit %d", NPCRegistryReferenceSlot, len(item.Content), NPCRegistryMaxBytes)
|
||||
}
|
||||
|
||||
codec := npccodec.New()
|
||||
value, err := codec.Decode(item.Content)
|
||||
if err != nil {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("decode NPC registry: %w", err)
|
||||
}
|
||||
if issues := identity.ValidateList(value); len(issues) > 0 {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("validate NPC registry identity: %s", formatNPCIdentityIssues(issues))
|
||||
}
|
||||
content, err := codec.Encode(value)
|
||||
if err != nil {
|
||||
return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: %w", err)
|
||||
}
|
||||
|
||||
digest := semanticNPCRegistryDigest(content)
|
||||
return npcRegistryPromptInput{
|
||||
input: contracts.NewLLMInputMaterial(NPCRegistryReferenceSlot, "application/json", content, digest, ""),
|
||||
digest: digest,
|
||||
count: len(value.NPCs),
|
||||
bound: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func semanticNPCRegistryDigest(content []byte) string {
|
||||
sum := sha256.Sum256(content)
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func formatNPCIdentityIssues(issues []identity.Issue) string {
|
||||
parts := make([]string, len(issues))
|
||||
for index, issue := range issues {
|
||||
parts[index] = fmt.Sprintf("%s at record %d", issue.Code, issue.RecordIndex)
|
||||
}
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
Reference in New Issue
Block a user