Run ordered processing with independent settings
This commit is contained in:
@@ -7,6 +7,7 @@ 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/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/proposals"
|
||||
@@ -38,6 +39,13 @@ type ModuleRunSpec struct {
|
||||
ModuleKey string `json:"module_key"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
|
||||
Index int `json:"-"`
|
||||
ID *string `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
ProposalClient StructuredLLMClient `json:"-"`
|
||||
ValidationClient StructuredLLMClient `json:"-"`
|
||||
}
|
||||
|
||||
// SectionMetadata captures chunk-level metadata without embedding full segment payloads.
|
||||
@@ -64,7 +72,7 @@ func SectionMetadataFromSection(section chunking.Section) SectionMetadata {
|
||||
type ExecutionContext struct {
|
||||
// ReportDiagnosticError may be called concurrently; it must not affect processing.
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Config *config.Config `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
WorkingTranscript *schema.Transcript `json:"-"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
@@ -87,32 +95,20 @@ type ProposalResult struct {
|
||||
// ValidationRequest is the input to validator execution.
|
||||
type ValidationRequest = validators.Request
|
||||
|
||||
// ResolveModuleRunSpecs deterministically resolves instance names from logical keys.
|
||||
// Repeated keys are suffixed with _<n> (1-based), while singleton keys keep their raw key.
|
||||
func ResolveModuleRunSpecs(moduleKeys []string) ([]ModuleRunSpec, error) {
|
||||
totals := make(map[string]int, len(moduleKeys))
|
||||
for i, key := range moduleKeys {
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("module key at index %d must not be empty", i)
|
||||
}
|
||||
totals[key]++
|
||||
// ResolveModuleRunSpecs derives isolated consumer views from resolved settings.
|
||||
func ResolveModuleRunSpecs(cfg *config.Config) []ModuleRunSpec {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]int, len(totals))
|
||||
specs := make([]ModuleRunSpec, len(moduleKeys))
|
||||
for i, key := range moduleKeys {
|
||||
seen[key]++
|
||||
|
||||
name := key
|
||||
if totals[key] > 1 {
|
||||
name = fmt.Sprintf("%s_%d", key, seen[key])
|
||||
}
|
||||
|
||||
specs[i] = ModuleRunSpec{
|
||||
ModuleKey: key,
|
||||
InstanceName: name,
|
||||
specs := make([]ModuleRunSpec, len(cfg.Pipeline.Stages))
|
||||
for i, item := range cfg.Pipeline.Stages {
|
||||
selected := glossary.Select(cfg.GlossaryCollections, item.Glossaries)
|
||||
protected := glossary.Select(cfg.GlossaryCollections, item.ProtectedTerms)
|
||||
var excluded []glossary.Entry
|
||||
if item.Module == modulecatalog.KeyGlossary {
|
||||
excluded = selected
|
||||
}
|
||||
specs[i] = ModuleRunSpec{ModuleKey: item.Module, InstanceName: fmt.Sprintf("%s_%d", item.Module, i+1), Index: i, ID: item.ID, Settings: item, Glossary: selected, ProtectedTerms: glossary.Vocabulary(protected, excluded)}
|
||||
}
|
||||
|
||||
return specs, nil
|
||||
return specs
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"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/contracts"
|
||||
glossarymodule "gitea.maximumdirect.net/eric/audita/v3/internal/modules/glossary"
|
||||
grammarmodule "gitea.maximumdirect.net/eric/audita/v3/internal/modules/grammar"
|
||||
@@ -32,19 +30,9 @@ func IsKnownModuleKey(key string) bool {
|
||||
return modulecatalog.IsSupported(key)
|
||||
}
|
||||
|
||||
// Dependencies holds explicit constructor dependencies for module creation.
|
||||
type Dependencies struct {
|
||||
Config *config.Config
|
||||
Glossary *schema.Glossary
|
||||
ProposalLLMClient contracts.StructuredLLMClient
|
||||
ValidationLLMClient contracts.StructuredLLMClient
|
||||
DiagnosticsDir string
|
||||
}
|
||||
|
||||
// ConstructRequest is one module-construction request.
|
||||
// ConstructRequest identifies the module to construct.
|
||||
type ConstructRequest struct {
|
||||
RunSpec contracts.ModuleRunSpec
|
||||
Dependencies
|
||||
}
|
||||
|
||||
// Constructor builds one module instance from a run spec and explicit deps.
|
||||
@@ -52,14 +40,12 @@ type Constructor func(ctx context.Context, req ConstructRequest) (contracts.Tran
|
||||
|
||||
// Factory resolves configured module specs into module instances.
|
||||
type Factory struct {
|
||||
deps Dependencies
|
||||
constructors map[string]Constructor
|
||||
}
|
||||
|
||||
// NewFactory creates a production module registry.
|
||||
func NewFactory(deps Dependencies) *Factory {
|
||||
func NewFactory() *Factory {
|
||||
factory := &Factory{
|
||||
deps: deps,
|
||||
constructors: make(map[string]Constructor, len(modulecatalog.SupportedKeys())),
|
||||
}
|
||||
_ = factory.RegisterConstructor(ModuleKeyGlossary, constructGlossaryModule)
|
||||
@@ -125,16 +111,7 @@ func (f *Factory) ModuleForSpec(spec contracts.ModuleRunSpec) (contracts.Transcr
|
||||
return nil, fmt.Errorf("internal module registry error: constructor for module %q is not configured", key)
|
||||
}
|
||||
|
||||
module, err := constructor(context.Background(), ConstructRequest{
|
||||
RunSpec: spec,
|
||||
Dependencies: Dependencies{
|
||||
Config: f.deps.Config,
|
||||
Glossary: f.deps.Glossary,
|
||||
ProposalLLMClient: f.deps.ProposalLLMClient,
|
||||
ValidationLLMClient: f.deps.ValidationLLMClient,
|
||||
DiagnosticsDir: f.deps.DiagnosticsDir,
|
||||
},
|
||||
})
|
||||
module, err := constructor(context.Background(), ConstructRequest{RunSpec: spec})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("construct module %q: %w", spec.InstanceName, err)
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ import (
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/glossary"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/modulecatalog"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
)
|
||||
|
||||
// Input contains already acquired application and transcript inputs.
|
||||
type Input struct {
|
||||
InspectProfile func(context.Context, string) error
|
||||
Config *config.Config
|
||||
Transcript *schema.SourceTranscript
|
||||
Output diagnostics.OutputSelection
|
||||
@@ -45,6 +48,9 @@ func Prepare(ctx context.Context, input Input) (*Prepared, error) {
|
||||
if input.Transcript == nil {
|
||||
return nil, fmt.Errorf("transcript is required")
|
||||
}
|
||||
if err := CheckReadiness(ctx, input.Config, input.InspectProfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
session, err := diagnostics.SelectOutput(input.Output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -74,3 +80,38 @@ func Prepare(ctx context.Context, input Input) (*Prepared, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// CheckReadiness checks all resolved processing selections before output preparation.
|
||||
func CheckReadiness(ctx context.Context, cfg *config.Config, inspectProfile func(context.Context, string) error) error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("application configuration is required")
|
||||
}
|
||||
label := func(index int, item config.Stage) string {
|
||||
if item.ID != nil {
|
||||
return fmt.Sprintf("stage %d (%s, id %q)", index+1, item.Module, *item.ID)
|
||||
}
|
||||
return fmt.Sprintf("stage %d (%s)", index+1, item.Module)
|
||||
}
|
||||
for index, item := range cfg.Pipeline.Stages {
|
||||
if item.Module == modulecatalog.KeyGlossary && len(glossary.Select(cfg.GlossaryCollections, item.Glossaries)) == 0 {
|
||||
return fmt.Errorf("%s has no glossary entries in selected collections %q", label(index, item), item.Glossaries)
|
||||
}
|
||||
}
|
||||
for index, item := range cfg.Pipeline.Stages {
|
||||
for _, selection := range []struct {
|
||||
role string
|
||||
profile *string
|
||||
}{{"proposal", item.LLM.ProposalProfile}, {"validation", item.LLM.ValidationProfile}} {
|
||||
if selection.profile == nil || *selection.profile == "" {
|
||||
return fmt.Errorf("%s requires a %s profile", label(index, item), selection.role)
|
||||
}
|
||||
if inspectProfile == nil {
|
||||
return fmt.Errorf("profile inspection is required")
|
||||
}
|
||||
if err := inspectProfile(ctx, *selection.profile); err != nil {
|
||||
return fmt.Errorf("%s %s profile %q: %w", label(index, item), selection.role, *selection.profile, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
39
internal/framework/preflight/readiness_test.go
Normal file
39
internal/framework/preflight/readiness_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/diagnostics"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/schema"
|
||||
)
|
||||
|
||||
func TestLaterEmptyGlossaryFailsBeforeOutputPreparation(t *testing.T) {
|
||||
profile, id := "selected", "spells"
|
||||
cfg := &config.Config{Pipeline: config.Pipeline{Stages: []config.Stage{
|
||||
{Module: "grammar", LLM: config.LLM{ProposalProfile: &profile, ValidationProfile: &profile}},
|
||||
{Module: "glossary", ID: &id, Glossaries: []string{"empty"}, LLM: config.LLM{ProposalProfile: &profile, ValidationProfile: &profile}},
|
||||
}}}
|
||||
output := filepath.Join(t.TempDir(), "transcript.json")
|
||||
calls := 0
|
||||
_, err := Prepare(context.Background(), Input{Config: cfg, Transcript: &schema.SourceTranscript{}, Output: diagnostics.OutputSelection{TranscriptPath: output}, InspectProfile: func(context.Context, string) error { calls++; return nil }})
|
||||
if err == nil || !strings.Contains(err.Error(), "stage 2") || !strings.Contains(err.Error(), "spells") || !strings.Contains(err.Error(), "empty") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if calls != 0 {
|
||||
t.Fatal("inspected profiles before checking all glossary selections")
|
||||
}
|
||||
if _, err := os.Stat(output); !os.IsNotExist(err) {
|
||||
t.Fatalf("output exists: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyPipelineNeedsNoProfile(t *testing.T) {
|
||||
if err := CheckReadiness(context.Background(), &config.Config{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@ package processreport
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/reporting"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/runner"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
|
||||
pk "gitea.maximumdirect.net/eric/promptkit"
|
||||
pk "go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
func mapLLMSummary(source llm.ExecutionSummary) reporting.LLMSummary {
|
||||
@@ -23,7 +22,7 @@ func mapLLMSummary(source llm.ExecutionSummary) reporting.LLMSummary {
|
||||
}
|
||||
|
||||
// BuildUtilization groups complete logical calls by their selected backend.
|
||||
func BuildUtilization(output *runner.RunOutput, backends map[string]config.Backend) reporting.UtilizationDiagnostics {
|
||||
func BuildUtilization(output *runner.RunOutput, backends map[string]*pk.BackendInspection) reporting.UtilizationDiagnostics {
|
||||
result := reporting.UtilizationDiagnostics{Backends: make(map[string]reporting.BackendUtilization, len(backends))}
|
||||
if output == nil {
|
||||
return result
|
||||
@@ -85,7 +84,7 @@ func BuildUtilization(output *runner.RunOutput, backends map[string]config.Backe
|
||||
}
|
||||
}
|
||||
result.Backends[id] = reporting.BackendUtilization{
|
||||
Concurrency: backend.Concurrency, QueueCapacity: backend.QueueCapacity,
|
||||
Concurrency: backend.ConcurrencyLimit, QueueCapacity: backend.QueueCapacity,
|
||||
LLMSummary: mapLLMSummary(llm.Summarize(*group)),
|
||||
CapacityWaitMS: wait.Milliseconds(), GenerationMS: generation.Milliseconds(),
|
||||
RetryDelayMS: retry.Milliseconds(), LogicalWallMS: wall.Milliseconds(),
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"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/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm"
|
||||
@@ -57,7 +56,6 @@ type Request struct {
|
||||
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
|
||||
WorkingTranscript *schema.Transcript `json:"-"`
|
||||
Section *contracts.SectionMetadata `json:"section,omitempty"`
|
||||
Config *config.Config `json:"-"`
|
||||
PromptID string `json:"prompt_id"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
StageName string `json:"stage_name,omitempty"`
|
||||
|
||||
@@ -35,10 +35,7 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
sectionIndex = req.ProposalRequest.Section.Index
|
||||
}
|
||||
|
||||
transcriptDescription := ""
|
||||
if req.ProposalRequest.Config != nil {
|
||||
transcriptDescription = req.ProposalRequest.Config.Context.Description
|
||||
}
|
||||
transcriptDescription := req.ProposalRequest.Settings.Context.Description
|
||||
|
||||
variables, err := req.BuildVariables(
|
||||
req.ProposalRequest.WorkingTranscript,
|
||||
@@ -61,7 +58,6 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
ReplacementPolicy: req.ProposalRequest.RunSpec.ReplacementPolicy,
|
||||
WorkingTranscript: req.ProposalRequest.WorkingTranscript,
|
||||
Section: req.ProposalRequest.Section,
|
||||
Config: req.ProposalRequest.Config,
|
||||
PromptID: promptMetadata.PromptID,
|
||||
Variables: variables,
|
||||
StageName: stagename.ModuleProposal(req.ProposalRequest.RunSpec.InstanceName, sectionIndexPtr(req.ProposalRequest.Section)),
|
||||
|
||||
67
internal/framework/runner/ordered_workflow_test.go
Normal file
67
internal/framework/runner/ordered_workflow_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"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/contracts"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
||||
)
|
||||
|
||||
type orderedFactory struct {
|
||||
seen chan contracts.ProposalRequest
|
||||
}
|
||||
|
||||
func (f orderedFactory) ModuleForSpec(spec contracts.ModuleRunSpec) (contracts.TranscriptModule, error) {
|
||||
return orderedModule{spec: spec, seen: f.seen}, nil
|
||||
}
|
||||
|
||||
type orderedModule struct {
|
||||
spec contracts.ModuleRunSpec
|
||||
seen chan contracts.ProposalRequest
|
||||
}
|
||||
|
||||
func (m orderedModule) Key() string { return m.spec.ModuleKey }
|
||||
func (m orderedModule) ReplacementPolicy() proposals.ReplacementPolicy {
|
||||
return proposals.ReplacementPolicyRequireUnique
|
||||
}
|
||||
func (m orderedModule) Validators() []contracts.Validator { return nil }
|
||||
func (m orderedModule) Propose(_ context.Context, req contracts.ProposalRequest) (contracts.ProposalResult, error) {
|
||||
m.seen <- req
|
||||
original, corrected := "party", "Party"
|
||||
if m.spec.Index == 1 {
|
||||
original, corrected = "spell", "Spell"
|
||||
}
|
||||
if m.spec.Index == 2 {
|
||||
original, corrected = "Party Spell", "Party Spell!"
|
||||
}
|
||||
return contracts.ProposalResult{Proposals: []proposals.CorrectionProposal{{TargetSegmentID: 1, OriginalText: original, CorrectedText: corrected, Confidence: 1}}}, nil
|
||||
}
|
||||
|
||||
func TestOrderedWorkflowUsesIndependentSelectionsAndPriorEdits(t *testing.T) {
|
||||
cfg := &config.Config{GlossaryCollections: map[string]config.GlossaryCollection{
|
||||
"party": {Entries: []config.GlossaryEntry{{ID: "party", Name: "Party", Category: "person"}}},
|
||||
"spells": {Entries: []config.GlossaryEntry{{ID: "spell", Name: "Spell", Category: "spell"}}},
|
||||
}, Pipeline: config.Pipeline{Stages: []config.Stage{
|
||||
{Module: "glossary", Glossaries: []string{"party"}, ProtectedTerms: []string{"party", "spells"}, Context: config.Context{Description: "party context"}},
|
||||
{Module: "glossary", Glossaries: []string{"spells"}, ProtectedTerms: []string{"party", "spells"}, Context: config.Context{Description: "spell context"}},
|
||||
{Module: "grammar", Glossaries: []string{"party", "spells"}, ProtectedTerms: []string{"party", "spells"}},
|
||||
}}}
|
||||
seen := make(chan contracts.ProposalRequest, 3)
|
||||
output, err := New(orderedFactory{seen: seen}).Run(context.Background(), RunInput{Transcript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "party spell"}}}, ModuleSpecs: contracts.ResolveModuleRunSpecs(cfg)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := output.FinalTranscript.Segments[0].Text; got != "Party Spell!" {
|
||||
t.Fatalf("final text = %q", got)
|
||||
}
|
||||
first, second, third := <-seen, <-seen, <-seen
|
||||
if first.Settings.Context.Description != "party context" || second.Settings.Context.Description != "spell context" || len(first.Glossary) != 1 || first.Glossary[0].Name != "Party" || len(second.Glossary) != 1 || second.Glossary[0].Name != "Spell" || len(third.Glossary) != 2 {
|
||||
t.Fatalf("incorrect context: %+v %+v %+v", first, second, third)
|
||||
}
|
||||
if second.WorkingTranscript.Segments[0].Text != "Party spell" || third.WorkingTranscript.Segments[0].Text != "Party Spell" {
|
||||
t.Fatalf("edits not carried forward: %q, %q", second.WorkingTranscript.Segments[0].Text, third.WorkingTranscript.Segments[0].Text)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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/modulecatalog"
|
||||
"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/llm"
|
||||
@@ -93,11 +94,8 @@ type RunInput struct {
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Config *config.Config
|
||||
Transcript *schema.Transcript
|
||||
Glossary *schema.Glossary
|
||||
ModuleSpecs []contracts.ModuleRunSpec
|
||||
ProposalLLMClient contracts.StructuredLLMClient
|
||||
ProposalDiagnosticsDir string
|
||||
ValidationLLMClient contracts.StructuredLLMClient
|
||||
ValidationDiagnosticsDir string
|
||||
CaptureCandidates bool
|
||||
timings *validatorTimingCollector
|
||||
@@ -185,12 +183,10 @@ func (r *Runner) run(ctx context.Context, input RunInput) (RunOutput, error) {
|
||||
Module: module,
|
||||
Spec: spec,
|
||||
Policy: policy,
|
||||
Config: input.Config,
|
||||
Glossary: input.Glossary,
|
||||
Sections: sections,
|
||||
ProposalClient: input.ProposalLLMClient,
|
||||
ProposalClient: spec.ProposalClient,
|
||||
DiagnosticsDir: input.ProposalDiagnosticsDir,
|
||||
ValidationClient: input.ValidationLLMClient,
|
||||
ValidationClient: spec.ValidationClient,
|
||||
ValidationDiagnosticsDir: input.ValidationDiagnosticsDir,
|
||||
Timings: input.timings,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
@@ -220,7 +216,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)
|
||||
updated, applied, skipped, appliedBySection, applyErr := applyWithinSections(sections, pipelineResult.Approved, policy, validators.NewProtectedVocabulary(spec.ProtectedTerms), spec.ModuleKey == modulecatalog.KeySpokenWord)
|
||||
if applyErr != nil {
|
||||
return snapshot(), fmt.Errorf("module %q application failed: %w", spec.InstanceName, applyErr)
|
||||
}
|
||||
@@ -268,7 +264,7 @@ func transcriptFromSections(sections []chunking.Section) *schema.Transcript {
|
||||
|
||||
// applyWithinSections leaves section membership and initial metadata unchanged.
|
||||
// Approved proposals are applied only to their identified section.
|
||||
func applyWithinSections(sections []chunking.Section, approved []proposals.EnrichedCorrectionProposal, policy proposals.ReplacementPolicy) ([]chunking.Section, []proposals.AppliedChange, []proposals.SkippedChange, []int, error) {
|
||||
func applyWithinSections(sections []chunking.Section, approved []proposals.EnrichedCorrectionProposal, policy proposals.ReplacementPolicy, vocabulary validators.ProtectedVocabulary, allowRepetitionCleanup bool) ([]chunking.Section, []proposals.AppliedChange, []proposals.SkippedChange, []int, error) {
|
||||
bySection := make(map[int][]proposals.EnrichedCorrectionProposal, len(sections))
|
||||
known := make(map[int]struct{}, len(sections))
|
||||
for _, section := range sections {
|
||||
@@ -289,7 +285,7 @@ func applyWithinSections(sections []chunking.Section, approved []proposals.Enric
|
||||
skipped := make([]proposals.SkippedChange, 0)
|
||||
appliedBySection := make([]int, len(sections))
|
||||
for i, section := range sections {
|
||||
result := proposals.ApplyProposals(transcriptFromSection(section), bySection[section.Index], policy)
|
||||
result := proposals.ApplyProposals(transcriptFromSection(section), bySection[section.Index], policy, func(before, after string) bool { return vocabulary.AllowsChange(before, after, allowRepetitionCleanup) })
|
||||
updated[i].Segments = result.Transcript.Segments
|
||||
applied = append(applied, result.Applied...)
|
||||
skipped = append(skipped, result.Skipped...)
|
||||
@@ -304,8 +300,6 @@ type collectSectionProposalsInput struct {
|
||||
Module contracts.TranscriptModule
|
||||
Spec contracts.ModuleRunSpec
|
||||
Policy proposals.ReplacementPolicy
|
||||
Config *config.Config
|
||||
Glossary *schema.Glossary
|
||||
Sections []chunking.Section
|
||||
ProposalClient contracts.StructuredLLMClient
|
||||
DiagnosticsDir string
|
||||
@@ -364,9 +358,10 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
|
||||
meta := contracts.SectionMetadataFromSection(section)
|
||||
proposalResult, err := input.Module.Propose(runCtx, contracts.ProposalRequest{
|
||||
ExecutionContext: contracts.ExecutionContext{
|
||||
Config: input.Config,
|
||||
Settings: input.Spec.Settings,
|
||||
WorkingTranscript: transcriptFromSection(section),
|
||||
Glossary: input.Glossary,
|
||||
Glossary: input.Spec.Glossary,
|
||||
ProtectedTerms: input.Spec.ProtectedTerms,
|
||||
Section: &meta,
|
||||
DiagnosticsDir: input.DiagnosticsDir,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
@@ -506,8 +501,6 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
|
||||
validated, err := validateSectionCandidates(runCtx, validateSectionCandidatesInput{
|
||||
Spec: input.Spec,
|
||||
Policy: input.Policy,
|
||||
Glossary: input.Glossary,
|
||||
Config: input.Config,
|
||||
WorkingTranscript: transcriptFromSection(sectionResult.section),
|
||||
SectionIndex: §ionMetadata.Index,
|
||||
Validators: validatorsOrdered,
|
||||
@@ -592,8 +585,6 @@ type validateSectionCandidatesInput struct {
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Spec contracts.ModuleRunSpec
|
||||
Policy proposals.ReplacementPolicy
|
||||
Glossary *schema.Glossary
|
||||
Config *config.Config
|
||||
WorkingTranscript *schema.Transcript
|
||||
SectionIndex *int
|
||||
Validators []contracts.Validator
|
||||
@@ -624,10 +615,16 @@ func validateSectionCandidates(ctx context.Context, input validateSectionCandida
|
||||
}
|
||||
var diagnosticsWriter validators.InteractionDiagnosticsWriter
|
||||
if input.DiagnosticsDir != "" {
|
||||
var secrets []string
|
||||
if source, ok := input.ValidationLLMClient.(interface {
|
||||
ConfiguredSecrets(context.Context) []string
|
||||
}); ok {
|
||||
secrets = source.ConfiguredSecrets(ctx)
|
||||
}
|
||||
diagnosticsWriter = &llmDiagnosticsWriterAdapter{
|
||||
writer: llm.NewDiagnosticsWriter(
|
||||
filepath.Join(input.DiagnosticsDir, input.Spec.InstanceName),
|
||||
llm.ConfiguredSecrets(input.Config),
|
||||
secrets,
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -641,8 +638,9 @@ func validateSectionCandidates(ctx context.Context, input validateSectionCandida
|
||||
SectionIndex: input.SectionIndex,
|
||||
ValidatorOrder: &validatorOrder,
|
||||
ReplacementPolicy: input.Policy,
|
||||
Glossary: input.Glossary,
|
||||
Config: input.Config,
|
||||
Glossary: input.Spec.Glossary,
|
||||
ProtectedTerms: input.Spec.ProtectedTerms,
|
||||
Settings: input.Spec.Settings,
|
||||
LLMClient: input.ValidationLLMClient,
|
||||
DiagnosticsWriter: diagnosticsWriter,
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
|
||||
@@ -39,7 +39,7 @@ type ConfidenceThresholdValidator struct{}
|
||||
func (v ConfidenceThresholdValidator) Name() string { return "confidence_threshold" }
|
||||
|
||||
func (v ConfidenceThresholdValidator) Validate(_ context.Context, req Request) (Result, error) {
|
||||
threshold := confidenceThresholdForModule(req.ModuleKey, req.Config)
|
||||
threshold := req.Settings.ConfidenceThreshold
|
||||
decisions := make([]Decision, 0, len(req.CandidateProposal))
|
||||
for _, c := range req.CandidateProposal {
|
||||
if c.Confidence < threshold {
|
||||
|
||||
@@ -72,8 +72,8 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
|
||||
}
|
||||
|
||||
maxTokens := config.DefaultValidationMaxPromptTokens
|
||||
if req.Config != nil && req.Config.Validation.MaxPromptTokens > 0 {
|
||||
maxTokens = req.Config.Validation.MaxPromptTokens
|
||||
if req.Settings.Validation.MaxPromptTokens > 0 {
|
||||
maxTokens = req.Settings.Validation.MaxPromptTokens
|
||||
}
|
||||
|
||||
for i := range validationReq.Items {
|
||||
@@ -131,9 +131,7 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
|
||||
return partialResult(), err
|
||||
}
|
||||
transcriptDescription := ""
|
||||
if req.Config != nil {
|
||||
transcriptDescription = req.Config.Context.Description
|
||||
}
|
||||
transcriptDescription = req.Settings.Context.Description
|
||||
variables, err := BuildValidationVariables(batch.Items, transcriptDescription)
|
||||
if err != nil {
|
||||
return partialResult(), err
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"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/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/proposals"
|
||||
@@ -44,7 +43,7 @@ type Request struct {
|
||||
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
|
||||
Glossary []glossary.Entry `json:"-"`
|
||||
ProtectedTerms []glossary.Term `json:"-"`
|
||||
Config *config.Config `json:"-"`
|
||||
Settings config.Stage `json:"-"`
|
||||
LLMClient StructuredLLMClient `json:"-"`
|
||||
DiagnosticsWriter InteractionDiagnosticsWriter `json:"-"`
|
||||
}
|
||||
@@ -142,21 +141,3 @@ func rejection(index int, reasonCode string, msg string) Decision {
|
||||
func unavailable(index int, reasonCode string, msg string) Decision {
|
||||
return Decision{ProposalIndex: index, Approved: false, Disposition: DispositionUnavailable, ReasonCode: reasonCode, Message: strings.TrimSpace(msg)}
|
||||
}
|
||||
|
||||
func confidenceThresholdForModule(moduleKey string, cfg *config.Config) float64 {
|
||||
if cfg == nil {
|
||||
return 0.0
|
||||
}
|
||||
switch moduleKey {
|
||||
case modulecatalog.KeyGlossary:
|
||||
return cfg.Thresholds.Glossary
|
||||
case modulecatalog.KeyGrammar:
|
||||
return cfg.Thresholds.Grammar
|
||||
case modulecatalog.KeyHomophones:
|
||||
return cfg.Thresholds.Homophones
|
||||
case modulecatalog.KeySpokenWord:
|
||||
return cfg.Thresholds.SpokenWord
|
||||
default:
|
||||
return 0.0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user