feat(harness): add information gain stop and audit
This commit is contained in:
@@ -79,6 +79,40 @@ public final class EvidenceGuard {
|
||||
: EvidenceGuardResult.invalid(violations);
|
||||
}
|
||||
|
||||
public EvidenceGuardResult validateNoConclusionReferences(
|
||||
RunContext context, DiagnosisDraft draft) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
if (draft == null) {
|
||||
return EvidenceGuardResult.invalid(List.of(
|
||||
violation(EvidenceViolationCode.DRAFT_MISSING, "draft")));
|
||||
}
|
||||
if (draft.conclusion() != null) {
|
||||
throw new IllegalArgumentException("no-conclusion validation requires conclusion=null");
|
||||
}
|
||||
|
||||
List<EvidenceViolation> violations = new ArrayList<>();
|
||||
for (int index = 0; index < draft.analysis().size(); index++) {
|
||||
DiagnosisDraft.AnalysisItem analysis = draft.analysis().get(index);
|
||||
if (analysis == null || analysis.toolCallIds().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (analysis.kind() == null) {
|
||||
violations.add(violation(
|
||||
EvidenceViolationCode.ANALYSIS_KIND_MISSING,
|
||||
"analysis[" + index + "].kind"));
|
||||
continue;
|
||||
}
|
||||
List<VerifiedEvidence> ignoredEvidence = new ArrayList<>();
|
||||
for (String toolCallId : analysis.toolCallIds()) {
|
||||
verifyInvocation(
|
||||
context, analysis, index, toolCallId, ignoredEvidence, violations);
|
||||
}
|
||||
}
|
||||
return violations.isEmpty()
|
||||
? EvidenceGuardResult.valid(VerifiedEvidenceSnapshot.empty())
|
||||
: EvidenceGuardResult.invalid(violations);
|
||||
}
|
||||
|
||||
private List<EvidenceViolation> validateDraft(DiagnosisDraft draft) {
|
||||
List<EvidenceViolation> violations = new ArrayList<>();
|
||||
if (draft == null) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.superbiz.agent.harness.core.BudgetExceededException;
|
||||
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
|
||||
import com.superbiz.agent.harness.core.RunAbortedException;
|
||||
import com.superbiz.agent.harness.core.RunContext;
|
||||
import com.superbiz.agent.harness.audit.ModelCallAuditor;
|
||||
import com.superbiz.agent.harness.audit.ModelCallComponent;
|
||||
import com.superbiz.agent.harness.audit.ModelCallLedger;
|
||||
import com.superbiz.agent.harness.retry.RetryFailure;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.metadata.Usage;
|
||||
@@ -26,15 +29,23 @@ public final class GuardModelCall {
|
||||
private final DiagnosisHarnessCore core;
|
||||
private final ChatModel chatModel;
|
||||
private final ExecutorService executor;
|
||||
private final ModelCallAuditor auditor;
|
||||
|
||||
public GuardModelCall(DiagnosisHarnessCore core, ChatModel chatModel,
|
||||
ExecutorService executor) {
|
||||
this(core, chatModel, executor, new ModelCallAuditor(core));
|
||||
}
|
||||
|
||||
public GuardModelCall(DiagnosisHarnessCore core, ChatModel chatModel,
|
||||
ExecutorService executor, ModelCallAuditor auditor) {
|
||||
this.core = Objects.requireNonNull(core, "core must not be null");
|
||||
this.chatModel = Objects.requireNonNull(chatModel, "chatModel must not be null");
|
||||
this.executor = Objects.requireNonNull(executor, "executor must not be null");
|
||||
this.auditor = Objects.requireNonNull(auditor, "auditor must not be null");
|
||||
}
|
||||
|
||||
public String call(RunContext context, Prompt prompt, Duration timeout, long maxOutputBytes) {
|
||||
public String call(RunContext context, ModelCallComponent component,
|
||||
Prompt prompt, Duration timeout, long maxOutputBytes) {
|
||||
Objects.requireNonNull(context, "context must not be null");
|
||||
Objects.requireNonNull(prompt, "prompt must not be null");
|
||||
Objects.requireNonNull(timeout, "timeout must not be null");
|
||||
@@ -42,20 +53,24 @@ public final class GuardModelCall {
|
||||
throw new IllegalArgumentException("timeout and output limit must be positive");
|
||||
}
|
||||
core.beforeModelCall(context);
|
||||
Future<String> future = executor.submit(() -> invoke(context, prompt, maxOutputBytes));
|
||||
ModelCallLedger.Call call = auditor.begin(context, component);
|
||||
Future<String> future = executor.submit(() -> invoke(context, call, prompt, maxOutputBytes));
|
||||
context.cancellation().onCancel(ignored -> future.cancel(true));
|
||||
try {
|
||||
return future.get(timeout.toNanos(), TimeUnit.NANOSECONDS);
|
||||
} catch (TimeoutException exception) {
|
||||
future.cancel(true);
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
throw new GuardModelCallException(
|
||||
RetryFailure.TIMEOUT, "Guard model attempt timed out", exception);
|
||||
} catch (CancellationException exception) {
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
core.checkActive(context);
|
||||
throw new GuardModelCallException(
|
||||
RetryFailure.TRANSPORT, "Guard model attempt was cancelled", exception);
|
||||
} catch (InterruptedException exception) {
|
||||
future.cancel(true);
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
Thread.currentThread().interrupt();
|
||||
core.checkActive(context);
|
||||
throw new GuardModelCallException(
|
||||
@@ -76,15 +91,17 @@ public final class GuardModelCall {
|
||||
}
|
||||
}
|
||||
|
||||
private String invoke(RunContext context, Prompt prompt, long maxOutputBytes) {
|
||||
private String invoke(RunContext context, ModelCallLedger.Call call,
|
||||
Prompt prompt, long maxOutputBytes) {
|
||||
ChatResponse response;
|
||||
try {
|
||||
response = chatModel.call(prompt);
|
||||
} catch (RuntimeException exception) {
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
throw new GuardModelCallException(
|
||||
RetryFailure.TRANSPORT, "Guard model transport failed", exception);
|
||||
}
|
||||
recordUsage(context, response);
|
||||
recordUsage(context, call, response);
|
||||
core.checkActive(context);
|
||||
AssistantMessage output = response == null || response.getResult() == null
|
||||
? null : response.getResult().getOutput();
|
||||
@@ -102,16 +119,18 @@ public final class GuardModelCall {
|
||||
return output.getText();
|
||||
}
|
||||
|
||||
private void recordUsage(RunContext context, ChatResponse response) {
|
||||
private void recordUsage(RunContext context, ModelCallLedger.Call call, ChatResponse response) {
|
||||
if (response == null || response.getMetadata() == null) {
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
return;
|
||||
}
|
||||
Usage usage = response.getMetadata().getUsage();
|
||||
if (usage == null) {
|
||||
auditor.recordUsage(context, call, 0, 0, false);
|
||||
return;
|
||||
}
|
||||
core.recordTokens(context, nonNegative(usage.getPromptTokens()),
|
||||
nonNegative(usage.getCompletionTokens()));
|
||||
auditor.recordUsage(context, call, nonNegative(usage.getPromptTokens()),
|
||||
nonNegative(usage.getCompletionTokens()), true);
|
||||
}
|
||||
|
||||
private static long nonNegative(Integer value) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.SemanticVerdict;
|
||||
import com.superbiz.agent.harness.audit.DiagnosisTraceRecorder;
|
||||
import com.superbiz.agent.harness.audit.TraceAuditEvents;
|
||||
import com.superbiz.agent.harness.audit.ModelCallComponent;
|
||||
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
|
||||
import com.superbiz.agent.harness.core.RunContext;
|
||||
import com.superbiz.agent.harness.retry.HarnessRetryExecutor;
|
||||
@@ -81,7 +82,8 @@ public final class SemanticGuard {
|
||||
context,
|
||||
context.retryPolicies().semanticGuard(),
|
||||
() -> parse(modelCall.call(
|
||||
context, modelPrompt, remainingTimeout(startedNanos), limits.maxOutputBytes())),
|
||||
context, ModelCallComponent.SEMANTIC_GUARD,
|
||||
modelPrompt, remainingTimeout(startedNanos), limits.maxOutputBytes())),
|
||||
this::classify,
|
||||
attempt -> {
|
||||
attemptRecorder.accept(attempt);
|
||||
|
||||
Reference in New Issue
Block a user