diff --git a/README.md b/README.md index 0d33aaa..7bd307f 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,65 @@ 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 ad1b078..90d9c9f 100644 --- a/build.gradle +++ b/build.gradle @@ -95,6 +95,8 @@ 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 f4e9c20..553af25 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -172,3 +172,17 @@ 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 new file mode 100644 index 0000000..102880d --- /dev/null +++ b/examples/src/main/java/dev/braintrust/examples/LangchainToolWrappingExample.java @@ -0,0 +1,158 @@ +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: + * + *

+ */ +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 a3aafa8..9645f88 100644 --- a/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java +++ b/src/main/java/dev/braintrust/instrumentation/langchain/BraintrustLangchain.java @@ -49,6 +49,28 @@ 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 new file mode 100644 index 0000000..0c4ba25 --- /dev/null +++ b/src/main/java/dev/braintrust/instrumentation/langchain/ByteBuddyToolWrapper.java @@ -0,0 +1,119 @@ +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 e2555bd..67d0927 100644 --- a/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java +++ b/src/test/java/dev/braintrust/instrumentation/langchain/BraintrustLangchainTest.java @@ -13,6 +13,8 @@ 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; @@ -215,4 +217,218 @@ 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 new file mode 100644 index 0000000..b289611 --- /dev/null +++ b/src/test/java/dev/braintrust/instrumentation/langchain/TestTools.java @@ -0,0 +1,22 @@ +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"); + } +}