Move LLM validators to PromptKit prompt variables

This commit is contained in:
2026-09-24 02:07:17 +00:00
parent f577847ca2
commit a4a72168d5
17 changed files with 310 additions and 257 deletions

View File

@@ -51,11 +51,15 @@ Decision cardinality is enforced: each candidate proposal must receive exactly o
LLM validators: LLM validators:
- build canonical validation request payloads; - build canonical validation request payloads;
- batch using the [configured prompt budget](../config.md#concurrency); - batch using `validation.max_prompt_tokens` and the existing domain token estimator;
- call structured LLM client using response schema registry. - select an embedded prompt ID and pass payload/description variables to the
role-bound structured client with the validator-decision schema.
Oversized single proposals are rejected with `validator_input_too_large`. Oversized single proposals are rejected with `validator_input_too_large`.
Malformed LLM validator responses are downgraded to warnings and rejected batch decisions. Exhausted structural repair becomes a warning and rejected batch decisions.
Provider, malformed HTTP envelope, deadline, and admission failures propagate
as runtime errors. Schema-valid decisions still undergo exact index/cardinality
checks and domain acceptance without a semantic retry.
## Decision and Rejection Reporting ## Decision and Rejection Reporting

View File

@@ -318,9 +318,11 @@ key and diagnostic context through the prepared client, retaining ordering,
warnings, and later domain validation. Updated proposal and module tests and warnings, and later domain validation. Updated proposal and module tests and
module internals documentation. The isolated variable-encoding test passed with module internals documentation. The isolated variable-encoding test passed with
`go test internal/framework/proposal_generation/variables.go internal/framework/proposal_generation/variables_test.go`; `go test internal/framework/proposal_generation/variables.go internal/framework/proposal_generation/variables_test.go`;
`git diff --check` passed. Assembled proposal/module tests are deferred because `git diff --check` passed. After the validator imports were migrated,
Stage 7's validator imports still reference removed message-rendering types. `go test ./internal/framework/proposal_generation ./internal/modules/glossary
The repaired-proposal/domain integration check remains deferred with those imports. ./internal/modules/homophones ./internal/modules/spoken_word ./internal/modules/grammar`
passed. A controlled HTTP integration test confirmed that repaired proposals
reach low-confidence and missing-original-text domain checks without an extra call.
**Depends on:** Stage 5. **Depends on:** Stage 5.
**Owns:** `internal/modules`, `internal/framework/proposal_generation`, and their tests. **Owns:** `internal/modules`, `internal/framework/proposal_generation`, and their tests.
@@ -343,7 +345,18 @@ ordering. Run the module and proposal-generation suites as imports become builda
## Stage 7: Migrate LLM Validators Without Changing Domain Decisions ## Stage 7: Migrate LLM Validators Without Changing Domain Decisions
**Status:** Pending. **Depends on:** Stage 6. **Status:** Complete (2026-09-24). All five LLM validator types now select
embedded prompt IDs and shared payload variables. Kept domain batching,
preview, index/cardinality, and deterministic validation; exhausted repair
rejects/warns while provider/protocol failures propagate. Removed duplicate
message builders and per-validator model selection. Updated internal validator
documentation. `go test ./internal/framework/validators
./internal/framework/proposal_generation ./internal/modules/glossary
./internal/modules/homophones ./internal/modules/spoken_word
./internal/modules/grammar ./internal/validators/... ./internal/core/config`
passed, including controlled HTTP repair and malformed-envelope cases;
`git diff --check` passed. Full runner behavior remains for Stage 8.
**Depends on:** Stage 6.
**Owns:** `internal/framework/validators`, validation prompt builders/batching, **Owns:** `internal/framework/validators`, validation prompt builders/batching,
and validator tests. and validator tests.

View File

@@ -18,6 +18,7 @@ const (
WorkDirRetentionNever WorkDirRetention = "never" WorkDirRetentionNever WorkDirRetention = "never"
DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar" DefaultModulesCSV = "glossary,homophones,glossary,spoken_word,grammar"
DefaultTranscriptDescriptionMaxChars = 500 DefaultTranscriptDescriptionMaxChars = 500
DefaultValidationMaxPromptTokens = 2048
) )
// Config is the version-2 application configuration. Invocation paths are not settings. // Config is the version-2 application configuration. Invocation paths are not settings.
@@ -89,7 +90,7 @@ func defaults() Config {
LLM: LLM{TransientRetries: 2, RepairAttempts: 2, OperationTimeout: 15 * time.Minute}, LLM: LLM{TransientRetries: 2, RepairAttempts: 2, OperationTimeout: 15 * time.Minute},
Pipeline: Pipeline{Modules: []string{"glossary", "homophones", "glossary", "spoken_word", "grammar"}}, Pipeline: Pipeline{Modules: []string{"glossary", "homophones", "glossary", "spoken_word", "grammar"}},
Output: Output{Schema: "bare-segments"}, Output: Output{Schema: "bare-segments"},
Validation: Validation{MaxPromptTokens: 2048}, Validation: Validation{MaxPromptTokens: DefaultValidationMaxPromptTokens},
Chunking: Chunking{MaxSectionTokens: 8192, MinSectionTokens: 2048}, Chunking: Chunking{MaxSectionTokens: 8192, MinSectionTokens: 2048},
Normalization: Normalization{MaxSegmentGap: 4, EllipsisGap: 3.5, MaxSegmentDuration: 60, MaxSegmentTokens: 2048}, Normalization: Normalization{MaxSegmentGap: 4, EllipsisGap: 3.5, MaxSegmentDuration: 60, MaxSegmentTokens: 2048},
Thresholds: Thresholds{Glossary: .8, Homophones: .8, SpokenWord: .8, Grammar: .8}, Thresholds: Thresholds{Glossary: .8, Homophones: .8, SpokenWord: .8, Grammar: .8},

View File

@@ -0,0 +1,85 @@
package proposal_generation
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
ck "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
"gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/internal/framework/validators"
"gitea.maximumdirect.net/eric/audita/internal/prompts"
)
func TestRepairedProposalStillFacesDomainChecks(t *testing.T) {
calls := 0
outputs := []string{`{"wrong":true}`, `{"corrections":[{"id":1,"original_text":"absent","corrected_text":"present","confidence":0.4}]}`}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
output := outputs[len(outputs)-1]
if calls < len(outputs) {
output = outputs[calls]
}
calls++
content, _ := json.Marshal(output)
_, _ = fmt.Fprintf(w, `{"choices":[{"message":{"content":%s}}]}`, content)
}))
defer server.Close()
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "profiles"), 0700); err != nil {
t.Fatal(err)
}
main := filepath.Join(dir, "config.yml")
content := fmt.Sprintf("version: 2\nbackends:\n offline:\n endpoint: %s/v1\nllm:\n proposal_profile: base\n transient_retries: 0\n repair_attempts: 1\n", server.URL)
if err := os.WriteFile(main, []byte(content), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "profiles", "base.yaml"), []byte("id: base\nbackend: offline\nmodel: local-model\n"), 0600); err != nil {
t.Fatal(err)
}
loaded, err := config.Load(ck.File(main), "")
if err != nil {
t.Fatal(err)
}
runtime, err := llm.NewRuntime(context.Background(), loaded.Config, main, server.Client())
if err != nil {
t.Fatal(err)
}
client, err := runtime.NewRoleClient("proposal", llm.CallHooks{})
if err != nil {
t.Fatal(err)
}
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "gestures"}}}
vars, err := BuildProposalVariables(transcript, &schema.Glossary{}, 0, "")
if err != nil {
t.Fatal(err)
}
generated, err := GenerateCandidates(context.Background(), Request{ModuleKey: "grammar", ModuleInstance: "grammar", ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, Config: &loaded.Config, PromptID: prompts.PromptIDModuleGrammarProposal, PromptVersion: prompts.VersionV1, Variables: vars, LLMClient: client})
if err != nil {
t.Fatal(err)
}
if calls != 2 || len(generated.Enriched) != 1 {
t.Fatalf("repair path: calls=%d, result=%+v", calls, generated)
}
cfg := loaded.Config
cfg.Thresholds.Grammar = .8
input := validators.Request{ModuleKey: "grammar", Config: &cfg, CandidateProposal: generated.Enriched, WorkingTranscript: transcript}
low, err := (validators.ConfidenceThresholdValidator{}).Validate(context.Background(), input)
if err != nil {
t.Fatal(err)
}
missing, err := (validators.OriginalTextPresenceValidator{}).Validate(context.Background(), input)
if err != nil {
t.Fatal(err)
}
if low.Decisions[0].ReasonCode != validators.ReasonLowConfidence || missing.Decisions[0].ReasonCode != validators.ReasonMissingOriginalText || calls != 2 {
t.Fatalf("domain checks or extra semantic retry: low=%+v missing=%+v calls=%d", low, missing, calls)
}
}

View File

@@ -116,7 +116,7 @@ func (c *sleepingStructuredClient) CompleteStructured(ctx context.Context, req c
func defaultRequest(t *testing.T) Request { func defaultRequest(t *testing.T) Request {
t.Helper() t.Helper()
cfg := config.Default() cfg := config.Config{}
return Request{ return Request{
ModuleKey: "test_module", ModuleKey: "test_module",
ModuleInstance: "test_module", ModuleInstance: "test_module",

View File

@@ -19,7 +19,7 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
}, },
} }
section := contracts.SectionMetadata{Index: 7} section := contracts.SectionMetadata{Index: 7}
cfg := config.Default() cfg := config.Config{}
cfg.Context.Description = "Hearing transcript with role titles." cfg.Context.Description = "Hearing transcript with role titles."
transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}} transcript := &schema.Transcript{Segments: []schema.Segment{{ID: 1, Speaker: "A", Start: 0, End: 1, Text: "teh"}}}
glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "X"}}} glossary := &schema.Glossary{Entries: []schema.GlossaryEntry{{Name: "X"}}}

View File

@@ -0,0 +1,96 @@
package validators
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
ck "gitea.maximumdirect.net/eric/ConfigKit"
"gitea.maximumdirect.net/eric/audita/internal/core/config"
"gitea.maximumdirect.net/eric/audita/internal/framework/llm"
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
)
func TestValidationRepairAndRuntimeFailuresThroughDomainBoundary(t *testing.T) {
valid := `{"validations":[{"correction_index":0,"approved":true,"confidence":0.9,"reason":"ok"}]}`
for _, tc := range []struct {
name string
responses []string
wantCalls int
wantApproved, wantWarning, wantRuntime bool
}{
{"repair reaches decisions", []string{`{"wrong":true}`, valid}, 2, true, false, false},
{"exhausted repair rejects batch", []string{`{"wrong":true}`, `{"wrong":true}`}, 2, false, true, false},
{"malformed envelope propagates", []string{"MALFORMED_ENVELOPE"}, 1, false, false, true},
} {
t.Run(tc.name, func(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
output := tc.responses[len(tc.responses)-1]
if calls <= len(tc.responses) {
output = tc.responses[calls-1]
}
if output == "MALFORMED_ENVELOPE" {
_, _ = fmt.Fprint(w, `{"choices":[]}`)
return
}
content, _ := json.Marshal(output)
_, _ = fmt.Fprintf(w, `{"choices":[{"message":{"content":%s}}]}`, content)
}))
defer server.Close()
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "profiles"), 0700); err != nil {
t.Fatal(err)
}
main := filepath.Join(dir, "config.yml")
content := fmt.Sprintf("version: 2\nbackends:\n offline:\n endpoint: %s/v1\nllm:\n proposal_profile: base\n transient_retries: 0\n repair_attempts: 1\n", server.URL)
if err := os.WriteFile(main, []byte(content), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "profiles", "base.yaml"), []byte("id: base\nbackend: offline\nmodel: local-model\n"), 0600); err != nil {
t.Fatal(err)
}
loaded, err := config.Load(ck.File(main), "")
if err != nil {
t.Fatal(err)
}
runtime, err := llm.NewRuntime(context.Background(), loaded.Config, main, server.Client())
if err != nil {
t.Fatal(err)
}
client, err := runtime.NewRoleClient("validation", llm.CallHooks{})
if err != nil {
t.Fatal(err)
}
validator, err := NewLLMBackedValidator("editorial_review", LLMValidatorTypeEditorialReview)
if err != nil {
t.Fatal(err)
}
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.Config = &loaded.Config
req.LLMClient = client
result, err := validator.Validate(context.Background(), req)
if calls != tc.wantCalls {
t.Fatalf("calls=%d, want %d", calls, tc.wantCalls)
}
if tc.wantRuntime {
if err == nil || len(result.Warnings) != 0 {
t.Fatalf("runtime failure downgraded: %+v, %v", result, err)
}
return
}
if err != nil {
t.Fatal(err)
}
if len(result.Decisions) != 1 || result.Decisions[0].Approved != tc.wantApproved || (len(result.Warnings) > 0) != tc.wantWarning {
t.Fatalf("domain result: %+v", result)
}
})
}
}

View File

@@ -1,92 +0,0 @@
package validators
import (
"encoding/json"
"fmt"
"gitea.maximumdirect.net/eric/audita/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/internal/prompts"
)
func BuildSpokenFormPlausibilityMessages(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error) {
payloadJSON, err := marshalPromptPayload(validationPayload)
if err != nil {
return nil, err
}
system, user, _, err := prompts.RenderUserSystem(prompts.PromptIDValidatorSpokenFormPlausibility, map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": payloadJSON,
})
if err != nil {
return nil, err
}
return []LLMMessage{{Role: "system", Content: system}, {Role: "user", Content: user}}, nil
}
func BuildMeaningReversalMessages(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error) {
payloadJSON, err := marshalPromptPayload(validationPayload)
if err != nil {
return nil, err
}
system, user, _, err := prompts.RenderUserSystem(prompts.PromptIDValidatorMeaningReversalReview, map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": payloadJSON,
})
if err != nil {
return nil, err
}
return []LLMMessage{{Role: "system", Content: system}, {Role: "user", Content: user}}, nil
}
func BuildEditorialMessages(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error) {
payloadJSON, err := marshalPromptPayload(validationPayload)
if err != nil {
return nil, err
}
system, user, _, err := prompts.RenderUserSystem(prompts.PromptIDValidatorEditorialReview, map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": payloadJSON,
})
if err != nil {
return nil, err
}
return []LLMMessage{{Role: "system", Content: system}, {Role: "user", Content: user}}, nil
}
func BuildGrammarReviewMessages(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error) {
payloadJSON, err := marshalPromptPayload(validationPayload)
if err != nil {
return nil, err
}
system, user, _, err := prompts.RenderUserSystem(prompts.PromptIDValidatorGrammarReview, map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": payloadJSON,
})
if err != nil {
return nil, err
}
return []LLMMessage{{Role: "system", Content: system}, {Role: "user", Content: user}}, nil
}
func BuildSpokenWordReviewMessages(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error) {
payloadJSON, err := marshalPromptPayload(validationPayload)
if err != nil {
return nil, err
}
system, user, _, err := prompts.RenderUserSystem(prompts.PromptIDValidatorSpokenWordReview, map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": payloadJSON,
})
if err != nil {
return nil, err
}
return []LLMMessage{{Role: "system", Content: system}, {Role: "user", Content: user}}, nil
}
func marshalPromptPayload(validationPayload []LLMValidationItem) (string, error) {
payloadJSON, err := json.MarshalIndent(validationPayload, "", " ")
if err != nil {
return "", fmt.Errorf("marshal validation payload: %w", err)
}
return string(payloadJSON), nil
}

View File

@@ -0,0 +1,19 @@
package validators
import (
"encoding/json"
"fmt"
"gitea.maximumdirect.net/eric/audita/internal/framework/promptcontext"
)
func BuildValidationVariables(validationPayload []LLMValidationItem, transcriptDescription string) (map[string]string, error) {
payloadJSON, err := json.MarshalIndent(validationPayload, "", " ")
if err != nil {
return nil, fmt.Errorf("marshal validation payload: %w", err)
}
return map[string]string{
"TranscriptDescriptionBlock": promptcontext.TranscriptDescriptionBlock(transcriptDescription),
"PayloadJSON": string(payloadJSON),
}, nil
}

View File

@@ -17,13 +17,9 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/prompts" "gitea.maximumdirect.net/eric/audita/internal/prompts"
) )
type LLMPromptBuilder func(validationPayload []LLMValidationItem, transcriptDescription string) ([]LLMMessage, error)
type LLMBackedValidator struct { type LLMBackedValidator struct {
name string name string
validatorType LLMValidatorType validatorType LLMValidatorType
promptBuilder LLMPromptBuilder
model string
estimator chunking.TokenEstimator estimator chunking.TokenEstimator
} }
@@ -39,10 +35,9 @@ func (v *LLMBackedValidator) SetTokenEstimator(estimator chunking.TokenEstimator
v.estimator = estimator v.estimator = estimator
} }
func NewLLMBackedValidator(name string, validatorType LLMValidatorType, model string) (*LLMBackedValidator, error) { func NewLLMBackedValidator(name string, validatorType LLMValidatorType) (*LLMBackedValidator, error) {
builder, err := promptBuilderForType(validatorType) if validatorPromptMetadata(validatorType).PromptID == "" {
if err != nil { return nil, fmt.Errorf("unsupported LLM validator type %q", validatorType)
return nil, err
} }
if strings.TrimSpace(name) == "" { if strings.TrimSpace(name) == "" {
return nil, fmt.Errorf("validator name must not be empty") return nil, fmt.Errorf("validator name must not be empty")
@@ -50,8 +45,6 @@ func NewLLMBackedValidator(name string, validatorType LLMValidatorType, model st
return &LLMBackedValidator{ return &LLMBackedValidator{
name: name, name: name,
validatorType: validatorType, validatorType: validatorType,
promptBuilder: builder,
model: strings.TrimSpace(model),
estimator: chunking.NewSimpleTokenEstimator(), estimator: chunking.NewSimpleTokenEstimator(),
}, nil }, nil
} }
@@ -77,8 +70,8 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
} }
maxTokens := config.DefaultValidationMaxPromptTokens maxTokens := config.DefaultValidationMaxPromptTokens
if req.Config != nil && req.Config.ValidationMaxPromptTokens > 0 { if req.Config != nil && req.Config.Validation.MaxPromptTokens > 0 {
maxTokens = req.Config.ValidationMaxPromptTokens maxTokens = req.Config.Validation.MaxPromptTokens
} }
warnings := append([]stagewarnings.StageWarning(nil), oversizedValidationWarnings(v.name, maxTokens, validationReq.Items, v.estimator)...) warnings := append([]stagewarnings.StageWarning(nil), oversizedValidationWarnings(v.name, maxTokens, validationReq.Items, v.estimator)...)
@@ -103,22 +96,23 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
for _, batch := range batches { for _, batch := range batches {
transcriptDescription := "" transcriptDescription := ""
if req.Config != nil { if req.Config != nil {
transcriptDescription = req.Config.TranscriptDescription transcriptDescription = req.Config.Context.Description
} }
messages, err := v.promptBuilder(batch.Items, transcriptDescription) variables, err := BuildValidationVariables(batch.Items, transcriptDescription)
if err != nil { if err != nil {
return Result{}, err return Result{}, err
} }
var response LLMValidationResponse var response LLMValidationResponse
var completion StructuredCompletionResponse
responseSchema := responseschema.MustLookup(responseschema.ValidatorDecisionSetKey) responseSchema := responseschema.MustLookup(responseschema.ValidatorDecisionSetKey)
promptMetadata := validatorPromptMetadata(v.validatorType)
stage := stagename.ValidatorBatch(req.ModuleInstance, v.name, batch.BatchIndex) stage := stagename.ValidatorBatch(req.ModuleInstance, v.name, batch.BatchIndex)
call := func(callCtx context.Context) error { call := func(callCtx context.Context) error {
_, err = req.LLMClient.CompleteStructured(callCtx, StructuredCompletionRequest{ completion, err = req.LLMClient.CompleteStructured(callCtx, StructuredCompletionRequest{
StageName: stage, PromptID: promptMetadata.PromptID, PromptVersion: promptMetadata.PromptVersion,
Messages: messages, Variables: variables, Schema: responseschema.ValidatorDecisionSetKey,
Model: resolvedValidationModel(req.Config, v.model), Context: structuredoutput.DiagnosticContext{Role: "validation", ModuleKey: req.ModuleKey, ModuleInstance: req.ModuleInstance, BatchIndex: &batch.BatchIndex, ValidatorName: v.name},
ResponseSchema: &responseSchema,
}, &response) }, &response)
return err return err
} }
@@ -129,18 +123,17 @@ func (v *LLMBackedValidator) Validate(ctx context.Context, req Request) (Result,
} }
artifacts := InteractionArtifacts{} artifacts := InteractionArtifacts{}
if req.DiagnosticsWriter != nil { if req.DiagnosticsWriter != nil {
promptMetadata := validatorPromptMetadata(v.validatorType)
artifacts, _ = req.DiagnosticsWriter.WriteInteraction( artifacts, _ = req.DiagnosticsWriter.WriteInteraction(
stage, stage,
map[string]any{ map[string]any{
"validator_name": v.name, "validator_name": v.name,
"validator_type": v.validatorType, "validator_type": v.validatorType,
"batch_index": batch.BatchIndex, "batch_index": batch.BatchIndex,
"prompt_metadata": promptMetadata.DiagnosticsMap(), "prompt": completion.Prompt,
"response_schema": responseSchema.DiagnosticsMap(), "response_schema": responseSchema.DiagnosticsMap(),
}, },
map[string]any{"messages": messages, "items": batch.Items}, completion.Prepared,
response, map[string]any{"decoded": response, "final_content": completion.FinalContent, "model": completion.Model},
errPayload(err), errPayload(err),
) )
} }
@@ -199,16 +192,6 @@ func errPayload(err error) any {
return map[string]any{"error": err.Error()} return map[string]any{"error": err.Error()}
} }
func resolvedValidationModel(cfg *config.Config, override string) string {
if strings.TrimSpace(override) != "" {
return strings.TrimSpace(override)
}
if cfg == nil {
return ""
}
return cfg.EffectiveValidationLLMConfig().Model
}
func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorType, req Request) (LLMValidationRequest, []Decision) { func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorType, req Request) (LLMValidationRequest, []Decision) {
items := make([]LLMValidationItem, 0, len(req.CandidateProposal)) items := make([]LLMValidationItem, 0, len(req.CandidateProposal))
immediate := make([]Decision, 0) immediate := make([]Decision, 0)
@@ -255,23 +238,6 @@ func BuildLLMValidationRequest(validatorName string, validatorType LLMValidatorT
}, immediate }, immediate
} }
func promptBuilderForType(validatorType LLMValidatorType) (LLMPromptBuilder, error) {
switch validatorType {
case LLMValidatorTypeSpokenFormPlausibility:
return BuildSpokenFormPlausibilityMessages, nil
case LLMValidatorTypeMeaningReversal:
return BuildMeaningReversalMessages, nil
case LLMValidatorTypeEditorialReview:
return BuildEditorialMessages, nil
case LLMValidatorTypeGrammarReview:
return BuildGrammarReviewMessages, nil
case LLMValidatorTypeSpokenWordReview:
return BuildSpokenWordReviewMessages, nil
default:
return nil, fmt.Errorf("unsupported LLM validator type %q", validatorType)
}
}
func mapLLMResponseToDecisions(items []LLMValidationItem, response LLMValidationResponse) ([]Decision, error) { func mapLLMResponseToDecisions(items []LLMValidationItem, response LLMValidationResponse) ([]Decision, error) {
expected := make(map[int]LLMValidationItem, len(items)) expected := make(map[int]LLMValidationItem, len(items))
for _, item := range items { for _, item := range items {

View File

@@ -15,6 +15,8 @@ import (
"gitea.maximumdirect.net/eric/audita/internal/core/schema" "gitea.maximumdirect.net/eric/audita/internal/core/schema"
"gitea.maximumdirect.net/eric/audita/internal/framework/proposals" "gitea.maximumdirect.net/eric/audita/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/internal/framework/responseschema" "gitea.maximumdirect.net/eric/audita/internal/framework/responseschema"
"gitea.maximumdirect.net/eric/audita/internal/framework/structuredoutput"
"gitea.maximumdirect.net/eric/audita/internal/prompts"
) )
type fakeStructuredLLMClient struct { type fakeStructuredLLMClient struct {
@@ -109,8 +111,8 @@ func (f *fakeStructuredLLMClient) CompleteStructured(ctx context.Context, req St
} }
func makeReq(candidates []proposals.EnrichedCorrectionProposal) Request { func makeReq(candidates []proposals.EnrichedCorrectionProposal) Request {
cfg := config.Default() cfg := config.Config{}
cfg.ValidationMaxPromptTokens = 10000 cfg.Validation.MaxPromptTokens = 10000
return Request{ return Request{
WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "There were gestures at the temple.", Categories: []string{"narration"}}}}, WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{{ID: 1, Text: "There were gestures at the temple.", Categories: []string{"narration"}}}},
CandidateProposal: candidates, CandidateProposal: candidates,
@@ -163,66 +165,12 @@ func TestChunkLLMValidationItemsMultipleBatchesStableNoDropNoDup(t *testing.T) {
} }
} }
func TestPromptBuildersContainRequiredContextAndInstructions(t *testing.T) {
payload := []LLMValidationItem{{CorrectionIndex: 0, SegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", OriginalSegmentText: "There were gestures", CorrectedSegmentText: "There were Jesters", Categories: []string{"narration"}}}
tests := []struct {
name string
build func([]LLMValidationItem, string) ([]LLMMessage, error)
mustHas []string
}{
{"spoken_form", BuildSpokenFormPlausibilityMessages, []string{"plausible spoken-form", "correction_index", "original_segment_text", "corrected_segment_text"}},
{"meaning_reversal", BuildMeaningReversalMessages, []string{"meaning reversals", "correction_index", "original_segment_text", "corrected_segment_text"}},
{"editorial", BuildEditorialMessages, []string{"acceptable editorial revision", "correction_index", "original_segment_text", "corrected_segment_text"}},
{"grammar_review", BuildGrammarReviewMessages, []string{"acceptable editorial revision", "correction_index"}},
{"spoken_word", BuildSpokenWordReviewMessages, []string{"acceptable editorial revision", "correction_index"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
msgs, err := tt.build(payload, "Discussion among party members in a dungeon.")
if err != nil {
t.Fatalf("build err: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("expected 2 messages, got %d", len(msgs))
}
combined := msgs[0].Content + "\n" + msgs[1].Content
for _, needle := range tt.mustHas {
if !strings.Contains(combined, needle) {
t.Fatalf("expected prompt to contain %q", needle)
}
}
for _, needle := range []string{
"Transcript description (background context only):",
"must not override the transcript content",
"Do not invent corrections, facts, names, events, motivations, or speaker intent based on this description.",
} {
if !strings.Contains(combined, needle) {
t.Fatalf("expected prompt to contain %q", needle)
}
}
})
}
}
func TestPromptBuildersOmitTranscriptDescriptionSectionWhenEmpty(t *testing.T) {
payload := []LLMValidationItem{{CorrectionIndex: 0, SegmentID: 1, OriginalText: "gestures", CorrectedText: "Jesters", OriginalSegmentText: "There were gestures", CorrectedSegmentText: "There were Jesters"}}
msgs, err := BuildSpokenFormPlausibilityMessages(payload, " ")
if err != nil {
t.Fatalf("build err: %v", err)
}
combined := msgs[0].Content + "\n" + msgs[1].Content
if strings.Contains(combined, "Transcript description (background context only):") {
t.Fatalf("did not expect empty transcript description section in prompt")
}
}
func TestLLMBackedValidatorApprovalAndRejection(t *testing.T) { func TestLLMBackedValidatorApprovalAndRejection(t *testing.T) {
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{ client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"}, {CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
{CorrectionIndex: 1, Approved: false, Confidence: 0.95, Reason: "bad"}, {CorrectionIndex: 1, Approved: false, Confidence: 0.95, Reason: "bad"},
}}}} }}}}
v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
if err != nil { if err != nil {
t.Fatalf("new validator error: %v", err) t.Fatalf("new validator error: %v", err)
} }
@@ -238,19 +186,15 @@ func TestLLMBackedValidatorApprovalAndRejection(t *testing.T) {
if len(client.calls) != 1 { if len(client.calls) != 1 {
t.Fatalf("expected one LLM call, got %d", len(client.calls)) t.Fatalf("expected one LLM call, got %d", len(client.calls))
} }
if client.calls[0].ResponseSchema == nil { call := client.calls[0]
t.Fatalf("expected response schema on structured validation request") if call.Schema != responseschema.ValidatorDecisionSetKey || call.PromptID != prompts.PromptIDValidatorSpokenFormPlausibility || call.Context.Role != "validation" || call.Context.ValidatorName != v.Name() || !strings.Contains(call.Variables["PayloadJSON"], "correction_index") {
} t.Fatalf("unexpected validator call: %+v", call)
want := responseschema.MustLookup(responseschema.ValidatorDecisionSetKey)
gotSchema := client.calls[0].ResponseSchema
if gotSchema.ID != want.ID || gotSchema.Version != want.Version || gotSchema.Name != want.Name || gotSchema.SHA256 != want.SHA256 {
t.Fatalf("unexpected validator response schema metadata: got=%+v want=%+v", *gotSchema, want)
} }
} }
func TestLLMBackedValidatorMalformedOutputRejectsBatch(t *testing.T) { func TestLLMBackedValidatorMalformedOutputRejectsBatch(t *testing.T) {
client := &fakeStructuredLLMClient{err: errors.New("malformed structured output")} client := &fakeStructuredLLMClient{err: &structuredoutput.Failure{Kind: structuredoutput.FailureMalformedOutput, Message: "malformed structured output"}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")}) req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client req.LLMClient = client
res, err := v.Validate(context.Background(), req) res, err := v.Validate(context.Background(), req)
@@ -265,26 +209,20 @@ func TestLLMBackedValidatorMalformedOutputRejectsBatch(t *testing.T) {
} }
} }
func TestLLMBackedValidatorProviderMalformedEnvelopeRejectsBatch(t *testing.T) { func TestLLMBackedValidatorProviderMalformedEnvelopePropagates(t *testing.T) {
client := &fakeStructuredLLMClient{err: errors.New("provider response assistant message content is empty")} client := &fakeStructuredLLMClient{err: &structuredoutput.Failure{Kind: structuredoutput.FailureRuntime, Message: "structured model call failed", Cause: errors.New("malformed provider envelope")}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")}) req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client req.LLMClient = client
res, err := v.Validate(context.Background(), req) result, err := v.Validate(context.Background(), req)
if err != nil { if err == nil || len(result.Warnings) != 0 {
t.Fatalf("expected malformed provider envelope downgrade, got %v", err) t.Fatalf("protocol error was downgraded: %+v, %v", result, err)
}
if len(res.Decisions) != 1 || res.Decisions[0].Approved || res.Decisions[0].ReasonCode != ReasonValidatorMalformed {
t.Fatalf("unexpected decisions: %+v", res.Decisions)
}
if len(res.Warnings) != 1 || res.Warnings[0].ReasonCode != ReasonValidatorMalformed {
t.Fatalf("expected malformed warning, got %+v", res.Warnings)
} }
} }
func TestLLMBackedValidatorMissingDecisionRejectsBatch(t *testing.T) { func TestLLMBackedValidatorMissingDecisionRejectsBatch(t *testing.T) {
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{}}}} client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{}}}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")}) req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client req.LLMClient = client
res, err := v.Validate(context.Background(), req) res, err := v.Validate(context.Background(), req)
@@ -301,7 +239,7 @@ func TestLLMBackedValidatorDuplicateDecisionRejectsBatch(t *testing.T) {
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"}, {CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
{CorrectionIndex: 0, Approved: false, Confidence: 0.9, Reason: "dup"}, {CorrectionIndex: 0, Approved: false, Confidence: 0.9, Reason: "dup"},
}}}} }}}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")}) req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client req.LLMClient = client
res, err := v.Validate(context.Background(), req) res, err := v.Validate(context.Background(), req)
@@ -315,7 +253,7 @@ func TestLLMBackedValidatorDuplicateDecisionRejectsBatch(t *testing.T) {
func TestLLMBackedValidatorUnknownProposalIndexRejectsBatch(t *testing.T) { func TestLLMBackedValidatorUnknownProposalIndexRejectsBatch(t *testing.T) {
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{{CorrectionIndex: 99, Approved: true, Confidence: 0.9, Reason: "unknown"}}}}} client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{{CorrectionIndex: 99, Approved: true, Confidence: 0.9, Reason: "unknown"}}}}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")}) req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client req.LLMClient = client
res, err := v.Validate(context.Background(), req) res, err := v.Validate(context.Background(), req)
@@ -331,7 +269,7 @@ func TestLLMBackedValidatorOversizedSingleProposalRejectsOnlyThatProposal(t *tes
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{ client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{
{CorrectionIndex: 1, Approved: true, Confidence: 0.9, Reason: "ok"}, {CorrectionIndex: 1, Approved: true, Confidence: 0.9, Reason: "ok"},
}}}} }}}}
v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, _ := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
huge := strings.Repeat("gestures ", 200) huge := strings.Repeat("gestures ", 200)
req := Request{ req := Request{
WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{ WorkingTranscript: &schema.Transcript{Segments: []schema.Segment{
@@ -353,8 +291,8 @@ func TestLLMBackedValidatorOversizedSingleProposalRejectsOnlyThatProposal(t *tes
ReplacementPolicy: proposals.ReplacementPolicyRequireUnique, ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
} }
req.LLMClient = client req.LLMClient = client
cfg := config.Default() cfg := config.Config{}
cfg.ValidationMaxPromptTokens = 200 cfg.Validation.MaxPromptTokens = 200
req.Config = &cfg req.Config = &cfg
res, err := v.Validate(context.Background(), req) res, err := v.Validate(context.Background(), req)
@@ -381,7 +319,7 @@ func TestLLMBackedValidatorRespectsSchedulerConcurrency(t *testing.T) {
entered: make(chan struct{}, 16), entered: make(chan struct{}, 16),
release: make(chan struct{}), release: make(chan struct{}),
} }
v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
if err != nil { if err != nil {
t.Fatalf("new validator error: %v", err) t.Fatalf("new validator error: %v", err)
} }
@@ -416,7 +354,7 @@ func TestLLMBackedValidatorDiagnosticsIncludeSchemaMetadata(t *testing.T) {
{CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"}, {CorrectionIndex: 0, Approved: true, Confidence: 0.9, Reason: "ok"},
}}}} }}}}
writer := &captureValidationDiagnosticsWriter{} writer := &captureValidationDiagnosticsWriter{}
v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility, "test-model") v, err := NewLLMBackedValidator("spoken_form_plausibility_review", LLMValidatorTypeSpokenFormPlausibility)
if err != nil { if err != nil {
t.Fatalf("new validator error: %v", err) t.Fatalf("new validator error: %v", err)
} }
@@ -439,19 +377,7 @@ func TestLLMBackedValidatorDiagnosticsIncludeSchemaMetadata(t *testing.T) {
if schemaMap["id"] != want.ID || schemaMap["version"] != want.Version || schemaMap["name"] != want.Name || schemaMap["sha256"] != want.SHA256 { if schemaMap["id"] != want.ID || schemaMap["version"] != want.Version || schemaMap["name"] != want.Name || schemaMap["sha256"] != want.SHA256 {
t.Fatalf("unexpected diagnostics schema metadata: got=%v want=%+v", schemaMap, want) t.Fatalf("unexpected diagnostics schema metadata: got=%v want=%+v", schemaMap, want)
} }
promptMap, ok := metadata["prompt_metadata"].(map[string]any)
if !ok {
t.Fatalf("expected prompt_metadata map, got %T", metadata["prompt_metadata"])
}
if promptMap["prompt_id"] != "validators.spoken_form_plausibility" || promptMap["prompt_version"] != "v1" || promptMap["prompt_source"] != "builtin" {
t.Fatalf("unexpected prompt metadata: %v", promptMap)
}
if _, ok := promptMap["embedded_path"].(string); !ok {
t.Fatalf("expected embedded_path in prompt metadata: %v", promptMap)
}
if _, ok := promptMap["sha256"].(string); !ok {
t.Fatalf("expected sha256 in prompt metadata: %v", promptMap)
}
} }
func waitForValidationEntries(t *testing.T, entered <-chan struct{}, want int) { func waitForValidationEntries(t *testing.T, entered <-chan struct{}, want int) {
@@ -470,3 +396,38 @@ func waitForValidationEntries(t *testing.T, entered <-chan struct{}, want int) {
t.Fatalf("timed out waiting for %d validator calls to enter (got %d)", want, got) t.Fatalf("timed out waiting for %d validator calls to enter (got %d)", want, got)
} }
} }
func TestEveryValidationTypeSelectsEmbeddedPromptAndVariables(t *testing.T) {
cases := []struct {
kind LLMValidatorType
prompt string
}{
{LLMValidatorTypeSpokenFormPlausibility, prompts.PromptIDValidatorSpokenFormPlausibility},
{LLMValidatorTypeMeaningReversal, prompts.PromptIDValidatorMeaningReversalReview},
{LLMValidatorTypeEditorialReview, prompts.PromptIDValidatorEditorialReview},
{LLMValidatorTypeGrammarReview, prompts.PromptIDValidatorGrammarReview},
{LLMValidatorTypeSpokenWordReview, prompts.PromptIDValidatorSpokenWordReview},
}
for _, tc := range cases {
t.Run(string(tc.kind), func(t *testing.T) {
client := &fakeStructuredLLMClient{responses: []LLMValidationResponse{{Validations: []LLMValidationDecision{{CorrectionIndex: 0, Approved: true, Confidence: .9, Reason: "ok"}}}}}
validator, err := NewLLMBackedValidator(string(tc.kind), tc.kind)
if err != nil {
t.Fatal(err)
}
req := makeReq([]proposals.EnrichedCorrectionProposal{mk(0, "gestures", "Jesters")})
req.LLMClient = client
req.Config.Context.Description = " Fantasy scene "
if _, err := validator.Validate(context.Background(), req); err != nil {
t.Fatal(err)
}
if len(client.calls) != 1 {
t.Fatalf("calls=%d", len(client.calls))
}
call := client.calls[0]
if call.PromptID != tc.prompt || call.PromptVersion != prompts.VersionV1 || call.Schema != responseschema.ValidatorDecisionSetKey || call.Context.Role != "validation" || call.Context.BatchIndex == nil || !strings.Contains(call.Variables["PayloadJSON"], "correction_index") || !strings.Contains(call.Variables["TranscriptDescriptionBlock"], "Fantasy scene") {
t.Fatalf("mapping: %+v", call)
}
})
}
}

View File

@@ -50,7 +50,7 @@ func TestEnforceDecisionCardinalityUnknownDecision(t *testing.T) {
} }
func TestConfidenceThresholdValidator(t *testing.T) { func TestConfidenceThresholdValidator(t *testing.T) {
cfg := config.Default() cfg := config.Config{}
cfg.Thresholds.Grammar = 0.8 cfg.Thresholds.Grammar = 0.8
req := Request{ModuleKey: "grammar", Config: &cfg, CandidateProposal: []proposals.EnrichedCorrectionProposal{ req := Request{ModuleKey: "grammar", Config: &cfg, CandidateProposal: []proposals.EnrichedCorrectionProposal{
mkCandidate(0, 1, "teh", "the", 0.9), mkCandidate(0, 1, "teh", "the", 0.9),

View File

@@ -186,7 +186,7 @@ func TestHomophonesConfidenceThresholdIsUsed(t *testing.T) {
t.Fatal("expected confidence_threshold validator") t.Fatal("expected confidence_threshold validator")
} }
cfg := config.Default() cfg := config.Config{}
cfg.Thresholds.Homophones = 0.95 cfg.Thresholds.Homophones = 0.95
result, err := thresholdValidator.Validate(context.Background(), validators.Request{ result, err := thresholdValidator.Validate(context.Background(), validators.Request{
Config: &cfg, Config: &cfg,

View File

@@ -186,7 +186,7 @@ func TestSpokenWordConfidenceThresholdIsUsed(t *testing.T) {
t.Fatal("expected confidence_threshold validator") t.Fatal("expected confidence_threshold validator")
} }
cfg := config.Default() cfg := config.Config{}
cfg.Thresholds.SpokenWord = 0.95 cfg.Thresholds.SpokenWord = 0.95
result, err := thresholdValidator.Validate(context.Background(), validators.Request{ result, err := thresholdValidator.Validate(context.Background(), validators.Request{
Config: &cfg, Config: &cfg,

View File

@@ -9,7 +9,7 @@ import (
const Key = "editorial_review" const Key = "editorial_review"
func New() (contracts.Validator, error) { func New() (contracts.Validator, error) {
v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeEditorialReview, "") v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeEditorialReview)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -9,7 +9,7 @@ import (
const Key = "meaning_reversal_review" const Key = "meaning_reversal_review"
func New() (contracts.Validator, error) { func New() (contracts.Validator, error) {
v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeMeaningReversal, "") v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeMeaningReversal)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -9,7 +9,7 @@ import (
const Key = "spoken_form_plausibility" const Key = "spoken_form_plausibility"
func New() (contracts.Validator, error) { func New() (contracts.Validator, error) {
v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeSpokenFormPlausibility, "") v, err := frameworkvalidators.NewLLMBackedValidator(Key, frameworkvalidators.LLMValidatorTypeSpokenFormPlausibility)
if err != nil { if err != nil {
return nil, err return nil, err
} }