Reuse prepared glossary data across stage sections
This commit is contained in:
@@ -137,7 +137,12 @@ func (v NoEffectValidator) Validate(_ context.Context, req Request) (Result, err
|
||||
}
|
||||
|
||||
func ValidateProtectedTerms(req Request) (Result, error) {
|
||||
vocab := NewProtectedVocabulary(req.ProtectedTerms)
|
||||
var vocab ProtectedVocabulary
|
||||
if req.GlossaryView != nil {
|
||||
vocab = req.GlossaryView.Protection
|
||||
} else {
|
||||
vocab = NewProtectedVocabulary(req.ProtectedTerms)
|
||||
}
|
||||
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
||||
glossaryStage := req.ModuleKey == modulecatalog.KeyGlossary
|
||||
for _, c := range req.CandidateProposal {
|
||||
|
||||
@@ -1,15 +1,43 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/chunking"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
)
|
||||
|
||||
// GlossaryView holds the read-only matching data shared by a stage's validators
|
||||
// and apply-time checks.
|
||||
type GlossaryView struct {
|
||||
Protection ProtectedVocabulary
|
||||
context glossaryContext
|
||||
}
|
||||
|
||||
func PrepareGlossaryView(entries []glossary.Entry, protected []glossary.Term) *GlossaryView {
|
||||
return &GlossaryView{Protection: NewProtectedVocabulary(protected), context: prepareGlossaryContext(entries)}
|
||||
}
|
||||
|
||||
type glossaryContext struct {
|
||||
entries []glossary.Entry
|
||||
positions map[glossary.Identity][]int
|
||||
vocabulary ProtectedVocabulary
|
||||
}
|
||||
|
||||
func prepareGlossaryContext(entries []glossary.Entry) glossaryContext {
|
||||
positions := make(map[glossary.Identity][]int, len(entries))
|
||||
for i, entry := range entries {
|
||||
identity := glossary.Identity{Collection: entry.Collection, ID: entry.ID}
|
||||
positions[identity] = append(positions[identity], i)
|
||||
}
|
||||
return glossaryContext{entries: entries, positions: positions, vocabulary: NewProtectedVocabulary(glossary.Vocabulary(entries, nil))}
|
||||
}
|
||||
|
||||
// validationContext supplies adjacent source segments within the current chunk
|
||||
// and glossary definitions mentioned in the candidate or that local context.
|
||||
// These fields travel in each item so batching accounts for their size.
|
||||
func validationContext(segments []schema.Segment, position int, selected []glossary.Entry, vocabulary ProtectedVocabulary, corrected string) ([]NeighboringSegment, []glossary.Entry) {
|
||||
func (view glossaryContext) validationContext(segments []schema.Segment, position int, corrected string) ([]NeighboringSegment, []glossary.Entry) {
|
||||
var neighbors []NeighboringSegment
|
||||
source := segments[position].Text
|
||||
if position > 0 {
|
||||
@@ -23,19 +51,20 @@ func validationContext(segments []schema.Segment, position int, selected []gloss
|
||||
for _, neighbor := range neighbors {
|
||||
texts = append(texts, neighbor.Text)
|
||||
}
|
||||
mentioned := make(map[glossary.Identity]struct{})
|
||||
for _, text := range texts {
|
||||
for _, occurrence := range view.vocabulary.occurrences(text) {
|
||||
mentioned[occurrence.identity] = struct{}{}
|
||||
}
|
||||
}
|
||||
var matched []int
|
||||
for identity := range mentioned {
|
||||
matched = append(matched, view.positions[identity]...)
|
||||
}
|
||||
slices.Sort(matched)
|
||||
var entries []glossary.Entry
|
||||
if selected != nil {
|
||||
mentioned := make(map[glossary.Identity]bool)
|
||||
for _, text := range texts {
|
||||
for identity := range vocabulary.occurrencesByIdentity(text) {
|
||||
mentioned[identity] = true
|
||||
}
|
||||
}
|
||||
for _, entry := range selected {
|
||||
if mentioned[glossary.Identity{Collection: entry.Collection, ID: entry.ID}] {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
for _, index := range matched {
|
||||
entries = append(entries, view.entries[index])
|
||||
}
|
||||
return neighbors, entries
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/chunking"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
@@ -254,7 +253,12 @@ func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorT
|
||||
immediate := make([]Decision, 0)
|
||||
var segments []schema.Segment
|
||||
positions := make(map[int]int)
|
||||
vocabulary := NewProtectedVocabulary(glossary.Vocabulary(req.Glossary, nil))
|
||||
var glossary glossaryContext
|
||||
if req.GlossaryView != nil {
|
||||
glossary = req.GlossaryView.context
|
||||
} else {
|
||||
glossary = prepareGlossaryContext(req.Glossary)
|
||||
}
|
||||
if req.WorkingTranscript != nil {
|
||||
segments = req.WorkingTranscript.Segments
|
||||
for i, seg := range segments {
|
||||
@@ -276,7 +280,7 @@ func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorT
|
||||
continue
|
||||
}
|
||||
|
||||
neighbors, glossaryEntries := validationContext(segments, position, req.Glossary, vocabulary, preview.CorrectedSegmentText)
|
||||
neighbors, glossaryEntries := glossary.validationContext(segments, position, preview.CorrectedSegmentText)
|
||||
items = append(items, LLMValidationItem{
|
||||
CorrectionIndex: p.ProposalIndex,
|
||||
SegmentID: p.TargetSegmentID,
|
||||
|
||||
@@ -45,6 +45,7 @@ type Request struct {
|
||||
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
GlossaryView *GlossaryView `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
LLMClient StructuredLLMClient `json:"-"`
|
||||
DiagnosticsWriter InteractionDiagnosticsWriter `json:"-"`
|
||||
|
||||
@@ -110,13 +110,14 @@ func TestExemptGlossarySpellingKeepsLongestMatchBoundary(t *testing.T) {
|
||||
|
||||
func TestValidationContextIncludesEachSharedDefinition(t *testing.T) {
|
||||
entries := []glossary.Entry{
|
||||
{Collection: "items", ID: "sword", Name: "Sword", Category: "item"},
|
||||
{Collection: "spells", ID: "shield", Name: "Shield", Category: "spell"},
|
||||
{Collection: "items", ID: "shield", Name: "Shield", Category: "item"},
|
||||
{Collection: "items", ID: "sword", Name: "Sword", Category: "item"},
|
||||
{Collection: "items", ID: "bow", Name: "Bow", Category: "item"},
|
||||
}
|
||||
segments := []schema.Segment{{ID: 1, Text: "Shield"}}
|
||||
_, selected := validationContext(segments, 0, entries, NewProtectedVocabulary(glossary.Vocabulary(entries, nil)), "Barrier")
|
||||
if len(selected) != 2 || selected[0].Category != "spell" || selected[1].ID != "shield" || selected[1].Collection != "items" {
|
||||
segments := []schema.Segment{{ID: 1, Text: "Shield Shield"}}
|
||||
_, selected := prepareGlossaryContext(entries).validationContext(segments, 0, "Sword")
|
||||
if len(selected) != 3 || selected[0].ID != "sword" || selected[1].Category != "spell" || selected[2].ID != "shield" || selected[2].Collection != "items" {
|
||||
t.Fatalf("selected=%+v", selected)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user