feat(agent): add executor evidence output contract
This commit is contained in:
@@ -101,6 +101,8 @@ public class DiagnosisTraceEvaluator {
|
||||
failedChecks.add("reject output does not use degraded template");
|
||||
}
|
||||
|
||||
failedChecks.addAll(validateExecutorStructuredOutput(trace));
|
||||
|
||||
Integer toolCallCount = trace.getToolInvocations() == null ? 0 : trace.getToolInvocations().size();
|
||||
Integer durationMs = trace.getSession() == null ? null : trace.getSession().getTotalDurationMs();
|
||||
|
||||
@@ -174,6 +176,32 @@ public class DiagnosisTraceEvaluator {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
|
||||
private List<String> validateExecutorStructuredOutput(DiagnosisTraceResponse trace) {
|
||||
Object structuredOutput = nestedValue(trace, "verifier_evaluation", "executor_structured_output");
|
||||
if (!(structuredOutput instanceof Map<?, ?> output)) {
|
||||
return List.of();
|
||||
}
|
||||
Object claims = output.get("claims");
|
||||
if (!(claims instanceof List<?> claimList)) {
|
||||
return List.of("executor structured output missing claims array");
|
||||
}
|
||||
|
||||
List<String> failedChecks = new ArrayList<>();
|
||||
for (Object item : claimList) {
|
||||
if (!(item instanceof Map<?, ?> claim)) {
|
||||
failedChecks.add("executor structured claim is not an object");
|
||||
continue;
|
||||
}
|
||||
Object claimIdValue = claim.get("claim_id");
|
||||
String claimId = claimIdValue == null ? "unknown" : String.valueOf(claimIdValue);
|
||||
Object bindings = claim.get("evidence_bindings");
|
||||
if (!(bindings instanceof List<?> bindingList) || bindingList.isEmpty()) {
|
||||
failedChecks.add("executor confirmed claim missing evidence bindings: " + claimId);
|
||||
}
|
||||
}
|
||||
return failedChecks;
|
||||
}
|
||||
|
||||
private Object nestedValue(DiagnosisTraceResponse trace, String firstKey, String secondKey) {
|
||||
if (trace.getSession() == null || trace.getSession().getSelfEvaluation() == null) {
|
||||
return null;
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.alibaba.cloud.ai.graph.agent.hook.HookPosition;
|
||||
import com.alibaba.cloud.ai.graph.agent.hook.HookPositions;
|
||||
import com.alibaba.cloud.ai.graph.agent.hook.messages.AgentCommand;
|
||||
import com.alibaba.cloud.ai.graph.agent.hook.messages.MessagesModelHook;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.service.ToolTraceSummaryService;
|
||||
import com.superbiz.agent.util.SessionContextHolder;
|
||||
@@ -27,6 +29,8 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
|
||||
private final ToolTraceSummaryService toolTraceSummaryService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
public VerifierInputHook(ToolTraceSummaryService toolTraceSummaryService) {
|
||||
this.toolTraceSummaryService = toolTraceSummaryService;
|
||||
@@ -48,6 +52,10 @@ 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);
|
||||
@@ -55,6 +63,8 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
Map<String, Object> verifierInput = new LinkedHashMap<>();
|
||||
verifierInput.put("original_query", VerifierContextHolder.getOriginalQuery());
|
||||
verifierInput.put("executor_final_answer", executorFinalAnswer);
|
||||
verifierInput.put("executor_structured_output", parseResult.structuredOutput());
|
||||
verifierInput.put("executor_output_parse_status", parseResult.status());
|
||||
verifierInput.put("tool_trace_summary", toolTraceSummary);
|
||||
verifierInput.put("retry_context", VerifierContextHolder.getRetryContext());
|
||||
|
||||
@@ -66,6 +76,59 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
}
|
||||
}
|
||||
|
||||
private ExecutorOutputParseResult parseExecutorOutput(String executorFinalAnswer) {
|
||||
if (executorFinalAnswer == null || executorFinalAnswer.isBlank()) {
|
||||
return new ExecutorOutputParseResult(null, status("missing", "executor_final_answer is blank"));
|
||||
}
|
||||
|
||||
String sanitized = sanitizeJsonPayload(executorFinalAnswer);
|
||||
if (!looksJsonLike(sanitized)) {
|
||||
return new ExecutorOutputParseResult(null, status("missing", "executor output is not JSON"));
|
||||
}
|
||||
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(sanitized);
|
||||
if (!root.isObject() || !root.path("claims").isArray()) {
|
||||
return new ExecutorOutputParseResult(null, status("malformed",
|
||||
"executor output JSON does not match evidence-attribution contract"));
|
||||
}
|
||||
Map<String, Object> structuredOutput = objectMapper.convertValue(root, MAP_TYPE);
|
||||
return new ExecutorOutputParseResult(structuredOutput, status("valid", "parsed executor evidence contract"));
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to parse executor structured output", e);
|
||||
return new ExecutorOutputParseResult(null, status("malformed", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private String sanitizeJsonPayload(String raw) {
|
||||
String trimmed = raw.trim();
|
||||
int fenceStart = trimmed.indexOf("```");
|
||||
if (fenceStart >= 0) {
|
||||
int firstNewline = trimmed.indexOf('\n', fenceStart);
|
||||
int lastFence = trimmed.indexOf("```", firstNewline + 1);
|
||||
if (firstNewline >= 0 && lastFence > firstNewline) {
|
||||
return trimmed.substring(firstNewline + 1, lastFence).trim();
|
||||
}
|
||||
}
|
||||
int objectStart = trimmed.indexOf('{');
|
||||
int objectEnd = trimmed.lastIndexOf('}');
|
||||
if (objectStart >= 0 && objectEnd > objectStart) {
|
||||
return trimmed.substring(objectStart, objectEnd + 1).trim();
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
private boolean looksJsonLike(String text) {
|
||||
return text.startsWith("{") && text.endsWith("}");
|
||||
}
|
||||
|
||||
private Map<String, Object> status(String status, String detail) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("status", status);
|
||||
result.put("detail", detail == null ? "" : detail);
|
||||
return result;
|
||||
}
|
||||
|
||||
private String extractLastAssistantText(List<Message> previousMessages) {
|
||||
for (int i = previousMessages.size() - 1; i >= 0; i--) {
|
||||
if (previousMessages.get(i) instanceof AssistantMessage assistantMessage) {
|
||||
@@ -102,4 +165,10 @@ public class VerifierInputHook extends MessagesModelHook {
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
private record ExecutorOutputParseResult(
|
||||
Map<String, Object> structuredOutput,
|
||||
Map<String, Object> status
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +444,8 @@ public class ChatService {
|
||||
}
|
||||
|
||||
if ("PASS".equals(finalDecision.verdict())) {
|
||||
answer = answer == null || answer.isBlank() ? "抱歉,多 Agent 分析未能生成有效结论。" : answer;
|
||||
answer = extractUserFacingAnswer(answer)
|
||||
.orElse(answer == null || answer.isBlank() ? "抱歉,多 Agent 分析未能生成有效结论。" : answer);
|
||||
persistVerifierEvaluation(session, finalDecision, round);
|
||||
break;
|
||||
}
|
||||
@@ -744,6 +745,10 @@ public class ChatService {
|
||||
verifierEvaluation.put("rationale", decision.rationale());
|
||||
verifierEvaluation.put("round", round);
|
||||
verifierEvaluation.put("traceability_version", "v1");
|
||||
verifierEvaluation.put("executor_output_parse_status",
|
||||
Optional.ofNullable(VerifierContextHolder.getExecutorOutputParseStatus())
|
||||
.orElse(Map.of("status", "missing", "detail", "executor parse status unavailable")));
|
||||
verifierEvaluation.put("executor_structured_output", VerifierContextHolder.getExecutorStructuredOutput());
|
||||
verifierEvaluation.put("tool_trace_summary",
|
||||
Optional.ofNullable(VerifierContextHolder.getToolTraceSummary()).orElse(List.of()));
|
||||
|
||||
@@ -795,6 +800,22 @@ public class ChatService {
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private Optional<String> extractUserFacingAnswer(String executorAnswer) {
|
||||
if (executorAnswer == null || executorAnswer.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(sanitizeJsonPayload(executorAnswer));
|
||||
String userFacingAnswer = root.path("user_facing_answer").asText("");
|
||||
if (!userFacingAnswer.isBlank()) {
|
||||
return Optional.of(userFacingAnswer);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.debug("Executor answer is not structured JSON, keep raw answer");
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private String buildDegradedOutput(VerifierDecision decision) {
|
||||
StringBuilder output = new StringBuilder(DEGRADED_PREFIX);
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ public final class VerifierContextHolder {
|
||||
private static final ThreadLocal<String> ORIGINAL_QUERY = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> RETRY_CONTEXT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> EXECUTOR_FINAL_ANSWER = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Map<String, Object>> EXECUTOR_STRUCTURED_OUTPUT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Map<String, Object>> EXECUTOR_OUTPUT_PARSE_STATUS = new ThreadLocal<>();
|
||||
private static final ThreadLocal<List<Map<String, Object>>> TOOL_TRACE_SUMMARY = new ThreadLocal<>();
|
||||
|
||||
private VerifierContextHolder() {
|
||||
@@ -40,6 +42,22 @@ public final class VerifierContextHolder {
|
||||
return EXECUTOR_FINAL_ANSWER.get();
|
||||
}
|
||||
|
||||
public static void setExecutorStructuredOutput(Map<String, Object> executorStructuredOutput) {
|
||||
EXECUTOR_STRUCTURED_OUTPUT.set(executorStructuredOutput);
|
||||
}
|
||||
|
||||
public static Map<String, Object> getExecutorStructuredOutput() {
|
||||
return EXECUTOR_STRUCTURED_OUTPUT.get();
|
||||
}
|
||||
|
||||
public static void setExecutorOutputParseStatus(Map<String, Object> executorOutputParseStatus) {
|
||||
EXECUTOR_OUTPUT_PARSE_STATUS.set(executorOutputParseStatus);
|
||||
}
|
||||
|
||||
public static Map<String, Object> getExecutorOutputParseStatus() {
|
||||
return EXECUTOR_OUTPUT_PARSE_STATUS.get();
|
||||
}
|
||||
|
||||
public static void setToolTraceSummary(List<Map<String, Object>> toolTraceSummary) {
|
||||
TOOL_TRACE_SUMMARY.set(toolTraceSummary);
|
||||
}
|
||||
@@ -52,6 +70,8 @@ public final class VerifierContextHolder {
|
||||
ORIGINAL_QUERY.remove();
|
||||
RETRY_CONTEXT.remove();
|
||||
EXECUTOR_FINAL_ANSWER.remove();
|
||||
EXECUTOR_STRUCTURED_OUTPUT.remove();
|
||||
EXECUTOR_OUTPUT_PARSE_STATUS.remove();
|
||||
TOOL_TRACE_SUMMARY.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user