Share section lookups and replacement previews across validators
This commit is contained in:
@@ -3,11 +3,9 @@ package validators
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
)
|
||||
|
||||
@@ -60,21 +58,16 @@ type OriginalTextPresenceValidator struct{}
|
||||
func (v OriginalTextPresenceValidator) Name() string { return "original_text_presence" }
|
||||
|
||||
func (v OriginalTextPresenceValidator) Validate(_ context.Context, req Request) (Result, error) {
|
||||
byID := make(map[int]string)
|
||||
if req.WorkingTranscript != nil {
|
||||
for _, seg := range req.WorkingTranscript.Segments {
|
||||
byID[seg.ID] = seg.Text
|
||||
}
|
||||
}
|
||||
section := req.sectionLookup()
|
||||
|
||||
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
||||
for _, c := range req.CandidateProposal {
|
||||
text, ok := byID[c.TargetSegmentID]
|
||||
position, ok := section.Position(c.TargetSegmentID)
|
||||
if !ok {
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found"))
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(text, c.OriginalText) {
|
||||
if !strings.Contains(section.segments[position].Text, c.OriginalText) {
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonMissingOriginalText, "original_text was not found in current segment text"))
|
||||
continue
|
||||
}
|
||||
@@ -91,21 +84,11 @@ type NonEmptyResultingSegmentValidator struct{}
|
||||
func (v NonEmptyResultingSegmentValidator) Name() string { return "non_empty_resulting_segment" }
|
||||
|
||||
func (v NonEmptyResultingSegmentValidator) Validate(_ context.Context, req Request) (Result, error) {
|
||||
segmentsByID := make(map[int]schema.Segment)
|
||||
if req.WorkingTranscript != nil {
|
||||
for _, seg := range req.WorkingTranscript.Segments {
|
||||
segmentsByID[seg.ID] = seg
|
||||
}
|
||||
}
|
||||
req.Section = req.sectionLookup()
|
||||
|
||||
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
||||
for _, c := range req.CandidateProposal {
|
||||
segment, ok := segmentsByID[c.TargetSegmentID]
|
||||
if !ok {
|
||||
decisions = append(decisions, approval(c.ProposalIndex))
|
||||
continue
|
||||
}
|
||||
preview := previewForEvidence(req, &segment, c.CorrectionProposal)
|
||||
preview := req.Preview(c)
|
||||
if preview.SkipReason == proposals.SkipReasonEmptyResultingText {
|
||||
decisions = append(decisions, rejection(c.ProposalIndex, ReasonEmptyResultingText, "proposal would leave the segment empty"))
|
||||
continue
|
||||
@@ -149,13 +132,11 @@ func ValidateProtectedTerms(req Request) (Result, error) {
|
||||
for _, c := range req.CandidateProposal {
|
||||
reason := vocab.violationReason(c.OriginalText, c.CorrectedText)
|
||||
repetitionCleanup := reason != "" && req.ModuleKey == modulecatalog.KeySpokenWord
|
||||
if vocab.pattern != nil && (glossaryStage || repetitionCleanup) && req.WorkingTranscript != nil {
|
||||
for i := range req.WorkingTranscript.Segments {
|
||||
segment := &req.WorkingTranscript.Segments[i]
|
||||
if segment.ID != c.TargetSegmentID {
|
||||
continue
|
||||
}
|
||||
preview := previewForEvidence(req, segment, c.CorrectionProposal)
|
||||
if vocab.pattern != nil && (glossaryStage || repetitionCleanup) {
|
||||
req.Section = req.sectionLookup()
|
||||
if position, ok := req.Section.Position(c.TargetSegmentID); ok {
|
||||
segment := &req.Section.segments[position]
|
||||
preview := req.Preview(c)
|
||||
if preview.Applicable || (req.IndependentEvidence && preview.SkipReason == proposals.SkipReasonEmptyResultingText) {
|
||||
if glossaryStage {
|
||||
// A correction span may sit inside a longer exempt name.
|
||||
@@ -164,7 +145,6 @@ func ValidateProtectedTerms(req Request) (Result, error) {
|
||||
reason = ""
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if reason != "" {
|
||||
@@ -178,10 +158,3 @@ func ValidateProtectedTerms(req Request) (Result, error) {
|
||||
}
|
||||
return Result{ValidatorName: "protected_terms", Decisions: decisions}, nil
|
||||
}
|
||||
|
||||
func previewForEvidence(req Request, segment *schema.Segment, correction proposals.CorrectionProposal) proposals.SegmentPreviewResult {
|
||||
if req.IndependentEvidence && (math.IsNaN(correction.Confidence) || correction.Confidence < 0 || correction.Confidence > 1) {
|
||||
correction.Confidence = 0.5
|
||||
}
|
||||
return proposals.PreviewProposalForSegment(segment, correction, req.ReplacementPolicy)
|
||||
}
|
||||
|
||||
@@ -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/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/stagename"
|
||||
@@ -243,30 +242,24 @@ func errPayload(err error) any {
|
||||
func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorType, req Request) (LLMValidationRequest, []Decision) {
|
||||
items := make([]LLMValidationItem, 0, len(req.CandidateProposal))
|
||||
immediate := make([]Decision, 0)
|
||||
var segments []schema.Segment
|
||||
positions := make(map[int]int)
|
||||
req.Section = req.sectionLookup()
|
||||
segments := req.Section.segments
|
||||
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 {
|
||||
positions[seg.ID] = i
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range req.CandidateProposal {
|
||||
position, ok := positions[p.TargetSegmentID]
|
||||
position, ok := req.Section.Position(p.TargetSegmentID)
|
||||
if !ok {
|
||||
immediate = append(immediate, rejection(p.ProposalIndex, ReasonMissingTargetSegment, "target segment was not found"))
|
||||
continue
|
||||
}
|
||||
|
||||
seg := segments[position]
|
||||
preview := previewForEvidence(req, &seg, p.CorrectionProposal)
|
||||
preview := req.Preview(p)
|
||||
if !preview.Applicable {
|
||||
immediate = append(immediate, rejection(p.ProposalIndex, string(preview.SkipReason), "proposal is not previewable for LLM validation"))
|
||||
continue
|
||||
|
||||
@@ -41,6 +41,8 @@ type Request struct {
|
||||
RepairAttempt *int `json:"repair_attempt,omitempty"`
|
||||
SectionIndex *int `json:"section_index,omitempty"`
|
||||
WorkingTranscript *schema.Transcript `json:"-"`
|
||||
Section *SectionLookup `json:"-"`
|
||||
Previews map[int]proposals.SegmentPreviewResult `json:"-"`
|
||||
CandidateProposal []proposals.EnrichedCorrectionProposal `json:"candidate_proposals"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
ModuleInstance string `json:"module_instance"`
|
||||
|
||||
61
internal/framework/validators/section.go
Normal file
61
internal/framework/validators/section.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package validators
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
)
|
||||
|
||||
// SectionLookup indexes the immutable source section shared by validation
|
||||
// and revalidation. Application checks its own, changing transcript.
|
||||
type SectionLookup struct {
|
||||
segments []schema.Segment
|
||||
positions map[int]int
|
||||
}
|
||||
|
||||
func NewSectionLookup(transcript *schema.Transcript) *SectionLookup {
|
||||
section := &SectionLookup{}
|
||||
if transcript != nil {
|
||||
section.segments = transcript.Segments
|
||||
section.positions = make(map[int]int, len(section.segments))
|
||||
for i, segment := range section.segments {
|
||||
section.positions[segment.ID] = i
|
||||
}
|
||||
}
|
||||
return section
|
||||
}
|
||||
|
||||
func (s *SectionLookup) Position(id int) (int, bool) {
|
||||
position, ok := s.positions[id]
|
||||
return position, ok
|
||||
}
|
||||
|
||||
func (r Request) sectionLookup() *SectionLookup {
|
||||
if r.Section != nil {
|
||||
return r.Section
|
||||
}
|
||||
return NewSectionLookup(r.WorkingTranscript)
|
||||
}
|
||||
|
||||
// Preview reuses the replacement preview for this proposal index within one
|
||||
// validation pass. A revision starts a new pass with its own preview map.
|
||||
func (r Request) Preview(candidate proposals.EnrichedCorrectionProposal) proposals.SegmentPreviewResult {
|
||||
if preview, ok := r.Previews[candidate.ProposalIndex]; ok {
|
||||
return preview
|
||||
}
|
||||
section := r.sectionLookup()
|
||||
var segment *schema.Segment
|
||||
if position, ok := section.Position(candidate.TargetSegmentID); ok {
|
||||
segment = §ion.segments[position]
|
||||
}
|
||||
correction := candidate.CorrectionProposal
|
||||
if r.IndependentEvidence && (math.IsNaN(correction.Confidence) || correction.Confidence < 0 || correction.Confidence > 1) {
|
||||
correction.Confidence = 0.5
|
||||
}
|
||||
preview := proposals.PreviewProposalForSegment(segment, correction, r.ReplacementPolicy)
|
||||
if r.Previews != nil {
|
||||
r.Previews[candidate.ProposalIndex] = preview
|
||||
}
|
||||
return preview
|
||||
}
|
||||
Reference in New Issue
Block a user