From 5c669d68183b08f99dcb454c134d2b44780c8266 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Mon, 6 Jul 2026 23:30:59 -0600 Subject: [PATCH 1/3] wip2 --- .../main/java/dev/braintrust/eval/Eval.java | 14 ++--- .../java/dev/braintrust/eval/EvalTest.java | 61 ++++++++++++------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java b/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java index fc9e12c0..d2fb8076 100644 --- a/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java +++ b/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java @@ -120,9 +120,7 @@ private void evalOne(String experimentId, DatasetCase datasetCase .setSpanKind(SpanKind.CLIENT) .setAttribute(PARENT, "experiment_id:" + experimentId) .setAttribute("braintrust.span_attributes", toJson(Map.of("type", "eval"))) - .setAttribute( - "braintrust.input_json", - toJson(Map.of("input", datasetCase.input()))) + .setAttribute("braintrust.input_json", toJson(datasetCase.input())) .setAttribute("braintrust.expected", toJson(datasetCase.expected())) .startSpan(); if (datasetCase.origin().isPresent()) { @@ -146,22 +144,20 @@ private void evalOne(String experimentId, DatasetCase datasetCase .setAttribute( "braintrust.span_attributes", toJson(Map.of("type", "task"))) + .setAttribute("braintrust.input_json", toJson(datasetCase.input())) .startSpan(); taskSpanId = taskSpan.getSpanContext().getSpanId(); try (var unused = BraintrustContext.ofExperiment(experimentId, taskSpan).makeCurrent()) { taskResult = task.apply(datasetCase, parameters); - rootSpan.setAttribute( - "braintrust.output_json", - toJson(Map.of("output", taskResult.result()))); + taskSpan.setAttribute("braintrust.output_json", toJson(taskResult.result())); + rootSpan.setAttribute("braintrust.output_json", toJson(taskResult.result())); } catch (Exception e) { taskSpan.setStatus(StatusCode.ERROR, e.getMessage()); taskSpan.recordException(e); taskSpan.end(); rootSpan.setStatus(StatusCode.ERROR, e.getMessage()); - rootSpan.setAttribute( - "braintrust.output_json", - toJson(Collections.singletonMap("output", null))); + rootSpan.setAttribute("braintrust.output_json", toJson(null)); log.debug("Task threw exception for input: " + datasetCase.input(), e); // run scoreForTaskException on each scorer for (var scorer : scorers) { diff --git a/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java b/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java index c4cd1f00..263808a9 100644 --- a/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java +++ b/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java @@ -86,12 +86,15 @@ public void evalOtelTraceWithProperAttributes() { "all eval spans must set the parent to the experiment id"); if (span.getParentSpanId().equals(SpanId.getInvalid())) { numRootSpans.incrementAndGet(); - var inputJson = + // input_json is the bare value, not wrapped in {"input": ...} + var input = fromJson( span.getAttributes() .get(AttributeKey.stringKey("braintrust.input_json")), - Map.class); - assertNotNull(inputJson.get("input"), "invlaid input: " + inputJson); + String.class); + assertTrue( + input.equals("strawberry") || input.equals("asparagus"), + "invalid input: " + input); var expected = fromJson( @@ -100,14 +103,30 @@ public void evalOtelTraceWithProperAttributes() { String.class); assertTrue(isFruitOrVegetable(expected), "invalid expected: " + expected); - var outputJson = + // output_json is the bare value, not wrapped in {"output": ...} + var output = fromJson( span.getAttributes() .get(AttributeKey.stringKey("braintrust.output_json")), - Map.class); - var output = outputJson.get("output"); - assertNotNull(output, "invlaid output: " + outputJson); - assertTrue(isFruitOrVegetable(String.valueOf(output)), "invalid output: " + output); + String.class); + assertNotNull(output, "invalid output: " + output); + assertTrue(isFruitOrVegetable(output), "invalid output: " + output); + } else if ("task".equals(span.getName())) { + // task span carries its own bare input/output + var input = + fromJson( + span.getAttributes() + .get(AttributeKey.stringKey("braintrust.input_json")), + String.class); + assertTrue( + input.equals("strawberry") || input.equals("asparagus"), + "invalid task input: " + input); + var output = + fromJson( + span.getAttributes() + .get(AttributeKey.stringKey("braintrust.output_json")), + String.class); + assertTrue(isFruitOrVegetable(output), "invalid task output: " + output); } } assertEquals(2, numRootSpans.get(), "each case should make a root span"); @@ -315,8 +334,7 @@ public void evalRootSpanPassesOriginIfPresent() { var inputJson = span.getAttributes().get(AttributeKey.stringKey("braintrust.input_json")); assertNotNull(inputJson); - fromJson(inputJson, Map.class); - var input = (String) (fromJson(inputJson, Map.class)).get("input"); + var input = fromJson(inputJson, String.class); assertNotNull(input); var origin = span.getAttributes().get(AttributeKey.stringKey("braintrust.origin")); switch (input) { @@ -481,14 +499,14 @@ void evalContinuesWhenTaskThrows() { erroredRootSpan.getStatus().getDescription().contains("task failed on bad-input"), "root span error should contain the exception message"); - // The errored root span should have output: null - var erroredOutputJson = - fromJson( - erroredRootSpan - .getAttributes() - .get(AttributeKey.stringKey("braintrust.output_json")), - Map.class); - assertNull(erroredOutputJson.get("output"), "errored case output should be null"); + // The errored root span should have a bare null output + var erroredOutputJsonRaw = + erroredRootSpan + .getAttributes() + .get(AttributeKey.stringKey("braintrust.output_json")); + assertEquals("null", erroredOutputJsonRaw, "errored case output should be bare null"); + assertNull( + fromJson(erroredOutputJsonRaw, Object.class), "errored case output should be null"); // Find the task span for the errored case (child of errored root, type=task, status=ERROR) var erroredTaskSpan = @@ -560,14 +578,13 @@ void evalContinuesWhenTaskThrows() { .filter(s -> s.getStatus().getStatusCode() != StatusCode.ERROR) .findFirst() .orElseThrow(() -> new AssertionError("expected a successful root span")); - var successOutputJson = + var successOutput = fromJson( successRootSpan .getAttributes() .get(AttributeKey.stringKey("braintrust.output_json")), - Map.class); - assertEquals( - "result", successOutputJson.get("output"), "successful case should have output"); + String.class); + assertEquals("result", successOutput, "successful case should have output"); } @Test From cddc5486c73667437e661862d65382bcff1f2f4b Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Mon, 6 Jul 2026 23:31:06 -0600 Subject: [PATCH 2/3] wip3 --- .../src/main/java/dev/braintrust/eval/Eval.java | 5 ++++- .../src/test/java/dev/braintrust/eval/EvalTest.java | 11 +++++------ .../dev/braintrust/examples/ExperimentExample.java | 4 +++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java b/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java index d2fb8076..c162e360 100644 --- a/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java +++ b/braintrust-sdk/src/main/java/dev/braintrust/eval/Eval.java @@ -145,6 +145,7 @@ private void evalOne(String experimentId, DatasetCase datasetCase "braintrust.span_attributes", toJson(Map.of("type", "task"))) .setAttribute("braintrust.input_json", toJson(datasetCase.input())) + .setAttribute("braintrust.expected", toJson(datasetCase.expected())) .startSpan(); taskSpanId = taskSpan.getSpanContext().getSpanId(); try (var unused = @@ -157,7 +158,9 @@ private void evalOne(String experimentId, DatasetCase datasetCase taskSpan.recordException(e); taskSpan.end(); rootSpan.setStatus(StatusCode.ERROR, e.getMessage()); - rootSpan.setAttribute("braintrust.output_json", toJson(null)); + // Matching the Go/Python SDKs: on task failure we do not write an + // output_json to the eval span at all (rather than a literal null), + // so the UI renders an empty output instead of the string "null". log.debug("Task threw exception for input: " + datasetCase.input(), e); // run scoreForTaskException on each scorer for (var scorer : scorers) { diff --git a/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java b/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java index 263808a9..447dfe8e 100644 --- a/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java +++ b/braintrust-sdk/src/test/java/dev/braintrust/eval/EvalTest.java @@ -499,14 +499,13 @@ void evalContinuesWhenTaskThrows() { erroredRootSpan.getStatus().getDescription().contains("task failed on bad-input"), "root span error should contain the exception message"); - // The errored root span should have a bare null output - var erroredOutputJsonRaw = + // On task failure, matching the Go/Python SDKs, the eval span does not set + // output_json at all (rather than a literal null), so the UI shows an empty output. + assertNull( erroredRootSpan .getAttributes() - .get(AttributeKey.stringKey("braintrust.output_json")); - assertEquals("null", erroredOutputJsonRaw, "errored case output should be bare null"); - assertNull( - fromJson(erroredOutputJsonRaw, Object.class), "errored case output should be null"); + .get(AttributeKey.stringKey("braintrust.output_json")), + "errored case should not set output_json on the eval span"); // Find the task span for the errored case (child of errored root, type=task, status=ERROR) var erroredTaskSpan = diff --git a/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java b/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java index 09a33835..24eb2442 100644 --- a/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java +++ b/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java @@ -36,6 +36,7 @@ public static void main(String[] args) throws Exception { .evalBuilder() // NOTE: pre-existing experiment names will append results .name("java-eval-x-" + System.currentTimeMillis()) + /* .cases( DatasetCase.of( "strawberry", @@ -47,8 +48,9 @@ public static void main(String[] args) throws Exception { DatasetCase.of("asparagus", "vegetable"), DatasetCase.of("apple", "fruit"), DatasetCase.of("banana", "fruit")) + */ // Or, to fetch a remote dataset: - // .dataset(braintrust.fetchDataset("my-dataset-name")) + .dataset(braintrust.fetchDataset("food")) .taskFunction(getFoodType) .scorers( // to fetch a remote scorer: From 77f876680c06d791fe412ec9428fd45985d56883 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Mon, 6 Jul 2026 23:31:33 -0600 Subject: [PATCH 3/3] wip4 --- .../main/java/dev/braintrust/examples/ExperimentExample.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java b/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java index 24eb2442..a7184392 100644 --- a/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java +++ b/examples/experiment/src/main/java/dev/braintrust/examples/ExperimentExample.java @@ -4,11 +4,8 @@ import com.openai.models.ChatModel; import com.openai.models.chat.completions.ChatCompletionCreateParams; import dev.braintrust.Braintrust; -import dev.braintrust.eval.DatasetCase; import dev.braintrust.eval.Scorer; import dev.braintrust.instrumentation.openai.BraintrustOpenAI; -import java.util.List; -import java.util.Map; import java.util.function.Function; public class ExperimentExample {