Skip to content

Commit f3bf340

Browse files
authored
feat: Add traceable function wrapper for LangSmith tracing (#101)
* Adds versioning resource to build * Adds initial version of traceable * Lint * Deflake * Progress * Progress * Fixes * Fixes * More refactor * Small bug * Refactor * Fix * Devin feedback * Tests and feedback * Remove redundant comment * Docstring
1 parent cd9f555 commit f3bf340

13 files changed

Lines changed: 2527 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.langchain.smith.client.okhttp
2+
3+
import com.langchain.smith.client.LangsmithClient
4+
import com.langchain.smith.tracing.LangsmithClientProvider
5+
6+
/** [LangsmithClientProvider] backed by [LangsmithOkHttpClient]. */
7+
class OkHttpLangsmithClientProvider : LangsmithClientProvider {
8+
override fun createClient(): LangsmithClient = LangsmithOkHttpClient.fromEnv()
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.langchain.smith.client.okhttp.OkHttpLangsmithClientProvider
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.langchain.smith.tracing
2+
3+
import com.langchain.smith.client.LangsmithClient
4+
5+
/**
6+
* Service-provider interface for automatic [LangsmithClient] discovery.
7+
*
8+
* When [traceable] (or [traceFunction], etc.) is called without an explicit client in
9+
* [TraceConfig], the runtime uses [java.util.ServiceLoader] to find an implementation of this
10+
* interface on the classpath and calls [createClient] to obtain a default client.
11+
*
12+
* The `langsmith-java-client-okhttp` module registers a provider automatically. Custom HTTP
13+
* backends can supply their own implementation by adding a
14+
* `META-INF/services/com.langchain.smith.tracing.LangsmithClientProvider` file that lists the
15+
* fully-qualified class name of their provider.
16+
*/
17+
fun interface LangsmithClientProvider {
18+
19+
/**
20+
* Creates a [LangsmithClient] configured from environment variables and system properties.
21+
*
22+
* Implementations should throw if the environment is not configured (e.g. missing API key) —
23+
* the caller will catch and treat it as "no client available".
24+
*/
25+
fun createClient(): LangsmithClient
26+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.langchain.smith.tracing
2+
3+
import java.lang.reflect.InvocationTargetException
4+
import org.slf4j.LoggerFactory
5+
6+
/**
7+
* Thread-scoped storage for the current [RunTree].
8+
*
9+
* On Java 21+ this uses `ScopedValue`, which propagates into child tasks forked via
10+
* `StructuredTaskScope`. On older JVMs it falls back to [ThreadLocal]. Neither mechanism
11+
* automatically propagates across unstructured async boundaries like `CompletableFuture` or
12+
* `ExecutorService` — use [withParent] for those cases.
13+
*/
14+
internal interface RunContext {
15+
/** Returns the current [RunTree], or `null` if there is no active run on this thread. */
16+
fun get(): RunTree?
17+
18+
/**
19+
* Executes [block] with [value] as the current run. The previous value is restored when [block]
20+
* completes (normally or exceptionally).
21+
*/
22+
fun <T> runWith(value: RunTree?, block: () -> T): T
23+
24+
companion object {
25+
/** Creates the best available [RunContext] for the current JVM. */
26+
fun create(): RunContext = scopedValueContext ?: ThreadLocalRunContext()
27+
}
28+
}
29+
30+
// ---------------------------------------------------------------------------
31+
// ThreadLocal implementation (Java 8+)
32+
// ---------------------------------------------------------------------------
33+
34+
private class ThreadLocalRunContext : RunContext {
35+
private val threadLocal = ThreadLocal<RunTree?>()
36+
37+
override fun get(): RunTree? = threadLocal.get()
38+
39+
override fun <T> runWith(value: RunTree?, block: () -> T): T {
40+
val previous = threadLocal.get()
41+
threadLocal.set(value)
42+
try {
43+
return block()
44+
} finally {
45+
threadLocal.set(previous)
46+
}
47+
}
48+
}
49+
50+
// ---------------------------------------------------------------------------
51+
// ScopedValue implementation (Java 21+, resolved via reflection)
52+
// ---------------------------------------------------------------------------
53+
54+
private val logger = LoggerFactory.getLogger("com.langchain.smith.tracing.RunContext")
55+
56+
/**
57+
* Attempts to create a [RunContext] backed by `java.lang.ScopedValue`. Returns `null` on JVMs that
58+
* don't have it.
59+
*/
60+
private val scopedValueContext: RunContext? by lazy {
61+
try {
62+
val svClass = Class.forName("java.lang.ScopedValue")
63+
val newValue = svClass.getMethod("newInstance")
64+
val get = svClass.getMethod("get")
65+
val isBound = svClass.getMethod("isBound")
66+
67+
// ScopedValue.where(key, value) returns a ScopedValue.Carrier
68+
val where = svClass.getMethod("where", svClass, Any::class.java)
69+
val carrierClass = Class.forName("java.lang.ScopedValue\$Carrier")
70+
// Carrier.call(Callable) returns T
71+
val carrierCall = carrierClass.getMethod("call", java.util.concurrent.Callable::class.java)
72+
73+
val scopedValue = newValue.invoke(null) // ScopedValue<RunTree?>
74+
75+
object : RunContext {
76+
override fun get(): RunTree? =
77+
try {
78+
if (isBound.invoke(scopedValue) == true) {
79+
get.invoke(scopedValue) as? RunTree
80+
} else {
81+
null
82+
}
83+
} catch (_: InvocationTargetException) {
84+
null
85+
}
86+
87+
override fun <T> runWith(value: RunTree?, block: () -> T): T {
88+
val carrier = where.invoke(null, scopedValue, value)
89+
try {
90+
// Unavoidable: Method.invoke returns Object; the Callable<T> guarantees the
91+
// actual runtime type matches T.
92+
@Suppress("UNCHECKED_CAST")
93+
return carrierCall.invoke(carrier, java.util.concurrent.Callable { block() })
94+
as T
95+
} catch (e: InvocationTargetException) {
96+
throw e.cause ?: e
97+
}
98+
}
99+
}
100+
} catch (_: ReflectiveOperationException) {
101+
logger.debug("ScopedValue not available; using ThreadLocal for run context")
102+
null
103+
}
104+
}

0 commit comments

Comments
 (0)