chore: refactor (#90)

* chore: refactor

* fix: lint
This commit is contained in:
ericdong-langchain
2026-02-20 10:13:38 -05:00
committed by GitHub
parent cff199b8fa
commit 1902c228dd
39 changed files with 2535 additions and 4351 deletions
+7 -22
View File
@@ -1,8 +1,8 @@
# LangSmith Java Examples
# LangSmith Examples
This module contains runnable examples organized by feature:
- **`otel/`** - OpenTelemetry tracing examples
- **`prompt/`** - Prompt management examples
This module contains runnable Kotlin examples organized by feature:
- **`example/`** - SDK examples (ListRuns, Dataset, PromptManagement, RecordExperiment, E2eEval)
- **`example/otel/`** - OpenTelemetry tracing examples
## Prerequisites
@@ -20,22 +20,7 @@ The `langchain.baseUrl` system property (or `LANGSMITH_ENDPOINT` environment var
## OpenTelemetry Tracing Examples
Located in `src/main/java/com/langchain/smith/example/otel/`
### Jaeger (Local)
Send traces to local Jaeger instance.
```bash
# Start Jaeger
docker run -d --name jaeger -p 4318:4318 -p 16686:16686 jaegertracing/all-in-one:latest
# Run example
./gradlew :langsmith-java-example:run -Pexample=OtelJaeger
# View traces
open http://localhost:16686
```
Located in `src/main/kotlin/com/langchain/smith/example/otel/`
### OpenAI + LangSmith (Real API Calls)
@@ -80,9 +65,9 @@ curl -X POST http://localhost:8080/api/chat \
curl "http://localhost:8080/api/analyze?text=This%20is%20great"
```
## Prompt Management Examples
## Prompt Management Example
Located in `src/main/java/com/langchain/smith/example/prompt/`
Located in `src/main/kotlin/com/langchain/smith/example/`
### Prompt Management (Getting Started)
+34 -31
View File
@@ -1,7 +1,7 @@
plugins {
id("langchain.java")
application
kotlin("jvm")
id("org.jetbrains.kotlin.plugin.spring") version "2.0.21"
id("org.springframework.boot") version "2.7.18" apply false
}
@@ -9,6 +9,12 @@ repositories {
mavenCentral()
}
// Align with Kotlin JVM target (Kotlin plugin applies Java plugin; keep targets consistent)
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
dependencies {
implementation(project(":langsmith-java"))
implementation(kotlin("stdlib"))
@@ -22,61 +28,58 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter")
}
tasks.withType<JavaCompile>().configureEach {
// Allow using more modern APIs, like `List.of` and `Map.of`, in examples.
options.release.set(9)
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_9)
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}
application {
// Use `./gradlew :langsmith-java-example:run` to run `Main`
// Use `./gradlew :langsmith-java-example:run -Pexample=Something` to run `SomethingExample`
// Require -Pexample=Name to run an example (e.g. -Pexample=ListRuns, -Pexample=OtelLangSmith)
mainClass = if (project.hasProperty("example")) {
val exampleName = project.property("example") as String
var exampleName = project.property("example") as String
val aliases = mapOf(
"OtelLangSmithSimple" to "OtelLangSmith",
"PromptManagmentExample" to "PromptManagement",
"PromptManagment" to "PromptManagement",
)
exampleName = aliases[exampleName] ?: exampleName
val baseName = if (exampleName.endsWith("Example")) exampleName else "${exampleName}Example"
// Search in multiple subdirectories: root, otel, prompt
val searchPaths = listOf(
"" to "com.langchain.smith.example",
"otel/" to "com.langchain.smith.example.otel",
"prompt/" to "com.langchain.smith.example.prompt"
"otel/" to "com.langchain.smith.example.otel"
)
var foundPackage = ""
var isKotlin = false
for ((subdir, packageName) in searchPaths) {
val javaFile = file("src/main/java/com/langchain/smith/example/${subdir}${baseName}.java")
val kotlinFile = file("src/main/kotlin/com/langchain/smith/example/${subdir}${baseName}.kt")
if (javaFile.exists()) {
if (kotlinFile.exists()) {
foundPackage = packageName
isKotlin = false
break
} else if (kotlinFile.exists()) {
foundPackage = packageName
isKotlin = true
break
}
}
if (foundPackage.isNotEmpty()) {
"${foundPackage}.${baseName}${if (isKotlin) "Kt" else ""}"
"${foundPackage}.${baseName}Kt"
} else {
// Default: assume Kotlin in root for backwards compatibility
"com.langchain.smith.example.${baseName}Kt"
throw GradleException(
"Example '$exampleName' not found. No ${baseName}.kt in " +
"src/main/kotlin/.../example/ or .../example/otel/. " +
"Use -Pexample=ListRuns, -Pexample=OtelLangSmith, -Pexample=OtelLangSmithSimple, -Pexample=OtelOpenAI, etc."
)
}
} else {
"Main"
"Main" // placeholder; run task doFirst will require -Pexample=
}
}
// Export stdin to examples for readln()
// Export stdin to examples for readln(); require -Pexample= when running (configuration-cache safe: no project access in doFirst)
tasks.named<JavaExec>("run") {
standardInput = System.`in`
doFirst {
if (mainClass.get() == "Main") {
throw GradleException(
"Example module requires -Pexample=ExampleName. " +
"e.g. ./gradlew :langsmith-java-example:run -Pexample=ListRuns"
)
}
}
}
@@ -1,117 +0,0 @@
package com.langchain.smith.example.otel;
import com.langchain.smith.otel.OtelSpanCreator;
import com.langchain.smith.otel.OtelTraceExporter;
import io.opentelemetry.api.common.AttributeKey;
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.time.Duration;
/**
* Example: Send live OpenTelemetry traces to Jaeger.
*
* Start Jaeger: docker run -d --name jaeger -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one:latest
* Run: ./gradlew :langsmith-java-example:run -Pexample=OtelJaegerExample
* View: http://localhost:16686
*/
public class OtelJaegerExample {
public static void main(String[] args) throws Exception {
System.out.println("=== LangSmith to Jaeger Example ===\n");
OtelTraceExporter exporter = OtelTraceExporter.builder()
.endpoint("http://localhost:4318/v1/traces")
.enabled(true)
.timeout(Duration.ofSeconds(10))
.serviceName("langsmith-java-example")
.build();
Tracer tracer = exporter.getTracer();
String projectName = exporter.getProjectName();
System.out.println("Creating waterfall trace with 5 spans...\n");
// ROOT SPAN: Main chain
Span rootSpan = OtelSpanCreator.createChainSpan(tracer, "langchain.chain", projectName, null);
try (Scope rootScope = rootSpan.makeCurrent()) {
System.out.println("→ Root span: langchain.chain started");
// CHILD 1: First LLM call
Span llmSpan1 = OtelSpanCreator.createLlmSpan(tracer, "openai.chat", "openai", "gpt-4", projectName, null);
try (Scope scope = llmSpan1.makeCurrent()) {
OtelSpanCreator.setInput(llmSpan1, "What's the weather?");
System.out.println(" → Child span 1: openai.chat started");
Thread.sleep(500);
OtelSpanCreator.setOutput(llmSpan1, "I'll check the weather for you.");
OtelSpanCreator.setTokenUsage(llmSpan1, 10, 8);
llmSpan1.setStatus(StatusCode.OK);
System.out.println(" ← Child span 1: openai.chat completed");
} finally {
llmSpan1.end();
}
// CHILD 2: Tool call
Span toolSpan = OtelSpanCreator.createToolSpan(tracer, "weather.tool", "get_weather", projectName, null);
try (Scope scope = toolSpan.makeCurrent()) {
System.out.println(" → Child span 2: weather.tool started");
Thread.sleep(300);
toolSpan.setStatus(StatusCode.OK);
System.out.println(" ← Child span 2: weather.tool completed");
} finally {
toolSpan.end();
}
// CHILD 3: Second LLM call with nested database query
Span llmSpan2 = OtelSpanCreator.createLlmSpan(tracer, "openai.chat", "openai", "gpt-4", projectName, null);
try (Scope scope2 = llmSpan2.makeCurrent()) {
OtelSpanCreator.setInput(llmSpan2, "Provide a detailed weather summary.");
System.out.println(" → Child span 3: openai.chat started");
// NESTED CHILD: Database query
Span dbSpan =
OtelSpanCreator.createToolSpan(tracer, "database.query", "postgresql_query", projectName, null);
try (Scope dbScope = dbSpan.makeCurrent()) {
dbSpan.setAttribute(AttributeKey.stringKey("db.system"), "postgresql");
OtelSpanCreator.setInput(dbSpan, "SELECT * FROM weather_data WHERE city='SF'");
System.out.println(" → Nested span: database.query started");
Thread.sleep(200);
// Simulate error
dbSpan.setStatus(StatusCode.ERROR, "Connection timeout");
dbSpan.setAttribute(AttributeKey.booleanKey("error"), true);
dbSpan.setAttribute(AttributeKey.stringKey("error.type"), "timeout");
System.out.println(" ← Nested span: database.query failed");
} finally {
dbSpan.end();
}
Thread.sleep(400);
OtelSpanCreator.setOutput(llmSpan2, "Unable to retrieve detailed data due to database error.");
OtelSpanCreator.setTokenUsage(llmSpan2, 20, 15);
llmSpan2.setStatus(StatusCode.OK);
System.out.println(" ← Child span 3: openai.chat completed");
} finally {
llmSpan2.end();
}
rootSpan.setStatus(StatusCode.OK);
System.out.println("← Root span: langchain.chain completed");
} finally {
rootSpan.end();
}
System.out.println("\nFlushing to Jaeger...");
exporter.flush().join(10, java.util.concurrent.TimeUnit.SECONDS);
Thread.sleep(6000);
exporter.shutdown().join(5, java.util.concurrent.TimeUnit.SECONDS);
System.out.println("\n✓ Complete! View at: http://localhost:16686");
}
}
@@ -1,161 +0,0 @@
package com.langchain.smith.example.otel;
import com.langchain.smith.otel.OtelConfig;
import com.langchain.smith.otel.OtelSpanCreator;
import com.langchain.smith.otel.OtelTraceExporter;
import io.opentelemetry.api.common.AttributeKey;
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.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Example: Send OpenTelemetry traces to LangSmith UI.
*
* This is a mock/demo example that simulates LLM calls without requiring API keys.
* It demonstrates the tracing structure and waterfall visualization.
*
* Usage:
* export LANGSMITH_API_KEY=your_api_key
* ./gradlew :langsmith-java-example:run -Pexample=OtelLangSmith
*/
public class OtelLangSmithExample {
public static void main(String[] args) throws Exception {
System.out.println("=== LangSmith OpenTelemetry Example ===\n");
// Get LangSmith API key
String apiKey = System.getenv("LANGSMITH_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
apiKey = System.getProperty("langsmith.api.key");
}
if (apiKey == null || apiKey.isEmpty()) {
System.err.println(
"ERROR: LANGSMITH_API_KEY environment variable or langsmith.api.key system property is required!");
return;
}
String projectName = System.getenv("LANGSMITH_PROJECT");
if (projectName == null || projectName.isEmpty()) {
projectName = System.getProperty("langsmith.project.name", "default");
}
System.out.println("Configuration:");
System.out.println(" Endpoint: https://api.smith.langchain.com/otel/v1/traces");
System.out.println(" Project: " + projectName);
System.out.println(" Service name: langsmith-java");
System.out.println();
// Configure the exporter for LangSmith
Map<String, String> headers = new HashMap<>();
headers.put("x-api-key", apiKey);
headers.put("Langsmith-Project", projectName);
OtelConfig config = OtelConfig.builder()
.enabled(true)
.endpoint("https://api.smith.langchain.com/otel/v1/traces")
.headers(headers)
.timeout(Duration.ofSeconds(30))
.serviceName("langsmith-java")
.build();
OtelTraceExporter exporter = OtelTraceExporter.fromConfig(config);
Tracer tracer = exporter.getTracer();
// Create a session ID for grouping
String sessionId = UUID.randomUUID().toString();
System.out.println("Creating waterfall with 5 spans:");
System.out.println(" 1. agent.chain (root, 2s)");
System.out.println(" ├─ 2. openai.llm (500ms)");
System.out.println(" ├─ 3. weather.tool (300ms)");
System.out.println(" └─ 4. openai.llm (600ms)");
System.out.println(" └─ 5. database.retriever (200ms)\n");
// ROOT SPAN: Main agent chain
String initialPrompt = "What's the weather in San Francisco?";
Span rootSpan = OtelSpanCreator.createChainSpan(tracer, "langsmith.java.example", projectName, sessionId);
try (Scope rootScope = rootSpan.makeCurrent()) {
// Set input on root span
OtelSpanCreator.setInput(rootSpan, initialPrompt);
// CHILD 1: First LLM call
Span llmSpan1 =
OtelSpanCreator.createLlmSpan(tracer, "openai.llm.call", "openai", "gpt-4", projectName, sessionId);
try (Scope llmScope1 = llmSpan1.makeCurrent()) {
OtelSpanCreator.setInput(llmSpan1, "What's the weather in San Francisco?");
Thread.sleep(500);
OtelSpanCreator.setOutput(llmSpan1, "Let me check the weather for you.");
OtelSpanCreator.setTokenUsage(llmSpan1, 15, 12);
llmSpan1.setStatus(StatusCode.OK);
} finally {
llmSpan1.end();
}
// CHILD 2: Tool call
String toolInput = "{\"location\":\"San Francisco\"}";
String toolOutput = "{\"temperature\":\"72°F\",\"condition\":\"Sunny\",\"humidity\":\"65%\"}";
Span toolSpan =
OtelSpanCreator.createToolSpan(tracer, "weather.tool", "get_weather", projectName, sessionId);
try (Scope toolScope = toolSpan.makeCurrent()) {
// Set tool input using gen_ai.prompt
OtelSpanCreator.setInput(toolSpan, toolInput);
// Set tool arguments attribute
toolSpan.setAttribute(AttributeKey.stringKey("gen_ai.tool.arguments"), toolInput);
Thread.sleep(300);
// Set tool output using gen_ai.completion
OtelSpanCreator.setOutput(toolSpan, toolOutput);
toolSpan.setStatus(StatusCode.OK);
} finally {
toolSpan.end();
}
// CHILD 3: Second LLM call with nested retriever
Span llmSpan2 = OtelSpanCreator.createLlmSpan(
tracer, "openai.llm.final", "openai", "gpt-4", projectName, sessionId);
try (Scope llmScope2 = llmSpan2.makeCurrent()) {
OtelSpanCreator.setInput(llmSpan2, "Based on the weather data, provide a summary.");
// NESTED CHILD: Retriever call inside LLM
Span retrieverSpan =
OtelSpanCreator.createRetrievalSpan(tracer, "database.retriever", projectName, sessionId);
try (Scope retrieverScope = retrieverSpan.makeCurrent()) {
OtelSpanCreator.setInput(retrieverSpan, "weather forecast data");
Thread.sleep(200);
OtelSpanCreator.setOutput(retrieverSpan, "Temperature: 72F, Sunny");
retrieverSpan.setStatus(StatusCode.OK);
} finally {
retrieverSpan.end();
}
Thread.sleep(400);
OtelSpanCreator.setOutput(
llmSpan2, "The weather in San Francisco is sunny with a temperature of 72°F.");
OtelSpanCreator.setTokenUsage(llmSpan2, 25, 18);
llmSpan2.setStatus(StatusCode.OK);
} finally {
llmSpan2.end();
}
// Set output on root span
String finalOutput = "The weather in San Francisco is sunny with a temperature of 72°F.";
OtelSpanCreator.setOutput(rootSpan, finalOutput);
rootSpan.setStatus(StatusCode.OK);
} finally {
rootSpan.end();
}
System.out.println("\nAll spans ended. Flushing to LangSmith...");
// Force flush to send the span immediately
exporter.flush().join(10, java.util.concurrent.TimeUnit.SECONDS);
// Wait for batch to be sent
Thread.sleep(6000);
exporter.shutdown().join(5, java.util.concurrent.TimeUnit.SECONDS);
}
}
@@ -1,325 +0,0 @@
package com.langchain.smith.example.otel;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig;
import com.langchain.smith.wrappers.openai.WrappedOpenAIClient;
import com.openai.core.JsonValue;
import com.openai.models.ChatModel;
import com.openai.models.FunctionDefinition;
import com.openai.models.FunctionParameters;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionFunctionTool;
import com.openai.models.chat.completions.ChatCompletionMessage;
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
import com.openai.models.chat.completions.ChatCompletionMessageParam;
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
import com.openai.models.chat.completions.ChatCompletionTool;
import com.openai.models.chat.completions.ChatCompletionToolChoiceOption;
import com.openai.models.chat.completions.ChatCompletionToolMessageParam;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Example: Make real OpenAI API calls with OpenTelemetry tracing to LangSmith.
*
* <p>This example demonstrates:
* <ul>
* <li>Configuring OpenTelemetry to send traces to LangSmith</li>
* <li>Using the wrapped OpenAI client for automatic tracing</li>
* <li>Making actual API calls to OpenAI with tool definitions</li>
* <li>Automatic tool call span creation</li>
* <li>Multi-turn conversations with tool execution</li>
* <li>Viewing rich traces in the LangSmith dashboard</li>
* </ul>
*
* <p>Usage:
* <pre>
* export OPENAI_API_KEY=your_openai_api_key
* export LANGSMITH_API_KEY=your_langsmith_api_key
* export LANGSMITH_PROJECT=your_project_name
* ./gradlew :langsmith-java-example:run -Pexample=OtelOpenAI
* </pre>
*/
public class OtelOpenAIExample {
private static final String SEPARATOR = "============================================================";
public static void main(String[] args) {
System.out.println("=== OpenAI + LangSmith OpenTelemetry Example ===\n");
// Check for required environment variables
String openaiKey = System.getenv("OPENAI_API_KEY");
if (openaiKey == null || openaiKey.isEmpty()) {
System.err.println("ERROR: OPENAI_API_KEY environment variable is required!");
System.err.println("Get your API key from: https://platform.openai.com/api-keys");
return;
}
String langsmithKey = System.getenv("LANGSMITH_API_KEY");
if (langsmithKey == null || langsmithKey.isEmpty()) {
System.err.println("ERROR: LANGSMITH_API_KEY environment variable is required!");
System.err.println("Get your API key from: https://smith.langchain.com/settings");
return;
}
String projectName = System.getenv("LANGSMITH_PROJECT");
if (projectName == null || projectName.isEmpty()) {
projectName = "default";
}
System.out.println("Configuration:");
System.out.println(" LangSmith Project: " + projectName);
System.out.println(" Service Name: langsmith-java-openai-example");
System.out.println();
// Configure OpenTelemetry to send traces to LangSmith
// Using SIMPLE processor for immediate export (best for short-lived examples)
try {
OpenTelemetryConfig.builder()
.apiKey(langsmithKey)
.projectName(projectName)
.serviceName("langsmith-java-openai-example")
.processorType(OpenTelemetryConfig.SpanProcessorType.SIMPLE)
.maxBatchSize(1)
.build();
System.out.println("✓ OpenTelemetry configured for LangSmith\n");
} catch (Exception e) {
System.err.println("✗ Failed to configure OpenTelemetry: " + e.getMessage());
e.printStackTrace();
return;
}
// Create wrapped OpenAI client - all calls will automatically be traced
WrappedOpenAIClient client = WrappedOpenAIClient.fromEnv();
// Create a parent span to wrap the workflow
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
Tracer tracer = openTelemetry.getTracer("langsmith-java-openai-example");
Span workflowSpan = tracer.spanBuilder("openai_agent_workflow")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("gen_ai.operation.name", "agent_workflow")
.setAttribute("langsmith.span.kind", "chain")
.setAttribute("langsmith.trace.name", "OpenAI Agent with Tools")
.startSpan();
try (Scope scope = workflowSpan.makeCurrent()) {
System.out.println(SEPARATOR);
System.out.println("Agent Workflow: Chat with Tool Calls");
System.out.println(SEPARATOR);
// Build tool (function) definition for weather API
Map<String, JsonValue> properties = new HashMap<>();
Map<String, JsonValue> locationProperty = new HashMap<>();
locationProperty.put("type", JsonValue.from("string"));
locationProperty.put("description", JsonValue.from("The city and state, e.g., San Francisco, CA"));
properties.put("location", JsonValue.from(locationProperty));
Map<String, JsonValue> parametersJson = new HashMap<>();
parametersJson.put("type", JsonValue.from("object"));
parametersJson.put("properties", JsonValue.from(properties));
parametersJson.put("required", JsonValue.from(Arrays.asList("location")));
// Create initial request with tool definitions
String initialUserMessage = "What is the capital of France and what's the current weather there?";
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage(initialUserMessage)
.tools(Arrays.asList(ChatCompletionTool.ofFunction(ChatCompletionFunctionTool.builder()
.function(FunctionDefinition.builder()
.name("get_weather")
.description("Get the current weather for a given location")
.parameters(FunctionParameters.builder()
.putAllAdditionalProperties(parametersJson)
.build())
.build())
.build())))
.toolChoice(
ChatCompletionToolChoiceOption.Companion.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO))
.build();
// Set input on workflow span
workflowSpan.setAttribute(
io.opentelemetry.api.common.AttributeKey.stringKey("gen_ai.prompt"), initialUserMessage);
System.out.println("\n1. Making initial API call with tool definitions...");
ChatCompletion completion = client.chat().completions().create(params);
// Check if the response contains tool calls
ChatCompletionMessage message = completion.choices().get(0).message();
java.util.Optional<List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
String finalContent;
if (toolCallsOpt.isPresent() && !toolCallsOpt.get().isEmpty()) {
System.out.println(" ✓ Tool calls detected in response!");
List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
// Build messages list for follow-up request
List<ChatCompletionMessageParam> messages = new ArrayList<>();
messages.add(params.messages().get(0)); // Original user message
// Add assistant message with tool calls
messages.add(ChatCompletionMessageParam.ofAssistant(ChatCompletionAssistantMessageParam.builder()
.content(message.content().orElse(""))
.toolCalls(toolCalls)
.build()));
// Execute each tool call
System.out.println("\n2. Executing tool calls...");
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
if (toolCall.isFunction()) {
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
String toolName = functionToolCall.function().name();
String toolArguments = functionToolCall.function().arguments();
String toolCallId = functionToolCall.id();
System.out.println(" - Tool: " + toolName + " | Args: " + toolArguments);
// Create a tool execution span to capture the tool execution and result
Span toolExecutionSpan = tracer.spanBuilder("tool_execution " + toolName)
.setSpanKind(SpanKind.INTERNAL)
.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "tool")
.setAttribute(AttributeKey.stringKey("gen_ai.tool.name"), toolName)
.setAttribute(AttributeKey.stringKey("gen_ai.tool.call.id"), toolCallId)
.setAttribute(AttributeKey.stringKey("gen_ai.tool.arguments"), toolArguments)
.setAttribute(AttributeKey.stringKey("langsmith.span.kind"), "tool")
.setAttribute(AttributeKey.stringKey("gen_ai.prompt"), toolArguments)
.startSpan();
String toolResult;
try (Scope toolExecutionScope = toolExecutionSpan.makeCurrent()) {
// Execute the tool (simulated weather API)
toolResult = executeTool(toolName, toolArguments);
System.out.println(" - Result: " + toolResult);
// Set tool execution result as output
toolExecutionSpan.setAttribute(AttributeKey.stringKey("gen_ai.completion"), toolResult);
toolExecutionSpan.setStatus(StatusCode.OK);
} catch (Exception e) {
toolExecutionSpan.recordException(e);
toolExecutionSpan.setStatus(StatusCode.ERROR);
toolResult = "{\"error\": \"" + e.getMessage() + "\"}";
} finally {
toolExecutionSpan.end();
}
// Add tool result message
messages.add(ChatCompletionMessageParam.ofTool(ChatCompletionToolMessageParam.builder()
.toolCallId(functionToolCall.id())
.content(toolResult)
.build()));
}
}
// Send follow-up request with tool results
System.out.println("\n3. Sending follow-up request with tool results...");
ChatCompletionCreateParams followUpParams = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.messages(messages)
.build();
completion = client.chat().completions().create(followUpParams);
finalContent = completion.choices().get(0).message().content().orElse("No content");
} else {
finalContent = message.content().orElse("No content");
}
// Display final response
System.out.println("\n" + SEPARATOR);
System.out.println("Final Response:");
System.out.println(finalContent);
System.out.println(SEPARATOR);
// Display token usage
completion.usage().ifPresent(usage -> {
System.out.println("\nTotal Token Usage:");
System.out.println(" Input: " + usage.promptTokens());
System.out.println(" Output: " + usage.completionTokens());
System.out.println(" Total: " + usage.totalTokens());
});
// Set output on workflow span
workflowSpan.setAttribute(
io.opentelemetry.api.common.AttributeKey.stringKey("gen_ai.completion"), finalContent);
workflowSpan.setAttribute("response.content", finalContent);
workflowSpan.setStatus(io.opentelemetry.api.trace.StatusCode.OK);
} catch (Exception e) {
System.err.println("\n✗ Error during API call: " + e.getMessage());
e.printStackTrace();
workflowSpan.recordException(e);
workflowSpan.setStatus(io.opentelemetry.api.trace.StatusCode.ERROR);
} finally {
workflowSpan.end();
}
// Close the client
client.close();
// Flush traces to ensure they're sent to LangSmith
System.out.println("\n" + SEPARATOR);
System.out.println("Flushing traces to LangSmith...");
boolean flushed = OpenTelemetryConfig.flush(10, java.util.concurrent.TimeUnit.SECONDS);
if (flushed) {
System.out.println("✓ Traces sent successfully!");
System.out.println("\nView your traces at:");
System.out.println(" https://smith.langchain.com/projects/" + projectName);
} else {
System.err.println("✗ Warning: Flush may not have completed successfully");
}
System.out.println(SEPARATOR);
System.out.println("\nNote: Check the trace waterfall in LangSmith UI to see:");
System.out.println(" - Parent workflow span (chain)");
System.out.println(" - Child LLM spans (automatically created)");
System.out.println(" - Tool call spans (automatically created by wrapper)");
}
/**
* Simulates executing a tool based on its name and arguments.
*
* @param toolName the name of the tool to execute
* @param arguments JSON string containing the tool arguments
* @return JSON string containing the tool result
*/
private static String executeTool(String toolName, String arguments) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode args = mapper.readTree(arguments);
if ("get_weather".equals(toolName)) {
String location = args.has("location") ? args.get("location").asText() : "unknown";
// Simulate weather API call
Map<String, Object> result = new HashMap<>();
result.put("location", location);
result.put("temperature", "18°C");
result.put("condition", "Partly Cloudy");
result.put("humidity", "65%");
result.put("wind", "15 km/h");
return mapper.writeValueAsString(result);
} else {
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("error", "Unknown tool: " + toolName);
return mapper.writeValueAsString(errorMap);
}
} catch (Exception e) {
return "{\"error\": \"" + e.getMessage() + "\"}";
}
}
}
@@ -1,51 +0,0 @@
package com.langchain.smith.example.otel;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot example: Send OpenTelemetry traces to LangSmith.
*
* Usage:
* export LANGSMITH_API_KEY=your_api_key
* export LANGSMITH_PROJECT=my-project # optional, defaults to "default"
* ./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith
*
* Then make requests to:
* http://localhost:8080/api/chat
* http://localhost:8080/api/analyze?text=hello
*/
@SpringBootApplication
public class SpringBootLangSmithExample {
public static void main(String[] args) {
System.out.println("=== Spring Boot + LangSmith OpenTelemetry Example ===\n");
// Check required environment variables
String apiKey = System.getenv("LANGSMITH_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("ERROR: LANGSMITH_API_KEY environment variable is required!");
System.err.println("\nUsage:");
System.err.println(" export LANGSMITH_API_KEY=your_api_key_here");
System.err.println(" export LANGSMITH_PROJECT=my-project # optional");
System.err.println(" ./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith");
System.exit(1);
}
String projectName = System.getenv("LANGSMITH_PROJECT");
if (projectName == null || projectName.isEmpty()) {
projectName = "default";
}
System.out.println("Configuration:");
System.out.println(" Project: " + projectName);
System.out.println(" Endpoint: https://api.smith.langchain.com/otel/v1/traces");
System.out.println("\nStarting Spring Boot application...");
System.out.println("Try these endpoints:");
System.out.println(" POST http://localhost:8080/api/chat");
System.out.println(" GET http://localhost:8080/api/analyze?text=hello");
System.out.println();
SpringApplication.run(SpringBootLangSmithExample.class, args);
}
}
@@ -1,45 +0,0 @@
package com.langchain.smith.example.otel.config;
import com.langchain.smith.otel.OtelConfig;
import com.langchain.smith.otel.OtelTraceExporter;
import io.opentelemetry.api.trace.Tracer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Spring configuration for OpenTelemetry integration with LangSmith.
*/
@Configuration
public class OtelConfiguration {
@Bean
public OtelTraceExporter otelTraceExporter() {
String apiKey = System.getenv("LANGSMITH_API_KEY");
String projectName = System.getenv("LANGSMITH_PROJECT");
if (projectName == null || projectName.isEmpty()) {
projectName = "default";
}
Map<String, String> headers = new HashMap<>();
headers.put("x-api-key", apiKey);
headers.put("Langsmith-Project", projectName);
OtelConfig config = OtelConfig.builder()
.enabled(true)
.endpoint("https://api.smith.langchain.com/otel/v1/traces")
.headers(headers)
.timeout(Duration.ofSeconds(30))
.serviceName("spring-boot-langsmith")
.build();
return OtelTraceExporter.fromConfig(config);
}
@Bean
public Tracer tracer(OtelTraceExporter exporter) {
return exporter.getTracer();
}
}
@@ -1,31 +0,0 @@
package com.langchain.smith.example.otel.config;
import com.langchain.smith.otel.OtelTraceExporter;
import javax.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Ensures OpenTelemetry traces are flushed on application shutdown.
*/
@Component
public class OtelShutdownHook {
private final OtelTraceExporter exporter;
@Autowired
public OtelShutdownHook(OtelTraceExporter exporter) {
this.exporter = exporter;
}
@PreDestroy
public void onShutdown() {
System.out.println("\n→ Flushing OpenTelemetry traces...");
try {
exporter.flush().join(10000, java.util.concurrent.TimeUnit.MILLISECONDS);
System.out.println("✓ Traces flushed successfully");
} catch (Exception e) {
System.err.println("✗ Failed to flush traces: " + e.getMessage());
}
}
}
@@ -1,105 +0,0 @@
package com.langchain.smith.example.otel.controller;
import com.langchain.smith.example.otel.service.LlmService;
import com.langchain.smith.otel.OtelSpanCreator;
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.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* REST controller demonstrating OpenTelemetry tracing with LangSmith.
*/
@RestController
@RequestMapping("/api")
public class ChatController {
private final Tracer tracer;
private final LlmService llmService;
@Autowired
public ChatController(Tracer tracer, LlmService llmService) {
this.tracer = tracer;
this.llmService = llmService;
}
@PostMapping("/chat")
public Map<String, Object> chat(@RequestBody Map<String, String> request) {
String userMessage = request.getOrDefault("message", "Hello!");
// Create a root span for the entire request
Span rootSpan = OtelSpanCreator.createChainSpan(tracer, "chat_request", "spring-boot-langsmith", null);
try (Scope scope = rootSpan.makeCurrent()) {
OtelSpanCreator.setInput(rootSpan, userMessage);
System.out.println("→ Processing chat request: " + userMessage);
// Call the LLM service (which creates its own span)
String response = llmService.generateResponse(userMessage);
OtelSpanCreator.setOutput(rootSpan, response);
rootSpan.setStatus(StatusCode.OK);
System.out.println("← Chat response generated");
return Map.of(
"request",
userMessage,
"response",
response,
"model",
"gpt-4",
"trace_id",
rootSpan.getSpanContext().getTraceId());
} catch (Exception e) {
rootSpan.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
rootSpan.end();
}
}
@GetMapping("/analyze")
public Map<String, Object> analyze(@RequestParam String text) {
// Create a span for the analysis operation
Span analysisSpan = OtelSpanCreator.createChainSpan(tracer, "text_analysis", "spring-boot-langsmith", null);
try (Scope scope = analysisSpan.makeCurrent()) {
OtelSpanCreator.setInput(analysisSpan, text);
System.out.println("→ Analyzing text: " + text);
// Simulate analysis with nested operations
int wordCount = text.split("\\s+").length;
String sentiment = llmService.analyzeSentiment(text);
String result = String.format("Word count: %d, Sentiment: %s", wordCount, sentiment);
OtelSpanCreator.setOutput(analysisSpan, result);
analysisSpan.setStatus(StatusCode.OK);
System.out.println("← Analysis complete");
return Map.of(
"text", text,
"word_count", wordCount,
"sentiment", sentiment,
"trace_id", analysisSpan.getSpanContext().getTraceId());
} catch (Exception e) {
analysisSpan.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
analysisSpan.end();
}
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "healthy", "service", "spring-boot-langsmith");
}
}
@@ -1,97 +0,0 @@
package com.langchain.smith.example.otel.service;
import com.langchain.smith.otel.OtelSpanCreator;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* Service layer demonstrating nested OpenTelemetry spans.
*/
@Service
public class LlmService {
private final Tracer tracer;
@Autowired
public LlmService(Tracer tracer) {
this.tracer = tracer;
}
/**
* Simulates an LLM API call with tracing.
*/
public String generateResponse(String input) {
Span llmSpan =
OtelSpanCreator.createLlmSpan(tracer, "openai.chat", "openai", "gpt-4", "spring-boot-langsmith", null);
try (Scope scope = llmSpan.makeCurrent()) {
OtelSpanCreator.setInput(llmSpan, input);
System.out.println(" → Calling OpenAI API...");
// Simulate LLM processing time
Thread.sleep(500);
String response = "I received your message: '" + input + "'. How can I help you today?";
OtelSpanCreator.setOutput(llmSpan, response);
OtelSpanCreator.setTokenUsage(llmSpan, 15, 20);
llmSpan.setStatus(StatusCode.OK);
System.out.println(" ← OpenAI API response received");
return response;
} catch (Exception e) {
llmSpan.setStatus(StatusCode.ERROR, e.getMessage());
throw new RuntimeException("LLM call failed", e);
} finally {
llmSpan.end();
}
}
/**
* Simulates sentiment analysis with tracing.
*/
public String analyzeSentiment(String text) {
Span sentimentSpan = OtelSpanCreator.createLlmSpan(
tracer, "sentiment_analysis", "openai", "gpt-4", "spring-boot-langsmith", null);
try (Scope scope = sentimentSpan.makeCurrent()) {
OtelSpanCreator.setInput(sentimentSpan, text);
System.out.println(" → Analyzing sentiment...");
// Simulate analysis time
Thread.sleep(300);
// Simple sentiment detection
String sentiment;
if (text.toLowerCase().contains("good") || text.toLowerCase().contains("great")) {
sentiment = "positive";
} else if (text.toLowerCase().contains("bad") || text.toLowerCase().contains("terrible")) {
sentiment = "negative";
} else {
sentiment = "neutral";
}
OtelSpanCreator.setOutput(sentimentSpan, sentiment);
OtelSpanCreator.setTokenUsage(sentimentSpan, 8, 2);
sentimentSpan.setStatus(StatusCode.OK);
System.out.println(" ← Sentiment: " + sentiment);
return sentiment;
} catch (Exception e) {
sentimentSpan.setStatus(StatusCode.ERROR, e.getMessage());
throw new RuntimeException("Sentiment analysis failed", e);
} finally {
sentimentSpan.end();
}
}
}
@@ -1,6 +1,8 @@
package com.langchain.smith.example
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.example.util.buildDatasetUrl
import com.langchain.smith.example.util.generateExampleId
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.datasets.DatasetCreateParams
@@ -86,7 +88,7 @@ fun main() {
// Configure LangSmith client first (needed to create session)
val langsmithClient: LangsmithClient = LangsmithOkHttpClient.fromEnv()
val datasetName = "Q&A Evaluation Dataset - Java Example"
val datasetName = "Q&A Evaluation Dataset - Kotlin Example"
val experimentName = "E2eEvalExample-${OffsetDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss"))}"
// Define test cases with questions and expected answers
@@ -11,7 +11,7 @@ import com.langchain.smith.models.repos.RepoListParams
import com.langchain.smith.models.repos.RepoWithLookups
/**
* Demonstrates how to manage prompts programmatically using the LangSmith Java
* Demonstrates how to manage prompts programmatically using the LangSmith
* SDK.
*
* This example shows:
@@ -369,4 +369,3 @@ private fun extractPromptContent(manifestJson: JsonValue): String {
private fun getOwnerFromEnv(): String =
System.getenv("LANGSMITH_OWNER")?.takeIf { it.isNotEmpty() } ?: "-"
@@ -1,6 +1,9 @@
package com.langchain.smith.example
import com.langchain.smith.client.LangsmithClient
import com.langchain.smith.example.util.buildDatasetUrl
import com.langchain.smith.example.util.buildSessionUrl
import com.langchain.smith.example.util.generateExampleId
import com.langchain.smith.client.okhttp.LangsmithOkHttpClient
import com.langchain.smith.core.JsonValue
import com.langchain.smith.models.datasets.Dataset
@@ -44,7 +47,7 @@ fun main() {
// Configure client from environment variables
val client: LangsmithClient = LangsmithOkHttpClient.fromEnv()
val datasetName = "Experiment Dataset - Java Example"
val datasetName = "Experiment Dataset - Kotlin Example"
val experimentName = "My First Experiment - ${OffsetDateTime.now()}"
println("=".repeat(60))
@@ -0,0 +1,160 @@
package com.langchain.smith.example.otel
import com.langchain.smith.otel.OtelConfig
import com.langchain.smith.otel.OtelSpanCreator
import com.langchain.smith.otel.OtelTraceExporter
import io.opentelemetry.api.common.AttributeKey
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.time.Duration
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
/**
* Example: Send OpenTelemetry traces to LangSmith UI.
*
* Mock/demo example that simulates LLM calls without requiring API keys.
* Demonstrates the tracing structure and waterfall visualization.
*
* Usage:
* export LANGSMITH_API_KEY=your_api_key
* ./gradlew :langsmith-java-example:run -Pexample=OtelLangSmith
*/
fun main() {
println("=== LangSmith OpenTelemetry Example ===\n")
var apiKey = System.getenv("LANGSMITH_API_KEY")
if (apiKey.isNullOrEmpty()) {
apiKey = System.getProperty("langsmith.api.key")
}
if (apiKey.isNullOrEmpty()) {
System.err.println(
"ERROR: LANGSMITH_API_KEY environment variable or langsmith.api.key system property is required!"
)
exitProcess(1)
}
var projectName = System.getenv("LANGSMITH_PROJECT")
if (projectName.isNullOrEmpty()) {
projectName = System.getProperty("langsmith.project.name", "default")
}
println("Configuration:")
println(" Endpoint: https://api.smith.langchain.com/otel/v1/traces")
println(" Project: $projectName")
println(" Service name: langsmith-kotlin")
println()
val headers = mapOf(
"x-api-key" to apiKey,
"Langsmith-Project" to projectName
)
val config = OtelConfig.builder()
.enabled(true)
.endpoint("https://api.smith.langchain.com/otel/v1/traces")
.headers(headers)
.timeout(Duration.ofSeconds(30))
.serviceName("langsmith-kotlin")
.build()
val exporter = OtelTraceExporter.fromConfig(config)
val tracer = exporter.tracer
val sessionId = UUID.randomUUID().toString()
println("Creating waterfall with 5 spans:")
println(" 1. agent.chain (root, 2s)")
println(" ├─ 2. openai.llm (500ms)")
println(" ├─ 3. weather.tool (300ms)")
println(" └─ 4. openai.llm (600ms)")
println(" └─ 5. database.retriever (200ms)\n")
val initialPrompt = "What's the weather in San Francisco?"
val rootSpan = OtelSpanCreator.createChainSpan(tracer, "langsmith.kotlin.example", projectName, sessionId)
try {
rootSpan.makeCurrent().use {
OtelSpanCreator.setInput(rootSpan, initialPrompt)
val llmSpan1 = OtelSpanCreator.createLlmSpan(
tracer, "openai.llm.call", "openai", "gpt-4", projectName, sessionId
)
try {
llmSpan1.makeCurrent().use {
OtelSpanCreator.setInput(llmSpan1, "What's the weather in San Francisco?")
Thread.sleep(500)
OtelSpanCreator.setOutput(llmSpan1, "Let me check the weather for you.")
OtelSpanCreator.setTokenUsage(llmSpan1, 15, 12)
llmSpan1.setStatus(StatusCode.OK)
}
} finally {
llmSpan1.end()
}
val toolInput = "{\"location\":\"San Francisco\"}"
val toolOutput = "{\"temperature\":\"72°F\",\"condition\":\"Sunny\",\"humidity\":\"65%\"}"
val toolSpan = OtelSpanCreator.createToolSpan(
tracer, "weather.tool", "get_weather", projectName, sessionId
)
try {
toolSpan.makeCurrent().use {
OtelSpanCreator.setInput(toolSpan, toolInput)
toolSpan.setAttribute(AttributeKey.stringKey("gen_ai.tool.arguments"), toolInput)
Thread.sleep(300)
OtelSpanCreator.setOutput(toolSpan, toolOutput)
toolSpan.setStatus(StatusCode.OK)
}
} finally {
toolSpan.end()
}
val llmSpan2 = OtelSpanCreator.createLlmSpan(
tracer, "openai.llm.final", "openai", "gpt-4", projectName, sessionId
)
try {
llmSpan2.makeCurrent().use {
OtelSpanCreator.setInput(llmSpan2, "Based on the weather data, provide a summary.")
val retrieverSpan = OtelSpanCreator.createRetrievalSpan(
tracer, "database.retriever", projectName, sessionId
)
try {
retrieverSpan.makeCurrent().use {
OtelSpanCreator.setInput(retrieverSpan, "weather forecast data")
Thread.sleep(200)
OtelSpanCreator.setOutput(retrieverSpan, "Temperature: 72F, Sunny")
retrieverSpan.setStatus(StatusCode.OK)
}
} finally {
retrieverSpan.end()
}
Thread.sleep(400)
OtelSpanCreator.setOutput(
llmSpan2,
"The weather in San Francisco is sunny with a temperature of 72°F."
)
OtelSpanCreator.setTokenUsage(llmSpan2, 25, 18)
llmSpan2.setStatus(StatusCode.OK)
}
} finally {
llmSpan2.end()
}
val finalOutput = "The weather in San Francisco is sunny with a temperature of 72°F."
OtelSpanCreator.setOutput(rootSpan, finalOutput)
rootSpan.setStatus(StatusCode.OK)
}
} finally {
rootSpan.end()
}
println("\nAll spans ended. Flushing to LangSmith...")
exporter.flush().join(10, TimeUnit.SECONDS)
Thread.sleep(6000)
exporter.shutdown().join(5, TimeUnit.SECONDS)
}
@@ -0,0 +1,285 @@
package com.langchain.smith.example.otel
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig
import com.langchain.smith.wrappers.openai.WrappedOpenAIClient
import com.openai.models.ChatModel
import com.openai.models.FunctionDefinition
import com.openai.models.FunctionParameters
import com.openai.models.chat.completions.ChatCompletionAssistantMessageParam
import com.openai.models.chat.completions.ChatCompletionCreateParams
import com.openai.models.chat.completions.ChatCompletionFunctionTool
import com.openai.models.chat.completions.ChatCompletionMessageParam
import com.openai.models.chat.completions.ChatCompletionMessageToolCall
import com.openai.models.chat.completions.ChatCompletionTool
import com.openai.models.chat.completions.ChatCompletionToolChoiceOption
import com.openai.models.chat.completions.ChatCompletionToolMessageParam
import io.opentelemetry.api.OpenTelemetry
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.context.Scope
import java.util.concurrent.TimeUnit
import kotlin.system.exitProcess
/**
* Example: Make real OpenAI API calls with OpenTelemetry tracing to LangSmith.
*
* Demonstrates:
* - Configuring OpenTelemetry to send traces to LangSmith
* - Using the wrapped OpenAI client for automatic tracing
* - Making actual API calls to OpenAI with tool definitions
* - Automatic tool call span creation
* - Multi-turn conversations with tool execution
*
* Usage:
* export OPENAI_API_KEY=your_openai_api_key
* export LANGSMITH_API_KEY=your_langsmith_api_key
* export LANGSMITH_PROJECT=your_project_name
* ./gradlew :langsmith-java-example:run -Pexample=OtelOpenAI
*/
private const val SEPARATOR = "============================================================"
fun main() {
println("=== OpenAI + LangSmith OpenTelemetry Example ===\n")
val openaiKey = System.getenv("OPENAI_API_KEY")
if (openaiKey.isNullOrEmpty()) {
System.err.println("ERROR: OPENAI_API_KEY environment variable is required!")
System.err.println("Get your API key from: https://platform.openai.com/api-keys")
exitProcess(1)
}
val langsmithKey = System.getenv("LANGSMITH_API_KEY")
if (langsmithKey.isNullOrEmpty()) {
System.err.println("ERROR: LANGSMITH_API_KEY environment variable is required!")
System.err.println("Get your API key from: https://smith.langchain.com/settings")
exitProcess(1)
}
val projectName = System.getenv("LANGSMITH_PROJECT") ?: "default"
println("Configuration:")
println(" LangSmith Project: $projectName")
println(" Service Name: langsmith-kotlin-openai-example")
println()
try {
OpenTelemetryConfig.builder()
.apiKey(langsmithKey)
.projectName(projectName)
.serviceName("langsmith-kotlin-openai-example")
.processorType(OpenTelemetryConfig.SpanProcessorType.SIMPLE)
.maxBatchSize(1)
.build()
println("✓ OpenTelemetry configured for LangSmith\n")
} catch (e: Exception) {
System.err.println("✗ Failed to configure OpenTelemetry: ${e.message}")
e.printStackTrace()
exitProcess(1)
}
val client = WrappedOpenAIClient.fromEnv()
val openTelemetry: OpenTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get()
val tracer: Tracer = openTelemetry.getTracer("langsmith-kotlin-openai-example")
val workflowSpan = tracer.spanBuilder("openai_agent_workflow")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute("gen_ai.operation.name", "agent_workflow")
.setAttribute("langsmith.span.kind", "chain")
.setAttribute("langsmith.trace.name", "OpenAI Agent with Tools")
.startSpan()
try {
workflowSpan.makeCurrent().use { _ ->
val span = workflowSpan
println(SEPARATOR)
println("Agent Workflow: Chat with Tool Calls")
println(SEPARATOR)
val locationProperty = mapOf(
"type" to com.openai.core.JsonValue.from("string"),
"description" to com.openai.core.JsonValue.from("The city and state, e.g., San Francisco, CA")
)
val properties = mapOf("location" to com.openai.core.JsonValue.from(locationProperty))
val parametersJson = mapOf(
"type" to com.openai.core.JsonValue.from("object"),
"properties" to com.openai.core.JsonValue.from(properties),
"required" to com.openai.core.JsonValue.from(listOf("location"))
)
val initialUserMessage = "What is the capital of France and what's the current weather there?"
val params = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.addUserMessage(initialUserMessage)
.tools(
listOf(
ChatCompletionTool.ofFunction(
ChatCompletionFunctionTool.builder()
.function(
FunctionDefinition.builder()
.name("get_weather")
.description("Get the current weather for a given location")
.parameters(FunctionParameters.builder().putAllAdditionalProperties(parametersJson).build())
.build()
)
.build()
)
)
)
.toolChoice(ChatCompletionToolChoiceOption.ofAuto(ChatCompletionToolChoiceOption.Auto.AUTO))
.build()
span.setAttribute(AttributeKey.stringKey("gen_ai.prompt"), initialUserMessage)
println("\n1. Making initial API call with tool definitions...")
var completion = client.chat().completions().create(params)
val message = completion.choices()[0].message()
val toolCallsOpt = message.toolCalls()
val finalContent = if (toolCallsOpt.isPresent && toolCallsOpt.get().isNotEmpty()) {
println(" ✓ Tool calls detected in response!")
val toolCalls = toolCallsOpt.get()
val messages = mutableListOf<ChatCompletionMessageParam>()
messages.add(params.messages()[0])
messages.add(
ChatCompletionMessageParam.ofAssistant(
ChatCompletionAssistantMessageParam.builder()
.content(message.content().orElse(""))
.toolCalls(toolCalls)
.build()
)
)
println("\n2. Executing tool calls...")
for (toolCall in toolCalls) {
if (toolCall.isFunction()) {
val functionToolCall = toolCall.asFunction()
val toolName = functionToolCall.function().name()
val toolArguments = functionToolCall.function().arguments()
val toolCallId = functionToolCall.id()
println(" - Tool: $toolName | Args: $toolArguments")
val toolExecutionSpan = tracer.spanBuilder("tool_execution $toolName")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute(AttributeKey.stringKey("gen_ai.operation.name"), "tool")
.setAttribute(AttributeKey.stringKey("gen_ai.tool.name"), toolName)
.setAttribute(AttributeKey.stringKey("gen_ai.tool.call.id"), toolCallId)
.setAttribute(AttributeKey.stringKey("gen_ai.tool.arguments"), toolArguments)
.setAttribute(AttributeKey.stringKey("langsmith.span.kind"), "tool")
.setAttribute(AttributeKey.stringKey("gen_ai.prompt"), toolArguments)
.startSpan()
val toolResult = try {
toolExecutionSpan.makeCurrent().use {
val result = executeTool(toolName, toolArguments)
println(" - Result: $result")
toolExecutionSpan.setAttribute(AttributeKey.stringKey("gen_ai.completion"), result)
toolExecutionSpan.setStatus(StatusCode.OK)
result
}
} catch (e: Exception) {
toolExecutionSpan.recordException(e)
toolExecutionSpan.setStatus(StatusCode.ERROR)
"{\"error\": \"${e.message}\"}"
} finally {
toolExecutionSpan.end()
}
messages.add(
ChatCompletionMessageParam.ofTool(
ChatCompletionToolMessageParam.builder()
.toolCallId(functionToolCall.id())
.content(toolResult)
.build()
)
)
}
}
println("\n3. Sending follow-up request with tool results...")
val followUpParams = ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4O_MINI)
.messages(messages)
.build()
completion = client.chat().completions().create(followUpParams)
completion.choices()[0].message().content().orElse("No content")
} else {
message.content().orElse("No content")
}
println("\n$SEPARATOR")
println("Final Response:")
println(finalContent)
println(SEPARATOR)
completion.usage().ifPresent { usage ->
println("\nTotal Token Usage:")
println(" Input: ${usage.promptTokens()}")
println(" Output: ${usage.completionTokens()}")
println(" Total: ${usage.totalTokens()}")
}
span.setAttribute(AttributeKey.stringKey("gen_ai.completion"), finalContent)
span.setAttribute("response.content", finalContent)
span.setStatus(StatusCode.OK)
}
} catch (e: Exception) {
workflowSpan.recordException(e)
System.err.println("\n✗ Error during API call: ${e.message}")
e.printStackTrace()
workflowSpan.recordException(e)
workflowSpan.setStatus(StatusCode.ERROR)
} finally {
workflowSpan.end()
}
client.close()
println("\n$SEPARATOR")
println("Flushing traces to LangSmith...")
val flushed = OpenTelemetryConfig.flush(10, TimeUnit.SECONDS)
if (flushed) {
println("✓ Traces sent successfully!")
println("\nView your traces at:")
println(" https://smith.langchain.com/projects/$projectName")
} else {
System.err.println("✗ Warning: Flush may not have completed successfully")
}
println(SEPARATOR)
println("\nNote: Check the trace waterfall in LangSmith UI to see:")
println(" - Parent workflow span (chain)")
println(" - Child LLM spans (automatically created)")
println(" - Tool call spans (automatically created by wrapper)")
}
private fun executeTool(toolName: String, arguments: String): String {
return try {
val mapper = ObjectMapper()
val args = mapper.readTree(arguments)
if (toolName == "get_weather") {
val location = if (args.has("location")) args.get("location").asText() else "unknown"
val result = mapOf(
"location" to location,
"temperature" to "18°C",
"condition" to "Partly Cloudy",
"humidity" to "65%",
"wind" to "15 km/h"
)
mapper.writeValueAsString(result)
} else {
val errorMap = mapOf("error" to "Unknown tool: $toolName")
mapper.writeValueAsString(errorMap)
}
} catch (e: Exception) {
"{\"error\": \"${e.message}\"}"
}
}
@@ -0,0 +1,47 @@
package com.langchain.smith.example.otel
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import kotlin.system.exitProcess
/**
* Spring Boot example: Send OpenTelemetry traces to LangSmith.
*
* Usage:
* export LANGSMITH_API_KEY=your_api_key
* export LANGSMITH_PROJECT=my-project # optional, defaults to "default"
* ./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith
*
* Then make requests to:
* http://localhost:8080/api/chat
* http://localhost:8080/api/analyze?text=hello
*/
@SpringBootApplication
class SpringBootLangSmithExample
fun main(args: Array<String>) {
println("=== Spring Boot + LangSmith OpenTelemetry Example ===\n")
val apiKey = System.getenv("LANGSMITH_API_KEY")
if (apiKey.isNullOrEmpty()) {
System.err.println("ERROR: LANGSMITH_API_KEY environment variable is required!")
System.err.println("\nUsage:")
System.err.println(" export LANGSMITH_API_KEY=your_api_key_here")
System.err.println(" export LANGSMITH_PROJECT=my-project # optional")
System.err.println(" ./gradlew :langsmith-java-example:run -Pexample=SpringBootLangSmith")
exitProcess(1)
}
val projectName = System.getenv("LANGSMITH_PROJECT") ?: "default"
println("Configuration:")
println(" Project: $projectName")
println(" Endpoint: https://api.smith.langchain.com/otel/v1/traces")
println("\nStarting Spring Boot application...")
println("Try these endpoints:")
println(" POST http://localhost:8080/api/chat")
println(" GET http://localhost:8080/api/analyze?text=hello")
println()
SpringApplication.run(SpringBootLangSmithExample::class.java, *args)
}
@@ -0,0 +1,42 @@
package com.langchain.smith.example.otel.config
import com.langchain.smith.otel.OtelConfig
import com.langchain.smith.otel.OtelTraceExporter
import io.opentelemetry.api.trace.Tracer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Duration
/**
* Spring configuration for OpenTelemetry integration with LangSmith.
*/
@Configuration
class OtelConfiguration {
@Bean
fun otelTraceExporter(): OtelTraceExporter {
val apiKey = System.getenv("LANGSMITH_API_KEY")
var projectName = System.getenv("LANGSMITH_PROJECT")
if (projectName.isNullOrEmpty()) {
projectName = "default"
}
val headers = mapOf(
"x-api-key" to apiKey,
"Langsmith-Project" to projectName
)
val config = OtelConfig.builder()
.enabled(true)
.endpoint("https://api.smith.langchain.com/otel/v1/traces")
.headers(headers)
.timeout(Duration.ofSeconds(30))
.serviceName("spring-boot-langsmith")
.build()
return OtelTraceExporter.fromConfig(config)
}
@Bean
fun tracer(exporter: OtelTraceExporter): Tracer = exporter.tracer
}
@@ -0,0 +1,27 @@
package com.langchain.smith.example.otel.config
import com.langchain.smith.otel.OtelTraceExporter
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import java.util.concurrent.TimeUnit
import javax.annotation.PreDestroy
/**
* Ensures OpenTelemetry traces are flushed on application shutdown.
*/
@Component
class OtelShutdownHook @Autowired constructor(
private val exporter: OtelTraceExporter
) {
@PreDestroy
fun onShutdown() {
println("\n→ Flushing OpenTelemetry traces...")
try {
exporter.flush().join(10000, TimeUnit.MILLISECONDS)
println("✓ Traces flushed successfully")
} catch (e: Exception) {
System.err.println("✗ Failed to flush traces: ${e.message}")
}
}
}
@@ -0,0 +1,94 @@
package com.langchain.smith.example.otel.controller
import com.langchain.smith.example.otel.service.LlmService
import com.langchain.smith.otel.OtelSpanCreator
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.context.Scope
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
/**
* REST controller demonstrating OpenTelemetry tracing with LangSmith.
*/
@RestController
@RequestMapping("/api")
class ChatController @Autowired constructor(
private val tracer: Tracer,
private val llmService: LlmService
) {
@PostMapping("/chat")
fun chat(@RequestBody request: Map<String, String>): Map<String, Any> {
val userMessage = request["message"] ?: "Hello!"
val rootSpan = OtelSpanCreator.createChainSpan(
tracer, "chat_request", "spring-boot-langsmith", null
)
try {
rootSpan.makeCurrent().use {
OtelSpanCreator.setInput(rootSpan, userMessage)
println("→ Processing chat request: $userMessage")
val response = llmService.generateResponse(userMessage)
OtelSpanCreator.setOutput(rootSpan, response)
rootSpan.setStatus(StatusCode.OK)
println("← Chat response generated")
return mapOf(
"request" to userMessage,
"response" to response,
"model" to "gpt-4",
"trace_id" to rootSpan.spanContext.traceId
)
}
} catch (e: Exception) {
rootSpan.setStatus(StatusCode.ERROR, e.message)
throw e
} finally {
rootSpan.end()
}
}
@GetMapping("/analyze")
fun analyze(@RequestParam text: String): Map<String, Any> {
val analysisSpan = OtelSpanCreator.createChainSpan(
tracer, "text_analysis", "spring-boot-langsmith", null
)
try {
analysisSpan.makeCurrent().use {
OtelSpanCreator.setInput(analysisSpan, text)
println("→ Analyzing text: $text")
val wordCount = text.split("\\s+".toRegex()).size
val sentiment = llmService.analyzeSentiment(text)
val result = "Word count: $wordCount, Sentiment: $sentiment"
OtelSpanCreator.setOutput(analysisSpan, result)
analysisSpan.setStatus(StatusCode.OK)
println("← Analysis complete")
return mapOf(
"text" to text,
"word_count" to wordCount,
"sentiment" to sentiment,
"trace_id" to analysisSpan.spanContext.traceId
)
}
} catch (e: Exception) {
analysisSpan.setStatus(StatusCode.ERROR, e.message)
throw e
} finally {
analysisSpan.end()
}
}
@GetMapping("/health")
fun health(): Map<String, String> = mapOf(
"status" to "healthy",
"service" to "spring-boot-langsmith"
)
}
@@ -0,0 +1,78 @@
package com.langchain.smith.example.otel.service
import com.langchain.smith.otel.OtelSpanCreator
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.Tracer
import io.opentelemetry.context.Scope
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
/**
* Service layer demonstrating nested OpenTelemetry spans.
*/
@Service
class LlmService @Autowired constructor(
private val tracer: Tracer
) {
/**
* Simulates an LLM API call with tracing.
*/
fun generateResponse(input: String): String {
val llmSpan = OtelSpanCreator.createLlmSpan(
tracer, "openai.chat", "openai", "gpt-4", "spring-boot-langsmith", null
)
try {
llmSpan.makeCurrent().use {
OtelSpanCreator.setInput(llmSpan, input)
println(" → Calling OpenAI API...")
Thread.sleep(500)
val response = "I received your message: '$input'. How can I help you today?"
OtelSpanCreator.setOutput(llmSpan, response)
OtelSpanCreator.setTokenUsage(llmSpan, 15, 20)
llmSpan.setStatus(StatusCode.OK)
println(" ← OpenAI API response received")
return response
}
} catch (e: Exception) {
llmSpan.setStatus(StatusCode.ERROR, e.message)
throw RuntimeException("LLM call failed", e)
} finally {
llmSpan.end()
}
}
/**
* Simulates sentiment analysis with tracing.
*/
fun analyzeSentiment(text: String): String {
val sentimentSpan = OtelSpanCreator.createLlmSpan(
tracer, "sentiment_analysis", "openai", "gpt-4", "spring-boot-langsmith", null
)
try {
sentimentSpan.makeCurrent().use {
OtelSpanCreator.setInput(sentimentSpan, text)
println(" → Analyzing sentiment...")
Thread.sleep(300)
val sentiment = when {
text.lowercase().contains("good") || text.lowercase().contains("great") -> "positive"
text.lowercase().contains("bad") || text.lowercase().contains("terrible") -> "negative"
else -> "neutral"
}
OtelSpanCreator.setOutput(sentimentSpan, sentiment)
OtelSpanCreator.setTokenUsage(sentimentSpan, 8, 2)
sentimentSpan.setStatus(StatusCode.OK)
println(" ← Sentiment: $sentiment")
return sentiment
}
} catch (e: Exception) {
sentimentSpan.setStatus(StatusCode.ERROR, e.message)
throw RuntimeException("Sentiment analysis failed", e)
} finally {
sentimentSpan.end()
}
}
}
@@ -1,4 +1,4 @@
package com.langchain.smith.example
package com.langchain.smith.example.util
import com.langchain.smith.models.datasets.Dataset
import java.nio.charset.StandardCharsets