From 4c26f5bdec3c98a3b1332969b06485d05075f2bd Mon Sep 17 00:00:00 2001
From: Andrew Kent <31153155+realark@users.noreply.github.com>
Date: Fri, 9 Jan 2026 10:21:29 -0700
Subject: [PATCH] Revert "Add wrapTools() for LangChain4j tool instrumentation"
---
README.md | 59 -----
build.gradle | 2 -
examples/build.gradle | 14 --
.../LangchainToolWrappingExample.java | 158 -------------
.../langchain/BraintrustLangchain.java | 22 --
.../langchain/ByteBuddyToolWrapper.java | 119 ----------
.../langchain/BraintrustLangchainTest.java | 216 ------------------
.../instrumentation/langchain/TestTools.java | 22 --
8 files changed, 612 deletions(-)
delete mode 100644 examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java
delete mode 100644 src/main/java/dev/braintrust/instrumentation/langchain/ByteBuddyToolWrapper.java
delete mode 100644 src/test/java/dev/braintrust/instrumentation/langchain/TestTools.java
diff --git a/README.md b/README.md
index 7bd307f..0d33aaa 100644
--- a/README.md
+++ b/README.md
@@ -82,65 +82,6 @@ var request =
var response = openAIClient.chat().completions().create(request);
```
-### LangChain4j Instrumentation
-
-```java
-var braintrust = Braintrust.get();
-var openTelemetry = braintrust.openTelemetryCreate();
-
-// Wrap the chat model to trace LLM calls
-ChatModel model = BraintrustLangchain.wrap(
- openTelemetry,
- OpenAiChatModel.builder()
- .apiKey(System.getenv("OPENAI_API_KEY"))
- .modelName("gpt-4o-mini")
- .temperature(0.0));
-
-var response = model.chat("What is the capital of France?");
-```
-
-#### Tool Wrapping
-
-Use `BraintrustLangchain.wrapTools()` to automatically trace tool executions in your LangChain4j agents:
-
-```java
-// Create your tool class
-public class WeatherTools {
- @Tool("Get current weather for a location")
- public String getWeather(String location) {
- return "The weather in " + location + " is sunny.";
- }
-}
-
-// Wrap tools to create spans for each tool execution
-WeatherTools tools = new WeatherTools();
-WeatherTools instrumentedTools = BraintrustLangchain.wrapTools(openTelemetry, tools);
-
-// Use instrumented tools in your AI service
-Assistant assistant = AiServices.builder(Assistant.class)
- .chatModel(model)
- .tools(instrumentedTools)
- .build();
-```
-
-Each tool call will automatically create an OpenTelemetry span in Braintrust with:
-- Tool name and parameters
-- Execution duration
-- Return values
-- Any exceptions thrown
-
-**Note:** For proper display in the Braintrust UI, ensure parent spans (conversation, turn, etc.) also set the required Braintrust attributes:
-```java
-var span = tracer.spanBuilder("my-span").startSpan();
-span.setAttribute("braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"my-span\"}");
-span.setAttribute("braintrust.input_json", "{\"user_message\":\"...\"}");
-// ... do work ...
-span.setAttribute("braintrust.output_json", "{\"result\":\"...\"}");
-span.end();
-```
-
-See [LangchainToolWrappingExample.java](./examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java) for a complete example with proper span hierarchy.
-
## Running Examples
Example source code can be found [here](./examples/src/main/java/dev/braintrust/examples)
diff --git a/build.gradle b/build.gradle
index 90d9c9f..ad1b078 100644
--- a/build.gradle
+++ b/build.gradle
@@ -95,8 +95,6 @@ dependencies {
testImplementation "dev.langchain4j:langchain4j:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
-
- implementation 'net.bytebuddy:byte-buddy:1.14.11' // ByteBuddy for LangChain4j tool wrapping
}
/**
diff --git a/examples/build.gradle b/examples/build.gradle
index 553af25..f4e9c20 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -172,17 +172,3 @@ task runLangchain(type: JavaExec) {
suspend = false
}
}
-
-task runLangchainToolWrapping(type: JavaExec) {
- group = 'Braintrust SDK Examples'
- description = 'Run the LangChain4j tool wrapping example. NOTE: this requires OPENAI_API_KEY to be exported and will make a small call to openai, using your tokens'
- classpath = sourceSets.main.runtimeClasspath
- mainClass = 'dev.braintrust.examples.LangchainToolWrappingExample'
- systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel
- debugOptions {
- enabled = true
- port = 5566
- server = true
- suspend = false
- }
-}
diff --git a/examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java b/examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java
deleted file mode 100644
index 102880d..0000000
--- a/examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package dev.braintrust.examples;
-
-import dev.braintrust.Braintrust;
-import dev.braintrust.instrumentation.langchain.BraintrustLangchain;
-import dev.langchain4j.agent.tool.Tool;
-import dev.langchain4j.model.chat.ChatModel;
-import dev.langchain4j.model.openai.OpenAiChatModel;
-import dev.langchain4j.service.AiServices;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.context.Scope;
-import io.opentelemetry.sdk.OpenTelemetrySdk;
-
-/**
- * Demonstrates how to use BraintrustLangchain.wrapTools() to automatically trace tool executions.
- *
- *
This example shows:
- *
- *
- * - Wrapping LangChain4j tool objects to create OpenTelemetry spans for each tool call
- *
- Creating a conversation hierarchy with turns and tool executions
- *
- How tool calls appear in Braintrust traces with full context
- *
- */
-public class LangchainToolWrappingExample {
-
- /** Example tool class with weather-related methods */
- public static class WeatherTools {
- @Tool("Get current weather for a location")
- public String getWeather(String location) {
- // Simulate a weather API call
- return String.format("The weather in %s is sunny with 72°F temperature.", location);
- }
-
- @Tool("Get weather forecast for next N days")
- public String getForecast(String location, int days) {
- // Simulate a forecast API call
- return String.format(
- "The %d-day forecast for %s: Mostly sunny with temperatures between 65-75°F.",
- days, location);
- }
- }
-
- /** AI Service interface for the assistant */
- interface Assistant {
- String chat(String userMessage);
- }
-
- public static void main(String[] args) throws Exception {
- if (null == System.getenv("OPENAI_API_KEY")) {
- System.err.println(
- "\nWARNING envar OPENAI_API_KEY not found. This example will likely fail.\n");
- }
-
- var braintrust = Braintrust.get();
- var openTelemetry = braintrust.openTelemetryCreate();
- var tracer = openTelemetry.getTracer("langchain-tool-wrapping");
-
- System.out.println("\n=== LangChain4j Tool Wrapping Example ===\n");
-
- // Create root span for the conversation
- var conversationSpan = tracer.spanBuilder("weather-assistant-conversation").startSpan();
- conversationSpan.setAttribute(
- "braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"conversation\"}");
- conversationSpan.setAttribute(
- "braintrust.input_json",
- "{\"description\":\"Weather assistant with tool wrapping\",\"turns\":2}");
-
- try (var ignored = conversationSpan.makeCurrent()) {
- // Wrap the LLM with Braintrust instrumentation
- ChatModel model =
- BraintrustLangchain.wrap(
- openTelemetry,
- OpenAiChatModel.builder()
- .apiKey(System.getenv("OPENAI_API_KEY"))
- .modelName("gpt-4o-mini")
- .temperature(0.0));
-
- // Create tools and wrap them with Braintrust instrumentation
- WeatherTools tools = new WeatherTools();
- WeatherTools instrumentedTools = BraintrustLangchain.wrapTools(openTelemetry, tools);
-
- System.out.println("Tools wrapped with Braintrust instrumentation");
- System.out.println("Each tool call will create a span in Braintrust\n");
-
- // Create AI service with the instrumented tools
- Assistant assistant =
- AiServices.builder(Assistant.class)
- .chatModel(model)
- .tools(instrumentedTools)
- .build();
-
- // Example 1: Single tool call
- System.out.println("--- Turn 1: Single Tool Call ---");
- String query1 = "What's the weather in San Francisco?";
- System.out.println("User: " + query1);
- Span turn1 = tracer.spanBuilder("turn_1").startSpan();
- turn1.setAttribute(
- "braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"turn_1\"}");
- turn1.setAttribute("braintrust.input_json", "{\"user_message\":\"" + query1 + "\"}");
- String response1;
- try (Scope scope = turn1.makeCurrent()) {
- response1 = assistant.chat(query1);
- System.out.println("Assistant: " + response1);
- turn1.setAttribute(
- "braintrust.output_json",
- "{\"assistant_message\":\"" + response1.replace("\"", "\\\"") + "\"}");
- } finally {
- turn1.end();
- }
- System.out.println();
-
- // Example 2: Tool with multiple parameters
- System.out.println("--- Turn 2: Multiple Parameters ---");
- String query2 = "What's the 5-day forecast for Tokyo?";
- System.out.println("User: " + query2);
- Span turn2 = tracer.spanBuilder("turn_2").startSpan();
- turn2.setAttribute(
- "braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"turn_2\"}");
- turn2.setAttribute("braintrust.input_json", "{\"user_message\":\"" + query2 + "\"}");
- String response2;
- try (Scope scope = turn2.makeCurrent()) {
- response2 = assistant.chat(query2);
- System.out.println("Assistant: " + response2);
- turn2.setAttribute(
- "braintrust.output_json",
- "{\"assistant_message\":\"" + response2.replace("\"", "\\\"") + "\"}");
- } finally {
- turn2.end();
- }
- System.out.println();
-
- } finally {
- conversationSpan.setAttribute(
- "braintrust.output_json", "{\"status\":\"completed\",\"turns\":2}");
- conversationSpan.end();
- }
-
- // Flush traces before exit
- if (openTelemetry instanceof OpenTelemetrySdk) {
- ((OpenTelemetrySdk) openTelemetry).close();
- }
-
- var url =
- braintrust.projectUri()
- + "/logs?r=%s&s=%s"
- .formatted(
- conversationSpan.getSpanContext().getTraceId(),
- conversationSpan.getSpanContext().getSpanId());
-
- System.out.println("Example complete!");
- System.out.println("\nIn Braintrust, you'll see:");
- System.out.println(" • Root conversation span");
- System.out.println(" • Nested turn spans for each user interaction");
- System.out.println(" • LLM call spans (from BraintrustLangchain.wrap())");
- System.out.println(" • Tool execution spans (from BraintrustLangchain.wrapTools())");
- System.out.println("\n View your traces: " + url + "\n");
- }
-}
diff --git a/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java b/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java
index 9645f88..a3aafa8 100644
--- a/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java
+++ b/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java
@@ -49,28 +49,6 @@ public static OpenAiStreamingChatModel wrap(
}
}
- /**
- * Wrap a tools object to instrument @Tool method executions with Braintrust traces. Returns a
- * proxy that intercepts all @Tool annotated methods and creates OpenTelemetry spans.
- *
- * Usage: StoryTools tools = new StoryTools(); StoryTools instrumented =
- * BraintrustLangchain.wrapTools(openTelemetry, tools);
- * AiServices.builder(Assistant.class).chatModel(model).tools(instrumented).build()
- *
- * @param otel OpenTelemetry instance from braintrust.openTelemetryCreate()
- * @param tools Tool object with @Tool annotated methods
- * @return Proxied tool object that creates spans for each @Tool method call
- */
- @SuppressWarnings("unchecked")
- public static T wrapTools(OpenTelemetry otel, T tools) {
- try {
- return (T) new ByteBuddyToolWrapper(otel).wrap(tools);
- } catch (Exception e) {
- log.warn("Failed to wrap tools with instrumentation, returning original", e);
- return tools;
- }
- }
-
private static HttpClientBuilder wrap(
OpenTelemetry otel, HttpClientBuilder builder, Options options) {
return new WrappedHttpClientBuilder(otel, builder, options);
diff --git a/src/main/java/dev/braintrust/instrumentation/langchain/ByteBuddyToolWrapper.java b/src/main/java/dev/braintrust/instrumentation/langchain/ByteBuddyToolWrapper.java
deleted file mode 100644
index 0c4ba25..0000000
--- a/src/main/java/dev/braintrust/instrumentation/langchain/ByteBuddyToolWrapper.java
+++ /dev/null
@@ -1,119 +0,0 @@
-package dev.braintrust.instrumentation.langchain;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import dev.langchain4j.agent.tool.Tool;
-import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.trace.Span;
-import io.opentelemetry.api.trace.StatusCode;
-import io.opentelemetry.api.trace.Tracer;
-import io.opentelemetry.context.Scope;
-import java.lang.reflect.Method;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.Callable;
-import lombok.SneakyThrows;
-import lombok.extern.slf4j.Slf4j;
-import net.bytebuddy.ByteBuddy;
-import net.bytebuddy.implementation.MethodDelegation;
-import net.bytebuddy.implementation.bind.annotation.*;
-import net.bytebuddy.matcher.ElementMatchers;
-
-/**
- * Uses ByteBuddy to create runtime subclass proxies that intercept @Tool methods and add
- * OpenTelemetry spans with Braintrust attributes.
- */
-@Slf4j
-class ByteBuddyToolWrapper {
- private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
- private final Tracer tracer;
-
- ByteBuddyToolWrapper(OpenTelemetry otel) {
- this.tracer = otel.getTracer("braintrust");
- }
-
- @SuppressWarnings("unchecked")
- public T wrap(T originalTools) throws Exception {
- Class> toolClass = originalTools.getClass();
-
- // Create subclass with interceptor, preserving all annotations
- Class> proxyClass =
- new ByteBuddy()
- .subclass(toolClass)
- .method(ElementMatchers.isAnnotatedWith(Tool.class))
- .intercept(
- MethodDelegation.to(
- new ToolMethodInterceptor(tracer, originalTools)))
- .attribute(
- net.bytebuddy.implementation.attribute.MethodAttributeAppender
- .ForInstrumentedMethod.INCLUDING_RECEIVER)
- .make()
- .load(toolClass.getClassLoader())
- .getLoaded();
-
- // Create instance - ByteBuddy subclass will delegate to original
- return (T) proxyClass.getDeclaredConstructor().newInstance();
- }
-
- /** Interceptor that wraps @Tool method calls with OpenTelemetry spans */
- public static class ToolMethodInterceptor {
- private final Tracer tracer;
- private final Object originalTools;
-
- public ToolMethodInterceptor(Tracer tracer, Object originalTools) {
- this.tracer = tracer;
- this.originalTools = originalTools;
- }
-
- @RuntimeType
- public Object intercept(
- @Origin Method method, @AllArguments Object[] args, @SuperCall Callable> zuper)
- throws Exception {
-
- String toolName = method.getName();
-
- // Build input map from parameters
- Map inputMap = new HashMap<>();
- var parameters = method.getParameters();
- for (int i = 0; i < parameters.length && i < args.length; i++) {
- inputMap.put(parameters[i].getName(), args[i]);
- }
-
- Span span = tracer.spanBuilder(toolName).startSpan();
- try (Scope scope = span.makeCurrent()) {
- // Set Braintrust span attributes
- span.setAttribute(
- "braintrust.span_attributes",
- json(Map.of("type", "tool", "name", toolName)));
-
- // Set input
- span.setAttribute("braintrust.input_json", json(inputMap));
-
- // Execute method and measure time
- long startTime = System.nanoTime();
- Object result = zuper.call(); // Call original method
- long endTime = System.nanoTime();
-
- // Set output
- span.setAttribute("braintrust.output_json", json(result));
-
- // Set metrics
- double executionTime = (endTime - startTime) / 1_000_000_000.0;
- span.setAttribute(
- "braintrust.metrics", json(Map.of("execution_time", executionTime)));
-
- return result;
- } catch (Throwable t) {
- span.setStatus(StatusCode.ERROR, t.getMessage());
- span.recordException(t);
- throw t;
- } finally {
- span.end();
- }
- }
-
- @SneakyThrows
- private static String json(Object o) {
- return JSON_MAPPER.writeValueAsString(o);
- }
- }
-}
diff --git a/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java b/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java
index 67d0927..e2555bd 100644
--- a/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java
+++ b/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java
@@ -13,8 +13,6 @@
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiStreamingChatModel;
import io.opentelemetry.api.common.AttributeKey;
-import io.opentelemetry.api.trace.StatusCode;
-import java.util.List;
import java.util.concurrent.CompletableFuture;
import lombok.SneakyThrows;
import org.junit.jupiter.api.BeforeEach;
@@ -217,218 +215,4 @@ public void onError(Throwable error) {
"Output should contain the complete streamed response");
assertNotNull(choice.get("finish_reason"), "Output should have finish_reason");
}
-
- @Test
- @SneakyThrows
- void testToolWrapping() {
- // Create and wrap tools
- TestTools tools = new TestTools();
- TestTools wrappedTools = BraintrustLangchain.wrapTools(testHarness.openTelemetry(), tools);
-
- // Call wrapped tool method directly
- String result = wrappedTools.getWeather("Paris");
- assertNotNull(result);
- assertTrue(result.contains("Paris"));
-
- // Verify span was created
- var spans = testHarness.awaitExportedSpans();
- assertEquals(1, spans.size(), "Expected one span for tool execution");
- var span = spans.get(0);
-
- // Verify span name
- assertEquals("getWeather", span.getName());
-
- // Verify span type
- var attributes = span.getAttributes();
- String spanAttrsJson = attributes.get(AttributeKey.stringKey("braintrust.span_attributes"));
- JsonNode spanAttrs = JSON_MAPPER.readTree(spanAttrsJson);
- assertEquals("tool", spanAttrs.get("type").asText());
- assertEquals("getWeather", spanAttrs.get("name").asText());
-
- // Verify input (parameter names may be arg0, arg1, etc without -parameters flag)
- String inputJson = attributes.get(AttributeKey.stringKey("braintrust.input_json"));
- assertNotNull(inputJson);
- JsonNode input = JSON_MAPPER.readTree(inputJson);
- assertTrue(input.isObject(), "Input should be an object");
- assertTrue(input.size() > 0, "Input should have at least one parameter");
- // Check if parameter value is present (either as "location" or "arg0")
- String paramValue =
- input.has("location")
- ? input.get("location").asText()
- : input.elements().next().asText();
- assertEquals("Paris", paramValue);
-
- // Verify output
- String outputJson = attributes.get(AttributeKey.stringKey("braintrust.output_json"));
- assertNotNull(outputJson);
- JsonNode output = JSON_MAPPER.readTree(outputJson);
- assertTrue(output.asText().contains("Paris"));
- assertTrue(output.asText().contains("72"));
-
- // Verify metrics
- String metricsJson = attributes.get(AttributeKey.stringKey("braintrust.metrics"));
- JsonNode metrics = JSON_MAPPER.readTree(metricsJson);
- assertTrue(metrics.has("execution_time"));
- assertTrue(metrics.get("execution_time").asDouble() >= 0);
- }
-
- @Test
- @SneakyThrows
- void testToolWrappingWithException() {
- TestTools tools = new TestTools();
- TestTools wrappedTools = BraintrustLangchain.wrapTools(testHarness.openTelemetry(), tools);
-
- // Execute and expect exception
- assertThrows(
- RuntimeException.class,
- () -> {
- wrappedTools.throwError();
- });
-
- // Verify span with error status
- var spans = testHarness.awaitExportedSpans();
- assertEquals(1, spans.size());
- var span = spans.get(0);
-
- assertEquals(StatusCode.ERROR, span.getStatus().getStatusCode());
- assertTrue(
- span.getEvents().stream().anyMatch(e -> e.getName().equals("exception")),
- "Span should have exception event");
- }
-
- @Test
- @SneakyThrows
- void testToolWrappingWithMultipleCalls() {
- TestTools tools = new TestTools();
- TestTools wrappedTools = BraintrustLangchain.wrapTools(testHarness.openTelemetry(), tools);
-
- // Call multiple tool methods
- wrappedTools.getWeather("Tokyo");
- int sum = wrappedTools.calculateSum(5, 7);
- assertEquals(12, sum);
-
- // Verify two spans created
- var spans = testHarness.awaitExportedSpans();
- assertEquals(2, spans.size(), "Expected two spans");
-
- // Verify first span (getWeather)
- var weatherSpan = spans.get(0);
- assertEquals("getWeather", weatherSpan.getName());
-
- // Verify second span (calculateSum)
- var sumSpan = spans.get(1);
- assertEquals("calculateSum", sumSpan.getName());
- String sumOutput =
- sumSpan.getAttributes().get(AttributeKey.stringKey("braintrust.output_json"));
- assertEquals("12", sumOutput);
- }
-
- @Test
- @SneakyThrows
- void testToolWrappingAllBraintrustAttributesPresent() {
- // This test verifies ALL required Braintrust attributes are present
- // to catch issues that would cause UI problems
- TestTools tools = new TestTools();
- TestTools wrappedTools = BraintrustLangchain.wrapTools(testHarness.openTelemetry(), tools);
-
- wrappedTools.getWeather("London");
-
- var spans = testHarness.awaitExportedSpans();
- var span = spans.get(0);
- var attributes = span.getAttributes();
-
- // CRITICAL: These attributes MUST be present for Braintrust UI to display properly
- assertNotNull(
- attributes.get(AttributeKey.stringKey("braintrust.span_attributes")),
- "braintrust.span_attributes is required for UI");
- assertNotNull(
- attributes.get(AttributeKey.stringKey("braintrust.input_json")),
- "braintrust.input_json is required for UI to show inputs");
- assertNotNull(
- attributes.get(AttributeKey.stringKey("braintrust.output_json")),
- "braintrust.output_json is required for UI to show outputs");
- assertNotNull(
- attributes.get(AttributeKey.stringKey("braintrust.metrics")),
- "braintrust.metrics is required for UI to show metrics");
-
- // Verify span_attributes has correct structure
- String spanAttrsJson = attributes.get(AttributeKey.stringKey("braintrust.span_attributes"));
- JsonNode spanAttrs = JSON_MAPPER.readTree(spanAttrsJson);
- assertTrue(spanAttrs.has("type"), "span_attributes must have 'type' field");
- assertTrue(spanAttrs.has("name"), "span_attributes must have 'name' field");
- assertEquals("tool", spanAttrs.get("type").asText(), "Tool spans must have type='tool'");
- }
-
- @Test
- @SneakyThrows
- void testToolWrappingIntegrationWithConversationHierarchy() {
- // This test simulates a realistic usage pattern like the example
- // and verifies ALL spans in the hierarchy have input/output for UI
- var tracer = testHarness.openTelemetry().getTracer("test");
- TestTools tools = new TestTools();
- TestTools wrappedTools = BraintrustLangchain.wrapTools(testHarness.openTelemetry(), tools);
-
- // Create conversation span (like example does)
- var conversationSpan = tracer.spanBuilder("conversation").startSpan();
- conversationSpan.setAttribute(
- "braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"conversation\"}");
- conversationSpan.setAttribute(
- "braintrust.input_json", "{\"description\":\"test conversation\"}");
-
- try (var ignored = conversationSpan.makeCurrent()) {
- // Create turn span (like example does)
- var turnSpan = tracer.spanBuilder("turn_1").startSpan();
- turnSpan.setAttribute(
- "braintrust.span_attributes", "{\"type\":\"task\",\"name\":\"turn_1\"}");
- turnSpan.setAttribute("braintrust.input_json", "{\"user_message\":\"test query\"}");
-
- try (var turnScope = turnSpan.makeCurrent()) {
- // Call tool within turn (tool wrapper should create tool span)
- String result = wrappedTools.getWeather("Paris");
- turnSpan.setAttribute(
- "braintrust.output_json", "{\"assistant_message\":\"" + result + "\"}");
- } finally {
- turnSpan.end();
- }
- } finally {
- conversationSpan.setAttribute("braintrust.output_json", "{\"status\":\"completed\"}");
- conversationSpan.end();
- }
-
- // Verify all 3 spans exist and have required attributes
- var spans = testHarness.awaitExportedSpans();
- assertEquals(3, spans.size(), "Expected 3 spans: conversation, turn, and tool");
-
- // Find each span type
- var toolSpan =
- spans.stream().filter(s -> s.getName().equals("getWeather")).findFirst().get();
- var turnSpanData =
- spans.stream().filter(s -> s.getName().equals("turn_1")).findFirst().get();
- var convSpan =
- spans.stream().filter(s -> s.getName().equals("conversation")).findFirst().get();
-
- // CRITICAL: Every span must have input/output for UI to display properly
- for (var span : List.of(toolSpan, turnSpanData, convSpan)) {
- var attrs = span.getAttributes();
- assertNotNull(
- attrs.get(AttributeKey.stringKey("braintrust.span_attributes")),
- span.getName() + " missing braintrust.span_attributes");
- assertNotNull(
- attrs.get(AttributeKey.stringKey("braintrust.input_json")),
- span.getName() + " missing braintrust.input_json - UI won't show input!");
- assertNotNull(
- attrs.get(AttributeKey.stringKey("braintrust.output_json")),
- span.getName() + " missing braintrust.output_json - UI won't show output!");
- }
-
- // Verify span hierarchy (parent-child relationships)
- assertEquals(
- convSpan.getSpanId(),
- turnSpanData.getParentSpanId(),
- "Turn should be child of conversation");
- assertEquals(
- turnSpanData.getSpanId(),
- toolSpan.getParentSpanId(),
- "Tool should be child of turn");
- }
}
diff --git a/src/test/java/dev/braintrust/instrumentation/langchain/TestTools.java b/src/test/java/dev/braintrust/instrumentation/langchain/TestTools.java
deleted file mode 100644
index b289611..0000000
--- a/src/test/java/dev/braintrust/instrumentation/langchain/TestTools.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package dev.braintrust.instrumentation.langchain;
-
-import dev.langchain4j.agent.tool.Tool;
-
-public class TestTools {
-
- @Tool("Get weather for a location")
- public String getWeather(String location) {
- return String.format(
- "{\"location\":\"%s\",\"temperature\":72,\"condition\":\"sunny\"}", location);
- }
-
- @Tool("Calculate sum")
- public int calculateSum(int a, int b) {
- return a + b;
- }
-
- @Tool("Tool that throws exception")
- public String throwError() {
- throw new RuntimeException("Intentional error for testing");
- }
-}