Skip to content

Commit 8c9d4a6

Browse files
committed
feat(langsmith)!: migrate wrapper to integration
Move the LangSmith instrumentation into braintrust.integrations.langsmith and reduce the legacy wrapper to a setup_langsmith compatibility shim. Add LangSmith to auto_instrument, add a dedicated nox session, and cover the patched traceable and evaluate surfaces with VCR-backed tests plus an auto-test script. BREAKING CHANGE: remove the public LangSmith helper exports wrap_traceable, wrap_client, wrap_evaluate, wrap_aevaluate, get_braintrust_results, and clear_braintrust_results from both braintrust.integrations.langsmith and braintrust.wrappers.langsmith_wrapper. The compatibility wrapper now exposes setup_langsmith only.
1 parent 5c35051 commit 8c9d4a6

17 files changed

+8690
-859
lines changed

py/noxfile.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def _pinned_python_version():
7777
"opentelemetry-exporter-otlp-proto-http",
7878
"google.genai",
7979
"google.adk",
80+
"langsmith",
8081
"temporalio",
8182
)
8283

@@ -104,6 +105,7 @@ def _pinned_python_version():
104105
DSPY_VERSIONS = (LATEST,)
105106
GOOGLE_ADK_VERSIONS = (LATEST, "1.14.1")
106107
LANGCHAIN_VERSIONS = (LATEST, "0.3.28")
108+
LANGSMITH_VERSIONS = (LATEST, "0.7.12")
107109
OPENROUTER_VERSIONS = (LATEST, "0.6.0")
108110
# temporalio 1.19.0+ requires Python >= 3.10; skip Python 3.9 entirely
109111
TEMPORAL_VERSIONS = (LATEST, "1.20.0", "1.19.0")
@@ -235,6 +237,17 @@ def test_langchain(session, version):
235237
_run_core_tests(session)
236238

237239

240+
@nox.session()
241+
@nox.parametrize("version", LANGSMITH_VERSIONS, ids=LANGSMITH_VERSIONS)
242+
def test_langsmith(session, version):
243+
"""Test LangSmith integration."""
244+
_install_test_deps(session)
245+
_install(session, "langsmith", version)
246+
_install(session, "langchain-core")
247+
_install(session, "langchain-openai")
248+
_run_tests(session, f"{INTEGRATION_DIR}/langsmith/test_langsmith.py")
249+
250+
238251
@nox.session()
239252
@nox.parametrize("version", OPENAI_VERSIONS, ids=OPENAI_VERSIONS)
240253
def test_openai(session, version):
@@ -371,9 +384,8 @@ def pylint(session):
371384
session.install("pydantic_ai>=1.10.0")
372385
session.install("google-adk")
373386
session.install("opentelemetry.instrumentation.openai")
374-
# langsmith is needed for the langsmith_wrapper module but not in VENDOR_PACKAGES
375387
# langchain-core, langchain-openai, langchain-anthropic are needed for the langchain integration
376-
session.install("langsmith", "langchain-core", "langchain-openai", "langchain-anthropic")
388+
session.install("langchain-core", "langchain-openai", "langchain-anthropic")
377389

378390
result = session.run("git", "ls-files", "**/*.py", silent=True, log=False)
379391
files = [path for path in result.strip().splitlines() if path not in GENERATED_LINT_EXCLUDES]

py/src/braintrust/auto.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
DSPyIntegration,
1717
GoogleGenAIIntegration,
1818
LangChainIntegration,
19+
LangSmithIntegration,
1920
LiteLLMIntegration,
2021
OpenRouterIntegration,
2122
PydanticAIIntegration,
@@ -52,6 +53,7 @@ def auto_instrument(
5253
dspy: bool = True,
5354
adk: bool = True,
5455
langchain: bool = True,
56+
langsmith: bool = True,
5557
) -> dict[str, bool]:
5658
"""
5759
Auto-instrument supported AI/ML libraries for Braintrust tracing.
@@ -75,6 +77,7 @@ def auto_instrument(
7577
dspy: Enable DSPy instrumentation (default: True)
7678
adk: Enable Google ADK instrumentation (default: True)
7779
langchain: Enable LangChain instrumentation (default: True)
80+
langsmith: Enable LangSmith instrumentation (default: True)
7881
7982
Returns:
8083
Dict mapping integration name to whether it was successfully instrumented.
@@ -146,6 +149,8 @@ def auto_instrument(
146149
results["adk"] = _instrument_integration(ADKIntegration)
147150
if langchain:
148151
results["langchain"] = _instrument_integration(LangChainIntegration)
152+
if langsmith:
153+
results["langsmith"] = _instrument_integration(LangSmithIntegration)
149154

150155
return results
151156

py/src/braintrust/integrations/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .dspy import DSPyIntegration
77
from .google_genai import GoogleGenAIIntegration
88
from .langchain import LangChainIntegration
9+
from .langsmith import LangSmithIntegration
910
from .litellm import LiteLLMIntegration
1011
from .openrouter import OpenRouterIntegration
1112
from .pydantic_ai import PydanticAIIntegration
@@ -21,6 +22,7 @@
2122
"GoogleGenAIIntegration",
2223
"LiteLLMIntegration",
2324
"LangChainIntegration",
25+
"LangSmithIntegration",
2426
"OpenRouterIntegration",
2527
"PydanticAIIntegration",
2628
]
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Test auto_instrument for LangSmith."""
2+
3+
import os
4+
from pathlib import Path
5+
6+
import langsmith.client
7+
import langsmith.evaluation._arunner
8+
import langsmith.evaluation._runner
9+
import langsmith.run_helpers
10+
from braintrust.auto import auto_instrument
11+
from braintrust.wrappers.test_utils import autoinstrument_test_context
12+
from langchain_core.prompts import ChatPromptTemplate
13+
from langchain_openai import ChatOpenAI
14+
15+
16+
_CASSETTES_DIR = Path(__file__).resolve().parent.parent / "langsmith" / "cassettes"
17+
18+
19+
# 1. Verify not patched initially.
20+
assert not getattr(langsmith.run_helpers.traceable, "__braintrust_patched_langsmith_traceable__", False)
21+
assert not getattr(langsmith.evaluation._runner.evaluate, "__braintrust_patched_langsmith_evaluate_sync__", False)
22+
assert not getattr(langsmith.evaluation._arunner.aevaluate, "__braintrust_patched_langsmith_evaluate_async__", False)
23+
assert not getattr(langsmith.client.Client.evaluate, "__braintrust_patched_langsmith_client_evaluate__", False)
24+
assert not getattr(langsmith.client.Client.aevaluate, "__braintrust_patched_langsmith_client_aevaluate__", False)
25+
26+
27+
# 2. Instrument with standalone mode so only Braintrust runs.
28+
os.environ["BRAINTRUST_LANGSMITH_STANDALONE"] = "1"
29+
results = auto_instrument(
30+
openai=False,
31+
anthropic=False,
32+
litellm=False,
33+
pydantic_ai=False,
34+
google_genai=False,
35+
openrouter=False,
36+
agno=False,
37+
agentscope=False,
38+
claude_agent_sdk=False,
39+
dspy=False,
40+
adk=False,
41+
langchain=False,
42+
langsmith=True,
43+
)
44+
assert results.get("langsmith") == True
45+
46+
assert getattr(langsmith.run_helpers.traceable, "__braintrust_patched_langsmith_traceable__", False)
47+
assert getattr(langsmith.evaluation._runner.evaluate, "__braintrust_patched_langsmith_evaluate_sync__", False)
48+
assert getattr(langsmith.evaluation._arunner.aevaluate, "__braintrust_patched_langsmith_evaluate_async__", False)
49+
assert getattr(langsmith.client.Client.evaluate, "__braintrust_patched_langsmith_client_evaluate__", False)
50+
assert getattr(langsmith.client.Client.aevaluate, "__braintrust_patched_langsmith_client_aevaluate__", False)
51+
52+
53+
# 3. Idempotent.
54+
results2 = auto_instrument(
55+
openai=False,
56+
anthropic=False,
57+
litellm=False,
58+
pydantic_ai=False,
59+
google_genai=False,
60+
openrouter=False,
61+
agno=False,
62+
agentscope=False,
63+
claude_agent_sdk=False,
64+
dspy=False,
65+
adk=False,
66+
langchain=False,
67+
langsmith=True,
68+
)
69+
assert results2.get("langsmith") == True
70+
71+
72+
# 4. Make an API call and verify span.
73+
with autoinstrument_test_context("test_auto_langsmith", cassettes_dir=_CASSETTES_DIR) as memory_logger:
74+
prompt = ChatPromptTemplate.from_template("What is 1 + {number}?")
75+
model = ChatOpenAI(
76+
model="gpt-4o-mini",
77+
temperature=1,
78+
top_p=1,
79+
frequency_penalty=0,
80+
presence_penalty=0,
81+
n=1,
82+
)
83+
chain = prompt | model
84+
85+
@langsmith.traceable(name="auto-langsmith")
86+
def run_chain(inputs: dict[str, str]) -> dict[str, str]:
87+
return {"answer": chain.invoke(inputs).content}
88+
89+
result = run_chain({"number": "2"})
90+
assert result == {"answer": "1 + 2 equals 3."}
91+
92+
spans = memory_logger.pop()
93+
assert len(spans) == 1, f"Expected 1 span, got {len(spans)}"
94+
span = spans[0]
95+
assert span["span_attributes"]["name"] == "auto-langsmith"
96+
assert span["output"] == {"answer": "1 + 2 equals 3."}
97+
98+
print("SUCCESS")
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Braintrust integration for LangSmith."""
2+
3+
import logging
4+
import os
5+
6+
from braintrust.logger import NOOP_SPAN, current_span, init_logger
7+
8+
from .integration import LangSmithIntegration
9+
10+
11+
logger = logging.getLogger(__name__)
12+
13+
__all__ = [
14+
"LangSmithIntegration",
15+
"setup_langsmith",
16+
]
17+
18+
19+
def setup_langsmith(
20+
api_key: str | None = None,
21+
project_id: str | None = None,
22+
project_name: str | None = None,
23+
standalone: bool = False,
24+
) -> bool:
25+
"""Setup Braintrust integration with LangSmith."""
26+
resolved_project_name = project_name or os.environ.get("LANGCHAIN_PROJECT")
27+
if current_span() == NOOP_SPAN:
28+
init_logger(project=resolved_project_name, api_key=api_key, project_id=project_id)
29+
30+
try:
31+
import langsmith # noqa: F401
32+
except ImportError as exc:
33+
logger.error("Failed to import langsmith: %s", exc)
34+
logger.error("langsmith is not installed. Please install it with: pip install langsmith")
35+
return False
36+
37+
logger.info("LangSmith integration with Braintrust enabled")
38+
return LangSmithIntegration.setup(standalone=True if standalone else None)

0 commit comments

Comments
 (0)