feat(agent): harden verifier evidence references
This commit is contained in:
@@ -17,9 +17,12 @@ import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Replaces verifier history with an explicit structured payload.
|
||||
@@ -60,14 +63,18 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
executorFinalAnswer = extractLastAssistantText(previousMessages);
|
||||
}
|
||||
|
||||
ExecutorOutputParseResult parseResult = parseExecutorOutput(executorFinalAnswer);
|
||||
VerifierContextHolder.setExecutorStructuredOutput(parseResult.structuredOutput());
|
||||
VerifierContextHolder.setExecutorOutputParseStatus(parseResult.status());
|
||||
|
||||
List<Map<String, Object>> toolTraceSummary =
|
||||
toolTraceSummaryService.buildVerifierTraceSummary(sessionId, executorFinalAnswer);
|
||||
VerifierContextHolder.setToolTraceSummary(toolTraceSummary);
|
||||
|
||||
ExecutorOutputParseResult parseResult = parseExecutorOutput(executorFinalAnswer);
|
||||
parseResult = new ExecutorOutputParseResult(
|
||||
enrichExecutorStructuredOutput(parseResult.structuredOutput(), toolTraceSummary),
|
||||
parseResult.status()
|
||||
);
|
||||
VerifierContextHolder.setExecutorStructuredOutput(parseResult.structuredOutput());
|
||||
VerifierContextHolder.setExecutorOutputParseStatus(parseResult.status());
|
||||
|
||||
Map<String, Object> gatekeeperResult = runGatekeeper(sessionId, parseResult);
|
||||
VerifierContextHolder.setGatekeeperResult(gatekeeperResult);
|
||||
|
||||
@@ -105,6 +112,8 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
private Map<String, Object> passGatekeeperResult() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("status", "pass");
|
||||
result.put("severity", "none");
|
||||
result.put("checked_bindings", List.of());
|
||||
result.put("failed_rules", List.of());
|
||||
result.put("warnings", List.of());
|
||||
result.put("errors", List.of());
|
||||
@@ -135,6 +144,129 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> enrichExecutorStructuredOutput(Map<String, Object> structuredOutput,
|
||||
List<Map<String, Object>> toolTraceSummary) {
|
||||
if (structuredOutput == null) {
|
||||
return null;
|
||||
}
|
||||
Map<String, List<Long>> invocationIdsByTool = invocationIdsByTool(toolTraceSummary);
|
||||
List<Map<String, Object>> warnings = new ArrayList<>();
|
||||
enrichEvidenceBindingsInSection(structuredOutput.get("claims"), invocationIdsByTool, warnings);
|
||||
enrichEvidenceBindingsInSection(structuredOutput.get("recommended_actions"), invocationIdsByTool, warnings);
|
||||
if (!warnings.isEmpty()) {
|
||||
structuredOutput.put("_gatekeeper_warnings", warnings);
|
||||
}
|
||||
return structuredOutput;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void enrichEvidenceBindingsInSection(Object sectionValue,
|
||||
Map<String, List<Long>> invocationIdsByTool,
|
||||
List<Map<String, Object>> warnings) {
|
||||
if (!(sectionValue instanceof List<?> items)) {
|
||||
return;
|
||||
}
|
||||
for (Object itemValue : items) {
|
||||
if (!(itemValue instanceof Map<?, ?> item)) {
|
||||
continue;
|
||||
}
|
||||
Object bindingsValue = item.get("evidence_bindings");
|
||||
if (!(bindingsValue instanceof List<?> bindings)) {
|
||||
continue;
|
||||
}
|
||||
for (Object bindingValue : bindings) {
|
||||
if (!(bindingValue instanceof Map<?, ?> rawBinding)) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> binding = (Map<String, Object>) rawBinding;
|
||||
String normalizedToolName = normalizeToolName(binding.get("tool_name"));
|
||||
if (!normalizedToolName.isBlank()) {
|
||||
binding.put("tool_name", normalizedToolName);
|
||||
}
|
||||
if (!hasInvocationId(binding)) {
|
||||
List<Long> ids = invocationIdsByTool.getOrDefault(normalizedToolName, List.of());
|
||||
if (ids.size() == 1) {
|
||||
binding.put("source_invocation_id", ids.get(0));
|
||||
warnings.add(Map.of(
|
||||
"rule", "evidence.invocation_auto_backfill",
|
||||
"message", "source_invocation_id was auto-filled from the unique tool invocation candidate; raw_path remains missing if Executor did not provide it",
|
||||
"tool_name", normalizedToolName,
|
||||
"source_invocation_id", ids.get(0)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<Long>> invocationIdsByTool(List<Map<String, Object>> toolTraceSummary) {
|
||||
Map<String, Set<Long>> idsByTool = new LinkedHashMap<>();
|
||||
for (Map<String, Object> summary : toolTraceSummary == null ? List.<Map<String, Object>>of() : toolTraceSummary) {
|
||||
String toolName = normalizeToolName(summary.get("tool_name"));
|
||||
if (toolName.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
List<Long> ids = toLongList(summary.get("source_invocation_ids"));
|
||||
if (ids.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
idsByTool.computeIfAbsent(toolName, ignored -> new LinkedHashSet<>()).addAll(ids);
|
||||
}
|
||||
|
||||
Map<String, List<Long>> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Set<Long>> entry : idsByTool.entrySet()) {
|
||||
result.put(entry.getKey(), new ArrayList<>(entry.getValue()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean hasInvocationId(Map<String, Object> binding) {
|
||||
if (asLong(binding.get("source_invocation_id")) != null) {
|
||||
return true;
|
||||
}
|
||||
return toLongList(binding.get("source_invocation_ids")).size() == 1;
|
||||
}
|
||||
|
||||
private List<Long> toLongList(Object value) {
|
||||
if (!(value instanceof List<?> values)) {
|
||||
return List.of();
|
||||
}
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (Object item : values) {
|
||||
Long id = asLong(item);
|
||||
if (id != null) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Long asLong(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.longValue();
|
||||
}
|
||||
if (value instanceof String text) {
|
||||
try {
|
||||
return Long.parseLong(text);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String normalizeToolName(Object value) {
|
||||
String toolName = value == null ? "" : String.valueOf(value);
|
||||
return switch (toolName) {
|
||||
case "lookupKnowledge" -> "lookup_knowledge";
|
||||
case "queryLogs" -> "query_logs";
|
||||
case "queryPrometheusAlerts" -> "query_metrics";
|
||||
case "getAvailableLogTopics" -> "get_available_log_topics";
|
||||
default -> toolName;
|
||||
};
|
||||
}
|
||||
|
||||
private String sanitizeJsonPayload(String raw) {
|
||||
String trimmed = raw.trim();
|
||||
int fenceStart = trimmed.indexOf("```");
|
||||
|
||||
Reference in New Issue
Block a user