Reuse prepared glossary data across stage sections
This commit is contained in:
@@ -37,9 +37,9 @@ Shared proposal-generation plumbing is centralized in:
|
||||
Module packages own:
|
||||
- prompt selection (`internal/prompts` prompt IDs).
|
||||
|
||||
`proposal_generation.BuildProposalVariables` encodes the glossary, selected
|
||||
transcript section, and trimmed description once for all four modules. PromptKit
|
||||
renders their embedded definitions from those variables and validates the
|
||||
Each stage prepares the encoded glossary context and protected spellings once.
|
||||
Proposal generation adds the selected transcript section and trimmed description
|
||||
for each request. PromptKit renders the selected definition and validates the
|
||||
correction-set schema before Audita maps proposals. Domain validators still
|
||||
check confidence, applicability, protected glossary terms, and replacement
|
||||
policy. Model-facing segment IDs are one-based positions within the proposal
|
||||
|
||||
@@ -120,10 +120,11 @@ is covered by [report assembly](diagnostics-reporting.md).
|
||||
|
||||
## Supplemental Validation Context
|
||||
|
||||
Request construction prepares one segment-position lookup and a matcher from the
|
||||
selected context entries for all candidates. Context selection remains candidate-specific,
|
||||
including the proposed replacement text. The independent protection view is used
|
||||
by the deterministic guard and application check.
|
||||
Each stage prepares a context matcher and indexes its selected glossary entries
|
||||
by identity. Request construction uses a segment-position lookup and returns
|
||||
matched entries in selection order for each candidate, including matches in the
|
||||
replacement text. The deterministic guard and application check share the
|
||||
stage's separate protection matcher.
|
||||
|
||||
Each candidate includes the immediately preceding and following source segments
|
||||
available within its fixed chunk, labeled `preceding` or `following` with speaker,
|
||||
|
||||
@@ -562,6 +562,8 @@ are preserved. Readiness keeps the same late-stage diagnostics.
|
||||
|
||||
## R16 — P2: Reuse Prepared Glossary Views and Index Validation Context
|
||||
|
||||
**Status:** Completed. Each stage prepares shared protection and context matchers and serializes its proposal glossary variables once. Context gathers matched entry positions in selection order. Tests cover shared identities, ordering, exemptions, and request content.
|
||||
|
||||
**Category**
|
||||
|
||||
Per-section rebuilds and per-candidate glossary scans in Stages 11 and 12.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
|
||||
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
|
||||
ProposalRequest: req,
|
||||
PromptID: prompts.PromptIDModuleGlossaryProposal,
|
||||
BuildVariables: proposal_generation.BuildProposalVariables,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
|
||||
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
|
||||
ProposalRequest: req,
|
||||
PromptID: prompts.PromptIDModuleGrammarProposal,
|
||||
BuildVariables: proposal_generation.BuildProposalVariables,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
|
||||
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
|
||||
ProposalRequest: req,
|
||||
PromptID: prompts.PromptIDModuleHomophonesProposal,
|
||||
BuildVariables: proposal_generation.BuildProposalVariables,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,5 @@ func (m *Module) Propose(ctx context.Context, req contracts.ProposalRequest) (co
|
||||
return proposal_generation.ExecuteModuleProposal(ctx, proposal_generation.ModuleProposalRequest{
|
||||
ProposalRequest: req,
|
||||
PromptID: prompts.PromptIDModuleSpokenWordProposal,
|
||||
BuildVariables: proposal_generation.BuildProposalVariables,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user