Adopt registry-backed item occurrences
This commit is contained in:
@@ -5,11 +5,12 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||
)
|
||||
|
||||
type orderedItemEventResponse struct {
|
||||
value itemEventResponse
|
||||
type orderedItemOccurrenceResponse struct {
|
||||
value itemOccurrenceResponse
|
||||
earliest int
|
||||
hasEvidence bool
|
||||
}
|
||||
@@ -18,11 +19,11 @@ func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOr
|
||||
if response == nil {
|
||||
return
|
||||
}
|
||||
ordered := make([]orderedItemEventResponse, len(response.Events))
|
||||
for index := range response.Events {
|
||||
earliest, hasEvidence := canonicalizeItemEvent(&response.Events[index], order, sourceID)
|
||||
ordered[index] = orderedItemEventResponse{
|
||||
value: response.Events[index],
|
||||
ordered := make([]orderedItemOccurrenceResponse, len(response.Occurrences))
|
||||
for index := range response.Occurrences {
|
||||
earliest, hasEvidence := canonicalizeItemOccurrence(&response.Occurrences[index], order, sourceID)
|
||||
ordered[index] = orderedItemOccurrenceResponse{
|
||||
value: response.Occurrences[index],
|
||||
earliest: earliest,
|
||||
hasEvidence: hasEvidence,
|
||||
}
|
||||
@@ -37,38 +38,43 @@ func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOr
|
||||
return ordered[left].earliest < ordered[right].earliest
|
||||
})
|
||||
for index := range ordered {
|
||||
response.Events[index] = ordered[index].value
|
||||
response.Occurrences[index] = ordered[index].value
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalizeItemEvent(event *itemEventResponse, order shared.SourceRefOrder, sourceID string) (int, bool) {
|
||||
func canonicalizeItemOccurrence(event *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) {
|
||||
if event == nil {
|
||||
return 0, false
|
||||
}
|
||||
refs := order.Canonicalize(itemEventSourceRefs(event.SourceRefs, sourceID))
|
||||
event.SourceRefs = itemEventResponseRefs(refs)
|
||||
refs := order.Canonicalize(itemOccurrenceSourceRefs(event.SourceRefs, sourceID))
|
||||
event.SourceRefs = itemOccurrenceResponseRefs(refs)
|
||||
return order.EarliestValid(refs)
|
||||
}
|
||||
|
||||
func canonicalItemEventList(response extractionResponse, sourceID string) dnd.ItemEventList {
|
||||
if response.Events == nil {
|
||||
return dnd.ItemEventList{}
|
||||
func canonicalItemOccurrenceList(response extractionResponse, sourceID string, registry *itemregistry.Registry) dnd.ItemOccurrenceList {
|
||||
if response.Occurrences == nil {
|
||||
return dnd.ItemOccurrenceList{}
|
||||
}
|
||||
events := make([]dnd.ItemEvent, len(response.Events))
|
||||
for index, event := range response.Events {
|
||||
events[index] = dnd.ItemEvent{
|
||||
occurrences := make([]dnd.ItemOccurrence, 0, len(response.Occurrences))
|
||||
for _, event := range response.Occurrences {
|
||||
item, found := registry.LookupID(event.ItemID)
|
||||
if !found || item.Name != event.Name {
|
||||
continue
|
||||
}
|
||||
occurrences = append(occurrences, dnd.ItemOccurrence{
|
||||
ItemID: event.ItemID,
|
||||
Name: event.Name,
|
||||
Kind: dnd.ItemEventKind(event.Kind),
|
||||
Kind: dnd.ItemOccurrenceKind(event.Kind),
|
||||
Quantity: cloneQuantity(event.Quantity),
|
||||
From: event.From,
|
||||
To: event.To,
|
||||
SourceRefs: itemEventSourceRefs(event.SourceRefs, sourceID),
|
||||
}
|
||||
SourceRefs: itemOccurrenceSourceRefs(event.SourceRefs, sourceID),
|
||||
})
|
||||
}
|
||||
return dnd.ItemEventList{Events: events}
|
||||
return dnd.ItemOccurrenceList{Occurrences: occurrences}
|
||||
}
|
||||
|
||||
func itemEventSourceRefs(refs []itemEventSourceRefResponse, sourceID string) []source.SourceRef {
|
||||
func itemOccurrenceSourceRefs(refs []itemOccurrenceSourceRefResponse, sourceID string) []source.SourceRef {
|
||||
if refs == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -79,13 +85,13 @@ func itemEventSourceRefs(refs []itemEventSourceRefResponse, sourceID string) []s
|
||||
return values
|
||||
}
|
||||
|
||||
func itemEventResponseRefs(refs []source.SourceRef) []itemEventSourceRefResponse {
|
||||
func itemOccurrenceResponseRefs(refs []source.SourceRef) []itemOccurrenceSourceRefResponse {
|
||||
if refs == nil {
|
||||
return nil
|
||||
}
|
||||
values := make([]itemEventSourceRefResponse, len(refs))
|
||||
values := make([]itemOccurrenceSourceRefResponse, len(refs))
|
||||
for index, ref := range refs {
|
||||
values[index] = itemEventSourceRefResponse{StartSegment: ref.StartUnitID, EndSegment: ref.EndUnitID}
|
||||
values[index] = itemOccurrenceSourceRefResponse{StartSegment: ref.StartUnitID, EndSegment: ref.EndUnitID}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
// Package itemevents extracts source-grounded D&D item events.
|
||||
// Package itemevents extracts source-grounded D&D item occurrences.
|
||||
package itemevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
itemregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/registry"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||
)
|
||||
|
||||
const Key = "dnd/item-events"
|
||||
|
||||
const (
|
||||
ItemRegistryReferenceSlot = itemregistry.ReferenceSlot
|
||||
ItemRegistryMaxBytes = itemregistry.MaxBytes
|
||||
)
|
||||
|
||||
const mappingPolicy = "dnd.item_events.extract_mapping.v1"
|
||||
|
||||
var requiredCapabilities = []string{
|
||||
@@ -32,10 +39,20 @@ var referenceSlotDescriptions = shared.ReferenceSlotDescriptions{
|
||||
}
|
||||
|
||||
func referenceSlots() []contracts.ReferenceSlot {
|
||||
return shared.ReferenceSlots(referenceSlotDescriptions)
|
||||
slots := shared.ReferenceSlots(referenceSlotDescriptions)
|
||||
slots = append(slots, contracts.ReferenceSlot{
|
||||
Name: ItemRegistryReferenceSlot,
|
||||
Description: "Required normalized item registry used only for item identity grounding, never as occurrence evidence.",
|
||||
Required: true,
|
||||
AcceptedMediaTypes: []string{"application/json"},
|
||||
AcceptedArtifactKinds: []contracts.ArtifactKind{dnd.ItemRegistryKind},
|
||||
MaxBytes: ItemRegistryMaxBytes,
|
||||
})
|
||||
sort.Slice(slots, func(left, right int) bool { return slots[left].Name < slots[right].Name })
|
||||
return slots
|
||||
}
|
||||
|
||||
var _ contracts.Extractor[dnd.ItemEventList] = (*Extractor)(nil)
|
||||
var _ contracts.Extractor[dnd.ItemOccurrenceList] = (*Extractor)(nil)
|
||||
var _ contracts.ManifestMetadataProvider = (*Extractor)(nil)
|
||||
var _ pipeline.CheckpointFingerprintProvider = (*Extractor)(nil)
|
||||
|
||||
@@ -43,6 +60,7 @@ type Options struct{}
|
||||
|
||||
type Extractor struct {
|
||||
llm contracts.StructuredLLMClient
|
||||
itemResolver *itemregistry.Resolver
|
||||
promptSHA string
|
||||
responseSchemaSHA string
|
||||
}
|
||||
@@ -54,6 +72,14 @@ func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contr
|
||||
if len(references) > 1 {
|
||||
return nil, extractorErrorf("at most one reference set may be supplied")
|
||||
}
|
||||
var referenceSet contracts.ReferenceSet
|
||||
if len(references) == 1 {
|
||||
referenceSet = references[0]
|
||||
}
|
||||
itemResolver, err := itemregistry.NewResolver(referenceSet)
|
||||
if err != nil {
|
||||
return nil, extractorErrorf("prepare item registry prompt input: %w", err)
|
||||
}
|
||||
promptSHA, err := promptAssetMetadata()
|
||||
if err != nil {
|
||||
return nil, extractorErrorf("load prompt metadata: %w", err)
|
||||
@@ -62,7 +88,7 @@ func New(llmClient contracts.StructuredLLMClient, _ Options, references ...contr
|
||||
if err != nil {
|
||||
return nil, extractorErrorf("load response schema: %w", err)
|
||||
}
|
||||
return &Extractor{llm: llmClient, promptSHA: promptSHA, responseSchemaSHA: responseSchema.SHA256}, nil
|
||||
return &Extractor{llm: llmClient, itemResolver: itemResolver, promptSHA: promptSHA, responseSchemaSHA: responseSchema.SHA256}, nil
|
||||
}
|
||||
|
||||
func (e *Extractor) Key() string { return Key }
|
||||
@@ -73,7 +99,7 @@ func (e *Extractor) ManifestMetadata() map[string]any {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
metadata := map[string]any{
|
||||
"prompt_id": PromptID,
|
||||
"prompt_version": SchemaVersion,
|
||||
"prompt_sha256": e.promptSHA,
|
||||
@@ -84,6 +110,12 @@ func (e *Extractor) ManifestMetadata() map[string]any {
|
||||
"response_schema_sha256": e.responseSchemaSHA,
|
||||
"mapping_policy": mappingPolicy,
|
||||
}
|
||||
seeded := e.itemResolver.Seeded()
|
||||
if seeded.Bound() {
|
||||
metadata["item_registry_digest"] = seeded.Digest()
|
||||
metadata["item_count"] = seeded.Count()
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
||||
@@ -94,35 +126,41 @@ func (e *Extractor) CheckpointFingerprints() []pipeline.CheckpointFingerprint {
|
||||
{Name: "prompt", Value: e.promptSHA},
|
||||
{Name: "response_schema", Value: e.responseSchemaSHA},
|
||||
{Name: "mapping_policy", Value: mappingPolicy},
|
||||
{Name: "item_registry", Value: e.itemResolver.Seeded().ProjectionDigest()},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.ItemEventList], error) {
|
||||
func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRequest) (contracts.TypedExtractionResult[dnd.ItemOccurrenceList], error) {
|
||||
if e == nil {
|
||||
return contracts.TypedExtractionResult[dnd.ItemEventList]{}, extractorErrorf("extractor must not be nil")
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("extractor must not be nil")
|
||||
}
|
||||
if e.llm == nil {
|
||||
return contracts.TypedExtractionResult[dnd.ItemEventList]{}, extractorErrorf("LLM client must not be nil")
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("LLM client must not be nil")
|
||||
}
|
||||
sourceInput, err := shared.PrepareChunkExtraction(ctx, req)
|
||||
if err != nil {
|
||||
return contracts.TypedExtractionResult[dnd.ItemEventList]{}, extractorErrorf("%w", err)
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("%w", err)
|
||||
}
|
||||
order := shared.NewSourceRefOrder(req.Source)
|
||||
|
||||
registry, err := e.itemResolver.Resolve(req.References)
|
||||
if err != nil {
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("resolve item registry: %w", err)
|
||||
}
|
||||
if !registry.Bound() {
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("item registry reference is required")
|
||||
}
|
||||
var response extractionResponse
|
||||
inputs := shared.PromptInputs(sourceInput, req.References)
|
||||
inputs[ItemRegistryReferenceSlot] = registry.PromptInput()
|
||||
if _, err := e.llm.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||
StageName: Key,
|
||||
PromptID: PromptID,
|
||||
PromptVersion: SchemaVersion,
|
||||
ProfileID: req.LLMProfile,
|
||||
SessionID: req.SessionID,
|
||||
Inputs: shared.PromptInputs(sourceInput, req.References),
|
||||
StageName: Key, PromptID: PromptID, PromptVersion: SchemaVersion,
|
||||
ProfileID: req.LLMProfile, SessionID: req.SessionID, Inputs: inputs,
|
||||
}, &response); err != nil {
|
||||
return contracts.TypedExtractionResult[dnd.ItemEventList]{}, extractorErrorf("complete structured output: %w", err)
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("complete structured output: %w", err)
|
||||
}
|
||||
canonicalizeResponse(&response, order, req.Source.ID)
|
||||
return contracts.TypedExtractionResult[dnd.ItemEventList]{Value: canonicalItemEventList(response, req.Source.ID)}, nil
|
||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{Value: canonicalItemOccurrenceList(response, req.Source.ID, registry)}, nil
|
||||
}
|
||||
|
||||
func ModuleSpec() pipeline.ModuleSpec {
|
||||
@@ -132,13 +170,13 @@ func ModuleSpec() pipeline.ModuleSpec {
|
||||
ExecutionClass: contracts.ExecutionClassLLMBacked,
|
||||
Requires: append([]string(nil), requiredCapabilities...),
|
||||
Provides: append([]string(nil), providedCapabilities...),
|
||||
ArtifactKind: dnd.ItemEventListKind,
|
||||
ArtifactKind: dnd.ItemOccurrenceListKind,
|
||||
ReferenceSlots: referenceSlots(),
|
||||
}
|
||||
}
|
||||
|
||||
func Register(registry *pipeline.ExtractorRegistry) error {
|
||||
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.ItemEventList], error) {
|
||||
return pipeline.RegisterExtractorBuilder(registry, ModuleSpec(), validateOptions, func(request pipeline.BuildRequest) (contracts.Extractor[dnd.ItemOccurrenceList], error) {
|
||||
options, err := DecodeOptions(request.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -160,5 +198,5 @@ func DecodeOptions(options map[string]any) (Options, error) {
|
||||
}
|
||||
|
||||
func extractorErrorf(format string, args ...any) error {
|
||||
return fmt.Errorf("dnd item events extractor: "+format, args...)
|
||||
return fmt.Errorf("dnd item occurrences extractor: "+format, args...)
|
||||
}
|
||||
|
||||
@@ -2,161 +2,61 @@ package itemevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/core/source"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd"
|
||||
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
||||
)
|
||||
|
||||
func TestExtractMapsAllEventKindsAndCanonicalizesEvidence(t *testing.T) {
|
||||
quantity := 20
|
||||
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{
|
||||
{Name: "Torch", Kind: "lost", From: "party", SourceRefs: responseRefs(5, 5)},
|
||||
{Name: "Moonblade", Kind: "transferred", From: "Aria", To: "Borin", SourceRefs: responseRefs(3, 3)},
|
||||
{Name: "Hidden Cache", Kind: "discovered", SourceRefs: []itemEventSourceRefResponse{{StartSegment: 1, EndSegment: 1}, {StartSegment: 1, EndSegment: 1}}},
|
||||
{Name: "Gold Pieces", Kind: "acquired", Quantity: &quantity, To: "party", SourceRefs: responseRefs(2, 2)},
|
||||
{Name: "Healing Potion", Kind: "consumed", From: "party", SourceRefs: responseRefs(4, 4)},
|
||||
}}}
|
||||
|
||||
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("Extract() error = %v", err)
|
||||
}
|
||||
if got := []dnd.ItemEventKind{result.Value.Events[0].Kind, result.Value.Events[1].Kind, result.Value.Events[2].Kind, result.Value.Events[3].Kind, result.Value.Events[4].Kind}; !reflect.DeepEqual(got, []dnd.ItemEventKind{
|
||||
dnd.ItemEventKindDiscovered, dnd.ItemEventKindAcquired, dnd.ItemEventKindTransferred, dnd.ItemEventKindConsumed, dnd.ItemEventKindLost,
|
||||
}) {
|
||||
t.Fatalf("event kinds = %#v, want source order", got)
|
||||
}
|
||||
acquired := result.Value.Events[1]
|
||||
if acquired.Name != "Gold Pieces" || acquired.To != "party" || acquired.Quantity == nil || *acquired.Quantity != 20 {
|
||||
t.Fatalf("acquired event = %#v", acquired)
|
||||
}
|
||||
if result.Value.Events[0].Quantity != nil || result.Value.Events[0].From != "" || result.Value.Events[0].To != "" {
|
||||
t.Fatalf("discovered event = %#v, want omitted optional fields", result.Value.Events[0])
|
||||
}
|
||||
if refs := result.Value.Events[0].SourceRefs; len(refs) != 1 || refs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}) {
|
||||
t.Fatalf("source refs = %#v, want source ID attachment and deduplication", refs)
|
||||
}
|
||||
if acquired.Quantity == client.response.Events[3].Quantity {
|
||||
t.Fatal("mapped quantity aliases private response")
|
||||
}
|
||||
if len(client.requests) != 1 {
|
||||
t.Fatalf("LLM calls = %d, want 1", len(client.requests))
|
||||
}
|
||||
request := client.requests[0]
|
||||
if request.StageName != Key || request.PromptID != PromptID || request.PromptVersion != SchemaVersion || request.ProfileID != "profile-item-events" || request.SessionID != "session-123" {
|
||||
t.Fatalf("LLM request identity = %#v", request)
|
||||
}
|
||||
if transcript := request.Inputs["transcript"]; string(transcript.Content) != string(extractionRequest().Chunk.Content) || transcript.Name != "transcript" {
|
||||
t.Fatalf("transcript input = %#v", transcript)
|
||||
}
|
||||
for _, name := range []string{"players", "party", "glossary"} {
|
||||
if input := request.Inputs[name]; string(input.Content) != " " {
|
||||
t.Fatalf("absent %s input = %#v, want retained empty prompt material", name, input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPreservesInvalidCandidatesAndEmptyResults(t *testing.T) {
|
||||
client := &fakeItemEventsLLMClient{content: []byte(`{"events":[{"name":"","kind":"transferred","quantity":0,"from":"party","to":"Party","source_refs":[{"start_segment":99,"end_segment":-1}]}]}`)}
|
||||
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
||||
if err != nil {
|
||||
t.Fatalf("Extract() error = %v", err)
|
||||
}
|
||||
event := result.Value.Events[0]
|
||||
if event.Name != "" || event.Kind != "transferred" || event.Quantity == nil || *event.Quantity != 0 || event.From != "party" || event.To != "Party" {
|
||||
t.Fatalf("invalid candidate = %#v, want values preserved", event)
|
||||
}
|
||||
if refs := event.SourceRefs; len(refs) != 1 || refs[0] != (source.SourceRef{SourceID: "session-alpha", StartUnitID: 99, EndUnitID: -1}) {
|
||||
t.Fatalf("source refs = %#v, want invalid range preserved", refs)
|
||||
}
|
||||
|
||||
empty, err := newExtractor(t, &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{}}}).Extract(context.Background(), extractionRequest())
|
||||
if err != nil || empty.Value.Events == nil || len(empty.Value.Events) != 0 {
|
||||
t.Fatalf("empty provider result = %#v, %v; want valid empty list", empty.Value, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractMapsNullableResponseFieldsToAbsentArtifactFields(t *testing.T) {
|
||||
client := &fakeItemEventsLLMClient{content: []byte(`{"events":[{"name":"Hidden Cache","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_segment":1,"end_segment":1}]}]}`)}
|
||||
result, err := newExtractor(t, client).Extract(context.Background(), extractionRequest())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.Value.Events) != 1 {
|
||||
t.Fatalf("events = %#v, want one", result.Value.Events)
|
||||
}
|
||||
event := result.Value.Events[0]
|
||||
if event.Quantity != nil || event.From != "" || event.To != "" {
|
||||
t.Fatalf("nullable response fields mapped to artifact values: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractUsesSourceDocumentOrderForCandidates(t *testing.T) {
|
||||
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{
|
||||
{Name: "Later", Kind: "discovered", SourceRefs: responseRefs(10, 10)},
|
||||
{Name: "Earlier", Kind: "discovered", SourceRefs: responseRefs(100, 100)},
|
||||
func TestExtractGroundsOccurrencesInRequiredRegistry(t *testing.T) {
|
||||
id := itemidentity.DeriveID("Torch")
|
||||
client := &fakeItemOccurrencesLLMClient{response: extractionResponse{Occurrences: []itemOccurrenceResponse{
|
||||
{ItemID: id, Name: "Torch", Kind: "lost", From: "party", SourceRefs: responseRefs(1, 1)},
|
||||
{ItemID: "unknown", Name: "Unknown", Kind: "lost", From: "party", SourceRefs: responseRefs(2, 2)},
|
||||
{ItemID: "torch", Name: "Lantern", Kind: "lost", From: "party", SourceRefs: responseRefs(3, 3)},
|
||||
}}}
|
||||
req := extractionRequest()
|
||||
req.Source.Units = []source.SourceUnit{{ID: 100}, {ID: 10}}
|
||||
req.Chunk.Units = append([]source.SourceUnit(nil), req.Source.Units...)
|
||||
req.Chunk.Ref = source.SourceRef{SourceID: req.Source.ID, StartUnitID: 100, EndUnitID: 10}
|
||||
result, err := newExtractor(t, client).Extract(context.Background(), req)
|
||||
req.References = itemRegistryReferences(t)
|
||||
result, err := newExtractor(t, client, req.References).Extract(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := []string{result.Value.Events[0].Name, result.Value.Events[1].Name}; !reflect.DeepEqual(got, []string{"Earlier", "Later"}) {
|
||||
t.Fatalf("event order = %#v, want source document order", got)
|
||||
if len(result.Value.Occurrences) != 1 || result.Value.Occurrences[0].ItemID != id || result.Value.Occurrences[0].Name != "Torch" {
|
||||
t.Fatalf("occurrences = %#v", result.Value.Occurrences)
|
||||
}
|
||||
input := client.requests[0].Inputs[ItemRegistryReferenceSlot]
|
||||
if input.Name != ItemRegistryReferenceSlot || string(input.Content) == "" {
|
||||
t.Fatalf("registry prompt input = %#v", input)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRequiresItemRegistry(t *testing.T) {
|
||||
_, err := newExtractor(t, &fakeItemOccurrencesLLMClient{}).Extract(context.Background(), extractionRequest())
|
||||
if err == nil {
|
||||
t.Fatal("Extract() error = nil, want required registry error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractPreservesNullableFields(t *testing.T) {
|
||||
id := itemidentity.DeriveID("Torch")
|
||||
client := &fakeItemOccurrencesLLMClient{content: []byte(`{"occurrences":[{"item_id":"` + id + `","name":"Torch","kind":"discovered","quantity":null,"from":null,"to":null,"source_refs":[{"start_segment":1,"end_segment":1}]}]}`)}
|
||||
req := extractionRequest()
|
||||
req.References = itemRegistryReferences(t)
|
||||
result, err := newExtractor(t, client, req.References).Extract(context.Background(), req)
|
||||
if err != nil || len(result.Value.Occurrences) != 1 || result.Value.Occurrences[0].Quantity != nil || result.Value.Occurrences[0].From != "" || result.Value.Occurrences[0].To != "" {
|
||||
t.Fatalf("result = %#v, %v", result, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractUsesOnlySupportedPromptInputs(t *testing.T) {
|
||||
client := &fakeItemEventsLLMClient{response: extractionResponse{Events: []itemEventResponse{}}}
|
||||
client := &fakeItemOccurrencesLLMClient{response: extractionResponse{Occurrences: []itemOccurrenceResponse{}}}
|
||||
req := extractionRequest()
|
||||
req.References = contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{
|
||||
"players": {Slot: contracts.ReferenceSlot{Name: "players"}, Items: []contracts.ReferenceItem{{SlotName: "players", Content: []byte("Dana: Aria")}}},
|
||||
"party": {Slot: contracts.ReferenceSlot{Name: "party"}, Items: []contracts.ReferenceItem{{SlotName: "party", Content: []byte("Aria: ranger")}}},
|
||||
"glossary": {Slot: contracts.ReferenceSlot{Name: "glossary"}, Items: []contracts.ReferenceItem{{SlotName: "glossary", Content: []byte("Moonblade: heirloom")}}},
|
||||
"npcs": {Slot: contracts.ReferenceSlot{Name: "npc_registry"}, Items: []contracts.ReferenceItem{{SlotName: "npc_registry", Content: []byte("must not be used")}}},
|
||||
}}
|
||||
if _, err := newExtractor(t, client).Extract(context.Background(), req); err != nil {
|
||||
req.References = itemRegistryReferences(t)
|
||||
req.References.Slots["unrelated"] = contracts.ResolvedReferenceSlot{Slot: contracts.ReferenceSlot{Name: "unrelated"}, Items: []contracts.ReferenceItem{{SlotName: "unrelated", Content: []byte("ignored")}}}
|
||||
if _, err := newExtractor(t, client, req.References).Extract(context.Background(), req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inputs := client.requests[0].Inputs
|
||||
if string(inputs["players"].Content) != "Dana: Aria" || string(inputs["party"].Content) != "Aria: ranger" || string(inputs["glossary"].Content) != "Moonblade: heirloom" {
|
||||
t.Fatalf("reference inputs = %#v", inputs)
|
||||
}
|
||||
if _, ok := inputs["npcs"]; ok {
|
||||
t.Fatalf("unexpected generated-lane input: %#v", inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractRetainsLocalErrorContext(t *testing.T) {
|
||||
request := extractionRequest()
|
||||
extractor := newExtractor(t, &fakeItemEventsLLMClient{})
|
||||
var nilExtractor *Extractor
|
||||
request.SourceInput = contracts.NewLLMInputMaterial("source", "application/json", []byte(`{"different":true}`), "sha256:other", "file:///other.json")
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
extractor *Extractor
|
||||
req contracts.TypedExtractionRequest
|
||||
want string
|
||||
}{
|
||||
{"nil extractor", nilExtractor, extractionRequest(), "extractor"},
|
||||
{"nil client", &Extractor{}, extractionRequest(), "LLM client"},
|
||||
{"preflight", extractor, request, "must match chunk"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, err := test.extractor.Extract(context.Background(), test.req); err == nil || !strings.Contains(err.Error(), "dnd item events") || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("Extract() error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
if _, err := newExtractor(t, &fakeItemEventsLLMClient{err: errors.New("provider unavailable")}).Extract(context.Background(), extractionRequest()); err == nil || !strings.Contains(err.Error(), "provider unavailable") {
|
||||
t.Fatalf("provider error = %v", err)
|
||||
if _, ok := client.requests[0].Inputs["unrelated"]; ok {
|
||||
t.Fatalf("unexpected prompt input: %#v", client.requests[0].Inputs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package itemevents
|
||||
|
||||
type extractionResponse struct {
|
||||
Events []itemEventResponse `json:"events"`
|
||||
Occurrences []itemOccurrenceResponse `json:"occurrences"`
|
||||
}
|
||||
|
||||
type itemEventResponse struct {
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Quantity *int `json:"quantity,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
SourceRefs []itemEventSourceRefResponse `json:"source_refs"`
|
||||
type itemOccurrenceResponse struct {
|
||||
ItemID string `json:"item_id"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Quantity *int `json:"quantity,omitempty"`
|
||||
From string `json:"from,omitempty"`
|
||||
To string `json:"to,omitempty"`
|
||||
SourceRefs []itemOccurrenceSourceRefResponse `json:"source_refs"`
|
||||
}
|
||||
|
||||
type itemEventSourceRefResponse struct {
|
||||
type itemOccurrenceSourceRefResponse struct {
|
||||
StartSegment int `json:"start_segment"`
|
||||
EndSegment int `json:"end_segment"`
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ var promptAssetManifest = shared.PromptAssetManifest{
|
||||
ModuleFiles: []promptfs.ModulePromptFile{
|
||||
{Name: "prompt.yaml", Path: "prompts/prompt.yaml"},
|
||||
{Name: "instructions.md", Path: "prompts/instructions.md"},
|
||||
{Name: "item-registry.md", Path: "prompts/item-registry.md"},
|
||||
},
|
||||
SharedFiles: []string{
|
||||
"common-dnd-system.md",
|
||||
@@ -43,7 +44,7 @@ func RegisterPromptAssets(registry *llm.AssetRegistry) error {
|
||||
}
|
||||
promptFS, err := promptAssetManifest.PromptFS(assets)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare item event prompt assets: %w", err)
|
||||
return fmt.Errorf("prepare item occurrence prompt assets: %w", err)
|
||||
}
|
||||
if err := registry.RegisterPromptFS(promptFS, promptAssetRoot); err != nil {
|
||||
return err
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) {
|
||||
func TestPromptAssetsPrepareItemOccurrencePrompt(t *testing.T) {
|
||||
registry := llm.NewAssetRegistry()
|
||||
if err := RegisterPromptAssets(registry); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -30,16 +30,17 @@ func TestPromptAssetsPrepareItemEventPrompt(t *testing.T) {
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{
|
||||
PromptID: PromptID, PromptVersion: SchemaVersion, ProfileID: "item-events-test-profile",
|
||||
Inputs: map[string]promptkit.ArtifactRef{
|
||||
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-event-transcript"}]}`),
|
||||
"players": promptkit.Inline("item-event-player"),
|
||||
"party": promptkit.Inline(" "),
|
||||
"glossary": promptkit.Inline(" "),
|
||||
"transcript": promptkit.InlineWithURI("file:///session.json", `{"units":[{"sentinel":"item-event-transcript"}]}`),
|
||||
"players": promptkit.Inline("item-event-player"),
|
||||
"party": promptkit.Inline(" "),
|
||||
"glossary": promptkit.Inline(" "),
|
||||
"item_registry": promptkit.Inline(`{"items":[{"id":"item:sha256:test","name":"Torch"}]}`),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_item_events_llm.v1.json" {
|
||||
if prepared.PromptID != PromptID || prepared.OutputContract.SchemaPath != "dnd_item_occurrences_llm.v1.json" {
|
||||
t.Fatalf("prepared prompt = %#v", prepared)
|
||||
}
|
||||
}
|
||||
@@ -49,12 +50,12 @@ func TestPromptAssetsDoNotLeakIntoMetadata(t *testing.T) {
|
||||
if err != nil || !strings.HasPrefix(hash, "sha256:") {
|
||||
t.Fatalf("promptAssetMetadata() = %q, %v", hash, err)
|
||||
}
|
||||
metadata := newExtractor(t, &fakeItemEventsLLMClient{}).ManifestMetadata()
|
||||
metadata := newExtractor(t, &fakeItemOccurrencesLLMClient{}).ManifestMetadata()
|
||||
payload, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, forbidden := range []string{"common-dnd-system", "dnd_item_events_llm.v1.json"} {
|
||||
for _, forbidden := range []string{"common-dnd-system", "dnd_item_occurrences_llm.v1.json"} {
|
||||
if strings.Contains(string(payload), forbidden) {
|
||||
t.Fatalf("metadata leaked raw asset content %q: %s", forbidden, payload)
|
||||
}
|
||||
|
||||
@@ -2,64 +2,42 @@ package itemevents
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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/dnd"
|
||||
itemcodec "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/codec/itemregistry"
|
||||
itemidentity "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/items/identity"
|
||||
)
|
||||
|
||||
func TestConstructorSpecOptionsAndMetadata(t *testing.T) {
|
||||
if _, err := New(nil, Options{}); err == nil || !strings.Contains(err.Error(), "LLM client") {
|
||||
t.Fatalf("New(nil) error = %v", err)
|
||||
func itemRegistryReferences(t *testing.T) contracts.ReferenceSet {
|
||||
t.Helper()
|
||||
content, err := itemcodec.New().Encode(dnd.ItemRegistry{Items: []dnd.Item{{ID: itemidentity.DeriveID("Torch"), Name: "Torch", SourceRefs: testSourceRefs()}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := New(&fakeItemEventsLLMClient{}, Options{}, contracts.ReferenceSet{}, contracts.ReferenceSet{}); err == nil || !strings.Contains(err.Error(), "at most one reference set") {
|
||||
t.Fatalf("New() error = %v", err)
|
||||
return contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ItemRegistryReferenceSlot: {Slot: contracts.ReferenceSlot{Name: ItemRegistryReferenceSlot}, Items: []contracts.ReferenceItem{{SlotName: ItemRegistryReferenceSlot, Content: content, MediaType: "application/json", ArtifactKind: dnd.ItemRegistryKind}}}}}
|
||||
}
|
||||
|
||||
func testSourceRefs() []source.SourceRef {
|
||||
return []source.SourceRef{{SourceID: "session-alpha", StartUnitID: 1, EndUnitID: 1}}
|
||||
}
|
||||
|
||||
func TestModuleSpecDeclaresRequiredRegistry(t *testing.T) {
|
||||
spec := ModuleSpec()
|
||||
var slot contracts.ReferenceSlot
|
||||
for _, candidate := range spec.ReferenceSlots {
|
||||
if candidate.Name == ItemRegistryReferenceSlot {
|
||||
slot = candidate
|
||||
}
|
||||
}
|
||||
want := pipeline.ModuleSpec{
|
||||
Key: Key, Stage: pipeline.StageExtract, ExecutionClass: contracts.ExecutionClassLLMBacked, Requires: []string{"chunks", "source.transcript"}, Provides: []string{"dnd.item_events"}, ArtifactKind: dnd.ItemEventListKind,
|
||||
ReferenceSlots: []contracts.ReferenceSlot{
|
||||
{Name: "glossary", Description: referenceSlotDescriptions.Glossary, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||
{Name: "party", Description: referenceSlotDescriptions.Party, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||
{Name: "players", Description: referenceSlotDescriptions.Players, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||
{Name: "roster", Description: referenceSlotDescriptions.Roster, AcceptedMediaTypes: []string{"application/json", "application/x-yaml", "application/yaml", "text/markdown", "text/plain"}},
|
||||
},
|
||||
}
|
||||
if got := ModuleSpec(); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("ModuleSpec() = %#v, want %#v", got, want)
|
||||
}
|
||||
if _, err := DecodeOptions(map[string]any{"unexpected": true}); err == nil || !strings.Contains(err.Error(), "unknown option") {
|
||||
t.Fatalf("DecodeOptions() error = %v", err)
|
||||
if !slot.Required || !reflect.DeepEqual(slot.AcceptedArtifactKinds, []contracts.ArtifactKind{dnd.ItemRegistryKind}) || slot.MaxBytes != ItemRegistryMaxBytes {
|
||||
t.Fatalf("registry slot = %#v", slot)
|
||||
}
|
||||
registry := pipeline.NewExtractorRegistry()
|
||||
if err := Register(registry); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, ok := registry.Spec(Key); !ok || !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("registry spec = %#v, %t", got, ok)
|
||||
}
|
||||
|
||||
extractor := newExtractor(t, &fakeItemEventsLLMClient{})
|
||||
metadata := extractor.ManifestMetadata()
|
||||
for key, want := range map[string]string{
|
||||
"prompt_id": PromptID, "prompt_version": SchemaVersion, "response_schema_key": string(ResponseSchemaKey), "response_schema_id": ResponseSchemaID,
|
||||
"response_schema_name": ResponseSchemaName, "response_schema_version": SchemaVersion, "mapping_policy": mappingPolicy,
|
||||
} {
|
||||
if metadata[key] != want {
|
||||
t.Fatalf("metadata[%q] = %#v, want %q", key, metadata[key], want)
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"prompt_sha256", "response_schema_sha256"} {
|
||||
if value, ok := metadata[key].(string); !ok || !strings.HasPrefix(value, "sha256:") {
|
||||
t.Fatalf("metadata[%q] = %#v", key, metadata[key])
|
||||
}
|
||||
}
|
||||
if got := extractor.CheckpointFingerprints(); !reflect.DeepEqual(got, []pipeline.CheckpointFingerprint{
|
||||
{Name: "prompt", Value: metadata["prompt_sha256"].(string)},
|
||||
{Name: "response_schema", Value: metadata["response_schema_sha256"].(string)},
|
||||
{Name: "mapping_policy", Value: mappingPolicy},
|
||||
}) {
|
||||
t.Fatalf("CheckpointFingerprints() = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import "gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||
|
||||
const (
|
||||
PromptID = "dnd.item_events"
|
||||
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_item_events_llm")
|
||||
ResponseSchemaID = "notarius.dnd.item_events.llm"
|
||||
ResponseSchemaName = "notarius_dnd_item_events_llm_v1"
|
||||
ResponseSchemaKey = llm.ResponseSchemaKey("dnd_item_occurrences_llm")
|
||||
ResponseSchemaID = "notarius.dnd.item_occurrences.llm"
|
||||
ResponseSchemaName = "notarius_dnd_item_occurrences_llm_v1"
|
||||
SchemaVersion = "v1"
|
||||
)
|
||||
|
||||
@@ -20,6 +20,6 @@ func loadResponseSchema() (llm.ResponseSchema, error) {
|
||||
ID: ResponseSchemaID,
|
||||
Version: SchemaVersion,
|
||||
Name: ResponseSchemaName,
|
||||
AssetPath: "schemas/dnd_item_events_llm.v1.json",
|
||||
AssetPath: "schemas/dnd_item_occurrences_llm.v1.json",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) {
|
||||
if schema.Key != ResponseSchemaKey || schema.ID != ResponseSchemaID || schema.Name != ResponseSchemaName || schema.Version != SchemaVersion || !strings.HasPrefix(schema.SHA256, "sha256:") || !json.Valid(schema.JSONSchema) {
|
||||
t.Fatalf("schema = %#v", schema)
|
||||
}
|
||||
valid := map[string]any{"events": []any{
|
||||
valid := map[string]any{"occurrences": []any{
|
||||
map[string]any{
|
||||
"name": "", "kind": "unsupported", "quantity": 0, "from": "party", "to": "Party",
|
||||
"item_id": "item:sha256:test", "name": "", "kind": "unsupported", "quantity": 0, "from": "party", "to": "Party",
|
||||
"source_refs": []any{map[string]any{"start_segment": 0, "end_segment": -1}},
|
||||
},
|
||||
map[string]any{
|
||||
"name": "Hidden Cache", "kind": "discovered", "quantity": nil, "from": nil, "to": nil,
|
||||
"item_id": "item:sha256:test", "name": "Hidden Cache", "kind": "discovered", "quantity": nil, "from": nil, "to": nil,
|
||||
"source_refs": []any{map[string]any{"start_segment": 1, "end_segment": 1}},
|
||||
},
|
||||
}}
|
||||
@@ -38,12 +38,12 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) {
|
||||
name string
|
||||
value map[string]any
|
||||
}{
|
||||
{"missing events", map[string]any{}},
|
||||
{"missing event name", map[string]any{"events": []any{withoutField(responseEvent(), "name")}}},
|
||||
{"missing nullable field", map[string]any{"events": []any{withoutField(responseEvent(), "quantity")}}},
|
||||
{"unknown event field", map[string]any{"events": []any{withField(responseEvent(), "extra", true)}}},
|
||||
{"unknown reference field", map[string]any{"events": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}},
|
||||
{"noninteger range", map[string]any{"events": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}},
|
||||
{"missing occurrences", map[string]any{}},
|
||||
{"missing event name", map[string]any{"occurrences": []any{withoutField(responseEvent(), "name")}}},
|
||||
{"missing nullable field", map[string]any{"occurrences": []any{withoutField(responseEvent(), "quantity")}}},
|
||||
{"unknown event field", map[string]any{"occurrences": []any{withField(responseEvent(), "extra", true)}}},
|
||||
{"unknown reference field", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1, "end_segment": 1, "extra": true}})}}},
|
||||
{"noninteger range", map[string]any{"occurrences": []any{withField(responseEvent(), "source_refs", []any{map[string]any{"start_segment": 1.5, "end_segment": 1}})}}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
content, err := json.Marshal(test.value)
|
||||
@@ -68,7 +68,7 @@ func TestResponseSchemaIsStrictlyStructuralAndPrivate(t *testing.T) {
|
||||
|
||||
func responseEvent() map[string]any {
|
||||
return map[string]any{
|
||||
"name": "Ring", "kind": "acquired", "quantity": nil, "from": nil, "to": "party", "source_refs": []any{},
|
||||
"item_id": "item:sha256:test", "name": "Ring", "kind": "acquired", "quantity": nil, "from": nil, "to": "party", "source_refs": []any{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ func sourceDocument() *source.SourceDocument {
|
||||
}
|
||||
}
|
||||
|
||||
func responseRefs(start, end int) []itemEventSourceRefResponse {
|
||||
return []itemEventSourceRefResponse{{StartSegment: start, EndSegment: end}}
|
||||
func responseRefs(start, end int) []itemOccurrenceSourceRefResponse {
|
||||
return []itemOccurrenceSourceRefResponse{{StartSegment: start, EndSegment: end}}
|
||||
}
|
||||
|
||||
func newExtractor(t *testing.T, client contracts.StructuredLLMClient, references ...contracts.ReferenceSet) *Extractor {
|
||||
@@ -61,14 +61,14 @@ func cloneStructuredCompletionRequest(req contracts.StructuredCompletionRequest)
|
||||
return req
|
||||
}
|
||||
|
||||
type fakeItemEventsLLMClient struct {
|
||||
type fakeItemOccurrencesLLMClient struct {
|
||||
response extractionResponse
|
||||
content []byte
|
||||
err error
|
||||
requests []contracts.StructuredCompletionRequest
|
||||
}
|
||||
|
||||
func (client *fakeItemEventsLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
func (client *fakeItemOccurrencesLLMClient) CompleteStructured(_ context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
|
||||
client.requests = append(client.requests, cloneStructuredCompletionRequest(req))
|
||||
if client.err != nil {
|
||||
return contracts.StructuredCompletionResponse{}, client.err
|
||||
|
||||
Reference in New Issue
Block a user