95 lines
3.0 KiB
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)
|
|
}
|