180 lines
7.0 KiB
Go
180 lines
7.0 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposal_generation"
|
|
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
|
|
"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/framework/validators"
|
|
)
|
|
|
|
// RepairAttempt retains compact facts for a single semantic request.
|
|
type RepairAttempt struct {
|
|
Revision *proposals.EnrichedCorrectionProposal
|
|
Decisions []ValidatorDecisionRecord
|
|
Rejected []ValidatorRejectedChange
|
|
Counts outcomes.ExecutionCounts
|
|
FailureReason outcomes.ReasonCode
|
|
Succeeded bool
|
|
Operations []outcomes.Operation
|
|
}
|
|
|
|
// RepairOutcome keeps the proposal identity through revision and application.
|
|
type RepairOutcome struct {
|
|
ProposalIndex int
|
|
Attempts []RepairAttempt
|
|
Outcome string
|
|
Original proposals.EnrichedCorrectionProposal
|
|
Final proposals.EnrichedCorrectionProposal
|
|
}
|
|
|
|
func actionable(index int, rejected []ValidatorRejectedChange) bool {
|
|
found := false
|
|
for _, decision := range rejected {
|
|
if decision.ProposalIndex != index {
|
|
continue
|
|
}
|
|
if decision.ReasonCode == validators.ReasonNoEffect {
|
|
return false
|
|
}
|
|
if decision.Disposition != validators.DispositionUnavailable {
|
|
found = true
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
// repairCandidates branches each rejected proposal from the same section exchange.
|
|
func repairCandidates(ctx context.Context, input validateSectionCandidatesInput, references []int, initial validateSectionCandidatesResult, promptID string) (validateSectionCandidatesResult, []RepairOutcome, error) {
|
|
if input.Spec.Settings.LLM.CorrectionRepairAttempts <= 0 {
|
|
return initial, nil, nil
|
|
}
|
|
var branches []RepairOutcome
|
|
for position, original := range input.SectionEnriched {
|
|
if !actionable(original.ProposalIndex, initial.rejected) {
|
|
continue
|
|
}
|
|
branch := RepairOutcome{ProposalIndex: original.ProposalIndex, Original: original, Final: original, Outcome: "interrupted"}
|
|
if err := ctx.Err(); err != nil {
|
|
branches = append(branches, branch)
|
|
return initial, branches, err
|
|
}
|
|
if input.Spec.RepairClient == nil || input.Exchange == nil || promptID == "" {
|
|
branches = append(branches, branch)
|
|
return initial, branches, fmt.Errorf("correction repair setup is unavailable")
|
|
}
|
|
localRef := 0
|
|
if position < len(references) {
|
|
localRef = references[position]
|
|
} else if input.WorkingTranscript != nil {
|
|
for i, s := range input.WorkingTranscript.Segments {
|
|
if s.ID == original.TargetSegmentID {
|
|
localRef = i + 1
|
|
break
|
|
}
|
|
}
|
|
}
|
|
current := original
|
|
findings := repairFindings(original.ProposalIndex, initial.rejected)
|
|
replay := &structuredoutput.Replay{Exchange: input.Exchange}
|
|
for attempt := 0; attempt < input.Spec.Settings.LLM.CorrectionRepairAttempts; attempt++ {
|
|
payload, err := (proposal_generation.RepairRequest{ProposalRef: position + 1, Correction: proposal_generation.StructuredCorrectionProposal{SegmentReference: localRef, OriginalText: current.OriginalText, CorrectedText: current.CorrectedText, Confidence: current.Confidence}, Findings: findings}).JSON()
|
|
if err != nil {
|
|
branches = append(branches, branch)
|
|
return initial, branches, err
|
|
}
|
|
var response proposal_generation.RepairResponse
|
|
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}}, &response)
|
|
record := RepairAttempt{Counts: completion.Execution}
|
|
if callErr != nil {
|
|
var failure *structuredoutput.Failure
|
|
if errors.As(callErr, &failure) {
|
|
record.FailureReason = failure.Reason
|
|
} else {
|
|
record.FailureReason = outcomes.ReasonInternalFailure
|
|
}
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
if structuredoutput.IsRecoverable(callErr) {
|
|
branch.Outcome = "unavailable"
|
|
break
|
|
}
|
|
branches = append(branches, branch)
|
|
return initial, branches, callErr
|
|
}
|
|
decoded, err := proposal_generation.DecodeRepairResponse(completion.Content, position+1)
|
|
if err != nil {
|
|
record.FailureReason = outcomes.ReasonInvalidOutput
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
branch.Outcome = "unavailable"
|
|
break
|
|
}
|
|
record.Succeeded = true
|
|
replay.Turns = append(replay.Turns, completion.RepairInstruction...)
|
|
replay.Turns = append(replay.Turns, structuredoutput.Message{Role: "assistant", Content: completion.FinalContent})
|
|
if decoded.Action == "withdraw" {
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
branch.Outcome = "withdrawn"
|
|
break
|
|
}
|
|
revision := decoded.Correction
|
|
localRef = revision.SegmentReference
|
|
target := 0
|
|
if input.WorkingTranscript != nil && localRef > 0 && localRef <= len(input.WorkingTranscript.Segments) {
|
|
target = input.WorkingTranscript.Segments[localRef-1].ID
|
|
}
|
|
current = original
|
|
current.CorrectionProposal = proposals.CorrectionProposal{TargetSegmentID: target, OriginalText: revision.OriginalText, CorrectedText: revision.CorrectedText, Confidence: revision.Confidence}
|
|
branch.Final = current
|
|
next := input
|
|
next.SectionEnriched = []proposals.EnrichedCorrectionProposal{current}
|
|
validated, validationErr := validateSectionCandidates(ctx, next)
|
|
revisionSnapshot := current
|
|
record.Revision = &revisionSnapshot
|
|
record.Decisions = validated.decisions
|
|
record.Rejected = validated.rejected
|
|
record.Operations = validated.operations
|
|
branch.Attempts = append(branch.Attempts, record)
|
|
initial.operations = append(initial.operations, validated.operations...)
|
|
initial.decisions = append(initial.decisions, validated.decisions...)
|
|
if validationErr != nil {
|
|
branches = append(branches, branch)
|
|
return initial, branches, validationErr
|
|
}
|
|
if len(validated.approved) == 1 {
|
|
branch.Outcome = "approved"
|
|
initial.approved = append(initial.approved, current)
|
|
retained := initial.filtered[:0]
|
|
for _, item := range initial.filtered {
|
|
if item.ProposalIndex != original.ProposalIndex {
|
|
retained = append(retained, item)
|
|
}
|
|
}
|
|
initial.filtered = retained
|
|
break
|
|
}
|
|
findings = repairFindings(original.ProposalIndex, validated.rejected)
|
|
if actionable(original.ProposalIndex, validated.rejected) {
|
|
if attempt+1 == input.Spec.Settings.LLM.CorrectionRepairAttempts {
|
|
branch.Outcome = "exhausted"
|
|
}
|
|
continue
|
|
}
|
|
branch.Outcome = "unavailable"
|
|
for _, d := range validated.rejected {
|
|
if d.Disposition != validators.DispositionUnavailable {
|
|
branch.Outcome = "rejected"
|
|
break
|
|
}
|
|
}
|
|
break
|
|
}
|
|
branches = append(branches, branch)
|
|
}
|
|
return initial, branches, nil
|
|
}
|