mirror of
https://github.com/langchain-ai/langsmith-java.git
synced 2026-08-24 22:01:31 -04:00
committed by
GitHub
parent
cff199b8fa
commit
1902c228dd
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-117
@@ -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");
|
||||
}
|
||||
}
|
||||
-161
@@ -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);
|
||||
}
|
||||
}
|
||||
-325
@@ -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() + "\"}";
|
||||
}
|
||||
}
|
||||
}
|
||||
-51
@@ -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);
|
||||
}
|
||||
}
|
||||
-45
@@ -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();
|
||||
}
|
||||
}
|
||||
-31
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
-105
@@ -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");
|
||||
}
|
||||
}
|
||||
-97
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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
|
||||
|
||||
+1
-2
@@ -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() } ?: "-"
|
||||
|
||||
|
||||
+4
-1
@@ -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))
|
||||
|
||||
+160
@@ -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)
|
||||
}
|
||||
+285
@@ -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}\"}"
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -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)
|
||||
}
|
||||
+42
@@ -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
|
||||
}
|
||||
+27
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -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"
|
||||
)
|
||||
}
|
||||
+78
@@ -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
-1
@@ -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
|
||||
Reference in New Issue
Block a user