Adopt registry-backed NPC occurrence artifacts

This commit is contained in:
2026-08-05 18:55:58 +00:00
parent 3f4a1f2647
commit 5e2ccffc0f
42 changed files with 676 additions and 528 deletions

View File

@@ -52,7 +52,7 @@ func referenceSlots() []contracts.ReferenceSlot {
return slots
}
var _ contracts.Extractor[dnd.NPCInteractionList] = (*Extractor)(nil)
var _ contracts.Extractor[dnd.NPCOccurrenceList] = (*Extractor)(nil)
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
@@ -132,33 +132,33 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
{Name: "prompt", Value: e.promptSHA},
{Name: "response_schema", Value: e.responseSchemaSHA},
{Name: "mapping_policy", Value: mappingPolicy},
{Name: "npc_registry", Value: seeded.ProjectionDigest()},
{Name: "npc_registry", Value: seeded.IdentityPromptInput().Digest},
}
}
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.NPCInteractionList], error) {
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.NPCOccurrenceList], error) {
if e == nil {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("extractor must not be nil")
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("extractor must not be nil")
}
if e.llm == nil {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("LLM client must not be nil")
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("LLM client must not be nil")
}
sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
if err != nil {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("%w", err)
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("%w", err)
}
order := shared.NewSourceRefOrder(req.Source)
npcRegistry, err := e.npcResolver.Resolve(req.References)
if err != nil {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("resolve NPC registry: %w", err)
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("resolve NPC registry: %w", err)
}
if !npcRegistry.Bound() {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("NPC registry reference is required")
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("NPC registry reference is required")
}
var response extractionResponse
inputs := shared.PromptInputs(sourceInput, req.References)
inputs[NPCRegistryReferenceSlot] = npcRegistry.PromptInput()
inputs[NPCRegistryReferenceSlot] = npcRegistry.IdentityPromptInput()
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: Key,
PromptID: PromptID,
@@ -167,10 +167,27 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
SessionID: req.SessionID,
Inputs: inputs,
}, &response); err != nil {
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{}, extractorErrorf("complete structured output: %w", err)
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("complete structured output: %w", err)
}
canonicalizeResponse(&response, order, req.Source.ID)
return contracts.TypedExtractionResult[dnd.NPCInteractionList]{Value: canonicalInteractionList(response, req.Source.ID)}, nil
value := canonicalOccurrenceList(response, req.Source.ID)
if err := validateRegistryPairs(value, npcRegistry); err != nil {
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{}, extractorErrorf("validate NPC registry pairs: %w", err)
}
return contracts.TypedExtractionResult[dnd.NPCOccurrenceList]{Value: value}, nil
}
func validateRegistryPairs(value dnd.NPCOccurrenceList, registry *npcregistry.Registry) error {
for index, occurrence := range value.Occurrences {
canonical, ok := registry.LookupID(occurrence.NPCID)
if !ok {
return fmt.Errorf("occurrences[%d].npc_id is not in the NPC registry", index)
}
if occurrence.Name != canonical.Name {
return fmt.Errorf("occurrences[%d].name does not match npc_id", index)
}
}
return nil
}
func ModuleSpec() pipeline.ModuleSpec {
@@ -180,13 +197,13 @@ func ModuleSpec() pipeline.ModuleSpec {
ExecutionClass: contracts.ExecutionClassLLMBacked,
Requires: append([]string(nil), requiredCapabilities...),
Provides: append([]string(nil), providedCapabilities...),
ArtifactKind: dnd.NPCInteractionListKind,
ArtifactKind: dnd.NPCOccurrenceListKind,
ReferenceSlots: referenceSlots(),
}
}
func Register(registry *pipeline.ExtractorRegistry) error {
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.NPCInteractionList], error) {
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.NPCOccurrenceList], error) {
options, err := DecodeOptions(request.Options)
if err != nil {
return nil, err