feat(harness): add rag and log projections
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package com.superbiz.agent.harness.tool.adapter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import com.superbiz.agent.harness.core.DiagnosisHarnessCore;
|
||||
import com.superbiz.agent.harness.core.HarnessCoreFixtures;
|
||||
import com.superbiz.agent.harness.core.MutableClock;
|
||||
import com.superbiz.agent.harness.core.RunContext;
|
||||
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
|
||||
import com.superbiz.agent.harness.tool.boundary.ToolBoundary;
|
||||
import com.superbiz.agent.harness.tool.boundary.ToolBoundaryResult;
|
||||
import com.superbiz.agent.harness.tool.boundary.ToolCallRequestEnvelope;
|
||||
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
|
||||
import com.superbiz.agent.harness.tool.projection.QueryLogsResultProjector;
|
||||
import com.superbiz.agent.harness.tool.projection.RagResultProjector;
|
||||
import com.superbiz.agent.harness.tool.projection.ToolProjectionLimits;
|
||||
import com.superbiz.agent.harness.tool.store.CanonicalInvocationLimits;
|
||||
import com.superbiz.agent.harness.tool.store.CanonicalInvocationStore;
|
||||
import com.superbiz.agent.harness.tool.store.CanonicalToolInvocation;
|
||||
import com.superbiz.agent.harness.tool.store.DuplicateInvocationException;
|
||||
import com.superbiz.agent.harness.tool.store.ToolCallKeyFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ToolAdapterTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void ragAdapterUsesFrameworkIdAndCanonicalBoundary() throws Exception {
|
||||
MutableClock clock = new MutableClock(Instant.parse("2026-07-21T10:00:00Z"));
|
||||
FakeStore store = new FakeStore();
|
||||
ToolBoundary boundary = boundary(clock, store);
|
||||
RagToolAdapter adapter = new RagToolAdapter(boundary, objectMapper,
|
||||
new RagResultProjector(objectMapper, ToolProjectionLimits.defaults()),
|
||||
query -> Map.of("found", true, "evidenceBlocks", java.util.List.of(
|
||||
Map.of("source", "doc-1", "content", "exact excerpt"))));
|
||||
RunContext context = HarnessCoreFixtures.core(clock).startRun("session", "run-rag");
|
||||
|
||||
ToolBoundaryResult result = adapter.execute(context, envelope("run-rag", "framework-rag-1",
|
||||
"{\"query\":\"timeout\"}"));
|
||||
|
||||
assertEquals("framework-rag-1", result.toolCallId());
|
||||
assertEquals(EvidenceStatus.EVIDENCE_FOUND, result.evidenceStatus());
|
||||
assertTrue(result.agentResult().contains("exact excerpt"));
|
||||
assertEquals("framework-rag-1", store.records.values().iterator().next().toolCallId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryLogsAdapterInjectsLegacyControlsAndPreservesLogicalScope() throws Exception {
|
||||
MutableClock clock = new MutableClock(Instant.parse("2026-07-21T10:00:00Z"));
|
||||
FakeStore store = new FakeStore();
|
||||
AtomicReference<String> topic = new AtomicReference<>();
|
||||
AtomicReference<String> region = new AtomicReference<>();
|
||||
ToolBoundary boundary = boundary(clock, store);
|
||||
QueryLogsToolAdapter adapter = new QueryLogsToolAdapter(boundary, objectMapper,
|
||||
new QueryLogsResultProjector(objectMapper, ToolProjectionLimits.defaults()),
|
||||
(actualRegion, actualTopic, query, limit) -> {
|
||||
region.set(actualRegion);
|
||||
topic.set(actualTopic);
|
||||
return "{\"success\":true,\"total\":1,\"logs\":[{\"timestamp\":\"2026-07-21 09:59:00\",\"level\":\"WARN\",\"service\":\"order-service\",\"message\":\"timeout\"}]}";
|
||||
}, clock);
|
||||
RunContext context = HarnessCoreFixtures.core(clock).startRun("session", "run-log");
|
||||
|
||||
ToolBoundaryResult result = adapter.execute(context, envelope("run-log", "framework-log-1",
|
||||
"{\"topic\":\"APPLICATION\",\"query\":\"timeout\",\"lookback_minutes\":30}"));
|
||||
|
||||
assertEquals("ap-guangzhou", region.get());
|
||||
assertEquals("application-logs", topic.get());
|
||||
assertEquals(EvidenceStatus.EVIDENCE_FOUND, result.evidenceStatus());
|
||||
assertTrue(result.agentResult().contains("2026-07-21T09:30:00Z"));
|
||||
assertTrue(result.agentResult().contains("2026-07-21T10:00:00Z"));
|
||||
assertEquals("framework-log-1", result.toolCallId());
|
||||
}
|
||||
|
||||
private ToolBoundary boundary(MutableClock clock, FakeStore store) {
|
||||
DiagnosisHarnessCore core = HarnessCoreFixtures.core(clock);
|
||||
return new ToolBoundary(core, new ToolCallKeyFactory("superbiz:harness:tool-call"),
|
||||
store, objectMapper, clock);
|
||||
}
|
||||
|
||||
private ToolCallRequestEnvelope envelope(String runId, String toolCallId, String request) {
|
||||
return new ToolCallRequestEnvelope(runId, toolCallId, "tool", request, true, true);
|
||||
}
|
||||
|
||||
private static final class FakeStore implements CanonicalInvocationStore {
|
||||
private final CanonicalInvocationLimits limits = new CanonicalInvocationLimits(Duration.ofHours(1), 20_000, 10_000);
|
||||
private final Map<String, CanonicalToolInvocation> records = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public CanonicalInvocationLimits limits() {
|
||||
return limits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin(String key, CanonicalToolInvocation invocation) {
|
||||
if (records.putIfAbsent(key, invocation) != null) {
|
||||
throw new DuplicateInvocationException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<CanonicalToolInvocation> find(String key) {
|
||||
return Optional.ofNullable(records.get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CanonicalToolInvocation markReady(String key, String rawResponse, String agentResult,
|
||||
EvidenceStatus evidenceStatus, Instant completedAt) {
|
||||
CanonicalToolInvocation updated = records.get(key).markReady(rawResponse, agentResult, evidenceStatus, completedAt);
|
||||
records.put(key, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CanonicalToolInvocation markError(String key, String rawResponse, String errorCode, Instant completedAt) {
|
||||
CanonicalToolInvocation updated = records.get(key).markError(rawResponse, errorCode, completedAt);
|
||||
records.put(key, updated);
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.superbiz.agent.harness.tool.projection;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
|
||||
import com.superbiz.agent.harness.tool.contract.LogQueryScope;
|
||||
import com.superbiz.agent.harness.tool.contract.LogTopic;
|
||||
import com.superbiz.agent.harness.tool.contract.QueryLogsRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class QueryLogsResultProjectorTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final LogQueryScope scope = new LogQueryScope(LogTopic.APPLICATION, "timeout",
|
||||
"2026-07-21T10:00:00Z", "2026-07-21T10:30:00Z");
|
||||
|
||||
@Test
|
||||
void projectsMockScopePatternsAndRedactsSensitiveFields() throws Exception {
|
||||
QueryLogsResultProjector projector = new QueryLogsResultProjector(objectMapper,
|
||||
new ToolProjectionLimits(2, 1200, 2, 2, 200, 100, 4096));
|
||||
String raw = """
|
||||
{"success":true,"total":3,"logs":[
|
||||
{"timestamp":"2026-07-21 10:29:00","level":"ERROR","service":"order-service","instance":"pod-secret-1","message":"password=abc token:xyz pod-order-1 10.0.0.1 SELECT * FROM x WHERE name='alice' at a.B.c(A.java:1)","metrics":{"secret":"x"}},
|
||||
{"timestamp":"2026-07-21 10:28:00","level":"ERROR","service":"order-service","instance":"pod-secret-2","message":"password=def token:qwe pod-order-2 10.0.0.2 SELECT * FROM x WHERE name='bob'"},
|
||||
{"timestamp":"2026-07-21 10:27:00","level":"WARN","service":"order-service","message":"other"}]}
|
||||
""";
|
||||
|
||||
ProjectedToolResult projected = projector.project(new QueryLogsRequest(LogTopic.APPLICATION, "timeout", 30),
|
||||
"framework-log-1", scope, raw);
|
||||
JsonNode json = objectMapper.readTree(projected.agentResult());
|
||||
|
||||
assertEquals(EvidenceStatus.EVIDENCE_FOUND, projected.evidenceStatus());
|
||||
assertEquals("MOCK", json.path("source_kind").asText());
|
||||
assertEquals("APPLICATION", json.path("scope").path("topic").asText());
|
||||
assertEquals(3, json.path("match_count").asInt());
|
||||
assertTrue(json.path("truncated").asBoolean());
|
||||
assertFalse(projected.agentResult().contains("abc"));
|
||||
assertFalse(projected.agentResult().contains("xyz"));
|
||||
assertFalse(projected.agentResult().contains("pod-order"));
|
||||
assertFalse(projected.agentResult().contains("10.0.0.1"));
|
||||
assertFalse(projected.agentResult().contains("metrics"));
|
||||
assertFalse(projected.agentResult().contains("instance"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNoEvidenceForSuccessfulEmptyLogs() throws Exception {
|
||||
QueryLogsResultProjector projector = new QueryLogsResultProjector(objectMapper, ToolProjectionLimits.defaults());
|
||||
|
||||
ProjectedToolResult projected = projector.project(new QueryLogsRequest(LogTopic.SYSTEM_EVENTS, "missing", 5),
|
||||
"framework-log-2", scope, "{\"success\":true,\"total\":0,\"logs\":[]}");
|
||||
|
||||
assertEquals(EvidenceStatus.NO_EVIDENCE, projected.evidenceStatus());
|
||||
assertEquals(0, objectMapper.readTree(projected.agentResult()).path("events").size());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.superbiz.agent.harness.tool.projection;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.superbiz.agent.harness.contract.EvidenceStatus;
|
||||
import com.superbiz.agent.harness.tool.boundary.ProjectedToolResult;
|
||||
import com.superbiz.agent.harness.tool.contract.RagToolRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RagResultProjectorTest {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void projectsOnlyDeduplicatedBoundedEvidence() throws Exception {
|
||||
RagResultProjector projector = new RagResultProjector(objectMapper,
|
||||
new ToolProjectionLimits(2, 8, 2, 2, 20, 30, 4096));
|
||||
String raw = """
|
||||
{"found":true,"evidenceBlocks":[
|
||||
{"source":"doc-1","title":"One","breadcrumb":"a","content":"123456789","score":0.99},
|
||||
{"source":"doc-1","title":"Duplicate","content":"ignored"},
|
||||
{"source":"doc-2","title":"Two","content":"second"}],
|
||||
"contextPack":{"packedText":"secret internal context"},"retrievalTrace":{"attempts":[]},"rerankTrace":{}}
|
||||
""";
|
||||
|
||||
ProjectedToolResult projected = projector.project(new RagToolRequest("timeout"), "framework-1", raw);
|
||||
JsonNode json = objectMapper.readTree(projected.agentResult());
|
||||
|
||||
assertEquals(EvidenceStatus.EVIDENCE_FOUND, projected.evidenceStatus());
|
||||
assertEquals("framework-1", json.path("tool_call_id").asText());
|
||||
assertEquals(2, json.path("returned_count").asInt());
|
||||
assertEquals("12345678", json.path("evidence").get(0).path("excerpt").asText());
|
||||
assertTrue(json.path("truncated").asBoolean());
|
||||
assertFalse(projected.agentResult().contains("contextPack"));
|
||||
assertFalse(projected.agentResult().contains("retrievalTrace"));
|
||||
assertFalse(projected.agentResult().contains("score"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNoEvidenceForEmptyUsableBlocks() throws Exception {
|
||||
RagResultProjector projector = new RagResultProjector(objectMapper, ToolProjectionLimits.defaults());
|
||||
|
||||
ProjectedToolResult projected = projector.project(new RagToolRequest("unknown"), "framework-2",
|
||||
"{\"found\":false,\"evidenceBlocks\":[{\"source\":\"doc\",\"content\":\"\"}]}");
|
||||
|
||||
assertEquals(EvidenceStatus.NO_EVIDENCE, projected.evidenceStatus());
|
||||
assertEquals(0, objectMapper.readTree(projected.agentResult()).path("evidence").size());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user