Carry proposal exchanges into repair replay
This commit is contained in:
@@ -37,16 +37,17 @@ type StructuredCompletionResponse = structuredoutput.Response
|
||||
|
||||
// ModuleRunSpec identifies one resolved module instance in a pipeline.
|
||||
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:"-"`
|
||||
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:"-"`
|
||||
CaptureProposalExchange bool `json:"-"`
|
||||
ProposalClient StructuredLLMClient `json:"-"`
|
||||
ValidationClient StructuredLLMClient `json:"-"`
|
||||
}
|
||||
|
||||
// SectionMetadata captures chunk-level metadata without embedding full segment payloads.
|
||||
@@ -92,6 +93,7 @@ type ProposalRequest struct {
|
||||
}
|
||||
|
||||
type ProposalResult struct {
|
||||
Exchange *structuredoutput.Exchange `json:"-"`
|
||||
Proposals []proposals.CorrectionProposal `json:"proposals,omitempty"`
|
||||
Operations []outcomes.Operation `json:"operations,omitempty"`
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -72,6 +73,7 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
|
||||
var record structuredoutput.CallRecord
|
||||
if c.hooks.OnCall != nil || c.hooks.OnEventWithCall != nil {
|
||||
record.CallID = c.runtime.nextCallID.Add(1)
|
||||
record.RequestedPromptID = req.PromptID
|
||||
record.Context = copyContext(req.Context)
|
||||
record.Context.Role = c.role
|
||||
}
|
||||
@@ -106,8 +108,27 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
|
||||
for name, value := range prompts.SharedVariables() {
|
||||
vars[name] = value
|
||||
}
|
||||
baseID, baseVars := req.PromptID, vars
|
||||
var suffix []pk.RenderedMessage
|
||||
if req.Replay != nil {
|
||||
if req.Replay.Exchange == nil {
|
||||
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonSetupFailure, Message: "repair exchange is unavailable"}
|
||||
}
|
||||
instruction, renderErr := c.runtime.Engine.Prepare(ctx, pk.RunRequest{PromptID: req.PromptID, ProfileID: c.profileID, Vars: vars})
|
||||
if renderErr != nil {
|
||||
return response, classifiedFailure(parent, ctx, renderErr, nil)
|
||||
}
|
||||
response.RepairInstruction = convertMessages(instruction.Messages)
|
||||
exchange := req.Replay.Exchange
|
||||
baseID, baseVars = exchange.PromptID, exchange.Variables
|
||||
suffix = append(suffix, pk.RenderedMessage{Role: pk.RoleAssistant, Content: exchange.FinalContent})
|
||||
for _, message := range req.Replay.Turns {
|
||||
suffix = append(suffix, pk.RenderedMessage{Role: message.Role, Content: message.Content})
|
||||
}
|
||||
suffix = append(suffix, instruction.Messages...)
|
||||
}
|
||||
handle, prepErr := c.runtime.Engine.PrepareExecution(ctx, pk.RunRequest{
|
||||
PromptID: req.PromptID, ProfileID: c.profileID, Vars: vars,
|
||||
PromptID: baseID, ProfileID: c.profileID, Vars: baseVars, AppendedMessages: suffix,
|
||||
TransientRetries: &c.settings.TransientRetries,
|
||||
Validation: contract,
|
||||
Observer: func(event pk.ExecutionEvent) {
|
||||
@@ -130,6 +151,12 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
|
||||
}
|
||||
defer handle.Discard()
|
||||
details := handle.Details()
|
||||
if req.Replay != nil {
|
||||
prefix := req.Replay.Exchange.Messages
|
||||
if len(details.Messages) < len(prefix) || !reflect.DeepEqual(convertMessages(details.Messages[:len(prefix)]), prefix) {
|
||||
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonInternalFailure, Message: "repair conversation prefix changed"}
|
||||
}
|
||||
}
|
||||
if name := details.EffectiveModelParams.APIKeyEnv; name != "" {
|
||||
c.credentialMu.Lock()
|
||||
if c.credentialNames == nil {
|
||||
@@ -168,6 +195,13 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
|
||||
}
|
||||
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonInternalFailure, Message: "validated structured output could not be decoded", Cause: decodeErr}
|
||||
}
|
||||
if req.CaptureExchange && req.Replay == nil {
|
||||
frozen := make(map[string]string, len(vars))
|
||||
for key, value := range vars {
|
||||
frozen[key] = value
|
||||
}
|
||||
response.Exchange = &structuredoutput.Exchange{PromptID: req.PromptID, Variables: frozen, Messages: convertMessages(details.Messages), FinalContent: result.RawOutput}
|
||||
}
|
||||
response.Content = json.RawMessage(result.RawOutput)
|
||||
response.Provider = "openai-compatible"
|
||||
return response, nil
|
||||
@@ -277,3 +311,11 @@ func (c *RoleClient) ConfiguredSecrets(ctx context.Context) []string {
|
||||
c.credentialMu.Unlock()
|
||||
return c.runtime.ConfiguredSecrets(ctx, names...)
|
||||
}
|
||||
|
||||
func convertMessages(messages []pk.RenderedMessage) []structuredoutput.Message {
|
||||
result := make([]structuredoutput.Message, len(messages))
|
||||
for i, message := range messages {
|
||||
result[i] = structuredoutput.Message{Role: string(message.Role), Content: message.Content}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package llm
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -11,9 +12,12 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
|
||||
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
|
||||
ck "go.maximumdirect.net/configkit"
|
||||
pk "go.maximumdirect.net/promptkit"
|
||||
)
|
||||
@@ -148,3 +152,140 @@ prompts:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleClientCapturesAndReplaysRepairConversation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
for name, content := range map[string]string{
|
||||
"config.yml": "version: 3\n",
|
||||
"prompts.yml": "profiles:\n - id: proposal\n endpoint: https://example.invalid/v1\n model: proposal\n - id: repair\n endpoint: https://example.invalid/v1\n model: repair\n",
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml"))}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var messages [][]structuredoutput.Message
|
||||
var models []string
|
||||
outputs := []string{`{"corrections":[]}`, `{"proposal_ref":1,"action":"revise","correction":{"id":1,"original_text":"a","corrected_text":"b","confidence":0.8}}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`}
|
||||
transport := transportFunc(func(r *http.Request) (*http.Response, error) {
|
||||
var req struct {
|
||||
Model string `json:"model"`
|
||||
Messages []structuredoutput.Message `json:"messages"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
models = append(models, req.Model)
|
||||
messages = append(messages, req.Messages)
|
||||
content, _ := json.Marshal(outputs[len(messages)-1])
|
||||
body := fmt.Sprintf(`{"model":%q,"choices":[{"message":{"content":%s}}]}`, req.Model, content)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
|
||||
})
|
||||
runtime, err := NewRuntime(context.Background(), loaded, &http.Client{Transport: transport})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := config.LLM{OperationTimeout: 10 * time.Second}
|
||||
proposal, err := runtime.NewRoleClient("proposal", "proposal", settings, CallHooks{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repair, err := runtime.NewRoleClient("repair", "repair", settings, CallHooks{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vars := map[string]string{"TranscriptDescriptionBlock": "", "GlossaryJSON": "[]", "SectionJSON": "[]"}
|
||||
initial, err := proposal.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarProposal, Schema: responseschema.CorrectionSetKey, Variables: vars, CaptureExchange: true}, &map[string]any{})
|
||||
if err != nil || initial.Exchange == nil {
|
||||
t.Fatalf("capture: %+v %v", initial, err)
|
||||
}
|
||||
vars["SectionJSON"] = "changed"
|
||||
replay := &structuredoutput.Replay{Exchange: initial.Exchange}
|
||||
for _, finding := range []string{"first", "second"} {
|
||||
result, err := repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": finding}, Replay: replay}, &map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(result.RepairInstruction) != 1 {
|
||||
t.Fatalf("instruction: %+v", result)
|
||||
}
|
||||
replay.Turns = append(replay.Turns, result.RepairInstruction...)
|
||||
replay.Turns = append(replay.Turns, structuredoutput.Message{Role: "assistant", Content: result.FinalContent})
|
||||
}
|
||||
if !reflect.DeepEqual(models, []string{"proposal", "repair", "repair"}) {
|
||||
t.Fatalf("models = %v", models)
|
||||
}
|
||||
prefix := initial.Exchange.Messages
|
||||
if len(messages) != 3 || !reflect.DeepEqual(messages[0], prefix) || !reflect.DeepEqual(messages[1][:len(prefix)], prefix) || !reflect.DeepEqual(messages[2][:len(prefix)], prefix) {
|
||||
t.Fatalf("prefixes: %+v", messages)
|
||||
}
|
||||
if len(messages[1]) != len(prefix)+2 || len(messages[2]) != len(prefix)+4 || messages[2][len(prefix)+2].Content != outputs[1] {
|
||||
t.Fatalf("turns: %+v", messages)
|
||||
}
|
||||
_, err = repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "independent"}, Replay: &structuredoutput.Replay{Exchange: initial.Exchange}}, &map[string]any{})
|
||||
if err != nil || len(messages) != 4 || len(messages[3]) != len(prefix)+2 || !strings.Contains(messages[3][len(prefix)+1].Content, "independent") {
|
||||
t.Fatalf("independent branch: %v %+v", err, messages)
|
||||
}
|
||||
bad := *initial.Exchange
|
||||
bad.Messages = append([]structuredoutput.Message(nil), bad.Messages...)
|
||||
bad.Messages[0].Content = "changed"
|
||||
_, err = repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "failure"}, Replay: &structuredoutput.Replay{Exchange: &bad}}, &map[string]any{})
|
||||
var failure *structuredoutput.Failure
|
||||
if !errors.As(err, &failure) || failure.Class != structuredoutput.FailureFatal || len(messages) != 4 {
|
||||
t.Fatalf("prefix failure: %v, calls %d", err, len(messages))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepairReplayUsesReplacementAndCountsStructuralRepair(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
files := map[string]string{
|
||||
"config.yml": "version: 3\n",
|
||||
"prompts.yml": "profiles:\n - id: fixture\n endpoint: https://example.invalid/v1\n model: fixture\ncollections:\n audita:\n prompts:\n - id: modules.grammar.repair\n messages:\n - role: user\n content_file: repair.txt\n output:\n format: json\n validation_mode: json_schema\n schema_path: correction_repair.v1.json\n",
|
||||
"repair.txt": "Replace {{.RepairRequestJSON}}",
|
||||
}
|
||||
for name, content := range files {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml"))}, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outputs := []string{`{"corrections":[]}`, `{"bad":true}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`}
|
||||
var requests [][]structuredoutput.Message
|
||||
transport := transportFunc(func(r *http.Request) (*http.Response, error) {
|
||||
var req struct {
|
||||
Messages []structuredoutput.Message `json:"messages"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
requests = append(requests, req.Messages)
|
||||
content, _ := json.Marshal(outputs[len(requests)-1])
|
||||
body := fmt.Sprintf(`{"model":"fixture","choices":[{"message":{"content":%s}}]}`, content)
|
||||
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
|
||||
})
|
||||
runtime, err := NewRuntime(context.Background(), loaded, &http.Client{Transport: transport})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, err := runtime.NewRoleClient("repair", "fixture", config.LLM{OperationTimeout: 10 * time.Second, RepairAttempts: 1}, CallHooks{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base, err := client.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarProposal, Schema: responseschema.CorrectionSetKey, Variables: map[string]string{"TranscriptDescriptionBlock": "", "GlossaryJSON": "[]", "SectionJSON": "[]"}, CaptureExchange: true}, &map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := client.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "fixture"}, Replay: &structuredoutput.Replay{Exchange: base.Exchange}}, &map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(requests) != 3 || result.Execution.ProviderAttempts != 2 || result.Execution.StructuralRepairs != 1 || result.FinalContent != outputs[2] || len(result.RepairInstruction) != 1 || result.RepairInstruction[0].Content != "Replace fixture" {
|
||||
t.Fatalf("result=%+v calls=%d", result, len(requests))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ type Request struct {
|
||||
Variables map[string]string `json:"variables"`
|
||||
StageName string `json:"stage_name,omitempty"`
|
||||
StartIndex int `json:"start_index"`
|
||||
CaptureExchange bool
|
||||
LLMClient contracts.StructuredLLMClient
|
||||
DiagnosticsDir string
|
||||
DiagnosticsWriter InteractionDiagnosticsWriter
|
||||
@@ -69,6 +70,7 @@ type Request struct {
|
||||
|
||||
// Result contains generated candidate proposals and optional diagnostics paths.
|
||||
type Result struct {
|
||||
Exchange *structuredoutput.Exchange `json:"-"`
|
||||
Corrections []proposals.CorrectionProposal `json:"corrections"`
|
||||
Enriched []proposals.EnrichedCorrectionProposal `json:"enriched"`
|
||||
Operations []outcomes.Operation `json:"operations,omitempty"`
|
||||
@@ -106,8 +108,9 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
|
||||
artifacts InteractionArtifacts
|
||||
)
|
||||
completion, callErr = req.LLMClient.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
|
||||
PromptID: req.PromptID,
|
||||
Variables: req.Variables, Schema: responseschema.CorrectionSetKey,
|
||||
CaptureExchange: req.CaptureExchange,
|
||||
PromptID: req.PromptID,
|
||||
Variables: req.Variables, Schema: responseschema.CorrectionSetKey,
|
||||
Context: structuredoutput.DiagnosticContext{StageIndex: req.StageIndex, StageID: req.StageID, Role: "proposal", ModuleKey: req.ModuleKey, ModuleInstance: req.ModuleInstance, SectionIndex: sectionIndexPtr(req.Section)},
|
||||
}, &response)
|
||||
sectionIndex := 0
|
||||
@@ -216,6 +219,7 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
|
||||
|
||||
return Result{
|
||||
Corrections: corrections,
|
||||
Exchange: completion.Exchange,
|
||||
Enriched: enriched,
|
||||
Operations: []outcomes.Operation{operation},
|
||||
Artifacts: artifacts,
|
||||
|
||||
@@ -63,6 +63,7 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
Variables: variables,
|
||||
StageName: stagename.ModuleProposal(req.ProposalRequest.RunSpec.InstanceName, sectionIndexPtr(req.ProposalRequest.Section)),
|
||||
StartIndex: 0,
|
||||
CaptureExchange: req.ProposalRequest.RunSpec.CaptureProposalExchange,
|
||||
LLMClient: req.ProposalRequest.LLMClient,
|
||||
DiagnosticsDir: req.ProposalRequest.DiagnosticsDir,
|
||||
ReportDiagnosticError: req.ProposalRequest.ReportDiagnosticError,
|
||||
@@ -73,6 +74,7 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
|
||||
|
||||
return contracts.ProposalResult{
|
||||
Proposals: generated.Corrections,
|
||||
Exchange: generated.Exchange,
|
||||
Operations: generated.Operations,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
)
|
||||
|
||||
func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing.T) {
|
||||
exchange := &structuredoutput.Exchange{PromptID: "captured", FinalContent: "response"}
|
||||
client := &fakeStructuredClient{
|
||||
completion: structuredoutput.Response{Exchange: exchange},
|
||||
responses: []StructuredCorrectionSet{
|
||||
{Corrections: []StructuredCorrectionProposal{{SegmentReference: 1, OriginalText: "teh", CorrectedText: "the", Confidence: 0.9}}},
|
||||
},
|
||||
@@ -39,9 +41,10 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
Section: §ion,
|
||||
},
|
||||
RunSpec: contracts.ModuleRunSpec{
|
||||
ModuleKey: "grammar",
|
||||
InstanceName: "grammar",
|
||||
ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
|
||||
CaptureProposalExchange: true,
|
||||
ModuleKey: "grammar",
|
||||
InstanceName: "grammar",
|
||||
ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
|
||||
},
|
||||
LLMClient: client,
|
||||
},
|
||||
@@ -53,6 +56,9 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
|
||||
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)
|
||||
}
|
||||
if !client.calls[0].CaptureExchange || out.Exchange != exchange {
|
||||
t.Fatalf("exchange was not passed through: %+v", out.Exchange)
|
||||
}
|
||||
variables := client.calls[0].Variables
|
||||
if !strings.Contains(variables["SectionJSON"], `"section_index": 7`) ||
|
||||
!strings.Contains(variables["SectionJSON"], "teh") ||
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"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"
|
||||
validatormetadata "gitea.maximumdirect.net/eric/audita/v3/internal/validators/metadata"
|
||||
)
|
||||
@@ -309,6 +310,7 @@ type sectionProposalResult struct {
|
||||
sectionPos int
|
||||
section chunking.Section
|
||||
corrected []proposals.CorrectionProposal
|
||||
exchange *structuredoutput.Exchange
|
||||
operations []outcomes.Operation
|
||||
started bool
|
||||
err error
|
||||
@@ -366,9 +368,10 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
|
||||
ReportDiagnosticError: input.ReportDiagnosticError,
|
||||
},
|
||||
RunSpec: contracts.ModuleRunSpec{
|
||||
ModuleKey: input.Spec.ModuleKey,
|
||||
InstanceName: input.Spec.InstanceName,
|
||||
ReplacementPolicy: input.Policy,
|
||||
ModuleKey: input.Spec.ModuleKey,
|
||||
InstanceName: input.Spec.InstanceName,
|
||||
ReplacementPolicy: input.Policy,
|
||||
CaptureProposalExchange: input.Spec.CaptureProposalExchange,
|
||||
},
|
||||
LLMClient: input.ProposalClient,
|
||||
})
|
||||
@@ -376,6 +379,7 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
|
||||
sectionPos: sectionPos,
|
||||
section: section,
|
||||
corrected: proposalResult.Proposals,
|
||||
exchange: proposalResult.Exchange,
|
||||
operations: proposalResult.Operations,
|
||||
started: true,
|
||||
err: err,
|
||||
@@ -504,6 +508,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
|
||||
SectionIndex: §ionMetadata.Index,
|
||||
Validators: validatorsOrdered,
|
||||
SectionEnriched: enriched,
|
||||
Exchange: sectionResult.exchange,
|
||||
ValidationLLMClient: input.ValidationClient,
|
||||
DiagnosticsDir: input.ValidationDiagnosticsDir,
|
||||
GlossaryView: input.GlossaryView,
|
||||
@@ -580,6 +585,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
|
||||
}
|
||||
|
||||
type validateSectionCandidatesInput struct {
|
||||
Exchange *structuredoutput.Exchange
|
||||
// ReportDiagnosticError may be called concurrently; it must not affect processing.
|
||||
ReportDiagnosticError func(error) `json:"-"`
|
||||
Spec contracts.ModuleRunSpec
|
||||
|
||||
@@ -24,10 +24,12 @@ type DiagnosticContext struct {
|
||||
// Request names an embedded prompt and response schema. Profile selection and
|
||||
// credentials are supplied by the role-bound runtime client.
|
||||
type Request struct {
|
||||
PromptID string `json:"prompt_id"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
Schema responseschema.Key `json:"schema"`
|
||||
Context DiagnosticContext `json:"context"`
|
||||
PromptID string `json:"prompt_id"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
Schema responseschema.Key `json:"schema"`
|
||||
Context DiagnosticContext `json:"context"`
|
||||
CaptureExchange bool `json:"-"`
|
||||
Replay *Replay `json:"-"`
|
||||
}
|
||||
|
||||
// PromptMetadata is the execution snapshot returned by the runtime when available.
|
||||
@@ -40,17 +42,33 @@ type PromptMetadata struct {
|
||||
Model string `json:"model,omitempty"`
|
||||
}
|
||||
|
||||
// Exchange holds a section's original prepared conversation for read-only replay.
|
||||
type Exchange struct {
|
||||
PromptID string
|
||||
Variables map[string]string
|
||||
Messages []Message
|
||||
FinalContent string
|
||||
}
|
||||
|
||||
// Replay carries a section exchange and the prior turns of one repair branch.
|
||||
type Replay struct {
|
||||
Exchange *Exchange
|
||||
Turns []Message
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Execution outcomes.ExecutionCounts `json:"execution"`
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens int `json:"completion_tokens,omitempty"`
|
||||
TotalTokens int `json:"total_tokens,omitempty"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
Exchange *Exchange `json:"-"`
|
||||
RepairInstruction []Message `json:"-"`
|
||||
Execution outcomes.ExecutionCounts `json:"execution"`
|
||||
Content json.RawMessage `json:"content,omitempty"`
|
||||
Provider string `json:"provider,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
PromptTokens int `json:"prompt_tokens,omitempty"`
|
||||
CompletionTokens int `json:"completion_tokens,omitempty"`
|
||||
TotalTokens int `json:"total_tokens,omitempty"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
}
|
||||
|
||||
// PreparedSnapshot is the exact rendered request snapshot. Its messages may
|
||||
@@ -68,14 +86,15 @@ type Message struct {
|
||||
// CallRecord brackets one logical operation, including preparation. Content
|
||||
// is present only when a model returned it; consumers must protect it.
|
||||
type CallRecord struct {
|
||||
CallID uint64 `json:"call_id"`
|
||||
Context DiagnosticContext `json:"context"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Started time.Time `json:"started"`
|
||||
Finished time.Time `json:"finished,omitempty"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
Failure FailureKind `json:"failure,omitempty"`
|
||||
RequestedPromptID string `json:"requested_prompt_id,omitempty"`
|
||||
CallID uint64 `json:"call_id"`
|
||||
Context DiagnosticContext `json:"context"`
|
||||
Prompt PromptMetadata `json:"prompt"`
|
||||
Started time.Time `json:"started"`
|
||||
Finished time.Time `json:"finished,omitempty"`
|
||||
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
|
||||
FinalContent string `json:"final_content,omitempty"`
|
||||
Failure FailureKind `json:"failure,omitempty"`
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
|
||||
Reference in New Issue
Block a user