feat(agent): support no-evidence references
This commit is contained in:
@@ -156,7 +156,8 @@ public class ExecutorGatekeeperService {
|
||||
SEVERITY_LOW_CONFID);
|
||||
continue;
|
||||
}
|
||||
validateEvidenceBinding(binding, validInvocations, target, claim.get("claim_id"), result);
|
||||
validateEvidenceBinding(binding, validInvocations, target,
|
||||
claim.get("claim_id"), claim.get("claim_type"), result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ public class ExecutorGatekeeperService {
|
||||
SEVERITY_LOW_CONFID);
|
||||
continue;
|
||||
}
|
||||
validateEvidenceBinding(binding, validInvocations, target, action.get("action_id"), result);
|
||||
validateEvidenceBinding(binding, validInvocations, target, action.get("action_id"), null, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +191,7 @@ public class ExecutorGatekeeperService {
|
||||
Map<Long, ToolInvocation> validInvocations,
|
||||
String target,
|
||||
Object ownerId,
|
||||
Object ownerType,
|
||||
GatekeeperResult result) {
|
||||
Map<String, Object> checked = new LinkedHashMap<>();
|
||||
checked.put("claim_id", ownerId == null ? "" : String.valueOf(ownerId));
|
||||
@@ -197,18 +199,6 @@ public class ExecutorGatekeeperService {
|
||||
checked.put("source_invocation_id", binding.get("source_invocation_id"));
|
||||
checked.put("raw_path", stringValue(binding.get("raw_path")));
|
||||
|
||||
Long id = singleInvocationId(binding);
|
||||
if (id == null) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
checked.put("rule", RULE_INVOCATION_REF);
|
||||
checked.put("message", "source_invocation_id is required");
|
||||
result.checked(checked);
|
||||
result.fail(RULE_INVOCATION_REF, target + ".source_invocation_id",
|
||||
"source_invocation_id is required", SEVERITY_LOW_CONFID);
|
||||
return;
|
||||
}
|
||||
checked.put("source_invocation_id", id);
|
||||
|
||||
String claimedToolName = stringValue(binding.get("tool_name"));
|
||||
if (claimedToolName.isBlank()) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
@@ -219,6 +209,46 @@ public class ExecutorGatekeeperService {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawPath = stringValue(binding.get("raw_path"));
|
||||
if ("negative_observation".equals(stringValue(ownerType)) && !rawPath.isBlank()
|
||||
&& !"$.no_evidence".equals(rawPath)) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
checked.put("rule", RULE_RAW_PATH);
|
||||
checked.put("message", "negative_observation must only bind $.no_evidence references");
|
||||
result.checked(checked);
|
||||
result.fail(RULE_RAW_PATH, target + ".raw_path",
|
||||
"negative_observation must only bind $.no_evidence references", SEVERITY_REJECT);
|
||||
return;
|
||||
}
|
||||
|
||||
Long id = singleInvocationId(binding);
|
||||
if (id == null) {
|
||||
id = uniqueInvocationIdByToolRawPathAndExcerpt(validInvocations, claimedToolName, rawPath,
|
||||
stringValue(binding.get("evidence_excerpt")));
|
||||
if (id == null) {
|
||||
id = uniqueInvocationIdByToolAndRawPath(validInvocations, claimedToolName, rawPath);
|
||||
}
|
||||
if (id != null) {
|
||||
result.warn(Map.of(
|
||||
"rule", "evidence.invocation_auto_backfill_by_raw_path",
|
||||
"message", "source_invocation_id was auto-filled from the unique evidence reference candidate",
|
||||
"tool_name", claimedToolName,
|
||||
"raw_path", rawPath,
|
||||
"source_invocation_id", id
|
||||
));
|
||||
}
|
||||
}
|
||||
if (id == null) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
checked.put("rule", RULE_INVOCATION_REF);
|
||||
checked.put("message", "source_invocation_id is required");
|
||||
result.checked(checked);
|
||||
result.fail(RULE_INVOCATION_REF, target + ".source_invocation_id",
|
||||
"source_invocation_id is required", SEVERITY_LOW_CONFID);
|
||||
return;
|
||||
}
|
||||
checked.put("source_invocation_id", id);
|
||||
|
||||
ToolInvocation invocation = validInvocations.get(id);
|
||||
if (invocation == null) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
@@ -240,7 +270,6 @@ public class ExecutorGatekeeperService {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawPath = stringValue(binding.get("raw_path"));
|
||||
if (rawPath.isBlank()) {
|
||||
checked.put("status", STATUS_FAIL);
|
||||
checked.put("rule", RULE_RAW_PATH);
|
||||
@@ -298,6 +327,82 @@ public class ExecutorGatekeeperService {
|
||||
result.checked(checked);
|
||||
}
|
||||
|
||||
private Long uniqueInvocationIdByToolAndRawPath(Map<Long, ToolInvocation> validInvocations,
|
||||
String toolName,
|
||||
String rawPath) {
|
||||
if (toolName == null || toolName.isBlank() || rawPath == null || rawPath.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Long matchedId = null;
|
||||
for (Map.Entry<Long, ToolInvocation> entry : validInvocations.entrySet()) {
|
||||
ToolInvocation invocation = entry.getValue();
|
||||
if (!Objects.equals(toolName, invocation.getToolName())) {
|
||||
continue;
|
||||
}
|
||||
if (!evidenceRefsByRawPath(invocation.getRetrievalDetails()).containsKey(rawPath)) {
|
||||
continue;
|
||||
}
|
||||
if (matchedId != null) {
|
||||
return null;
|
||||
}
|
||||
matchedId = entry.getKey();
|
||||
}
|
||||
return matchedId;
|
||||
}
|
||||
|
||||
private Long uniqueInvocationIdByToolRawPathAndExcerpt(Map<Long, ToolInvocation> validInvocations,
|
||||
String toolName,
|
||||
String rawPath,
|
||||
String excerpt) {
|
||||
if (toolName == null || toolName.isBlank()
|
||||
|| rawPath == null || rawPath.isBlank()
|
||||
|| excerpt == null || excerpt.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
Long matchedId = null;
|
||||
for (Map.Entry<Long, ToolInvocation> entry : validInvocations.entrySet()) {
|
||||
ToolInvocation invocation = entry.getValue();
|
||||
if (!Objects.equals(toolName, invocation.getToolName())) {
|
||||
continue;
|
||||
}
|
||||
String matchedText = evidenceRefsByRawPath(invocation.getRetrievalDetails()).get(rawPath);
|
||||
if (matchedText == null || !isBackfillCandidateSupported(rawPath, excerpt, matchedText)) {
|
||||
continue;
|
||||
}
|
||||
if (matchedId != null) {
|
||||
return null;
|
||||
}
|
||||
matchedId = entry.getKey();
|
||||
}
|
||||
return matchedId;
|
||||
}
|
||||
|
||||
private boolean isBackfillCandidateSupported(String rawPath, String excerpt, String matchedText) {
|
||||
if ("$.no_evidence".equals(rawPath)) {
|
||||
String excerptQuery = semicolonField(excerpt, "query");
|
||||
String matchedQuery = semicolonField(matchedText, "query");
|
||||
if (!excerptQuery.isBlank() && !matchedQuery.isBlank()
|
||||
&& !normalized(excerptQuery).equals(normalized(matchedQuery))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return isExcerptSupported(excerpt, matchedText);
|
||||
}
|
||||
|
||||
private String semicolonField(String text, String field) {
|
||||
if (text == null || text.isBlank() || field == null || field.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String prefix = field + "=";
|
||||
for (String part : text.split(";")) {
|
||||
String trimmed = part.trim();
|
||||
if (trimmed.regionMatches(true, 0, prefix, 0, prefix.length())) {
|
||||
return trimmed.substring(prefix.length()).trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private Long singleInvocationId(Map<?, ?> binding) {
|
||||
Long singular = asLong(binding.get("source_invocation_id"));
|
||||
if (singular != null) {
|
||||
|
||||
Reference in New Issue
Block a user