Reuse prepared glossary data across stage sections
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user