Files
notarius/internal/modules/dnd/extract/spells/npc_registry.go

95 lines
3.0 KiB
Go

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/diagnostics"
"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: invalid approved NPC JSON")
}
if issues := identity.ValidateList(value); len(issues) > 0 {
return npcRegistryPromptInput{}, fmt.Errorf("%s", formatNPCIdentityIssues(issues))
}
content, err := codec.Encode(value)
if err != nil {
return npcRegistryPromptInput{}, fmt.Errorf("encode canonical NPC registry: approved NPC value could not be encoded")
}
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 {
location := fmt.Sprintf("record %d", issue.RecordIndex)
if issue.AliasIndex >= 0 {
location += fmt.Sprintf(" alias %d", issue.AliasIndex)
}
parts[index] = fmt.Sprintf("%s at %s", issue.Code, location)
}
return diagnostics.Aggregate("validate NPC registry identity", parts)
}