mirror of
https://github.com/langchain-ai/langsmith-java.git
synced 2026-08-26 18:17:05 -04:00
chore: Move LangSmith OTel Java wrappers PoC into SDK
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
plugins {
|
||||
id("langchain.java")
|
||||
id("langchain.publish")
|
||||
}
|
||||
|
||||
// Suppress obsolete Java 8 warnings since we're using Java 21 toolchain but targeting Java 8
|
||||
tasks.withType<JavaCompile>().configureEach {
|
||||
options.compilerArgs.add("-Xlint:-options")
|
||||
}
|
||||
|
||||
// Customize POM for wrappers module
|
||||
configure<com.vanniktech.maven.publish.MavenPublishBaseExtension> {
|
||||
pom {
|
||||
name.set("LangSmith Java Wrappers")
|
||||
description.set("OpenTelemetry integration wrappers for LangSmith in Java. " +
|
||||
"This package provides OpenTelemetry wrappers for AI model clients to enable " +
|
||||
"automatic tracing and monitoring with LangSmith.")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// OpenAI Java SDK - Required for examples to run
|
||||
// The wrappers wrap the OpenAI SDK generated by Stainless
|
||||
// Users should add this dependency when using the wrappers in their own projects
|
||||
// Example: implementation("com.openai:openai-java:4.6.1")
|
||||
implementation("com.openai:openai-java:4.6.1")
|
||||
|
||||
// OpenTelemetry API
|
||||
api("io.opentelemetry:opentelemetry-api:1.32.0")
|
||||
api("io.opentelemetry:opentelemetry-context:1.32.0")
|
||||
|
||||
// OpenTelemetry SDK (for configuring exporters)
|
||||
implementation("io.opentelemetry:opentelemetry-sdk:1.32.0")
|
||||
implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.32.0")
|
||||
// Note: OtlpHttpSpanExporter should be in opentelemetry-exporter-otlp, but if not found,
|
||||
// we may need to check the actual package structure
|
||||
|
||||
// Test dependencies
|
||||
testImplementation("com.openai:openai-java:4.6.1")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
// Task to run examples
|
||||
tasks.register<JavaExec>("runExample") {
|
||||
group = "application"
|
||||
description = "Run an example class"
|
||||
|
||||
classpath = sourceSets["main"].runtimeClasspath
|
||||
|
||||
// Get the example class from project property, or use default
|
||||
val exampleClass = project.findProperty("exampleClass") as String?
|
||||
mainClass.set(exampleClass ?: "com.langchain.smith.wrappers.openai.examples.SimpleChatCompletionExample")
|
||||
|
||||
// Pass through environment variables (especially OPENAI_API_KEY, LANGSMITH_API_KEY, etc.)
|
||||
environment.putAll(System.getenv())
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
|
||||
/**
|
||||
* Utility class for wrapping OpenAI clients with LangSmith tracing
|
||||
* capabilities.
|
||||
*
|
||||
* <p>
|
||||
* This class provides a simple way to wrap OpenAI clients similar to the Python
|
||||
* langsmith-sdk wrapper functionality.
|
||||
*/
|
||||
public final class OpenAIWrappers {
|
||||
|
||||
private OpenAIWrappers() {
|
||||
// Utility class - prevent instantiation
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps an OpenAI client to add LangSmith tracing capabilities.
|
||||
*
|
||||
* <p>
|
||||
* This is a no-op wrapper that maintains the same developer experience as the
|
||||
* original client. All configuration options and methods work exactly as they
|
||||
* would with the original client.
|
||||
*
|
||||
* @param client the OpenAI client to wrap
|
||||
* @return a wrapped client that delegates to the original client
|
||||
* @throws IllegalArgumentException if client is null
|
||||
*/
|
||||
public static WrappedOpenAIClient wrap(OpenAIClient client) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("Client cannot be null");
|
||||
}
|
||||
return new WrappedOpenAIClient(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapped OpenAI client from environment variables.
|
||||
*
|
||||
* <p>
|
||||
* This is equivalent to:
|
||||
*
|
||||
* <pre>{@code
|
||||
* wrap(OpenAIOkHttpClient.fromEnv())
|
||||
* }</pre>
|
||||
*
|
||||
* @return a wrapped OpenAI client configured from environment variables
|
||||
*/
|
||||
public static WrappedOpenAIClient wrapFromEnv() {
|
||||
return wrap(OpenAIOkHttpClient.fromEnv());
|
||||
}
|
||||
}
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
|
||||
import io.opentelemetry.sdk.OpenTelemetrySdk;
|
||||
import io.opentelemetry.sdk.resources.Resource;
|
||||
import io.opentelemetry.sdk.trace.SdkTracerProvider;
|
||||
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
|
||||
import io.opentelemetry.sdk.trace.export.SpanExporter;
|
||||
import io.opentelemetry.sdk.trace.SpanProcessor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Configuration utility for setting up OpenTelemetry to export traces to
|
||||
* LangSmith.
|
||||
*
|
||||
* <p>
|
||||
* This class provides a convenient way to configure OpenTelemetry with
|
||||
* LangSmith's OTLP
|
||||
* endpoint. You can use this programmatically, or configure via environment
|
||||
* variables.
|
||||
*
|
||||
* <p>
|
||||
* Example usage:
|
||||
*
|
||||
* <pre>{@code
|
||||
* // Configure OpenTelemetry for LangSmith before using the wrapper
|
||||
* OpenTelemetryConfig.configureForLangSmith(
|
||||
* "your-langsmith-api-key",
|
||||
* "your-project-name");
|
||||
*
|
||||
* // Now use the wrapped client - traces will be sent to LangSmith
|
||||
* WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
|
||||
* }</pre>
|
||||
*/
|
||||
public final class OpenTelemetryConfig {
|
||||
|
||||
private OpenTelemetryConfig() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* LangSmith OTLP endpoint for traces.
|
||||
* Can be overridden via LANGSMITH_OTLP_ENDPOINT environment variable or
|
||||
* by passing a custom endpoint to the configuration methods.
|
||||
*/
|
||||
public static final String LANGSMITH_OTLP_ENDPOINT = "https://api.smith.langchain.com/otel/v1/traces";
|
||||
|
||||
/**
|
||||
* Span processor type for configuring how spans are exported.
|
||||
*/
|
||||
public enum SpanProcessorType {
|
||||
/**
|
||||
* BatchSpanProcessor - Queues spans and exports them in batches.
|
||||
* Best for production use - efficient and non-blocking.
|
||||
*/
|
||||
BATCH,
|
||||
|
||||
/**
|
||||
* SimpleSpanProcessor - Exports spans synchronously on span.end().
|
||||
* Best for testing - predictable but blocks application thread.
|
||||
* WARNING: Not recommended for production due to performance impact.
|
||||
*/
|
||||
SIMPLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry to export traces to LangSmith.
|
||||
*
|
||||
* <p>
|
||||
* This method configures the global OpenTelemetry instance to send traces to
|
||||
* LangSmith's
|
||||
* OTLP endpoint. After calling this method, all spans created by the wrapped
|
||||
* OpenAI client
|
||||
* will be exported to LangSmith.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @param projectName your LangSmith project name (optional, can be null)
|
||||
* @return the configured OpenTelemetry instance
|
||||
*/
|
||||
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName) {
|
||||
return configureForLangSmith(apiKey, projectName, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry to export traces to LangSmith with a custom service
|
||||
* name.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @param projectName your LangSmith project name (optional, can be null)
|
||||
* @param serviceName the service name to identify your application (defaults to
|
||||
* "langsmith-java-otel-wrappers")
|
||||
* @return the configured OpenTelemetry instance
|
||||
*/
|
||||
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
|
||||
String serviceName) {
|
||||
return configureForLangSmith(apiKey, projectName, serviceName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry to export traces to LangSmith with a custom service
|
||||
* name and endpoint.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @param projectName your LangSmith project name (optional, can be null)
|
||||
* @param serviceName the service name to identify your application (defaults to
|
||||
* "langsmith-java-otel-wrappers")
|
||||
* @param endpoint the OTLP endpoint URL (optional, defaults to
|
||||
* LANGSMITH_OTLP_ENDPOINT constant)
|
||||
* @return the configured OpenTelemetry instance
|
||||
*/
|
||||
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
|
||||
String serviceName, String endpoint) {
|
||||
return configureForLangSmith(apiKey, projectName, serviceName, endpoint,
|
||||
SpanProcessorType.BATCH, 512);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry to export traces to LangSmith with custom batch
|
||||
* size.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @param projectName your LangSmith project name (optional, can be null)
|
||||
* @param serviceName the service name to identify your application (defaults
|
||||
* to
|
||||
* "langsmith-java-otel-wrappers")
|
||||
* @param endpoint the OTLP endpoint URL (optional, defaults to
|
||||
* LANGSMITH_OTLP_ENDPOINT constant)
|
||||
* @param maxBatchSize the maximum batch size before export is triggered
|
||||
* (set to 1 for immediate export, default 512)
|
||||
* @return the configured OpenTelemetry instance
|
||||
*/
|
||||
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
|
||||
String serviceName, String endpoint, int maxBatchSize) {
|
||||
return configureForLangSmith(apiKey, projectName, serviceName, endpoint,
|
||||
SpanProcessorType.BATCH, maxBatchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry to export traces to LangSmith with custom span
|
||||
* processor type and batch size.
|
||||
*
|
||||
* @param apiKey your LangSmith API key
|
||||
* @param projectName your LangSmith project name (optional, can be null)
|
||||
* @param serviceName the service name to identify your application
|
||||
* (defaults to "langsmith-java-otel-wrappers")
|
||||
* @param endpoint the OTLP endpoint URL (optional, defaults to
|
||||
* LANGSMITH_OTLP_ENDPOINT constant)
|
||||
* @param processorType the span processor type (BATCH or SIMPLE)
|
||||
* @param maxBatchSize the maximum batch size before export is triggered
|
||||
* (only used for BATCH processor, set to 1 for
|
||||
* immediate export)
|
||||
* @return the configured OpenTelemetry instance
|
||||
*/
|
||||
public static OpenTelemetry configureForLangSmith(String apiKey, String projectName,
|
||||
String serviceName, String endpoint, SpanProcessorType processorType, int maxBatchSize) {
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
throw new IllegalArgumentException("LangSmith API key cannot be null or empty");
|
||||
}
|
||||
|
||||
// Use provided endpoint or default to LANGSMITH_OTLP_ENDPOINT
|
||||
String endpointUrl = endpoint != null && !endpoint.isEmpty()
|
||||
? endpoint
|
||||
: LANGSMITH_OTLP_ENDPOINT;
|
||||
|
||||
// Create OTLP HTTP exporter configured for LangSmith
|
||||
// Build the exporter with conditional headers
|
||||
// Note: Using Object to work around Java 8 limitation (can't use var)
|
||||
// The builder() method returns a builder that supports method chaining
|
||||
Object builder = OtlpHttpSpanExporter.builder()
|
||||
.setEndpoint(endpointUrl)
|
||||
.addHeader("x-api-key", apiKey);
|
||||
|
||||
// Only add project header if projectName is not null and not empty
|
||||
if (projectName != null && !projectName.isEmpty()) {
|
||||
// Use reflection to call addHeader on the builder
|
||||
try {
|
||||
java.lang.reflect.Method addHeaderMethod = builder.getClass().getMethod("addHeader", String.class,
|
||||
String.class);
|
||||
builder = addHeaderMethod.invoke(builder, "langsmith-project", projectName);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to add project header", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the exporter using reflection
|
||||
OtlpHttpSpanExporter spanExporter;
|
||||
try {
|
||||
java.lang.reflect.Method buildMethod = builder.getClass().getMethod("build");
|
||||
spanExporter = (OtlpHttpSpanExporter) buildMethod.invoke(builder);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to build OtlpHttpSpanExporter", e);
|
||||
}
|
||||
|
||||
// Wrap exporter to log export errors
|
||||
SpanExporter loggingExporter = new LoggingSpanExporter(spanExporter);
|
||||
|
||||
// Create resource with service name
|
||||
Resource resource = Resource.getDefault()
|
||||
.merge(Resource.builder()
|
||||
.put("service.name",
|
||||
serviceName != null ? serviceName : "langsmith-java-otel-wrappers")
|
||||
.build());
|
||||
|
||||
// Build and configure span processor based on type
|
||||
SpanProcessor spanProcessor;
|
||||
if (processorType == SpanProcessorType.SIMPLE) {
|
||||
// SimpleSpanProcessor - exports synchronously on span.end()
|
||||
// Good for testing, but blocks the application thread
|
||||
spanProcessor = SimpleSpanProcessor.create(loggingExporter);
|
||||
} else {
|
||||
// BatchSpanProcessor - queues and exports in batches
|
||||
// Good for production, non-blocking
|
||||
// If maxBatchSize is 1, spans are exported immediately as they complete
|
||||
spanProcessor = BatchSpanProcessor.builder(loggingExporter)
|
||||
.setScheduleDelay(100, TimeUnit.MILLISECONDS) // Export every 100ms
|
||||
.setMaxExportBatchSize(maxBatchSize) // Trigger export when batch reaches this size
|
||||
.setExporterTimeout(5, TimeUnit.SECONDS) // 5 second timeout
|
||||
.build();
|
||||
}
|
||||
|
||||
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
|
||||
.addSpanProcessor(spanProcessor)
|
||||
.setResource(resource)
|
||||
.build();
|
||||
|
||||
OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
|
||||
.setTracerProvider(tracerProvider)
|
||||
.buildAndRegisterGlobal();
|
||||
|
||||
return openTelemetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OpenTelemetry from environment variables.
|
||||
*
|
||||
* <p>
|
||||
* Reads configuration from the following environment variables:
|
||||
* <ul>
|
||||
* <li>LANGSMITH_API_KEY - Required: Your LangSmith API key</li>
|
||||
* <li>LANGSMITH_PROJECT - Optional: Your LangSmith project name</li>
|
||||
* <li>OTEL_SERVICE_NAME - Optional: Service name (defaults to
|
||||
* "langsmith-java-otel-wrappers")</li>
|
||||
* <li>LANGSMITH_OTLP_ENDPOINT - Optional: Custom OTLP endpoint URL (defaults to
|
||||
* LANGSMITH_OTLP_ENDPOINT constant)</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the configured OpenTelemetry instance
|
||||
* @throws IllegalStateException if LANGSMITH_API_KEY is not set
|
||||
*/
|
||||
public static OpenTelemetry configureFromEnv() {
|
||||
String apiKey = System.getenv("LANGSMITH_API_KEY");
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"LANGSMITH_API_KEY environment variable is required. " +
|
||||
"Please set it with your LangSmith API key.");
|
||||
}
|
||||
|
||||
String projectName = System.getenv("LANGSMITH_PROJECT");
|
||||
String serviceName = System.getenv("OTEL_SERVICE_NAME");
|
||||
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
|
||||
|
||||
return configureForLangSmith(apiKey, projectName, serviceName, endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces flushing of all pending spans to ensure they are exported.
|
||||
* This should be called before application shutdown to ensure all spans
|
||||
* are sent to LangSmith.
|
||||
*
|
||||
* @return true if flush completed successfully, false otherwise
|
||||
*/
|
||||
public static boolean flush() {
|
||||
return flush(5, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces flushing of all pending spans to ensure they are exported.
|
||||
* This should be called before application shutdown to ensure all spans
|
||||
* are sent to LangSmith.
|
||||
*
|
||||
* @param timeout the maximum time to wait for flush to complete
|
||||
* @param unit the time unit of the timeout
|
||||
* @return true if flush completed successfully, false otherwise
|
||||
*/
|
||||
public static boolean flush(long timeout, TimeUnit unit) {
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
if (openTelemetry instanceof OpenTelemetrySdk) {
|
||||
// Don't close the SDK instance - it's the global instance that should remain
|
||||
// alive
|
||||
@SuppressWarnings("resource")
|
||||
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
|
||||
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
|
||||
if (tracerProvider != null) {
|
||||
try {
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = tracerProvider.forceFlush();
|
||||
result.join(timeout, unit);
|
||||
if (!result.isSuccess()) {
|
||||
System.err.println("Warning: Flush did not complete successfully");
|
||||
}
|
||||
return result.isSuccess();
|
||||
} catch (Exception e) {
|
||||
System.err.println("Warning: Failed to flush spans: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the OpenTelemetry SDK and ensures all spans are exported.
|
||||
* This should be called before application shutdown.
|
||||
*/
|
||||
public static void shutdown() {
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
if (openTelemetry instanceof OpenTelemetrySdk) {
|
||||
// Don't close the SDK instance - it's the global instance that should remain
|
||||
// alive
|
||||
@SuppressWarnings("resource")
|
||||
OpenTelemetrySdk sdk = (OpenTelemetrySdk) openTelemetry;
|
||||
SdkTracerProvider tracerProvider = sdk.getSdkTracerProvider();
|
||||
if (tracerProvider != null) {
|
||||
try {
|
||||
tracerProvider.shutdown().join(5, TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
System.err.println("Warning: Failed to shutdown OpenTelemetry: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper SpanExporter that logs export errors to help debug issues.
|
||||
*/
|
||||
private static class LoggingSpanExporter implements SpanExporter {
|
||||
private final SpanExporter delegate;
|
||||
private static final boolean DEBUG = Boolean.getBoolean("langsmith.debug")
|
||||
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
|
||||
LoggingSpanExporter(SpanExporter delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode export(
|
||||
java.util.Collection<io.opentelemetry.sdk.trace.data.SpanData> spans) {
|
||||
if (DEBUG) {
|
||||
System.out.println("[LangSmith] Exporting " + spans.size() + " span(s):");
|
||||
for (io.opentelemetry.sdk.trace.data.SpanData span : spans) {
|
||||
System.out.println(" - " + span.getName()
|
||||
+ " (kind=" + span.getKind()
|
||||
+ ", attributes=" + span.getAttributes().size() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.export(spans);
|
||||
|
||||
// For SimpleSpanProcessor, wait for the result synchronously to get immediate
|
||||
// feedback
|
||||
// For BatchSpanProcessor, this will return immediately but we can still check
|
||||
// status
|
||||
if (DEBUG) {
|
||||
// Wait up to 5 seconds for the result
|
||||
try {
|
||||
result.join(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!result.isSuccess()) {
|
||||
System.err.println("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
|
||||
System.err.println(" This usually indicates a network error or authentication problem");
|
||||
System.err.println(" Check your LANGSMITH_API_KEY and network connectivity");
|
||||
} else {
|
||||
System.out.println("[LangSmith] Successfully exported " + spans.size() + " span(s)");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[LangSmith ERROR] Exception waiting for export result: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
} else {
|
||||
// Without DEBUG, still log errors but don't block
|
||||
result.whenComplete(() -> {
|
||||
if (!result.isSuccess()) {
|
||||
System.err.println("[LangSmith ERROR] Failed to export " + spans.size() + " span(s) to LangSmith");
|
||||
System.err.println(" This usually indicates a network error or authentication problem");
|
||||
System.err.println(" Check your LANGSMITH_API_KEY and network connectivity");
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode flush() {
|
||||
if (DEBUG) {
|
||||
System.out.println("[LangSmith] Flushing spans...");
|
||||
}
|
||||
io.opentelemetry.sdk.common.CompletableResultCode result = delegate.flush();
|
||||
result.whenComplete(() -> {
|
||||
if (!result.isSuccess()) {
|
||||
System.err.println("[LangSmith ERROR] Failed to flush spans");
|
||||
} else if (DEBUG) {
|
||||
System.out.println("[LangSmith] Flush completed successfully");
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public io.opentelemetry.sdk.common.CompletableResultCode shutdown() {
|
||||
if (DEBUG) {
|
||||
System.out.println("[LangSmith] Shutting down span exporter...");
|
||||
}
|
||||
return delegate.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanBuilder;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
|
||||
/**
|
||||
* Utility class for creating and managing OpenTelemetry spans for OpenAI API
|
||||
* calls.
|
||||
*
|
||||
* <p>
|
||||
* This class follows the LangSmith OTEL conventions documented in
|
||||
* LANGSMITH_OTEL.md.
|
||||
*/
|
||||
final class TracingUtils {
|
||||
|
||||
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
|
||||
|
||||
private TracingUtils() {
|
||||
// Utility class
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a tracer for OpenAI operations.
|
||||
*
|
||||
* @return a tracer instance
|
||||
*/
|
||||
static Tracer getTracer() {
|
||||
// Try to get the global tracer if available
|
||||
try {
|
||||
Tracer tracer = io.opentelemetry.api.GlobalOpenTelemetry.get()
|
||||
.getTracer(INSTRUMENTATION_NAME);
|
||||
|
||||
// Debug: Check if tracer is a noop tracer
|
||||
boolean debug = Boolean.getBoolean("langsmith.debug")
|
||||
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
if (debug) {
|
||||
// Check if the OpenTelemetry instance is a noop
|
||||
io.opentelemetry.api.OpenTelemetry otel = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
boolean isNoop = otel.getClass().getName().contains("Noop");
|
||||
System.out.println("[TracingUtils] Tracer obtained: " + tracer.getClass().getName()
|
||||
+ ", OpenTelemetry isNoop: " + isNoop);
|
||||
}
|
||||
|
||||
return tracer;
|
||||
} catch (Exception e) {
|
||||
// Fall back to noop - GlobalOpenTelemetry.get() will return a noop
|
||||
// implementation
|
||||
// if OpenTelemetry is not configured
|
||||
return io.opentelemetry.api.GlobalOpenTelemetry.get()
|
||||
.getTracer(INSTRUMENTATION_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a span builder for an OpenAI operation with LangSmith-specific
|
||||
* attributes.
|
||||
*
|
||||
* @param model the model name (e.g., "gpt-4o-mini")
|
||||
* @param operationType the operation type (e.g., "chat", "response")
|
||||
* @param spanKind the LangSmith span kind (e.g., "llm") - can be null
|
||||
* @return a span builder with CLIENT kind and core attributes
|
||||
*/
|
||||
static SpanBuilder createSpanBuilder(String model, String operationType, String spanKind) {
|
||||
Tracer tracer = getTracer();
|
||||
String spanName = operationType + " " + (model != null ? model : "unknown");
|
||||
SpanBuilder builder = tracer.spanBuilder(spanName)
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.system", "openai")
|
||||
.setAttribute("gen_ai.operation.name", operationType)
|
||||
.setAttribute("gen_ai.provider.name", "openai");
|
||||
|
||||
// Set LangSmith span kind on the builder (important for LangSmith detection)
|
||||
if (spanKind != null) {
|
||||
builder.setAttribute("langsmith.span.kind", spanKind);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a span builder for an OpenAI operation (defaults to "llm" span kind).
|
||||
*
|
||||
* @param model the model name (e.g., "gpt-4o-mini")
|
||||
* @param operationType the operation type (e.g., "chat", "response")
|
||||
* @return a span builder with CLIENT kind and core attributes
|
||||
*/
|
||||
static SpanBuilder createSpanBuilder(String model, String operationType) {
|
||||
return createSpanBuilder(model, operationType, "llm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets common span attributes for OpenAI LLM requests.
|
||||
*
|
||||
* Note: Core attributes (gen_ai.system, gen_ai.operation.name,
|
||||
* gen_ai.provider.name,
|
||||
* langsmith.span.kind) are already set on the SpanBuilder. This method sets
|
||||
* additional
|
||||
* request-specific attributes.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param model the model name
|
||||
*/
|
||||
static void setRequestAttributes(Span span, String model) {
|
||||
if (model != null) {
|
||||
span.setAttribute("gen_ai.request.model", model);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets request parameter attributes on a span.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param temperature the temperature parameter (nullable)
|
||||
* @param topP the top_p parameter (nullable)
|
||||
* @param maxTokens the max_tokens parameter (nullable)
|
||||
*/
|
||||
static void setRequestParameters(Span span, Double temperature, Double topP, Long maxTokens) {
|
||||
if (temperature != null) {
|
||||
span.setAttribute("gen_ai.request.temperature", temperature);
|
||||
}
|
||||
if (topP != null) {
|
||||
span.setAttribute("gen_ai.request.top_p", topP);
|
||||
}
|
||||
if (maxTokens != null) {
|
||||
span.setAttribute("gen_ai.request.max_tokens", maxTokens);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets input messages as JSON array string.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param messagesJson the messages in JSON array format
|
||||
*/
|
||||
static void setInputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute("gen_ai.input.messages", messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets output messages as JSON array string.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param messagesJson the messages in JSON array format
|
||||
*/
|
||||
static void setOutputMessages(Span span, String messagesJson) {
|
||||
if (messagesJson != null) {
|
||||
span.setAttribute("gen_ai.output.messages", messagesJson);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets response attributes on a span.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param inputTokens number of input tokens
|
||||
* @param outputTokens number of output tokens
|
||||
* @param totalTokens total tokens used
|
||||
*/
|
||||
static void setResponseAttributes(Span span, Long inputTokens, Long outputTokens,
|
||||
Long totalTokens) {
|
||||
if (inputTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.input_tokens", inputTokens);
|
||||
}
|
||||
if (outputTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.output_tokens", outputTokens);
|
||||
}
|
||||
if (totalTokens != null) {
|
||||
span.setAttribute("gen_ai.usage.total_tokens", totalTokens);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets response model and finish reason attributes.
|
||||
*
|
||||
* @param span the span to set attributes on
|
||||
* @param responseModel the model used in the response
|
||||
* @param finishReason the finish reason
|
||||
*/
|
||||
static void setResponseMetadata(Span span, String responseModel, String finishReason) {
|
||||
if (responseModel != null) {
|
||||
span.setAttribute("gen_ai.response.model", responseModel);
|
||||
}
|
||||
if (finishReason != null) {
|
||||
span.setAttribute("gen_ai.response.finish_reason", finishReason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records an exception on a span and marks it as an error.
|
||||
*
|
||||
* @param span the span to record the exception on
|
||||
* @param exception the exception that occurred
|
||||
*/
|
||||
static void recordException(Span span, Throwable exception) {
|
||||
span.recordException(exception);
|
||||
span.setAttribute("error", true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a JSON string by replacing special characters.
|
||||
*
|
||||
* @param str the string to escape
|
||||
* @return the escaped string
|
||||
*/
|
||||
static String escapeJsonString(String str) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
return str.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
+841
@@ -0,0 +1,841 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.models.chat.completions.ChatCompletion;
|
||||
import com.openai.models.chat.completions.ChatCompletionCreateParams;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
|
||||
import com.openai.models.chat.completions.StructuredChatCompletion;
|
||||
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
|
||||
import com.openai.core.RequestOptions;
|
||||
import com.openai.core.http.StreamResponse;
|
||||
import com.openai.models.chat.completions.ChatCompletionChunk;
|
||||
import com.openai.services.blocking.ChatService;
|
||||
import com.openai.services.blocking.chat.ChatCompletionService;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.function.Consumer;
|
||||
import com.openai.core.ClientOptions;
|
||||
|
||||
/**
|
||||
* Wrapped ChatService that adds OpenTelemetry tracing to chat completion
|
||||
* operations.
|
||||
*/
|
||||
class WrappedChatService implements ChatService {
|
||||
|
||||
private final ChatService delegate;
|
||||
|
||||
WrappedChatService(ChatService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedChatService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService completions() {
|
||||
return new WrappedChatCompletionService(delegate.completions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapped ChatCompletionService that adds tracing to create operations.
|
||||
*/
|
||||
private static class WrappedChatCompletionService implements ChatCompletionService {
|
||||
|
||||
private final ChatCompletionService delegate;
|
||||
|
||||
WrappedChatCompletionService(ChatCompletionService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedChatCompletionService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.chat.completions.MessageService messages() {
|
||||
return delegate.messages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion create(ChatCompletionCreateParams params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion create(ChatCompletionCreateParams params,
|
||||
RequestOptions requestOptions) {
|
||||
// Extract model from params for span naming
|
||||
String model = params.model() != null ? params.model().toString() : "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat")
|
||||
.startSpan();
|
||||
|
||||
// Debug: Check if span is recording (not a noop span)
|
||||
boolean debug = Boolean.getBoolean("langsmith.debug")
|
||||
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
if (debug) {
|
||||
boolean isRecording = span.isRecording();
|
||||
System.out.println("[WrappedChatService] Created span: " + span.getSpanContext().getSpanId()
|
||||
+ ", isRecording: " + isRecording + ", traceId: " + span.getSpanContext().getTraceId());
|
||||
}
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
Long maxTokens = params.maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params);
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
|
||||
ChatCompletion result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// Extract response model and finish reason
|
||||
String responseModel = result.model() != null ? result.model().toString() : null;
|
||||
String finishReason = !result.choices().isEmpty() && result.choices().get(0).finishReason() != null
|
||||
? result.choices().get(0).finishReason().toString()
|
||||
: "stop";
|
||||
TracingUtils.setResponseMetadata(span, responseModel, finishReason);
|
||||
|
||||
// Capture output messages in JSON format
|
||||
String outputMessagesJson = formatOutputMessages(result);
|
||||
TracingUtils.setOutputMessages(span, outputMessagesJson);
|
||||
|
||||
// Extract usage information from result
|
||||
result.usage().ifPresent(usage -> {
|
||||
TracingUtils.setResponseAttributes(span,
|
||||
(long) usage.promptTokens(),
|
||||
(long) usage.completionTokens(),
|
||||
(long) usage.totalTokens());
|
||||
});
|
||||
|
||||
// Create tool call spans for any tool calls in the response
|
||||
createToolCallSpans(result, span);
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (debug) {
|
||||
System.out.println("[WrappedChatService] Ending span: " + span.getSpanContext().getSpanId());
|
||||
}
|
||||
span.end();
|
||||
if (debug) {
|
||||
System.out.println("[WrappedChatService] Span ended: " + span.getSpanContext().getSpanId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats input messages from ChatCompletionCreateParams as a JSON array
|
||||
* string.
|
||||
*
|
||||
* <p>
|
||||
* Properly formats messages as JSON with role and content, following
|
||||
* LangSmith conventions.
|
||||
*/
|
||||
private String formatInputMessages(ChatCompletionCreateParams params) {
|
||||
if (params.messages() == null || params.messages().isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
boolean debug = Boolean.getBoolean("langsmith.debug")
|
||||
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Processing " + params.messages().size() + " message(s)");
|
||||
}
|
||||
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
for (com.openai.models.chat.completions.ChatCompletionMessageParam messageParam : params.messages()) {
|
||||
if (!first) {
|
||||
json.append(",");
|
||||
}
|
||||
first = false;
|
||||
|
||||
json.append("{");
|
||||
|
||||
String role = null;
|
||||
String content = null;
|
||||
|
||||
// Try to extract role and content based on message type
|
||||
// The OpenAI SDK uses different message types (UserMessage, SystemMessage,
|
||||
// etc.)
|
||||
String className = messageParam.getClass().getSimpleName();
|
||||
String fullClassName = messageParam.getClass().getName();
|
||||
|
||||
// Debug logging (debug variable already declared above)
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Processing message: " + fullClassName);
|
||||
java.lang.reflect.Method[] allMethods = messageParam.getClass().getMethods();
|
||||
System.out.println("[formatInputMessages] Available methods: " +
|
||||
java.util.Arrays.stream(allMethods)
|
||||
.map(m -> m.getName() + "(" + m.getParameterCount() + ")")
|
||||
.collect(java.util.stream.Collectors.joining(", ")));
|
||||
}
|
||||
|
||||
// ChatCompletionMessageParam is a union type - check type FIRST before calling
|
||||
// as*() methods
|
||||
// This prevents InvocationTargetException when calling asUser() on non-user
|
||||
// messages
|
||||
Object actualMessage = null;
|
||||
|
||||
try {
|
||||
// Use type checking methods first to avoid exceptions
|
||||
java.lang.reflect.Method isUserMethod = messageParam.getClass().getMethod("isUser");
|
||||
java.lang.reflect.Method isSystemMethod = messageParam.getClass().getMethod("isSystem");
|
||||
java.lang.reflect.Method isAssistantMethod = messageParam.getClass().getMethod("isAssistant");
|
||||
java.lang.reflect.Method isToolMethod = messageParam.getClass().getMethod("isTool");
|
||||
|
||||
boolean isUser = (Boolean) isUserMethod.invoke(messageParam);
|
||||
boolean isSystem = (Boolean) isSystemMethod.invoke(messageParam);
|
||||
boolean isAssistant = (Boolean) isAssistantMethod.invoke(messageParam);
|
||||
boolean isTool = (Boolean) isToolMethod.invoke(messageParam);
|
||||
|
||||
if (isUser) {
|
||||
java.lang.reflect.Method asUserMethod = messageParam.getClass().getMethod("asUser");
|
||||
actualMessage = asUserMethod.invoke(messageParam);
|
||||
role = "user";
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Found user message: " + actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isSystem) {
|
||||
java.lang.reflect.Method asSystemMethod = messageParam.getClass().getMethod("asSystem");
|
||||
actualMessage = asSystemMethod.invoke(messageParam);
|
||||
role = "system";
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Found system message: " + actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isAssistant) {
|
||||
java.lang.reflect.Method asAssistantMethod = messageParam.getClass().getMethod("asAssistant");
|
||||
actualMessage = asAssistantMethod.invoke(messageParam);
|
||||
role = "assistant";
|
||||
if (debug) {
|
||||
System.out
|
||||
.println("[formatInputMessages] Found assistant message: " + actualMessage.getClass().getName());
|
||||
}
|
||||
} else if (isTool) {
|
||||
java.lang.reflect.Method asToolMethod = messageParam.getClass().getMethod("asTool");
|
||||
actualMessage = asToolMethod.invoke(messageParam);
|
||||
role = "tool";
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Found tool message: " + actualMessage.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Now get content from the actual message object
|
||||
if (actualMessage != null) {
|
||||
try {
|
||||
java.lang.reflect.Method contentMethod = actualMessage.getClass().getMethod("content");
|
||||
Object contentResult = contentMethod.invoke(actualMessage);
|
||||
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] content() returned: " +
|
||||
(contentResult != null ? contentResult.getClass().getName() : "null"));
|
||||
}
|
||||
|
||||
// Content might be a Content object with text() method
|
||||
if (contentResult != null) {
|
||||
try {
|
||||
java.lang.reflect.Method textMethod = contentResult.getClass().getMethod("text");
|
||||
Object textResult = textMethod.invoke(contentResult);
|
||||
if (textResult instanceof java.util.Optional) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Optional<String> textOpt = (java.util.Optional<String>) textResult;
|
||||
if (textOpt.isPresent()) {
|
||||
content = textOpt.get();
|
||||
}
|
||||
} else if (textResult instanceof String) {
|
||||
content = (String) textResult;
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Content might be a String directly
|
||||
if (contentResult instanceof String) {
|
||||
content = (String) contentResult;
|
||||
} else if (contentResult instanceof java.util.Optional) {
|
||||
@SuppressWarnings("unchecked")
|
||||
java.util.Optional<String> contentOpt = (java.util.Optional<String>) contentResult;
|
||||
if (contentOpt.isPresent()) {
|
||||
content = contentOpt.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] No content() method on actual message");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Error accessing message: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// Role should already be set from asUser/asSystem/asAssistant above
|
||||
// If not, try to get it from the message
|
||||
if (role == null) {
|
||||
try {
|
||||
java.lang.reflect.Method roleMethod = messageParam.getClass().getMethod("role");
|
||||
Object roleResult = roleMethod.invoke(messageParam);
|
||||
if (roleResult != null) {
|
||||
role = roleResult.toString().toLowerCase();
|
||||
}
|
||||
} catch (NoSuchMethodException e) {
|
||||
// Fallback: determine role from class name
|
||||
if (className.contains("User") || fullClassName.contains("User")) {
|
||||
role = "user";
|
||||
} else if (className.contains("System") || fullClassName.contains("System")) {
|
||||
role = "system";
|
||||
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
|
||||
role = "assistant";
|
||||
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
|
||||
role = "tool";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Error calling role(): " + e.getMessage());
|
||||
}
|
||||
// Fallback to class name
|
||||
if (className.contains("User") || fullClassName.contains("User")) {
|
||||
role = "user";
|
||||
} else if (className.contains("System") || fullClassName.contains("System")) {
|
||||
role = "system";
|
||||
} else if (className.contains("Assistant") || fullClassName.contains("Assistant")) {
|
||||
role = "assistant";
|
||||
} else if (className.contains("Tool") || fullClassName.contains("Tool")) {
|
||||
role = "tool";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: try toString parsing
|
||||
if (content == null) {
|
||||
String messageStr = messageParam.toString();
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] toString(): " + messageStr);
|
||||
}
|
||||
|
||||
// For tool messages, look for content=Content{text={...}} pattern
|
||||
if (role != null && role.equals("tool")) {
|
||||
// Extract JSON content from tool messages:
|
||||
// content=Content{text={"key":"value"}}
|
||||
java.util.regex.Pattern toolContentPattern = java.util.regex.Pattern.compile("text=\\{([^}]+)\\}");
|
||||
java.util.regex.Matcher toolMatcher = toolContentPattern.matcher(messageStr);
|
||||
if (toolMatcher.find()) {
|
||||
content = "{" + toolMatcher.group(1) + "}";
|
||||
} else {
|
||||
// Try simpler pattern: text="..."
|
||||
java.util.regex.Pattern simplePattern = java.util.regex.Pattern.compile("text=[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher simpleMatcher = simplePattern.matcher(messageStr);
|
||||
if (simpleMatcher.find()) {
|
||||
content = simpleMatcher.group(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Improved extraction: try multiple patterns
|
||||
if (content == null) {
|
||||
// Pattern 1: content="..."
|
||||
int contentIdx = messageStr.indexOf("content=");
|
||||
if (contentIdx >= 0) {
|
||||
// Look for Content{text="..."} pattern
|
||||
int textIdx = messageStr.indexOf("text=", contentIdx);
|
||||
if (textIdx > contentIdx) {
|
||||
int start = messageStr.indexOf("\"", textIdx);
|
||||
if (start >= 0) {
|
||||
// Find matching closing quote, handling escaped quotes
|
||||
int end = start + 1;
|
||||
while (end < messageStr.length() && messageStr.charAt(end) != '"') {
|
||||
if (messageStr.charAt(end) == '\\') {
|
||||
end += 2; // Skip escaped character
|
||||
} else {
|
||||
end++;
|
||||
}
|
||||
}
|
||||
if (end < messageStr.length()) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to simple quote matching
|
||||
if (content == null) {
|
||||
int start = messageStr.indexOf("\"", contentIdx);
|
||||
if (start >= 0) {
|
||||
int end = messageStr.indexOf("\"", start + 1);
|
||||
if (end > start) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
} else {
|
||||
// Try single quotes
|
||||
start = messageStr.indexOf("'", contentIdx);
|
||||
if (start >= 0) {
|
||||
int end = messageStr.indexOf("'", start + 1);
|
||||
if (end > start) {
|
||||
content = messageStr.substring(start + 1, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Look for content in JSON-like format
|
||||
if (content == null) {
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("content[=:]\\s*[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
|
||||
if (matcher.find()) {
|
||||
content = matcher.group(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: If still null, try to extract from the end of the string
|
||||
if (content == null && messageStr.length() > 0) {
|
||||
// Last resort: try to find any quoted string that might be content
|
||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("[\"']([^\"']+)[\"']");
|
||||
java.util.regex.Matcher matcher = pattern.matcher(messageStr);
|
||||
if (matcher.find()) {
|
||||
String potentialContent = matcher.group(1);
|
||||
// Only use if it looks like actual content (not a class name or role)
|
||||
if (!potentialContent.contains("com.openai") &&
|
||||
!potentialContent.equals("user") &&
|
||||
!potentialContent.equals("system") &&
|
||||
!potentialContent.equals("assistant") &&
|
||||
!potentialContent.equals("tool") &&
|
||||
potentialContent.length() > 0) {
|
||||
content = potentialContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build JSON
|
||||
if (role != null) {
|
||||
json.append("\"role\":\"").append(role).append("\"");
|
||||
}
|
||||
if (content != null) {
|
||||
if (role != null) {
|
||||
json.append(",");
|
||||
}
|
||||
json.append("\"content\":\"")
|
||||
.append(TracingUtils.escapeJsonString(content))
|
||||
.append("\"");
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Final: role=" + role + ", content="
|
||||
+ (content != null ? content.substring(0, Math.min(50, content.length())) : "null"));
|
||||
}
|
||||
|
||||
json.append("}");
|
||||
}
|
||||
json.append("]");
|
||||
|
||||
String result = json.toString();
|
||||
if (debug) {
|
||||
System.out.println("[formatInputMessages] Final JSON: " + result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tool call spans for any tool calls detected in the chat completion
|
||||
* response.
|
||||
*
|
||||
* @param completion the chat completion response
|
||||
* @param parentSpan the parent span (the chat completion span)
|
||||
*/
|
||||
private void createToolCallSpans(ChatCompletion completion, Span parentSpan) {
|
||||
if (completion.choices() == null || completion.choices().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean debug = Boolean.getBoolean("langsmith.debug")
|
||||
|| "true".equalsIgnoreCase(System.getenv("LANGSMITH_DEBUG"));
|
||||
|
||||
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
|
||||
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
|
||||
|
||||
// Check if message has tool calls
|
||||
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
|
||||
if (toolCallsOpt.isPresent()) {
|
||||
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
|
||||
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
|
||||
// Check if it's a function tool call
|
||||
if (toolCall.isFunction()) {
|
||||
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
|
||||
createToolCallSpan(functionToolCall, parentSpan, debug);
|
||||
}
|
||||
// Note: Custom tool calls are not yet supported
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single tool call span from a function tool call object.
|
||||
*
|
||||
* @param functionToolCall the function tool call object from the OpenAI SDK
|
||||
* @param parentSpan the parent span (the chat completion span)
|
||||
* @param debug whether debug logging is enabled
|
||||
*/
|
||||
private void createToolCallSpan(ChatCompletionMessageFunctionToolCall functionToolCall, Span parentSpan,
|
||||
boolean debug) {
|
||||
try {
|
||||
io.opentelemetry.api.OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
io.opentelemetry.api.trace.Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
|
||||
|
||||
// Extract tool call information directly from the SDK objects
|
||||
String toolCallId = functionToolCall.id();
|
||||
com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall.Function function = functionToolCall.function();
|
||||
String toolName = function.name();
|
||||
String toolArguments = function.arguments();
|
||||
|
||||
// Create span name
|
||||
String spanName = toolName != null ? "tool_call " + toolName : "tool_call";
|
||||
|
||||
// Create tool call span as a child of the parent span
|
||||
io.opentelemetry.api.trace.Span toolCallSpan = tracer.spanBuilder(spanName)
|
||||
.setSpanKind(io.opentelemetry.api.trace.SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.operation.name", "tool_call")
|
||||
.setAttribute("langsmith.span.kind", "tool")
|
||||
.setAttribute("gen_ai.system", "openai")
|
||||
.setAttribute("gen_ai.provider.name", "openai")
|
||||
.startSpan();
|
||||
|
||||
try (io.opentelemetry.context.Scope toolScope = toolCallSpan.makeCurrent()) {
|
||||
// Set tool call attributes
|
||||
if (toolCallId != null) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.call.id", toolCallId);
|
||||
}
|
||||
if (toolName != null) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.name", toolName);
|
||||
toolCallSpan.setAttribute("langsmith.trace.name", "Tool Call: " + toolName);
|
||||
}
|
||||
if (toolArguments != null && !toolArguments.isEmpty()) {
|
||||
toolCallSpan.setAttribute("gen_ai.tool.arguments", toolArguments);
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
System.out
|
||||
.println("[WrappedChatService] Created tool call span: " + toolCallSpan.getSpanContext().getSpanId()
|
||||
+ ", tool=" + toolName + ", arguments=" + toolArguments + ", parent="
|
||||
+ parentSpan.getSpanContext().getSpanId());
|
||||
}
|
||||
|
||||
// Note: Tool call result would be set when the tool is actually executed
|
||||
// This span represents the tool call request, not the execution
|
||||
} finally {
|
||||
toolCallSpan.end();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (debug) {
|
||||
System.out.println("[WrappedChatService] Error creating tool call span: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats output messages from ChatCompletion as a JSON array string.
|
||||
*/
|
||||
private String formatOutputMessages(ChatCompletion completion) {
|
||||
if (completion.choices() == null || completion.choices().isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
for (com.openai.models.chat.completions.ChatCompletion.Choice choice : completion.choices()) {
|
||||
if (!first) {
|
||||
json.append(",");
|
||||
}
|
||||
first = false;
|
||||
|
||||
com.openai.models.chat.completions.ChatCompletionMessage message = choice.message();
|
||||
json.append("{");
|
||||
|
||||
// Add role
|
||||
json.append("\"role\":\"assistant\"");
|
||||
|
||||
// Add content if present
|
||||
message.content().ifPresent(content -> {
|
||||
json.append(",\"content\":\"")
|
||||
.append(TracingUtils.escapeJsonString(content))
|
||||
.append("\"");
|
||||
});
|
||||
|
||||
json.append("}");
|
||||
}
|
||||
json.append("]");
|
||||
|
||||
return json.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredChatCompletion<T> create(
|
||||
StructuredChatCompletionCreateParams<T> params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredChatCompletion<T> create(
|
||||
StructuredChatCompletionCreateParams<T> params, RequestOptions requestOptions) {
|
||||
// Get model from the underlying params - StructuredChatCompletionCreateParams
|
||||
// wraps ChatCompletionCreateParams
|
||||
String model = params != null && params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available
|
||||
if (params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
Long maxTokens = params.rawParams().maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params.rawParams());
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
}
|
||||
|
||||
StructuredChatCompletion<T> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// For structured completions, we'll just set basic model info
|
||||
// The actual structured output is in the parsed result
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
result.usage().ifPresent(usage -> {
|
||||
TracingUtils.setResponseAttributes(span,
|
||||
(long) usage.promptTokens(),
|
||||
(long) usage.completionTokens(),
|
||||
(long) usage.totalTokens());
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ChatCompletionChunk> createStreaming(
|
||||
ChatCompletionCreateParams params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ChatCompletionChunk> createStreaming(
|
||||
ChatCompletionCreateParams params, RequestOptions requestOptions) {
|
||||
String model = params.model() != null ? params.model().toString() : "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "chat")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
span.setAttribute("gen_ai.streaming", true);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
Long maxTokens = params.maxCompletionTokens().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, maxTokens);
|
||||
|
||||
// Capture input messages in JSON format
|
||||
String inputMessagesJson = formatInputMessages(params);
|
||||
TracingUtils.setInputMessages(span, inputMessagesJson);
|
||||
|
||||
// For streaming, we can't easily extract usage info, so we'll just mark it as
|
||||
// streaming
|
||||
StreamResponse<ChatCompletionChunk> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
// Note: For streaming, the span will end immediately
|
||||
// This is a simplified implementation - in production you might want to
|
||||
// wrap the stream to collect the full response
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate other methods without tracing (for now)
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId) {
|
||||
return delegate.retrieve(completionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
|
||||
return delegate.retrieve(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(completionId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params) {
|
||||
return delegate.retrieve(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion retrieve(
|
||||
com.openai.models.chat.completions.ChatCompletionRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
|
||||
return delegate.update(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.update(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params) {
|
||||
return delegate.update(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletion update(
|
||||
com.openai.models.chat.completions.ChatCompletionUpdateParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.update(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list() {
|
||||
return delegate.list();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.list(requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(
|
||||
com.openai.models.chat.completions.ChatCompletionListParams params) {
|
||||
return delegate.list(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionListPage list(
|
||||
com.openai.models.chat.completions.ChatCompletionListParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.list(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId) {
|
||||
return delegate.delete(completionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.delete(completionId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
|
||||
return delegate.delete(completionId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(String completionId,
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.delete(completionId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params) {
|
||||
return delegate.delete(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.models.chat.completions.ChatCompletionDeleted delete(
|
||||
com.openai.models.chat.completions.ChatCompletionDeleteParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.delete(params, requestOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import java.util.function.Consumer;
|
||||
import com.openai.core.ClientOptions;
|
||||
|
||||
/**
|
||||
* Wrapped OpenAI client that maintains the same developer experience as the
|
||||
* original client
|
||||
* while adding LangSmith tracing capabilities.
|
||||
*
|
||||
* <p>
|
||||
* This wrapper delegates all calls to the underlying OpenAI client, allowing
|
||||
* all
|
||||
* configuration options and methods to work exactly as they would with the
|
||||
* original client.
|
||||
*/
|
||||
public class WrappedOpenAIClient implements OpenAIClient {
|
||||
|
||||
private final OpenAIClient delegate;
|
||||
|
||||
/**
|
||||
* Creates a new wrapped client that delegates to the provided client.
|
||||
*
|
||||
* @param delegate the underlying OpenAI client to wrap
|
||||
*/
|
||||
public WrappedOpenAIClient(OpenAIClient delegate) {
|
||||
if (delegate == null) {
|
||||
throw new IllegalArgumentException("Delegate client cannot be null");
|
||||
}
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying delegate client.
|
||||
*
|
||||
* @return the wrapped OpenAI client
|
||||
*/
|
||||
public OpenAIClient getDelegate() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.client.OpenAIClientAsync async() {
|
||||
return delegate.async();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAIClient.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OpenAIClient withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return delegate.withOptions(options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.CompletionService completions() {
|
||||
return delegate.completions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ChatService chat() {
|
||||
return new WrappedChatService(delegate.chat());
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.EmbeddingService embeddings() {
|
||||
return delegate.embeddings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.FileService files() {
|
||||
return delegate.files();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ImageService images() {
|
||||
return delegate.images();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.AudioService audio() {
|
||||
return delegate.audio();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ModerationService moderations() {
|
||||
return delegate.moderations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ModelService models() {
|
||||
return delegate.models();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.FineTuningService fineTuning() {
|
||||
return delegate.fineTuning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.GraderService graders() {
|
||||
return delegate.graders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.VectorStoreService vectorStores() {
|
||||
return delegate.vectorStores();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.WebhookService webhooks() {
|
||||
return delegate.webhooks();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.BetaService beta() {
|
||||
return delegate.beta();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.BatchService batches() {
|
||||
return delegate.batches();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.UploadService uploads() {
|
||||
return delegate.uploads();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ResponseService responses() {
|
||||
return new WrappedResponseService(delegate.responses());
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.RealtimeService realtime() {
|
||||
return delegate.realtime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ConversationService conversations() {
|
||||
return delegate.conversations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.EvalService evals() {
|
||||
return delegate.evals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.ContainerService containers() {
|
||||
return delegate.containers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.VideoService videos() {
|
||||
return delegate.videos();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating wrapped OpenAI clients with the same configuration
|
||||
* options
|
||||
* as the original client builder.
|
||||
*/
|
||||
public static class Builder {
|
||||
private final OpenAIOkHttpClient.Builder delegateBuilder;
|
||||
|
||||
/**
|
||||
* Creates a new builder that wraps the OpenAI client builder.
|
||||
*/
|
||||
public Builder() {
|
||||
this.delegateBuilder = OpenAIOkHttpClient.builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder that wraps the OpenAI client builder, starting from
|
||||
* environment variables.
|
||||
*
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder fromEnv() {
|
||||
delegateBuilder.fromEnv();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the API key.
|
||||
*
|
||||
* @param apiKey the OpenAI API key
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder apiKey(String apiKey) {
|
||||
delegateBuilder.apiKey(apiKey);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the organization ID.
|
||||
*
|
||||
* @param organization the organization ID
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder organization(String organization) {
|
||||
delegateBuilder.organization(organization);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the project ID.
|
||||
*
|
||||
* @param project the project ID
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder project(String project) {
|
||||
delegateBuilder.project(project);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the webhook secret.
|
||||
*
|
||||
* @param webhookSecret the webhook secret
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder webhookSecret(String webhookSecret) {
|
||||
delegateBuilder.webhookSecret(webhookSecret);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the base URL.
|
||||
*
|
||||
* @param baseUrl the base URL
|
||||
* @return this builder for method chaining
|
||||
*/
|
||||
public Builder baseUrl(String baseUrl) {
|
||||
delegateBuilder.baseUrl(baseUrl);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the wrapped OpenAI client.
|
||||
*
|
||||
* @return a new wrapped OpenAI client
|
||||
*/
|
||||
public WrappedOpenAIClient build() {
|
||||
return new WrappedOpenAIClient(delegateBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new builder for constructing wrapped OpenAI clients.
|
||||
*
|
||||
* @return a new builder instance
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapped OpenAI client from environment variables.
|
||||
*
|
||||
* @return a new wrapped OpenAI client configured from environment variables
|
||||
*/
|
||||
public static WrappedOpenAIClient fromEnv() {
|
||||
return builder().fromEnv().build();
|
||||
}
|
||||
}
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.models.responses.Response;
|
||||
import com.openai.models.responses.ResponseCreateParams;
|
||||
import com.openai.models.responses.StructuredResponse;
|
||||
import com.openai.models.responses.StructuredResponseCreateParams;
|
||||
import com.openai.core.RequestOptions;
|
||||
import com.openai.core.http.StreamResponse;
|
||||
import com.openai.models.responses.ResponseStreamEvent;
|
||||
import com.openai.services.blocking.ResponseService;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.function.Consumer;
|
||||
import com.openai.core.ClientOptions;
|
||||
|
||||
/**
|
||||
* Wrapped ResponseService that adds OpenTelemetry tracing to response operations.
|
||||
*/
|
||||
class WrappedResponseService implements ResponseService {
|
||||
|
||||
private final ResponseService delegate;
|
||||
|
||||
WrappedResponseService(ResponseService delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseService.WithRawResponse withRawResponse() {
|
||||
return delegate.withRawResponse();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseService withOptions(Consumer<ClientOptions.Builder> options) {
|
||||
return new WrappedResponseService(delegate.withOptions(options));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.responses.InputItemService inputItems() {
|
||||
return delegate.inputItems();
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.openai.services.blocking.responses.InputTokenService inputTokens() {
|
||||
return delegate.inputTokens();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create() {
|
||||
return create((ResponseCreateParams) null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(RequestOptions requestOptions) {
|
||||
return create((ResponseCreateParams) null, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(ResponseCreateParams params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response create(ResponseCreateParams params, RequestOptions requestOptions) {
|
||||
// Extract model from params
|
||||
String model = params != null && params.model() != null
|
||||
? params.model().toString()
|
||||
: "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null) {
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
Response result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// Extract response model (simplified - just use the request model)
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
// Extract usage information from result
|
||||
if (result.usage().isPresent()) {
|
||||
com.openai.models.responses.ResponseUsage usage = result.usage().get();
|
||||
TracingUtils.setResponseAttributes(span,
|
||||
(long) usage.inputTokens(),
|
||||
(long) usage.outputTokens(),
|
||||
(long) usage.totalTokens());
|
||||
}
|
||||
|
||||
if (result.status() != null) {
|
||||
span.setAttribute("gen_ai.response.status", result.status().toString());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params) {
|
||||
return create(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> StructuredResponse<T> create(StructuredResponseCreateParams<T> params,
|
||||
RequestOptions requestOptions) {
|
||||
// Get model from rawParams
|
||||
String model = params != null && params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null && params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StructuredResponse<T> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.create(params);
|
||||
} else {
|
||||
result = delegate.create(params, requestOptions);
|
||||
}
|
||||
|
||||
// Extract response model (simplified - just use the request model)
|
||||
TracingUtils.setResponseMetadata(span, model, null);
|
||||
|
||||
if (result.usage().isPresent()) {
|
||||
com.openai.models.responses.ResponseUsage usage = result.usage().get();
|
||||
TracingUtils.setResponseAttributes(span,
|
||||
(long) usage.inputTokens(),
|
||||
(long) usage.outputTokens(),
|
||||
(long) usage.totalTokens());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming() {
|
||||
return createStreaming((ResponseCreateParams) null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(RequestOptions requestOptions) {
|
||||
return createStreaming((ResponseCreateParams) null, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(ResponseCreateParams params,
|
||||
RequestOptions requestOptions) {
|
||||
String model = params != null && params.model() != null
|
||||
? params.model().toString()
|
||||
: "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
span.setAttribute("gen_ai.streaming", true);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null) {
|
||||
Double temperature = params.temperature().orElse(null);
|
||||
Double topP = params.topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StreamResponse<ResponseStreamEvent> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
// Note: For streaming, the span will end immediately
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(
|
||||
StructuredResponseCreateParams<?> params) {
|
||||
return createStreaming(params, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> createStreaming(
|
||||
StructuredResponseCreateParams<?> params, RequestOptions requestOptions) {
|
||||
// Get model from rawParams
|
||||
String model = params != null && params.rawParams() != null
|
||||
&& params.rawParams().model() != null
|
||||
? params.rawParams().model().toString()
|
||||
: "unknown";
|
||||
|
||||
Span span = TracingUtils.createSpanBuilder(model, "response")
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set request attributes (core attributes already set on builder)
|
||||
TracingUtils.setRequestAttributes(span, model);
|
||||
span.setAttribute("gen_ai.streaming", true);
|
||||
|
||||
// Set request parameters if available (handle Optional types)
|
||||
if (params != null && params.rawParams() != null) {
|
||||
Double temperature = params.rawParams().temperature().orElse(null);
|
||||
Double topP = params.rawParams().topP().orElse(null);
|
||||
TracingUtils.setRequestParameters(span, temperature, topP, null);
|
||||
}
|
||||
|
||||
StreamResponse<ResponseStreamEvent> result;
|
||||
// If requestOptions is null, use the single-parameter version
|
||||
if (requestOptions == null) {
|
||||
result = delegate.createStreaming(params);
|
||||
} else {
|
||||
result = delegate.createStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
TracingUtils.recordException(span, e);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate other methods without tracing (for now)
|
||||
@Override
|
||||
public Response retrieve(String responseId) {
|
||||
return delegate.retrieve(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(String responseId, RequestOptions requestOptions) {
|
||||
return delegate.retrieve(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieve(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieve(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response retrieve(com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieve(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId) {
|
||||
return delegate.retrieveStreaming(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieveStreaming(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(String responseId,
|
||||
com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
com.openai.models.responses.ResponseRetrieveParams params) {
|
||||
return delegate.retrieveStreaming(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamResponse<ResponseStreamEvent> retrieveStreaming(
|
||||
com.openai.models.responses.ResponseRetrieveParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.retrieveStreaming(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId) {
|
||||
delegate.delete(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId, RequestOptions requestOptions) {
|
||||
delegate.delete(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId,
|
||||
com.openai.models.responses.ResponseDeleteParams params) {
|
||||
delegate.delete(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String responseId,
|
||||
com.openai.models.responses.ResponseDeleteParams params,
|
||||
RequestOptions requestOptions) {
|
||||
delegate.delete(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(com.openai.models.responses.ResponseDeleteParams params) {
|
||||
delegate.delete(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(com.openai.models.responses.ResponseDeleteParams params,
|
||||
RequestOptions requestOptions) {
|
||||
delegate.delete(params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId) {
|
||||
return delegate.cancel(responseId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId, RequestOptions requestOptions) {
|
||||
return delegate.cancel(responseId, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId,
|
||||
com.openai.models.responses.ResponseCancelParams params) {
|
||||
return delegate.cancel(responseId, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(String responseId,
|
||||
com.openai.models.responses.ResponseCancelParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.cancel(responseId, params, requestOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(com.openai.models.responses.ResponseCancelParams params) {
|
||||
return delegate.cancel(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response cancel(com.openai.models.responses.ResponseCancelParams params,
|
||||
RequestOptions requestOptions) {
|
||||
return delegate.cancel(params, requestOptions);
|
||||
}
|
||||
}
|
||||
|
||||
+446
@@ -0,0 +1,446 @@
|
||||
package com.langchain.smith.wrappers.openai.examples;
|
||||
|
||||
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig;
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import com.openai.models.chat.completions.ChatCompletion;
|
||||
import com.openai.models.chat.completions.ChatCompletionCreateParams;
|
||||
import com.openai.models.ChatModel;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
import io.opentelemetry.context.Scope;
|
||||
|
||||
/**
|
||||
* Example demonstrating manual OpenTelemetry span creation for LangSmith
|
||||
* tracing.
|
||||
*
|
||||
* <p>
|
||||
* This example shows how to manually create spans with proper gen_ai attributes
|
||||
* that will be correctly interpreted by LangSmith. It demonstrates:
|
||||
* <ul>
|
||||
* <li>Single span LLM invocation wrapped in a manually created span</li>
|
||||
* <li>Nested spans with parent-child relationships (root with two
|
||||
* children)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The spans created here follow the LangSmith OTEL conventions documented in
|
||||
* LANGSMITH_OTEL.md, ensuring proper mapping to LangSmith runs.
|
||||
*/
|
||||
public class ManualSpansExample {
|
||||
|
||||
private static final String INSTRUMENTATION_NAME = "langsmith-java-otel-wrappers";
|
||||
|
||||
/**
|
||||
* Main method demonstrating manual span creation.
|
||||
*
|
||||
* <p>
|
||||
* Prerequisites:
|
||||
* <ol>
|
||||
* <li>Set OPENAI_API_KEY environment variable</li>
|
||||
* <li>Set LANGSMITH_API_KEY environment variable (your LangSmith API key)</li>
|
||||
* <li>Optionally set LANGSMITH_PROJECT environment variable</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param args command line arguments (not used)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// Configure OpenTelemetry for LangSmith with batch size 1 for immediate export
|
||||
// This eliminates the need for sleep delays before flush
|
||||
System.out.println("Configuring OpenTelemetry for LangSmith...");
|
||||
try {
|
||||
String apiKey = System.getenv("LANGSMITH_API_KEY");
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
throw new IllegalStateException("LANGSMITH_API_KEY environment variable is required");
|
||||
}
|
||||
String projectName = System.getenv("LANGSMITH_PROJECT");
|
||||
String serviceName = System.getenv("OTEL_SERVICE_NAME");
|
||||
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
|
||||
|
||||
// Option 1: Use SimpleSpanProcessor for synchronous, immediate export
|
||||
// This ensures spans are sent immediately without buffering
|
||||
// SimpleSpanProcessor blocks on span.end() until export completes
|
||||
OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName, endpoint,
|
||||
OpenTelemetryConfig.SpanProcessorType.SIMPLE, 1);
|
||||
System.out.println("✓ OpenTelemetry configured with SimpleSpanProcessor (immediate export)\n");
|
||||
|
||||
// Option 2: Use BatchSpanProcessor with batch size = 1 for non-blocking
|
||||
// immediate export
|
||||
// Uncomment this line and comment Option 1 above to test BatchSpanProcessor
|
||||
// This is non-blocking but still exports immediately due to batch size = 1
|
||||
// OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName,
|
||||
// endpoint,
|
||||
// OpenTelemetryConfig.SpanProcessorType.BATCH, 1);
|
||||
// System.out.println("✓ OpenTelemetry configured with BatchSpanProcessor
|
||||
// (batch size = 1)\n");
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("✗ Error configuring OpenTelemetry: " + e.getMessage());
|
||||
System.err.println("\nPlease set the following environment variables:");
|
||||
System.err.println(" - LANGSMITH_API_KEY (required)");
|
||||
System.err.println(" - LANGSMITH_PROJECT (optional)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get OpenAI API key
|
||||
String openaiApiKey = System.getenv("OPENAI_API_KEY");
|
||||
if (openaiApiKey == null || openaiApiKey.isEmpty()) {
|
||||
System.err.println("✗ OPENAI_API_KEY environment variable is required");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create OpenAI client (not wrapped - we'll create spans manually)
|
||||
OpenAIClient client = OpenAIOkHttpClient.builder()
|
||||
.apiKey(openaiApiKey)
|
||||
.build();
|
||||
|
||||
try {
|
||||
// Example 1: Single span LLM invocation
|
||||
System.out.println(repeatString("=", 60));
|
||||
System.out.println("Example 1: Single Span LLM Invocation");
|
||||
System.out.println(repeatString("=", 60));
|
||||
exampleSingleSpan(client);
|
||||
|
||||
// Example 2: Nested spans (root with two children)
|
||||
System.out.println("\n" + repeatString("=", 60));
|
||||
System.out.println("Example 2: Nested Spans (Root with Two Children)");
|
||||
System.out.println(repeatString("=", 60));
|
||||
exampleNestedSpans(client);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("✗ Error during execution: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
// Close the client
|
||||
client.close();
|
||||
|
||||
System.out.println("\n" + repeatString("=", 60));
|
||||
boolean flushed = OpenTelemetryConfig.flush(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!flushed) {
|
||||
System.err.println("✗ Warning: Flush did not complete successfully");
|
||||
System.err.println(" Some spans may not have been exported to LangSmith");
|
||||
} else {
|
||||
System.out.println("✓ Spans flushed successfully");
|
||||
}
|
||||
|
||||
System.out.println("\n✓ Check your LangSmith dashboard to see the traces!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 1: Single span LLM invocation.
|
||||
*
|
||||
* <p>
|
||||
* Creates a single span wrapping an OpenAI chat completion call with all
|
||||
* required gen_ai attributes according to LangSmith conventions.
|
||||
*/
|
||||
private static void exampleSingleSpan(OpenAIClient client) {
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME);
|
||||
|
||||
// Create span builder for LLM operation
|
||||
Span span = tracer.spanBuilder("chat gpt-4o-mini")
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.startSpan();
|
||||
|
||||
try (Scope scope = span.makeCurrent()) {
|
||||
// Set core gen_ai attributes for LLM type detection
|
||||
span.setAttribute("gen_ai.system", "openai");
|
||||
span.setAttribute("gen_ai.operation.name", "chat");
|
||||
span.setAttribute("gen_ai.provider.name", "openai");
|
||||
|
||||
// Create chat completion request
|
||||
String userMessage = "What is the capital of France?";
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model(ChatModel.GPT_4O_MINI)
|
||||
.addUserMessage(userMessage)
|
||||
.temperature(0.7)
|
||||
.build();
|
||||
|
||||
// Set request attributes
|
||||
span.setAttribute("gen_ai.request.model", "gpt-4o-mini");
|
||||
span.setAttribute("gen_ai.request.temperature", 0.7);
|
||||
|
||||
// Set input messages as JSON array (following LangSmith format)
|
||||
String inputMessagesJson = String.format(
|
||||
"[{\"role\":\"user\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(userMessage));
|
||||
span.setAttribute("gen_ai.input.messages", inputMessagesJson);
|
||||
|
||||
System.out.println("Making OpenAI API call...");
|
||||
// Make the actual API call
|
||||
ChatCompletion completion = client.chat().completions().create(params);
|
||||
|
||||
// Extract response data
|
||||
String assistantContent = completion.choices().get(0).message().content()
|
||||
.orElse("No content");
|
||||
String finishReason = completion.choices().get(0).finishReason() != null
|
||||
? completion.choices().get(0).finishReason().toString()
|
||||
: "stop";
|
||||
|
||||
// Set response attributes
|
||||
String responseModel = completion.model().toString();
|
||||
span.setAttribute("gen_ai.response.model", responseModel);
|
||||
span.setAttribute("gen_ai.response.finish_reason", finishReason);
|
||||
|
||||
// Set output messages as JSON array
|
||||
String outputMessagesJson = String.format(
|
||||
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(assistantContent));
|
||||
span.setAttribute("gen_ai.output.messages", outputMessagesJson);
|
||||
|
||||
// Set token usage attributes
|
||||
completion.usage().ifPresent(usage -> {
|
||||
span.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
|
||||
span.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
|
||||
span.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
|
||||
});
|
||||
|
||||
System.out.println("Response: " + assistantContent);
|
||||
completion.usage().ifPresent(usage -> {
|
||||
System.out.println("Tokens - Input: " + usage.promptTokens()
|
||||
+ ", Output: " + usage.completionTokens()
|
||||
+ ", Total: " + usage.totalTokens());
|
||||
});
|
||||
|
||||
System.out.println("✓ Single span created successfully");
|
||||
|
||||
} catch (Exception e) {
|
||||
// Record exception on span
|
||||
span.recordException(e);
|
||||
span.setAttribute("error", true);
|
||||
throw e;
|
||||
} finally {
|
||||
span.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example 2: Nested spans (root with two children).
|
||||
*
|
||||
* <p>
|
||||
* Creates a root span representing a workflow/chain, with two child spans
|
||||
* representing individual LLM calls. This demonstrates parent-child
|
||||
* relationships in LangSmith.
|
||||
*/
|
||||
private static void exampleNestedSpans(OpenAIClient client) {
|
||||
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
Tracer tracer = openTelemetry.getTracer(INSTRUMENTATION_NAME);
|
||||
|
||||
// Create root span (chain/workflow)
|
||||
Span rootSpan = tracer.spanBuilder("nested_spans_chain")
|
||||
.setSpanKind(SpanKind.INTERNAL)
|
||||
.setAttribute("gen_ai.operation.name", "nested_spans")
|
||||
.setAttribute("langsmith.span.kind", "chain")
|
||||
.setAttribute("langsmith.trace.name", "nested_spans_chain_" + System.currentTimeMillis())
|
||||
.startSpan();
|
||||
|
||||
try (Scope rootScope = rootSpan.makeCurrent()) {
|
||||
// Set attributes for root span (chain type)
|
||||
|
||||
System.out.println("Creating root span: nested_spans_chain");
|
||||
System.out.println("Root Span ID: " + rootSpan.getSpanContext().getSpanId());
|
||||
System.out.println("Root Trace ID: " + rootSpan.getSpanContext().getTraceId());
|
||||
|
||||
// Child span 1: First LLM call
|
||||
System.out.println("\n--- Child Span 1: First Query ---");
|
||||
Span childSpan1 = tracer.spanBuilder("chat gpt-4o-mini (step 1)")
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.operation.name", "chat")
|
||||
.setAttribute("langsmith.span.kind", "llm")
|
||||
.setAttribute("langsmith.trace.name", "First LLM Call")
|
||||
.startSpan();
|
||||
|
||||
try (Scope childScope1 = childSpan1.makeCurrent()) {
|
||||
// Set LLM attributes for child span 1
|
||||
childSpan1.setAttribute("gen_ai.system", "openai");
|
||||
childSpan1.setAttribute("gen_ai.operation.name", "chat");
|
||||
childSpan1.setAttribute("gen_ai.provider.name", "openai");
|
||||
childSpan1.setAttribute("gen_ai.request.model", "gpt-4o-mini");
|
||||
childSpan1.setAttribute("gen_ai.request.temperature", 0.7);
|
||||
|
||||
String query1 = "What is the capital of France?";
|
||||
ChatCompletionCreateParams params1 = ChatCompletionCreateParams.builder()
|
||||
.model(ChatModel.GPT_4O_MINI)
|
||||
.addUserMessage(query1)
|
||||
.temperature(0.7)
|
||||
.build();
|
||||
|
||||
String inputMessages1 = String.format(
|
||||
"[{\"role\":\"user\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(query1));
|
||||
childSpan1.setAttribute("gen_ai.input.messages", inputMessages1);
|
||||
|
||||
ChatCompletion completion1 = client.chat().completions().create(params1);
|
||||
String response1 = completion1.choices().get(0).message().content()
|
||||
.orElse("No content");
|
||||
|
||||
childSpan1.setAttribute("gen_ai.response.model", completion1.model().toString());
|
||||
String outputMessages1 = String.format(
|
||||
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(response1));
|
||||
childSpan1.setAttribute("gen_ai.output.messages", outputMessages1);
|
||||
|
||||
completion1.usage().ifPresent(usage -> {
|
||||
childSpan1.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
|
||||
childSpan1.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
|
||||
childSpan1.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
|
||||
});
|
||||
|
||||
System.out.println("Query 1: " + query1);
|
||||
System.out.println("Response 1: " + response1);
|
||||
System.out.println("Child Span 1 ID: " + childSpan1.getSpanContext().getSpanId());
|
||||
System.out.println("Child Span 1 Parent: " + rootSpan.getSpanContext().getSpanId());
|
||||
} catch (Exception e) {
|
||||
childSpan1.recordException(e);
|
||||
childSpan1.setAttribute("error", true);
|
||||
throw e;
|
||||
} finally {
|
||||
childSpan1.end();
|
||||
}
|
||||
|
||||
// Child span 2: Second LLM call
|
||||
System.out.println("\n--- Child Span 2: Follow-up Query ---");
|
||||
Span childSpan2 = tracer.spanBuilder("chat gpt-4o-mini (step 2)")
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.operation.name", "chat")
|
||||
.setAttribute("langsmith.span.kind", "llm")
|
||||
.setAttribute("langsmith.trace.name", "Second LLM Call")
|
||||
.startSpan();
|
||||
|
||||
try (Scope childScope2 = childSpan2.makeCurrent()) {
|
||||
// Set LLM attributes for child span 2
|
||||
childSpan2.setAttribute("gen_ai.system", "openai");
|
||||
childSpan2.setAttribute("gen_ai.operation.name", "chat");
|
||||
childSpan2.setAttribute("gen_ai.provider.name", "openai");
|
||||
childSpan2.setAttribute("gen_ai.request.model", "gpt-4o-mini");
|
||||
childSpan2.setAttribute("gen_ai.request.temperature", 0.7);
|
||||
|
||||
String query2 = "What is the population of that city?";
|
||||
ChatCompletionCreateParams params2 = ChatCompletionCreateParams.builder()
|
||||
.model(ChatModel.GPT_4O_MINI)
|
||||
.addUserMessage(query2)
|
||||
.temperature(0.7)
|
||||
.build();
|
||||
|
||||
String inputMessages2 = String.format(
|
||||
"[{\"role\":\"user\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(query2));
|
||||
childSpan2.setAttribute("gen_ai.input.messages", inputMessages2);
|
||||
|
||||
ChatCompletion completion2 = client.chat().completions().create(params2);
|
||||
String response2 = completion2.choices().get(0).message().content()
|
||||
.orElse("No content");
|
||||
|
||||
childSpan2.setAttribute("gen_ai.response.model", completion2.model().toString());
|
||||
String outputMessages2 = String.format(
|
||||
"[{\"role\":\"assistant\",\"content\":\"%s\"}]",
|
||||
escapeJsonString(response2));
|
||||
childSpan2.setAttribute("gen_ai.output.messages", outputMessages2);
|
||||
|
||||
completion2.usage().ifPresent(usage -> {
|
||||
childSpan2.setAttribute("gen_ai.usage.input_tokens", (long) usage.promptTokens());
|
||||
childSpan2.setAttribute("gen_ai.usage.output_tokens", (long) usage.completionTokens());
|
||||
childSpan2.setAttribute("gen_ai.usage.total_tokens", (long) usage.totalTokens());
|
||||
});
|
||||
|
||||
System.out.println("Query 2: " + query2);
|
||||
System.out.println("Response 2: " + response2);
|
||||
System.out.println("Child Span 2 ID: " + childSpan2.getSpanContext().getSpanId());
|
||||
System.out.println("Child Span 2 Parent: " + rootSpan.getSpanContext().getSpanId());
|
||||
|
||||
} catch (Exception e) {
|
||||
childSpan2.recordException(e);
|
||||
childSpan2.setAttribute("error", true);
|
||||
throw e;
|
||||
} finally {
|
||||
childSpan2.end();
|
||||
}
|
||||
|
||||
// Child span 3: Tool call
|
||||
System.out.println("\n--- Child Span 3: Tool call ---");
|
||||
Span childSpan3 = tracer.spanBuilder("tool_call")
|
||||
.setSpanKind(SpanKind.CLIENT)
|
||||
.setAttribute("gen_ai.operation.name", "tool_call")
|
||||
.setAttribute("langsmith.span.kind", "tool")
|
||||
.setAttribute("langsmith.trace.name", "Tool Call")
|
||||
.startSpan();
|
||||
|
||||
try (Scope childScope3 = childSpan3.makeCurrent()) {
|
||||
// Set tool call attributes for child span 3
|
||||
childSpan3.setAttribute("gen_ai.system", "openai");
|
||||
childSpan3.setAttribute("gen_ai.operation.name", "tool_call");
|
||||
childSpan3.setAttribute("gen_ai.provider.name", "openai");
|
||||
childSpan3.setAttribute("gen_ai.request.model", "gpt-4o-mini");
|
||||
childSpan3.setAttribute("gen_ai.request.temperature", 0.7);
|
||||
|
||||
String toolName = "get_weather";
|
||||
String toolDescription = "Get the weather for a given city";
|
||||
String toolArguments = "{\"city\":\"Paris\"}";
|
||||
|
||||
childSpan3.setAttribute("gen_ai.tool.name", toolName);
|
||||
childSpan3.setAttribute("gen_ai.tool.description", toolDescription);
|
||||
childSpan3.setAttribute("gen_ai.tool.arguments", toolArguments);
|
||||
|
||||
System.out.println("Tool Name: " + toolName);
|
||||
System.out.println("Tool Description: " + toolDescription);
|
||||
System.out.println("Tool Arguments: " + toolArguments);
|
||||
System.out.println("Child Span 3 ID: " + childSpan3.getSpanContext().getSpanId());
|
||||
System.out.println("Child Span 3 Parent: " + rootSpan.getSpanContext().getSpanId());
|
||||
} catch (Exception e) {
|
||||
childSpan3.recordException(e);
|
||||
childSpan3.setAttribute("error", true);
|
||||
throw e;
|
||||
} finally {
|
||||
childSpan3.end();
|
||||
}
|
||||
|
||||
System.out.println("\n✓ Nested spans created successfully");
|
||||
System.out.println(" - Root span: multi-step-query (chain)");
|
||||
System.out.println(" - Child span 1: First LLM call");
|
||||
System.out.println(" - Child span 2: Second LLM call");
|
||||
System.out.println("\nAll spans ended, will be exported on flush...");
|
||||
|
||||
} catch (Exception e) {
|
||||
rootSpan.recordException(e);
|
||||
rootSpan.setAttribute("error", true);
|
||||
throw e;
|
||||
} finally {
|
||||
rootSpan.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeats a string a given number of times (Java 8 compatible replacement for String.repeat()).
|
||||
*
|
||||
* @param str the string to repeat
|
||||
* @param count the number of times to repeat
|
||||
* @return the repeated string
|
||||
*/
|
||||
private static String repeatString(String str, int count) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a JSON string by replacing special characters.
|
||||
*
|
||||
* @param str the string to escape
|
||||
* @return the escaped string
|
||||
*/
|
||||
private static String escapeJsonString(String str) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
}
|
||||
return str.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
package com.langchain.smith.wrappers.openai.examples;
|
||||
|
||||
import com.langchain.smith.wrappers.openai.OpenTelemetryConfig;
|
||||
import com.langchain.smith.wrappers.openai.WrappedOpenAIClient;
|
||||
import com.langchain.smith.wrappers.openai.OpenAIWrappers;
|
||||
import com.openai.models.chat.completions.ChatCompletion;
|
||||
import com.openai.models.chat.completions.ChatCompletionCreateParams;
|
||||
import com.openai.models.chat.completions.ChatCompletionFunctionTool;
|
||||
import com.openai.models.chat.completions.ChatCompletionTool;
|
||||
import com.openai.models.chat.completions.ChatCompletionToolChoiceOption;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageToolCall;
|
||||
import com.openai.models.chat.completions.ChatCompletionMessageFunctionToolCall;
|
||||
import com.openai.models.FunctionDefinition;
|
||||
import com.openai.models.FunctionParameters;
|
||||
import com.openai.models.ChatModel;
|
||||
import com.openai.core.JsonValue;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import io.opentelemetry.api.OpenTelemetry;
|
||||
import io.opentelemetry.api.trace.Span;
|
||||
import io.opentelemetry.api.trace.SpanKind;
|
||||
import io.opentelemetry.api.trace.Tracer;
|
||||
import io.opentelemetry.context.Scope;
|
||||
import java.util.Arrays;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Example demonstrating how to configure OpenTelemetry to send traces to
|
||||
* LangSmith.
|
||||
*
|
||||
* <p>
|
||||
* This example shows how to:
|
||||
* <ul>
|
||||
* <li>Configure OpenTelemetry to export traces to LangSmith</li>
|
||||
* <li>Use the wrapped OpenAI client with automatic tracing</li>
|
||||
* <li>Traces will appear in your LangSmith dashboard</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class WithLangSmithExample {
|
||||
|
||||
/**
|
||||
* Main method demonstrating LangSmith integration.
|
||||
*
|
||||
* <p>
|
||||
* Prerequisites:
|
||||
* <ol>
|
||||
* <li>Set OPENAI_API_KEY environment variable</li>
|
||||
* <li>Set LANGSMITH_API_KEY environment variable (your LangSmith API key)</li>
|
||||
* <li>Optionally set LANGSMITH_PROJECT environment variable</li>
|
||||
* <li>Optionally set LANGSMITH_OTLP_ENDPOINT environment variable to override
|
||||
* the default endpoint</li>
|
||||
* <li>Optionally set LANGSMITH_DEBUG=true environment variable for detailed
|
||||
* logging</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param args command line arguments (not used)
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
// Configure OpenTelemetry for LangSmith with SimpleSpanProcessor for immediate
|
||||
// export
|
||||
// This ensures spans are sent immediately without buffering
|
||||
// To enable debug logging, set LANGSMITH_DEBUG=true environment variable
|
||||
System.out.println("Configuring OpenTelemetry for LangSmith...");
|
||||
try {
|
||||
String apiKey = System.getenv("LANGSMITH_API_KEY");
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
throw new IllegalStateException("LANGSMITH_API_KEY environment variable is required");
|
||||
}
|
||||
String projectName = System.getenv("LANGSMITH_PROJECT");
|
||||
String serviceName = System.getenv("OTEL_SERVICE_NAME");
|
||||
String endpoint = System.getenv("LANGSMITH_OTLP_ENDPOINT");
|
||||
|
||||
// Option 1: Use SimpleSpanProcessor for synchronous, immediate export
|
||||
// This ensures spans are sent immediately without buffering
|
||||
// SimpleSpanProcessor blocks on span.end() until export completes
|
||||
// This is the recommended configuration for examples and short-lived
|
||||
// applications
|
||||
OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName, endpoint,
|
||||
OpenTelemetryConfig.SpanProcessorType.SIMPLE, 1);
|
||||
System.out.println("✓ OpenTelemetry configured with SimpleSpanProcessor (immediate export)\n");
|
||||
|
||||
// Option 2: Use BatchSpanProcessor with batch size = 1 for non-blocking
|
||||
// immediate export
|
||||
// Uncomment this line and comment the SimpleSpanProcessor configuration above
|
||||
// to test
|
||||
// This is non-blocking but still exports immediately due to batch size = 1
|
||||
// OpenTelemetryConfig.configureForLangSmith(apiKey, projectName, serviceName,
|
||||
// endpoint,
|
||||
// OpenTelemetryConfig.SpanProcessorType.BATCH, 1);
|
||||
// System.out.println("✓ OpenTelemetry configured with BatchSpanProcessor (batch
|
||||
// size = 1)\n");
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("✗ Error configuring OpenTelemetry: " + e.getMessage());
|
||||
System.err.println("\nPlease set the following environment variables:");
|
||||
System.err.println(" - LANGSMITH_API_KEY (required)");
|
||||
System.err.println(" - LANGSMITH_PROJECT (optional)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Now use the wrapped client - all API calls will automatically create spans
|
||||
// that are sent to LangSmith
|
||||
WrappedOpenAIClient client = OpenAIWrappers.wrapFromEnv();
|
||||
|
||||
// Create a chat completion request with tool definitions
|
||||
// This will trigger tool calls when the model needs to use the get_weather tool
|
||||
// Build function parameters as JSON schema
|
||||
Map<String, JsonValue> properties = new HashMap<>();
|
||||
Map<String, JsonValue> locationProperty = new HashMap<>();
|
||||
locationProperty.put("type", JsonValue.from("string"));
|
||||
locationProperty.put("description",
|
||||
JsonValue.from("The location to get weather for, e.g., 'Paris arrondissement 9'"));
|
||||
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")));
|
||||
|
||||
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
|
||||
.model(ChatModel.GPT_4O_MINI)
|
||||
.addUserMessage("What is the capital of France and what was the temperature there today?")
|
||||
.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();
|
||||
|
||||
// Create a parent span (chain/workflow) that wraps the entire operation
|
||||
OpenTelemetry openTelemetry = io.opentelemetry.api.GlobalOpenTelemetry.get();
|
||||
Tracer tracer = openTelemetry.getTracer("langsmith-java-otel-wrappers");
|
||||
|
||||
Span parentSpan = tracer.spanBuilder("with_langsmith_example_workflow")
|
||||
.setSpanKind(SpanKind.INTERNAL)
|
||||
.setAttribute("gen_ai.operation.name", "chat_completion_workflow")
|
||||
.setAttribute("langsmith.span.kind", "chain")
|
||||
.setAttribute("langsmith.trace.name", "with_langsmith_example_workflow")
|
||||
.startSpan();
|
||||
|
||||
try (Scope parentScope = parentSpan.makeCurrent()) {
|
||||
System.out.println("\n" + repeatString("=", 60));
|
||||
System.out.println("Workflow: Chat Completion with Tool Call");
|
||||
System.out.println(repeatString("=", 60));
|
||||
System.out.println("Parent Span ID: " + parentSpan.getSpanContext().getSpanId());
|
||||
System.out.println("Parent Trace ID: " + parentSpan.getSpanContext().getTraceId());
|
||||
|
||||
// Execute the request - this will automatically create an OpenTelemetry span
|
||||
// that gets sent to LangSmith (as a child of the parent span)
|
||||
System.out.println("\nSending chat completion request (traces will be sent to LangSmith)...");
|
||||
ChatCompletion completion = client.chat().completions().create(params);
|
||||
|
||||
// Check if the response contains tool calls
|
||||
com.openai.models.chat.completions.ChatCompletionMessage message = completion.choices().get(0).message();
|
||||
java.util.Optional<java.util.List<ChatCompletionMessageToolCall>> toolCallsOpt = message.toolCalls();
|
||||
|
||||
if (toolCallsOpt.isPresent() && !toolCallsOpt.get().isEmpty()) {
|
||||
System.out.println("\n--- Tool calls detected, executing tools ---");
|
||||
java.util.List<ChatCompletionMessageToolCall> toolCalls = toolCallsOpt.get();
|
||||
|
||||
// Build messages list for follow-up request
|
||||
List<com.openai.models.chat.completions.ChatCompletionMessageParam> messages = new ArrayList<>();
|
||||
|
||||
// Add original user message
|
||||
messages.add(params.messages().get(0));
|
||||
|
||||
// Add assistant message with tool calls
|
||||
messages.add(com.openai.models.chat.completions.ChatCompletionMessageParam.ofAssistant(
|
||||
com.openai.models.chat.completions.ChatCompletionAssistantMessageParam.builder()
|
||||
.content(message.content().orElse(""))
|
||||
.toolCalls(toolCalls)
|
||||
.build()));
|
||||
|
||||
// Execute each tool call and add results
|
||||
for (ChatCompletionMessageToolCall toolCall : toolCalls) {
|
||||
if (toolCall.isFunction()) {
|
||||
ChatCompletionMessageFunctionToolCall functionToolCall = toolCall.asFunction();
|
||||
String toolName = functionToolCall.function().name();
|
||||
String toolArguments = functionToolCall.function().arguments();
|
||||
|
||||
System.out.println("Executing tool: " + toolName + " with arguments: " + toolArguments);
|
||||
|
||||
// Execute the tool
|
||||
String toolResult = executeTool(toolName, toolArguments);
|
||||
|
||||
System.out.println("Tool result: " + toolResult);
|
||||
|
||||
// Add tool result message
|
||||
messages.add(com.openai.models.chat.completions.ChatCompletionMessageParam.ofTool(
|
||||
com.openai.models.chat.completions.ChatCompletionToolMessageParam.builder()
|
||||
.toolCallId(functionToolCall.id())
|
||||
.content(toolResult)
|
||||
.build()));
|
||||
}
|
||||
}
|
||||
|
||||
// Send follow-up request with tool results
|
||||
System.out.println("\nSending follow-up request with tool results...");
|
||||
ChatCompletionCreateParams followUpParams = ChatCompletionCreateParams.builder()
|
||||
.model(ChatModel.GPT_4O_MINI)
|
||||
.messages(messages)
|
||||
.tools(params.tools().orElse(null))
|
||||
.build();
|
||||
|
||||
completion = client.chat().completions().create(followUpParams);
|
||||
}
|
||||
|
||||
// Display the result
|
||||
String content = completion.choices().get(0).message().content()
|
||||
.orElse("No content");
|
||||
System.out.println("\nFinal Response: " + content);
|
||||
|
||||
// Usage information
|
||||
completion.usage().ifPresent(usage -> {
|
||||
System.out.println("\nToken usage:");
|
||||
System.out.println(" Input tokens: " + usage.promptTokens());
|
||||
System.out.println(" Output tokens: " + usage.completionTokens());
|
||||
System.out.println(" Total tokens: " + usage.totalTokens());
|
||||
});
|
||||
|
||||
// Note: Tool call spans are automatically created by WrappedChatService
|
||||
// when tool calls are detected in the response. No manual span creation needed!
|
||||
|
||||
System.out.println("\n✓ Workflow completed successfully");
|
||||
System.out.println(" - Parent span: workflow (chain)");
|
||||
System.out.println(" - Child span: Chat completion (LLM) - automatically created by WrappedChatService");
|
||||
System.out.println(" - Tool call spans: Automatically created if tool calls are present in the response");
|
||||
} catch (Exception e) {
|
||||
parentSpan.recordException(e);
|
||||
parentSpan.setAttribute("error", true);
|
||||
System.err.println("✗ Error in workflow: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
parentSpan.end();
|
||||
System.out.println("\n✓ Parent span ended");
|
||||
}
|
||||
|
||||
// Close the client when done
|
||||
client.close();
|
||||
|
||||
// Force flush to ensure all spans are exported
|
||||
System.out.println("\n" + repeatString("=", 60));
|
||||
boolean flushed = OpenTelemetryConfig.flush(10, java.util.concurrent.TimeUnit.SECONDS);
|
||||
if (!flushed) {
|
||||
System.err.println("✗ Warning: Flush did not complete successfully");
|
||||
System.err.println(" Some spans may not have been exported to LangSmith");
|
||||
} else {
|
||||
System.out.println("✓ Spans flushed successfully");
|
||||
}
|
||||
|
||||
System.out.println("\n✓ Check your LangSmith dashboard to see the traces!");
|
||||
System.out.println(" Endpoint: " + (System.getenv("LANGSMITH_OTLP_ENDPOINT") != null
|
||||
? System.getenv("LANGSMITH_OTLP_ENDPOINT")
|
||||
: OpenTelemetryConfig.LANGSMITH_OTLP_ENDPOINT));
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeats a string a given number of times (Java 8 compatible replacement for
|
||||
* String.repeat()).
|
||||
*
|
||||
* @param str the string to repeat
|
||||
* @param count the number of times to repeat
|
||||
* @return the repeated string
|
||||
*/
|
||||
private static String repeatString(String str, int count) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes 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
|
||||
// In a real implementation, this would call an actual weather API
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("location", location);
|
||||
result.put("temperature", "15°C");
|
||||
result.put("condition", "Sunny");
|
||||
result.put("humidity", "65%");
|
||||
|
||||
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() + "\"}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* LangSmith OpenTelemetry Wrappers for Java.
|
||||
*
|
||||
* <p>
|
||||
* This package provides OpenTelemetry integration wrappers for LangSmith.
|
||||
*/
|
||||
package com.langchain.smith.wrappers;
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.langchain.smith.wrappers.openai;
|
||||
|
||||
import com.openai.client.OpenAIClient;
|
||||
import com.openai.client.okhttp.OpenAIOkHttpClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for the WrappedOpenAIClient wrapper.
|
||||
*/
|
||||
class WrappedOpenAIClientTest {
|
||||
|
||||
@Test
|
||||
void testWrapWithNullClientThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
new WrappedOpenAIClient(null);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWrapDelegatesToOriginalClient() {
|
||||
// Create a mock or use a real client if API key is available
|
||||
// For now, we'll test that the wrapper can be created
|
||||
// In a real scenario, you'd need an API key to test with an actual client
|
||||
|
||||
// Test that builder works
|
||||
WrappedOpenAIClient.Builder builder = WrappedOpenAIClient.builder();
|
||||
assertNotNull(builder);
|
||||
|
||||
// Test that fromEnv() method exists (will fail at runtime if env vars not set, but that's expected)
|
||||
// WrappedOpenAIClient client = WrappedOpenAIClient.fromEnv();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBuilderDelegatesToOpenAIClientBuilder() {
|
||||
WrappedOpenAIClient.Builder builder = WrappedOpenAIClient.builder();
|
||||
|
||||
// Test that builder methods exist and can be chained
|
||||
builder.apiKey("test-key");
|
||||
builder.organization("test-org");
|
||||
builder.project("test-project");
|
||||
builder.baseUrl("https://api.openai.com/v1");
|
||||
|
||||
// Build should succeed (even if API key is invalid)
|
||||
assertDoesNotThrow(() -> builder.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOpenAIWrappersWrapMethod() {
|
||||
// Create a minimal client to wrap
|
||||
OpenAIClient originalClient = OpenAIOkHttpClient.builder()
|
||||
.apiKey("test-key")
|
||||
.build();
|
||||
|
||||
WrappedOpenAIClient wrapped = OpenAIWrappers.wrap(originalClient);
|
||||
assertNotNull(wrapped);
|
||||
assertEquals(originalClient, wrapped.getDelegate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOpenAIWrappersWrapWithNullThrowsException() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
OpenAIWrappers.wrap(null);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Test package for LangSmith OpenTelemetry Wrappers.
|
||||
*/
|
||||
package com.langchain.smith.wrappers;
|
||||
|
||||
Reference in New Issue
Block a user