|
|
|
|
@@ -18,25 +18,20 @@ import (
|
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// RepairAttempt retains compact facts for a single semantic request.
|
|
|
|
|
// RepairAttempt holds optional debug evidence for one semantic request.
|
|
|
|
|
type RepairAttempt struct {
|
|
|
|
|
Artifacts llm.InteractionArtifacts
|
|
|
|
|
Revision *proposals.EnrichedCorrectionProposal
|
|
|
|
|
Decisions []ValidatorDecisionRecord
|
|
|
|
|
Rejected []ValidatorRejectedChange
|
|
|
|
|
Counts outcomes.ExecutionCounts
|
|
|
|
|
FailureReason outcomes.ReasonCode
|
|
|
|
|
Succeeded bool
|
|
|
|
|
Approved bool
|
|
|
|
|
Operations []outcomes.Operation
|
|
|
|
|
Filtered []outcomes.FilteredProposal
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RepairOutcome keeps the proposal identity through revision and application.
|
|
|
|
|
// RepairOutcome summarizes the branch and carries its lineage when debug is enabled.
|
|
|
|
|
type RepairOutcome struct {
|
|
|
|
|
ProposalIndex int
|
|
|
|
|
Attempts []RepairAttempt
|
|
|
|
|
AttemptCount int
|
|
|
|
|
Outcome string
|
|
|
|
|
Attempts []RepairAttempt
|
|
|
|
|
Original proposals.EnrichedCorrectionProposal
|
|
|
|
|
Final proposals.EnrichedCorrectionProposal
|
|
|
|
|
}
|
|
|
|
|
@@ -68,12 +63,19 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
}
|
|
|
|
|
superseded := make(map[int]bool)
|
|
|
|
|
var branches []RepairOutcome
|
|
|
|
|
var repairOps []outcomes.Operation
|
|
|
|
|
debug := input.DiagnosticsDir != ""
|
|
|
|
|
section := 0
|
|
|
|
|
if input.SectionIndex != nil {
|
|
|
|
|
section = *input.SectionIndex
|
|
|
|
|
}
|
|
|
|
|
finish := func(err error) (validateSectionCandidatesResult, []RepairOutcome, error) {
|
|
|
|
|
if len(superseded) > 0 {
|
|
|
|
|
initial.filtered = slices.DeleteFunc(initial.filtered, func(item outcomes.FilteredProposal) bool {
|
|
|
|
|
return superseded[item.ProposalIndex]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
initial.operations = append(initial.operations, repairOps...)
|
|
|
|
|
return initial, branches, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -82,7 +84,10 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
if !actionable(original.ProposalIndex, rejected) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
branch := RepairOutcome{ProposalIndex: original.ProposalIndex, Original: original, Final: original, Outcome: "interrupted"}
|
|
|
|
|
branch := RepairOutcome{ProposalIndex: original.ProposalIndex, Outcome: "interrupted"}
|
|
|
|
|
if debug {
|
|
|
|
|
branch.Original, branch.Final = original, original
|
|
|
|
|
}
|
|
|
|
|
if err := ctx.Err(); err != nil {
|
|
|
|
|
branches = append(branches, branch)
|
|
|
|
|
return finish(err)
|
|
|
|
|
@@ -122,40 +127,47 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
var response proposal_generation.RepairResponse
|
|
|
|
|
proposalIndex, attemptNumber := original.ProposalIndex, attempt+1
|
|
|
|
|
completion, callErr := input.Spec.RepairClient.CompleteStructured(ctx, structuredoutput.Request{PromptID: promptID, Variables: map[string]string{"RepairRequestJSON": payload}, Schema: responseschema.CorrectionRepairKey, Replay: replay, Context: structuredoutput.DiagnosticContext{StageIndex: &input.Spec.Index, StageID: input.Spec.ID, Role: "correction_repair", ModuleKey: input.Spec.ModuleKey, ModuleInstance: input.Spec.InstanceName, SectionIndex: input.SectionIndex, ProposalIndex: &proposalIndex, RepairAttempt: &attemptNumber}}, &response)
|
|
|
|
|
record := RepairAttempt{Counts: completion.Execution}
|
|
|
|
|
save := func(err error) {
|
|
|
|
|
if input.DiagnosticsDir == "" {
|
|
|
|
|
operation := outcomes.Operation{
|
|
|
|
|
Reference: outcomes.Reference{
|
|
|
|
|
StageIndex: &input.Spec.Index, StageID: input.Spec.ID, ModuleKey: input.Spec.ModuleKey,
|
|
|
|
|
ModuleInstance: input.Spec.InstanceName, SectionIndex: section, Kind: outcomes.OperationCorrectionRepair,
|
|
|
|
|
ProposalIndex: &proposalIndex, RepairAttempt: &attemptNumber,
|
|
|
|
|
},
|
|
|
|
|
Counts: completion.Execution, ProposalIndices: []int{proposalIndex},
|
|
|
|
|
}
|
|
|
|
|
finishAttempt := func(err error, revision *proposals.EnrichedCorrectionProposal, validated validateSectionCandidatesResult) {
|
|
|
|
|
branch.AttemptCount++
|
|
|
|
|
repairOps = append(repairOps, operation)
|
|
|
|
|
if !debug {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
secrets := repairSecrets(ctx, input)
|
|
|
|
|
section := 0
|
|
|
|
|
if input.SectionIndex != nil {
|
|
|
|
|
section = *input.SectionIndex
|
|
|
|
|
record := RepairAttempt{Decisions: validated.decisions, FailureReason: operation.FailureReason}
|
|
|
|
|
if revision != nil {
|
|
|
|
|
snapshot := *revision
|
|
|
|
|
record.Revision = &snapshot
|
|
|
|
|
}
|
|
|
|
|
name := fmt.Sprintf("%s:section-%04d:proposal-%04d:attempt-%04d:repair", input.Spec.InstanceName, section, proposalIndex, attemptNumber)
|
|
|
|
|
var failure any
|
|
|
|
|
if err != nil {
|
|
|
|
|
failure = map[string]string{"error": err.Error()}
|
|
|
|
|
}
|
|
|
|
|
writer := llm.NewDiagnosticsWriter(filepath.Join(input.DiagnosticsDir, input.Spec.InstanceName), secrets)
|
|
|
|
|
artifacts, writeErr := writer.WriteInteraction(name, map[string]any{"stage_index": input.Spec.Index, "stage_id": input.Spec.ID, "proposal_index": proposalIndex, "repair_attempt": attemptNumber, "prompt_id": promptID, "prompt": completion.Prompt, "feedback": findings}, completion.Prepared, map[string]any{"decoded": response, "final_content": completion.FinalContent, "revision": record.Revision, "decisions": record.Decisions, "rejections": record.Rejected}, failure)
|
|
|
|
|
if writeErr != nil {
|
|
|
|
|
if input.ReportDiagnosticError != nil {
|
|
|
|
|
input.ReportDiagnosticError(fmt.Errorf("repair interaction %s: %w", name, writeErr))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
writer := llm.NewDiagnosticsWriter(filepath.Join(input.DiagnosticsDir, input.Spec.InstanceName), repairSecrets(ctx, input))
|
|
|
|
|
artifacts, writeErr := writer.WriteInteraction(name, map[string]any{"stage_index": input.Spec.Index, "stage_id": input.Spec.ID, "proposal_index": proposalIndex, "repair_attempt": attemptNumber, "prompt_id": promptID, "prompt": completion.Prompt, "feedback": findings}, completion.Prepared, map[string]any{"decoded": response, "final_content": completion.FinalContent, "revision": record.Revision, "decisions": record.Decisions, "rejections": validated.rejected}, failure)
|
|
|
|
|
if writeErr == nil {
|
|
|
|
|
record.Artifacts = artifacts
|
|
|
|
|
} else if input.ReportDiagnosticError != nil {
|
|
|
|
|
input.ReportDiagnosticError(fmt.Errorf("repair interaction %s: %w", name, writeErr))
|
|
|
|
|
}
|
|
|
|
|
record.Artifacts = artifacts
|
|
|
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
|
|
|
}
|
|
|
|
|
if callErr != nil {
|
|
|
|
|
var failure *structuredoutput.Failure
|
|
|
|
|
if errors.As(callErr, &failure) {
|
|
|
|
|
record.FailureReason = failure.Reason
|
|
|
|
|
operation.FailureReason = failure.Reason
|
|
|
|
|
} else {
|
|
|
|
|
record.FailureReason = outcomes.ReasonInternalFailure
|
|
|
|
|
operation.FailureReason = outcomes.ReasonInternalFailure
|
|
|
|
|
}
|
|
|
|
|
save(callErr)
|
|
|
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
|
|
|
finishAttempt(callErr, nil, validateSectionCandidatesResult{})
|
|
|
|
|
if structuredoutput.IsRecoverable(callErr) {
|
|
|
|
|
branch.Outcome = "unavailable"
|
|
|
|
|
break
|
|
|
|
|
@@ -165,18 +177,17 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
}
|
|
|
|
|
decoded, err := proposal_generation.DecodeRepairResponse(completion.Content, position+1)
|
|
|
|
|
if err != nil {
|
|
|
|
|
record.FailureReason = outcomes.ReasonInvalidOutput
|
|
|
|
|
save(err)
|
|
|
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
|
|
|
operation.FailureReason = outcomes.ReasonInvalidOutput
|
|
|
|
|
finishAttempt(err, nil, validateSectionCandidatesResult{})
|
|
|
|
|
branch.Outcome = "unavailable"
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
record.Succeeded = true
|
|
|
|
|
operation.Succeeded = true
|
|
|
|
|
operation.RecoveryReasons = outcomes.ExecutionRecoveries(operation.Counts)
|
|
|
|
|
replay.Turns = append(replay.Turns, completion.RepairInstruction...)
|
|
|
|
|
replay.Turns = append(replay.Turns, structuredoutput.Message{Role: "assistant", Content: completion.FinalContent})
|
|
|
|
|
if decoded.Action == "withdraw" {
|
|
|
|
|
save(nil)
|
|
|
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
|
|
|
finishAttempt(nil, nil, validateSectionCandidatesResult{})
|
|
|
|
|
branch.Outcome = "withdrawn"
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
@@ -188,35 +199,34 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
}
|
|
|
|
|
current = original
|
|
|
|
|
current.CorrectionProposal = proposals.CorrectionProposal{TargetSegmentID: target, OriginalText: revision.OriginalText, CorrectedText: revision.CorrectedText, Confidence: revision.Confidence}
|
|
|
|
|
branch.Final = current
|
|
|
|
|
if debug {
|
|
|
|
|
branch.Final = current
|
|
|
|
|
}
|
|
|
|
|
next := input
|
|
|
|
|
next.RepairProposalIndex, next.RepairAttempt = &proposalIndex, &attemptNumber
|
|
|
|
|
next.SectionEnriched = []proposals.EnrichedCorrectionProposal{current}
|
|
|
|
|
validated, validationErr := validateSectionCandidates(ctx, next)
|
|
|
|
|
revisionSnapshot := current
|
|
|
|
|
record.Revision = &revisionSnapshot
|
|
|
|
|
record.Decisions = validated.decisions
|
|
|
|
|
record.Rejected = validated.rejected
|
|
|
|
|
if len(validated.filtered) > 0 {
|
|
|
|
|
// A repair response contains one proposal. Attribute its first
|
|
|
|
|
// defect once, as for proposals in the original response.
|
|
|
|
|
record.Filtered = validated.filtered[:1]
|
|
|
|
|
// Each repair response contains only one proposal.
|
|
|
|
|
operation.Filtered = validated.filtered[:1]
|
|
|
|
|
operation.RecoveryReasons = append(operation.RecoveryReasons, outcomes.RecoveryProposalsFiltered)
|
|
|
|
|
}
|
|
|
|
|
for i := range validated.operations {
|
|
|
|
|
proposalIndex, attemptNumber := original.ProposalIndex, attempt+1
|
|
|
|
|
validated.operations[i].Reference.ProposalIndex = &proposalIndex
|
|
|
|
|
validated.operations[i].Reference.RepairAttempt = &attemptNumber
|
|
|
|
|
}
|
|
|
|
|
record.Operations = validated.operations
|
|
|
|
|
save(validationErr)
|
|
|
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
|
|
|
approved := validationErr == nil && len(validated.approved) == 1
|
|
|
|
|
if approved {
|
|
|
|
|
operation.RecoveryReasons = append(operation.RecoveryReasons, outcomes.RecoveryCorrectionRevised)
|
|
|
|
|
}
|
|
|
|
|
finishAttempt(validationErr, ¤t, validated)
|
|
|
|
|
initial.operations = append(initial.operations, validated.operations...)
|
|
|
|
|
if validationErr != nil {
|
|
|
|
|
branches = append(branches, branch)
|
|
|
|
|
return finish(validationErr)
|
|
|
|
|
}
|
|
|
|
|
if len(validated.approved) == 1 {
|
|
|
|
|
branch.Attempts[len(branch.Attempts)-1].Approved = true
|
|
|
|
|
if approved {
|
|
|
|
|
branch.Outcome = "approved"
|
|
|
|
|
initial.approved = append(initial.approved, current)
|
|
|
|
|
superseded[original.ProposalIndex] = true
|
|
|
|
|
@@ -244,32 +254,6 @@ func repairCandidates(ctx context.Context, input validateSectionCandidatesInput,
|
|
|
|
|
return finish(nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func repairOperations(input validateSectionCandidatesInput, branches []RepairOutcome) []outcomes.Operation {
|
|
|
|
|
var result []outcomes.Operation
|
|
|
|
|
section := 0
|
|
|
|
|
if input.SectionIndex != nil {
|
|
|
|
|
section = *input.SectionIndex
|
|
|
|
|
}
|
|
|
|
|
for _, branch := range branches {
|
|
|
|
|
for i, attempt := range branch.Attempts {
|
|
|
|
|
index, number := branch.ProposalIndex, i+1
|
|
|
|
|
operation := outcomes.Operation{Reference: outcomes.Reference{StageIndex: &input.Spec.Index, StageID: input.Spec.ID, ModuleKey: input.Spec.ModuleKey, ModuleInstance: input.Spec.InstanceName, SectionIndex: section, Kind: outcomes.OperationCorrectionRepair, ProposalIndex: &index, RepairAttempt: &number}, Counts: attempt.Counts, Succeeded: attempt.Succeeded, FailureReason: attempt.FailureReason, ProposalIndices: []int{index}}
|
|
|
|
|
if attempt.Succeeded {
|
|
|
|
|
operation.RecoveryReasons = outcomes.ExecutionRecoveries(attempt.Counts)
|
|
|
|
|
if len(attempt.Filtered) > 0 {
|
|
|
|
|
operation.Filtered = attempt.Filtered
|
|
|
|
|
operation.RecoveryReasons = append(operation.RecoveryReasons, outcomes.RecoveryProposalsFiltered)
|
|
|
|
|
}
|
|
|
|
|
if attempt.Approved {
|
|
|
|
|
operation.RecoveryReasons = append(operation.RecoveryReasons, outcomes.RecoveryCorrectionRevised)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result = append(result, operation)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func repairSecrets(ctx context.Context, input validateSectionCandidatesInput) []string {
|
|
|
|
|
var secrets []string
|
|
|
|
|
for _, client := range []contracts.StructuredLLMClient{input.Spec.RepairClient, input.ValidationLLMClient} {
|
|
|
|
|
|