Simplify contextual entity grounding
This commit is contained in:
@@ -5,8 +5,8 @@ across records, or infer ownership that the transcript does not establish.
|
|||||||
|
|
||||||
For every occurrence, use the supplied canonical item `name`. Record a stated
|
For every occurrence, use the supplied canonical item `name`. Record a stated
|
||||||
quantity as an integer and leave it null when the transcript does not state
|
quantity as an integer and leave it null when the transcript does not state
|
||||||
one. Use a concise observed item name and preserve the stated currency
|
one. Preserve the stated currency denomination through the selected canonical
|
||||||
denomination.
|
registry name.
|
||||||
|
|
||||||
Use `discovered` when the party learns of or encounters an item without
|
Use `discovered` when the party learns of or encounters an item without
|
||||||
establishing possession. Use `acquired` when the party or a party member gains
|
establishing possession. Use `acquired` when the party or a party member gains
|
||||||
|
|||||||
@@ -274,8 +274,9 @@ path.
|
|||||||
registry and the current `*source.SourceDocument`. Its API must provide:
|
registry and the current `*source.SourceDocument`. Its API must provide:
|
||||||
- a cloned `contracts.LLMInputMaterial` for the `location_registry` slot;
|
- a cloned `contracts.LLMInputMaterial` for the `location_registry` slot;
|
||||||
- deterministic resolution of a returned selector to one cloned
|
- deterministic resolution of a returned selector to one cloned
|
||||||
`dnd.Location`; and
|
`dnd.Location`.
|
||||||
- the digest of the exact model projection.
|
The prompt material's existing `Digest` field owns the digest of the exact
|
||||||
|
model projection; do not expose a second grounding-specific digest API.
|
||||||
4. Construct the projection in registry order. Group entries by the existing
|
4. Construct the projection in registry order. Group entries by the existing
|
||||||
location comparison key:
|
location comparison key:
|
||||||
- every projection entry has exactly the required fields `name`,
|
- every projection entry has exactly the required fields `name`,
|
||||||
@@ -299,10 +300,10 @@ path.
|
|||||||
7. Separate deterministic identity fingerprinting from LLM material. Add
|
7. Separate deterministic identity fingerprinting from LLM material. Add
|
||||||
`IdentityDigest()` over the registry's ordered `{id,name}` identity
|
`IdentityDigest()` over the registry's ordered `{id,name}` identity
|
||||||
projection, and update the location normalizer and registry-validator
|
projection, and update the location normalizer and registry-validator
|
||||||
checkpoint consumers to use it. The new operation grounding owns the model
|
checkpoint consumers to use it. The new operation grounding carries the
|
||||||
projection digest. Retain the old ID-bearing prompt accessor only as a
|
model projection digest in its `LLMInputMaterial`. Retain the old ID-bearing
|
||||||
documented transitional dependency of the still-unchanged location
|
prompt accessor only as a documented transitional dependency of the
|
||||||
occurrence extractor; do not add new callers.
|
still-unchanged location occurrence extractor; do not add new callers.
|
||||||
8. Add focused tests for unique names, same-name context and selectors,
|
8. Add focused tests for unique names, same-name context and selectors,
|
||||||
canonical range order, deterministic projection/digest, defensive copies,
|
canonical range order, deterministic projection/digest, defensive copies,
|
||||||
exact selector resolution, nil/foreign/invalid references, selector
|
exact selector resolution, nil/foreign/invalid references, selector
|
||||||
@@ -367,10 +368,12 @@ remove durable location IDs from the prompt and private response.
|
|||||||
7. Change `mappingPolicy` to
|
7. Change `mappingPolicy` to
|
||||||
`dnd.location_occurrences.extract_mapping.v2`.
|
`dnd.location_occurrences.extract_mapping.v2`.
|
||||||
8. Remove the legacy ID-bearing registry `PromptInput` and its model-projection
|
8. Remove the legacy ID-bearing registry `PromptInput` and its model-projection
|
||||||
digest once the extractor uses operation grounding. Update the occurrence
|
digest once the extractor uses operation grounding. Keep the occurrence's
|
||||||
checkpoint to combine `IdentityDigest()` with the new grounding projection
|
static module fingerprint based on `IdentityDigest()`, prompt/schema
|
||||||
digest, prompt/schema fingerprint, mapping policy, and its existing inputs;
|
fingerprints, and mapping policy. The operation-scoped projection is already
|
||||||
do not retain dead compatibility aliases.
|
covered by source/chunk identity and configured or generated reference
|
||||||
|
dependencies, while its `LLMInputMaterial.Digest` identifies the exact model
|
||||||
|
input; do not add a second digest API or operation-aware static fingerprint.
|
||||||
9. Update focused schema, prompt, extractor, canonicalization, checkpoint, and
|
9. Update focused schema, prompt, extractor, canonicalization, checkpoint, and
|
||||||
generated-reference tests. Cover unique-name empty selectors, successful
|
generated-reference tests. Cover unique-name empty selectors, successful
|
||||||
same-name selection, failure for an unsupported ambiguous mention,
|
same-name selection, failure for an unsupported ambiguous mention,
|
||||||
|
|||||||
@@ -10,26 +10,34 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
"gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
type orderedItemOccurrenceResponse struct {
|
type orderedItemOccurrence struct {
|
||||||
value itemOccurrenceResponse
|
value dnd.ItemOccurrence
|
||||||
earliest int
|
earliest int
|
||||||
hasEvidence bool
|
hasEvidence bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOrder, sourceID string, registry *itemregistry.Registry) error {
|
func canonicalItemOccurrenceList(response extractionResponse, order shared.SourceRefOrder, sourceID string, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, error) {
|
||||||
if response == nil {
|
if response.Occurrences == nil {
|
||||||
return nil
|
return dnd.ItemOccurrenceList{}, nil
|
||||||
}
|
}
|
||||||
ordered := make([]orderedItemOccurrenceResponse, len(response.Occurrences))
|
ordered := make([]orderedItemOccurrence, len(response.Occurrences))
|
||||||
for index := range response.Occurrences {
|
for index, occurrence := range response.Occurrences {
|
||||||
canonical, found := registry.Lookup(response.Occurrences[index].Name)
|
item, found := registry.Lookup(occurrence.Name)
|
||||||
if !found {
|
if !found {
|
||||||
return fmt.Errorf("occurrences[%d].name is not in the item registry", index)
|
return dnd.ItemOccurrenceList{}, fmt.Errorf("occurrences[%d].name is not in the item registry", index)
|
||||||
}
|
}
|
||||||
response.Occurrences[index].Name = canonical.Name
|
refs := order.Canonicalize(itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID))
|
||||||
earliest, hasEvidence := canonicalizeItemOccurrence(&response.Occurrences[index], order, sourceID)
|
earliest, hasEvidence := order.EarliestValid(refs)
|
||||||
ordered[index] = orderedItemOccurrenceResponse{
|
ordered[index] = orderedItemOccurrence{
|
||||||
value: response.Occurrences[index],
|
value: dnd.ItemOccurrence{
|
||||||
|
ItemID: item.ID,
|
||||||
|
Name: item.Name,
|
||||||
|
Kind: dnd.ItemOccurrenceKind(occurrence.Kind),
|
||||||
|
Quantity: cloneQuantity(occurrence.Quantity),
|
||||||
|
From: occurrence.From,
|
||||||
|
To: occurrence.To,
|
||||||
|
SourceRefs: refs,
|
||||||
|
},
|
||||||
earliest: earliest,
|
earliest: earliest,
|
||||||
hasEvidence: hasEvidence,
|
hasEvidence: hasEvidence,
|
||||||
}
|
}
|
||||||
@@ -43,40 +51,9 @@ func canonicalizeResponse(response *extractionResponse, order shared.SourceRefOr
|
|||||||
}
|
}
|
||||||
return ordered[left].earliest < ordered[right].earliest
|
return ordered[left].earliest < ordered[right].earliest
|
||||||
})
|
})
|
||||||
for index := range ordered {
|
occurrences := make([]dnd.ItemOccurrence, len(ordered))
|
||||||
response.Occurrences[index] = ordered[index].value
|
for index, occurrence := range ordered {
|
||||||
}
|
occurrences[index] = occurrence.value
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalizeItemOccurrence(occurrence *itemOccurrenceResponse, order shared.SourceRefOrder, sourceID string) (int, bool) {
|
|
||||||
if occurrence == nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
refs := order.Canonicalize(itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID))
|
|
||||||
occurrence.SourceRefs = itemOccurrenceResponseRefs(refs)
|
|
||||||
return order.EarliestValid(refs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalItemOccurrenceList(response extractionResponse, sourceID string, registry *itemregistry.Registry) (dnd.ItemOccurrenceList, error) {
|
|
||||||
if response.Occurrences == nil {
|
|
||||||
return dnd.ItemOccurrenceList{}, nil
|
|
||||||
}
|
|
||||||
occurrences := make([]dnd.ItemOccurrence, 0, len(response.Occurrences))
|
|
||||||
for index, occurrence := range response.Occurrences {
|
|
||||||
item, found := registry.Lookup(occurrence.Name)
|
|
||||||
if !found {
|
|
||||||
return dnd.ItemOccurrenceList{}, fmt.Errorf("occurrences[%d].name is not in the item registry", index)
|
|
||||||
}
|
|
||||||
occurrences = append(occurrences, dnd.ItemOccurrence{
|
|
||||||
ItemID: item.ID,
|
|
||||||
Name: item.Name,
|
|
||||||
Kind: dnd.ItemOccurrenceKind(occurrence.Kind),
|
|
||||||
Quantity: cloneQuantity(occurrence.Quantity),
|
|
||||||
From: occurrence.From,
|
|
||||||
To: occurrence.To,
|
|
||||||
SourceRefs: itemOccurrenceSourceRefs(occurrence.SourceRefs, sourceID),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return dnd.ItemOccurrenceList{Occurrences: occurrences}, nil
|
return dnd.ItemOccurrenceList{Occurrences: occurrences}, nil
|
||||||
}
|
}
|
||||||
@@ -92,17 +69,6 @@ func itemOccurrenceSourceRefs(refs []itemOccurrenceSourceRefResponse, sourceID s
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
func itemOccurrenceResponseRefs(refs []source.SourceRef) []itemOccurrenceSourceRefResponse {
|
|
||||||
if refs == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
values := make([]itemOccurrenceSourceRefResponse, len(refs))
|
|
||||||
for index, ref := range refs {
|
|
||||||
values[index] = itemOccurrenceSourceRefResponse{StartSegment: ref.StartUnitID, EndSegment: ref.EndUnitID}
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneQuantity(value *int) *int {
|
func cloneQuantity(value *int) *int {
|
||||||
if value == nil {
|
if value == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -161,12 +161,9 @@ func (e *Extractor) Extract(ctx context.Context, req contracts.TypedExtractionRe
|
|||||||
}, &response); err != nil {
|
}, &response); err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("complete structured output: %w", err)
|
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("complete structured output: %w", err)
|
||||||
}
|
}
|
||||||
if err := canonicalizeResponse(&response, order, req.Source.ID, registry); err != nil {
|
value, err := canonicalItemOccurrenceList(response, order, req.Source.ID, registry)
|
||||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("map item occurrence response: %w", err)
|
|
||||||
}
|
|
||||||
value, err := canonicalItemOccurrenceList(response, req.Source.ID, registry)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("resolve canonical item occurrence names: %w", err)
|
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{}, extractorErrorf("map item occurrence response: %w", err)
|
||||||
}
|
}
|
||||||
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{Value: value}, nil
|
return contracts.TypedExtractionResult[dnd.ItemOccurrenceList]{Value: value}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ type ContextUnit struct {
|
|||||||
// Its prompt input contains no durable IDs or source IDs.
|
// Its prompt input contains no durable IDs or source IDs.
|
||||||
type Grounding struct {
|
type Grounding struct {
|
||||||
promptInput contracts.LLMInputMaterial
|
promptInput contracts.LLMInputMaterial
|
||||||
projectionDigest string
|
|
||||||
locationsBySelector map[string]dnd.Location
|
locationsBySelector map[string]dnd.Location
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +103,6 @@ func NewGrounding(registry *Registry, doc *source.SourceDocument) (*Grounding, e
|
|||||||
digest := semanticDigest(content)
|
digest := semanticDigest(content)
|
||||||
return &Grounding{
|
return &Grounding{
|
||||||
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, locationcodec.MediaType, content, digest, ""),
|
promptInput: contracts.NewLLMInputMaterial(ReferenceSlot, locationcodec.MediaType, content, digest, ""),
|
||||||
projectionDigest: digest,
|
|
||||||
locationsBySelector: locationsBySelector,
|
locationsBySelector: locationsBySelector,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -118,14 +116,6 @@ func (g *Grounding) PromptInput() contracts.LLMInputMaterial {
|
|||||||
return g.promptInput.Clone()
|
return g.promptInput.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectionDigest returns the digest of the exact contextual prompt input.
|
|
||||||
func (g *Grounding) ProjectionDigest() string {
|
|
||||||
if g == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return g.projectionDigest
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve maps a contextual selector to one canonical location.
|
// Resolve maps a contextual selector to one canonical location.
|
||||||
func (g *Grounding) Resolve(selector Selector) (dnd.Location, bool) {
|
func (g *Grounding) Resolve(selector Selector) (dnd.Location, bool) {
|
||||||
if g == nil {
|
if g == nil {
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ func TestGroundingProjectsAndResolvesUniqueAndSameNameLocations(t *testing.T) {
|
|||||||
if bytes.Contains(grounding.PromptInput().Content, []byte("location:sha256:")) || bytes.Contains(grounding.PromptInput().Content, []byte(`"source_id"`)) {
|
if bytes.Contains(grounding.PromptInput().Content, []byte("location:sha256:")) || bytes.Contains(grounding.PromptInput().Content, []byte(`"source_id"`)) {
|
||||||
t.Fatalf("grounding projection exposed durable identity: %s", grounding.PromptInput().Content)
|
t.Fatalf("grounding projection exposed durable identity: %s", grounding.PromptInput().Content)
|
||||||
}
|
}
|
||||||
if grounding.ProjectionDigest() == "" || grounding.ProjectionDigest() == registry.IdentityDigest() || grounding.PromptInput().Digest != grounding.ProjectionDigest() {
|
if grounding.PromptInput().Digest == "" || grounding.PromptInput().Digest == registry.IdentityDigest() {
|
||||||
t.Fatalf("grounding/identity digests = %q/%q", grounding.ProjectionDigest(), registry.IdentityDigest())
|
t.Fatalf("grounding/identity digests = %q/%q", grounding.PromptInput().Digest, registry.IdentityDigest())
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved, ok := grounding.Resolve(Selector{Name: " the tavern ", RegistryRefs: []RegistryRef{{StartUnitID: 30, EndUnitID: 30}, {StartUnitID: 20, EndUnitID: 20}}})
|
resolved, ok := grounding.Resolve(Selector{Name: " the tavern ", RegistryRefs: []RegistryRef{{StartUnitID: 30, EndUnitID: 30}, {StartUnitID: 20, EndUnitID: 20}}})
|
||||||
@@ -276,8 +276,8 @@ func TestGroundingHandlesEmptyRegistriesAndIdentityFingerprints(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if base.IdentityDigest() != expanded.IdentityDigest() || baseGrounding.ProjectionDigest() != expandedGrounding.ProjectionDigest() {
|
if base.IdentityDigest() != expanded.IdentityDigest() || baseGrounding.PromptInput().Digest != expandedGrounding.PromptInput().Digest {
|
||||||
t.Fatalf("identity/model fingerprints = %q/%q and %q/%q", base.IdentityDigest(), expanded.IdentityDigest(), baseGrounding.ProjectionDigest(), expandedGrounding.ProjectionDigest())
|
t.Fatalf("identity/model fingerprints = %q/%q and %q/%q", base.IdentityDigest(), expanded.IdentityDigest(), baseGrounding.PromptInput().Digest, expandedGrounding.PromptInput().Digest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user