Reuse prepared glossary data across stage sections
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"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/outcomes"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
|
||||
@@ -73,13 +74,14 @@ type ExecutionContext struct {
|
||||
StageIndex *int `json:"stage_index,omitempty"`
|
||||
StageID *string `json:"stage_id,omitempty"`
|
||||
// ReportDiagnosticError may be called concurrently; it must not affect processing.
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
WorkingTranscript *schema.Transcript `json:"-"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
Section *SectionMetadata `json:"section,omitempty"`
|
||||
DiagnosticsDir string `json:"diagnostics_dir,omitempty"`
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
WorkingTranscript *schema.Transcript `json:"-"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
GlossaryVariables *promptcontext.GlossaryVariables `json:"-"`
|
||||
Section *SectionMetadata `json:"section,omitempty"`
|
||||
DiagnosticsDir string `json:"diagnostics_dir,omitempty"`
|
||||
}
|
||||
|
||||
// ProposalRequest is the input to module proposal generation.
|
||||
|
||||
32
internal/framework/promptcontext/glossary.go
Normal file
32
internal/framework/promptcontext/glossary.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package promptcontext
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
|
||||
)
|
||||
|
||||
// GlossaryVariables contains the glossary inputs shared by a stage's proposals.
|
||||
type GlossaryVariables struct {
|
||||
ContextJSON string
|
||||
ProtectionJSON string
|
||||
}
|
||||
|
||||
func PrepareGlossaryVariables(entries []glossary.Entry, terms []glossary.Term) (GlossaryVariables, error) {
|
||||
contextJSON, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
return GlossaryVariables{}, fmt.Errorf("marshal glossary prompt context: %w", err)
|
||||
}
|
||||
protected := make([]string, 0, len(terms))
|
||||
for _, term := range terms {
|
||||
if !term.Exempt {
|
||||
protected = append(protected, term.Spelling)
|
||||
}
|
||||
}
|
||||
protectionJSON, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return GlossaryVariables{}, fmt.Errorf("marshal protected spellings: %w", err)
|
||||
}
|
||||
return GlossaryVariables{ContextJSON: string(contextJSON), ProtectionJSON: string(protectionJSON)}, nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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/llm"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
||||
@@ -60,7 +61,11 @@ func TestRepairedProposalStillFacesDomainChecks(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "gestures"}}}
|
||||
vars, err := BuildProposalVariables(transcript, nil, 0, "")
|
||||
prepared, err := promptcontext.PrepareGlossaryVariables(nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vars, err := buildProposalVariables(transcript, prepared, 0, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"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/promptcontext"
|
||||
)
|
||||
|
||||
func TestProposalContextPreservesDistinctEntries(t *testing.T) {
|
||||
@@ -13,7 +14,11 @@ func TestProposalContextPreservesDistinctEntries(t *testing.T) {
|
||||
{Collection: "spells", ID: "shield", Name: "Shield", Category: "spell", Description: "A spell"},
|
||||
{Collection: "items", ID: "shield", Name: "Shield", Category: "item", Description: "An item"},
|
||||
}
|
||||
variables, err := BuildProposalVariables(&schema.Transcript{}, entries, 0, "")
|
||||
prepared, err := promptcontext.PrepareGlossaryVariables(entries, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
variables, err := buildProposalVariables(&schema.Transcript{}, prepared, 0, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -2,31 +2,22 @@ package proposal_generation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/stagename"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
||||
)
|
||||
|
||||
type ProposalVariableBuilder func(transcript *schema.Transcript, entries []glossary.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error)
|
||||
|
||||
type ModuleProposalRequest struct {
|
||||
ProposalRequest contracts.ProposalRequest
|
||||
PromptID string
|
||||
BuildVariables ProposalVariableBuilder
|
||||
}
|
||||
|
||||
// ExecuteModuleProposal runs shared proposal generation plumbing for one
|
||||
// module, leaving only module-specific prompt message building at call sites.
|
||||
// ExecuteModuleProposal prepares and runs a module's selected prompt.
|
||||
func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (contracts.ProposalResult, error) {
|
||||
if req.BuildVariables == nil {
|
||||
return contracts.ProposalResult{}, fmt.Errorf("proposal variable builder is required")
|
||||
}
|
||||
if strings.TrimSpace(req.PromptID) == "" {
|
||||
return contracts.ProposalResult{}, fmt.Errorf("prompt ID must not be empty")
|
||||
}
|
||||
@@ -38,9 +29,17 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
|
||||
transcriptDescription := req.ProposalRequest.Settings.Context.Description
|
||||
|
||||
variables, err := req.BuildVariables(
|
||||
glossary := req.ProposalRequest.GlossaryVariables
|
||||
if glossary == nil {
|
||||
prepared, err := promptcontext.PrepareGlossaryVariables(req.ProposalRequest.Glossary, req.ProposalRequest.ProtectedTerms)
|
||||
if err != nil {
|
||||
return contracts.ProposalResult{}, err
|
||||
}
|
||||
glossary = &prepared
|
||||
}
|
||||
variables, err := buildProposalVariables(
|
||||
req.ProposalRequest.WorkingTranscript,
|
||||
req.ProposalRequest.Glossary,
|
||||
*glossary,
|
||||
sectionIndex,
|
||||
transcriptDescription,
|
||||
)
|
||||
@@ -48,18 +47,6 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
return contracts.ProposalResult{}, err
|
||||
}
|
||||
|
||||
protected := make([]string, 0, len(req.ProposalRequest.ProtectedTerms))
|
||||
for _, term := range req.ProposalRequest.ProtectedTerms {
|
||||
if !term.Exempt {
|
||||
protected = append(protected, term.Spelling)
|
||||
}
|
||||
}
|
||||
payload, err := json.Marshal(protected)
|
||||
if err != nil {
|
||||
return contracts.ProposalResult{}, fmt.Errorf("marshal protected spellings: %w", err)
|
||||
}
|
||||
variables["ProtectedTermsJSON"] = string(payload)
|
||||
|
||||
promptMetadata, ok := prompts.LookupMetadata(req.PromptID)
|
||||
if !ok {
|
||||
return contracts.ProposalResult{}, fmt.Errorf("unknown prompt ID %q", req.PromptID)
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
@@ -29,11 +29,6 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}}
|
||||
glossary := []domain.Entry{{ID: "x", Name: "X"}}
|
||||
|
||||
var gotSectionIndex int
|
||||
var gotDescription string
|
||||
var gotTranscript *schema.Transcript
|
||||
var gotGlossary []domain.Entry
|
||||
|
||||
out, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
|
||||
ProposalRequest: contracts.ProposalRequest{
|
||||
ExecutionContext: contracts.ExecutionContext{
|
||||
@@ -51,32 +46,20 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
LLMClient: client,
|
||||
},
|
||||
PromptID: prompts.PromptIDModuleGrammarProposal,
|
||||
BuildVariables: func(inTranscript *schema.Transcript, inGlossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
gotSectionIndex = sectionIndex
|
||||
gotDescription = transcriptDescription
|
||||
gotTranscript = inTranscript
|
||||
gotGlossary = inGlossary
|
||||
return map[string]string{"SectionJSON": "[]"}, nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteModuleProposal error: %v", err)
|
||||
}
|
||||
if gotSectionIndex != 7 {
|
||||
t.Fatalf("section index: got=%d want=%d", gotSectionIndex, 7)
|
||||
}
|
||||
if gotDescription != cfg.Context.Description {
|
||||
t.Fatalf("transcript description: got=%q want=%q", gotDescription, cfg.Context.Description)
|
||||
}
|
||||
if gotTranscript != transcript {
|
||||
t.Fatalf("expected shared transcript pointer")
|
||||
}
|
||||
if !reflect.DeepEqual(gotGlossary, glossary) {
|
||||
t.Fatalf("expected glossary entries")
|
||||
}
|
||||
if len(client.calls) != 1 || client.calls[0].PromptID != prompts.PromptIDModuleGrammarProposal || client.calls[0].Context.SectionIndex == nil || *client.calls[0].Context.SectionIndex != 7 {
|
||||
t.Fatalf("unexpected stage name calls: %+v", client.calls)
|
||||
}
|
||||
variables := client.calls[0].Variables
|
||||
if !strings.Contains(variables["SectionJSON"], `"section_index": 7`) ||
|
||||
!strings.Contains(variables["SectionJSON"], "teh") ||
|
||||
!strings.Contains(variables["TranscriptDescriptionBlock"], cfg.Context.Description) ||
|
||||
!strings.Contains(variables["GlossaryJSON"], `"name": "X"`) {
|
||||
t.Fatalf("missing section, glossary, or description: %+v", variables)
|
||||
}
|
||||
if got := client.calls[0].Variables["ProtectedTermsJSON"]; got != `["Separate protection"]` {
|
||||
t.Fatalf("protected spellings = %s", got)
|
||||
}
|
||||
@@ -86,40 +69,10 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
}
|
||||
|
||||
func TestExecuteModuleProposalValidatesInputs(t *testing.T) {
|
||||
if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{}); err == nil {
|
||||
t.Fatalf("expected missing message builder error")
|
||||
}
|
||||
|
||||
_, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
|
||||
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected empty prompt ID error")
|
||||
}
|
||||
|
||||
_, err = ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
|
||||
PromptID: "missing.prompt.id",
|
||||
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
return map[string]string{"SectionJSON": "[]"}, nil
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected unknown prompt ID error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteModuleProposalPropagatesBuilderError(t *testing.T) {
|
||||
wantErr := errors.New("builder failed")
|
||||
_, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{
|
||||
PromptID: prompts.PromptIDModuleGlossaryProposal,
|
||||
BuildVariables: func(transcript *schema.Transcript, glossary []domain.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
return nil, wantErr
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("expected builder error, got %v", err)
|
||||
for _, promptID := range []string{"", "missing.prompt.id"} {
|
||||
if _, err := ExecuteModuleProposal(context.Background(), ModuleProposalRequest{PromptID: promptID}); err == nil {
|
||||
t.Fatalf("invalid prompt ID accepted: %q", promptID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,9 +87,6 @@ func TestExecuteModuleProposalPreservesFatalOperation(t *testing.T) {
|
||||
LLMClient: client,
|
||||
},
|
||||
PromptID: prompts.PromptIDModuleGrammarProposal,
|
||||
BuildVariables: func(*schema.Transcript, []domain.Entry, int, string) (map[string]string, error) {
|
||||
return map[string]string{"SectionJSON": "[]"}, nil
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, cause) || len(result.Operations) != 1 || result.Operations[0].Reference.ModuleInstance != "grammar_2" || result.Operations[0].Reference.SectionIndex != 4 {
|
||||
t.Fatalf("fatal operation lost: %+v, %v", result, err)
|
||||
@@ -157,7 +107,7 @@ func TestModuleProposalMapsLocalReferencesWithoutGuessing(t *testing.T) {
|
||||
RunSpec: contracts.ModuleRunSpec{ModuleKey: "grammar", InstanceName: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique},
|
||||
LLMClient: client,
|
||||
},
|
||||
PromptID: prompts.PromptIDModuleGrammarProposal, BuildVariables: BuildProposalVariables,
|
||||
PromptID: prompts.PromptIDModuleGrammarProposal,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
package proposal_generation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"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/promptcontext"
|
||||
)
|
||||
|
||||
// BuildProposalVariables preserves the domain inputs while PromptKit renders
|
||||
// the selected embedded proposal definition.
|
||||
func BuildProposalVariables(transcript *schema.Transcript, entries []glossary.Entry, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
glossaryJSON, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal glossary prompt context: %w", err)
|
||||
}
|
||||
func buildProposalVariables(transcript *schema.Transcript, glossary promptcontext.GlossaryVariables, sectionIndex int, transcriptDescription string) (map[string]string, error) {
|
||||
sectionJSON, err := promptcontext.MarshalTranscriptSectionJSON(transcript, sectionIndex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal transcript prompt context: %w", err)
|
||||
}
|
||||
return map[string]string{
|
||||
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
|
||||
"GlossaryJSON": string(glossaryJSON),
|
||||
"GlossaryJSON": glossary.ContextJSON,
|
||||
"ProtectedTermsJSON": glossary.ProtectionJSON,
|
||||
"SectionJSON": string(sectionJSON),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -6,19 +6,24 @@ import (
|
||||
|
||||
domain "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/promptcontext"
|
||||
)
|
||||
|
||||
func TestBuildProposalVariablesPreservesSectionGlossaryAndDescription(t *testing.T) {
|
||||
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "gestures"}}}
|
||||
glossary := []domain.Entry{{ID: "jesters", Name: "Jesters"}}
|
||||
variables, err := BuildProposalVariables(transcript, glossary, 0, " Fantasy names ")
|
||||
prepared, err := promptcontext.PrepareGlossaryVariables(glossary, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
variables, err := buildProposalVariables(transcript, prepared, 0, " Fantasy names ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(variables["SectionJSON"], `"section_index": 0`) || !strings.Contains(variables["SectionJSON"], "gestures") || !strings.Contains(variables["GlossaryJSON"], "Jesters") || !strings.Contains(variables["TranscriptDescriptionBlock"], "Fantasy names") {
|
||||
t.Fatalf("encoded variables incomplete: %+v", variables)
|
||||
}
|
||||
empty, err := BuildProposalVariables(transcript, glossary, 0, " ")
|
||||
empty, err := buildProposalVariables(transcript, prepared, 0, " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
|
||||
validatormetadata "gitea.maximumdirect.net/eric/audita/v3/internal/validators/metadata"
|
||||
@@ -163,6 +164,11 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
||||
}
|
||||
|
||||
policy := module.ReplacementPolicy()
|
||||
glossaryVariables, err := promptcontext.PrepareGlossaryVariables(spec.Glossary, spec.ProtectedTerms)
|
||||
if err != nil {
|
||||
return snapshot(), fmt.Errorf("module %q glossary preparation: %w", spec.InstanceName, err)
|
||||
}
|
||||
glossaryView := validators.PrepareGlossaryView(spec.Glossary, spec.ProtectedTerms)
|
||||
pipelineResult, pipelineErr := runModulePipeline(ctx, collectSectionProposalsInput{
|
||||
Module: module,
|
||||
Spec: spec,
|
||||
@@ -172,6 +178,8 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
||||
DiagnosticsDir: input.ProposalDiagnosticsDir,
|
||||
ValidationClient: spec.ValidationClient,
|
||||
ValidationDiagnosticsDir: input.ValidationDiagnosticsDir,
|
||||
GlossaryVariables: &glossaryVariables,
|
||||
GlossaryView: glossaryView,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
})
|
||||
if input.CaptureCandidates {
|
||||
@@ -198,7 +206,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
|
||||
return snapshot(), fmt.Errorf("module %q failed: %w", spec.InstanceName, pipelineErr)
|
||||
}
|
||||
|
||||
updated, applied, skipped, appliedBySection, applyErr := applyWithinSections(sections, pipelineResult.Approved, policy, validators.NewProtectedVocabulary(spec.ProtectedTerms), spec.ModuleKey == modulecatalog.KeySpokenWord)
|
||||
updated, applied, skipped, appliedBySection, applyErr := applyWithinSections(sections, pipelineResult.Approved, policy, glossaryView.Protection, spec.ModuleKey == modulecatalog.KeySpokenWord)
|
||||
if applyErr != nil {
|
||||
return snapshot(), fmt.Errorf("module %q application failed: %w", spec.InstanceName, applyErr)
|
||||
}
|
||||
@@ -286,6 +294,8 @@ type collectSectionProposalsInput struct {
|
||||
DiagnosticsDir string
|
||||
ValidationClient contracts.StructuredLLMClient
|
||||
ValidationDiagnosticsDir string
|
||||
GlossaryVariables *promptcontext.GlossaryVariables
|
||||
GlossaryView *validators.GlossaryView
|
||||
}
|
||||
|
||||
type sectionProposalResult struct {
|
||||
@@ -343,6 +353,7 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
|
||||
WorkingTranscript: transcriptFromSection(section),
|
||||
Glossary: input.Spec.Glossary,
|
||||
ProtectedTerms: input.Spec.ProtectedTerms,
|
||||
GlossaryVariables: input.GlossaryVariables,
|
||||
Section: &meta,
|
||||
DiagnosticsDir: input.DiagnosticsDir,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
@@ -488,6 +499,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
|
||||
SectionEnriched: enriched,
|
||||
ValidationLLMClient: input.ValidationClient,
|
||||
DiagnosticsDir: input.ValidationDiagnosticsDir,
|
||||
GlossaryView: input.GlossaryView,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
})
|
||||
setErr(err)
|
||||
@@ -571,6 +583,7 @@ type validateSectionCandidatesInput struct {
|
||||
SectionEnriched []proposals.EnrichedCorrectionProposal
|
||||
ValidationLLMClient contracts.StructuredLLMClient
|
||||
DiagnosticsDir string
|
||||
GlossaryView *validators.GlossaryView
|
||||
}
|
||||
|
||||
type validateSectionCandidatesResult struct {
|
||||
@@ -618,6 +631,7 @@ func validateSectionCandidates(ctx context.Context, input validateSectionCandida
|
||||
ReplacementPolicy: input.Policy,
|
||||
Glossary: input.Spec.Glossary,
|
||||
ProtectedTerms: input.Spec.ProtectedTerms,
|
||||
GlossaryView: input.GlossaryView,
|
||||
Settings: input.Spec.Settings,
|
||||
LLMClient: input.ValidationLLMClient,
|
||||
DiagnosticsWriter: diagnosticsWriter,
|
||||
|
||||
@@ -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