Skip to content

Commit 0f1d139

Browse files
authored
Merge branch 'main' into maplexu/langsmith-runtime-override
2 parents aa93011 + 5bff3b0 commit 0f1d139

4 files changed

Lines changed: 290 additions & 12 deletions

File tree

temporalio/contrib/langsmith/_interceptor.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import Any, ClassVar, NoReturn, Protocol
1313

1414
import langsmith
15+
import langsmith.utils
1516
import nexusrpc.handler
1617
from langsmith import tracing_context
1718
from langsmith.run_helpers import get_current_run_tree
@@ -43,6 +44,7 @@
4344
}
4445
)
4546

47+
4648
# ---------------------------------------------------------------------------
4749
# Context helpers
4850
# ---------------------------------------------------------------------------
@@ -455,14 +457,24 @@ def _maybe_run(
455457
) -> Iterator[None]:
456458
"""Create a LangSmith run, handling errors.
457459
458-
- If add_temporal_runs is False, yields None (no run created).
460+
- If add_temporal_runs is False **or** ``langsmith.utils.tracing_is_enabled()``
461+
returns False, yields None (no run created).
459462
Context propagation is handled unconditionally by callers.
460463
- When a run IS created, uses :class:`_ReplaySafeRunTree` for
461464
replay and event loop safety, then sets it as ambient context via
462465
``tracing_context(parent=run_tree)`` so ``get_current_run_tree()``
463466
returns it and ``_inject_current_context()`` can inject it.
464467
- On exception: marks run as errored (unless benign ApplicationError), re-raises.
465468
469+
Note on ``tracing_is_enabled()`` and cross-process traces:
470+
``tracing_is_enabled()`` checks for an active run tree in context
471+
*before* consulting the ``LANGSMITH_TRACING`` env var (langsmith
472+
semantics). If a parent run is propagated into this worker via
473+
headers from an upstream tracer, tracing continues regardless of
474+
``LANGSMITH_TRACING=false``. This matches langsmith's "continue
475+
mid-trace" model: the env var suppresses *new* local traces but
476+
does not break an inbound parent trace.
477+
466478
Args:
467479
client: LangSmith client instance.
468480
name: Display name for the run.
@@ -475,7 +487,7 @@ def _maybe_run(
475487
project_name: LangSmith project name override.
476488
executor: ThreadPoolExecutor for background I/O.
477489
"""
478-
if not add_temporal_runs:
490+
if not add_temporal_runs or not langsmith.utils.tracing_is_enabled():
479491
yield None
480492
return
481493

@@ -715,12 +727,8 @@ async def execute_activity(
715727
"temporalRunID": info.workflow_run_id or "",
716728
"temporalActivityID": info.activity_id or "",
717729
}
718-
# Unconditionally set tracing context so @traceable functions inside
719-
# activities inherit the plugin's client and parent, regardless of
720-
# the add_temporal_runs toggle.
721730
tracing_args: dict[str, Any] = {
722731
"client": self._config._client,
723-
"enabled": True,
724732
"project_name": self._config._project_name,
725733
"parent": parent,
726734
}
@@ -784,7 +792,6 @@ def _workflow_maybe_run(
784792
)
785793
tracing_args: dict[str, Any] = {
786794
"client": self._config._client,
787-
"enabled": True,
788795
"project_name": self._config._project_name,
789796
"parent": tracing_parent,
790797
}
@@ -945,7 +952,6 @@ async def execute_nexus_operation_start(
945952
)
946953
tracing_args: dict[str, Any] = {
947954
"client": self._config._client,
948-
"enabled": True,
949955
"project_name": self._config._project_name,
950956
"parent": parent,
951957
}
@@ -965,7 +971,6 @@ async def execute_nexus_operation_cancel(
965971
)
966972
tracing_args: dict[str, Any] = {
967973
"client": self._config._client,
968-
"enabled": True,
969974
"project_name": self._config._project_name,
970975
"parent": parent,
971976
}

tests/contrib/langsmith/conftest.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,37 @@
66
from typing import Any
77
from unittest.mock import MagicMock
88

9+
import pytest
10+
11+
12+
@pytest.fixture(autouse=True)
13+
def _clear_langsmith_env_cache() -> Any: # pyright: ignore[reportUnusedFunction]
14+
"""Clear langsmith's lru_cache before and after each test.
15+
16+
Tests manipulate LANGSMITH_TRACING / LANGCHAIN_TRACING_V2 env vars.
17+
langsmith.utils.get_env_var caches results, so stale values would
18+
leak across tests (or into other test modules in the same session).
19+
"""
20+
import langsmith.utils
21+
22+
langsmith.utils.get_env_var.cache_clear() # type: ignore[attr-defined]
23+
yield
24+
langsmith.utils.get_env_var.cache_clear() # type: ignore[attr-defined]
25+
26+
27+
@pytest.fixture(autouse=True)
28+
def _enable_langsmith_tracing(monkeypatch: pytest.MonkeyPatch) -> None: # pyright: ignore[reportUnusedFunction]
29+
"""Enable LangSmith tracing by default for all tests in this directory.
30+
31+
The plugin defers to ``langsmith.utils.tracing_is_enabled()``, which
32+
requires ``LANGSMITH_TRACING=true`` (or equivalent). Without this
33+
fixture, tests that expect runs would see zero.
34+
35+
Individual tests can override with ``monkeypatch.setenv("LANGSMITH_TRACING", "false")``
36+
to verify disabled behavior.
37+
"""
38+
monkeypatch.setenv("LANGSMITH_TRACING", "true")
39+
940

1041
@dataclass
1142
class _RunRecord:

tests/contrib/langsmith/test_interceptor.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,6 @@ async def test_false_still_propagates_context(
11111111
# (unconditionally, before _maybe_run)
11121112
mock_tracing_ctx.assert_called_once_with(
11131113
client=config._client,
1114-
enabled=True,
11151114
project_name=None,
11161115
parent=mock_extracted_parent,
11171116
)
@@ -1143,8 +1142,8 @@ async def test_false_activity_no_parent_no_context(
11431142
await act_interceptor.execute_activity(mock_act_input)
11441143

11451144
MockRunTree.assert_not_called()
1146-
# tracing_context called with client and enabled (no parent)
1145+
# tracing_context called with client (no parent)
11471146
mock_tracing_ctx.assert_called_once_with(
1148-
client=config._client, enabled=True, project_name=None, parent=None
1147+
client=config._client, project_name=None, parent=None
11491148
)
11501149
mock_act_next.execute_activity.assert_called_once()
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
"""Tests that LangSmithPlugin defers to ``langsmith.utils.tracing_is_enabled()``.
2+
3+
Tracing requires the env to explicitly say so (``LANGSMITH_TRACING=true`` etc);
4+
it is off when the env is unset or set to ``false``. Tests verify that
5+
``LANGSMITH_TRACING=false`` produces zero runs and ``LANGSMITH_TRACING=true``
6+
produces runs.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import uuid
12+
from datetime import timedelta
13+
14+
import pytest
15+
from langsmith import traceable
16+
17+
from temporalio import activity, workflow
18+
from temporalio.client import Client
19+
from temporalio.testing import WorkflowEnvironment
20+
from tests.contrib.langsmith.test_integration import (
21+
DirectTraceableNexusService,
22+
NexusDirectTraceableWorkflow,
23+
_make_client_and_collector,
24+
)
25+
from tests.helpers import new_worker
26+
from tests.helpers.nexus import make_nexus_endpoint_name
27+
28+
# ---------------------------------------------------------------------------
29+
# Sample workflow / activity
30+
# ---------------------------------------------------------------------------
31+
32+
33+
@traceable(name="inner_call")
34+
async def _inner_call(prompt: str) -> str:
35+
return f"response to: {prompt}"
36+
37+
38+
@traceable
39+
@activity.defn
40+
async def env_override_activity() -> str:
41+
result = await _inner_call("hello")
42+
return result
43+
44+
45+
@workflow.defn
46+
class EnvOverrideWorkflow:
47+
@workflow.run
48+
async def run(self) -> str:
49+
return await workflow.execute_activity(
50+
env_override_activity,
51+
start_to_close_timeout=timedelta(seconds=10),
52+
)
53+
54+
55+
# ---------------------------------------------------------------------------
56+
# Tests
57+
# ---------------------------------------------------------------------------
58+
59+
60+
class TestTracingEnvOverride:
61+
"""LangSmithPlugin must respect LANGSMITH_TRACING=false."""
62+
63+
async def test_no_runs_when_tracing_disabled_with_temporal_runs(
64+
self,
65+
client: Client,
66+
env: WorkflowEnvironment, # type:ignore[reportUnusedParameter]
67+
monkeypatch: pytest.MonkeyPatch,
68+
) -> None:
69+
"""With LANGSMITH_TRACING=false and add_temporal_runs=True, no runs."""
70+
monkeypatch.setenv("LANGSMITH_TRACING", "false")
71+
monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False)
72+
73+
temporal_client, collector, _ = _make_client_and_collector(
74+
client, add_temporal_runs=True
75+
)
76+
77+
async with new_worker(
78+
temporal_client,
79+
EnvOverrideWorkflow,
80+
activities=[env_override_activity],
81+
max_cached_workflows=0,
82+
) as worker:
83+
handle = await temporal_client.start_workflow(
84+
EnvOverrideWorkflow.run,
85+
id=f"env-override-temporal-{uuid.uuid4()}",
86+
task_queue=worker.task_queue,
87+
)
88+
result = await handle.result()
89+
90+
assert result == "response to: hello"
91+
assert len(collector.runs) == 0, (
92+
f"Expected zero LangSmith runs when LANGSMITH_TRACING=false, "
93+
f"but got {len(collector.runs)}: "
94+
f"{[r.name for r in collector.runs]}"
95+
)
96+
97+
async def test_no_runs_when_tracing_disabled_without_temporal_runs(
98+
self,
99+
client: Client,
100+
env: WorkflowEnvironment, # type:ignore[reportUnusedParameter]
101+
monkeypatch: pytest.MonkeyPatch,
102+
) -> None:
103+
"""With LANGSMITH_TRACING=false and add_temporal_runs=False, no runs."""
104+
monkeypatch.setenv("LANGSMITH_TRACING", "false")
105+
monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False)
106+
107+
temporal_client, collector, _ = _make_client_and_collector(
108+
client, add_temporal_runs=False
109+
)
110+
111+
async with new_worker(
112+
temporal_client,
113+
EnvOverrideWorkflow,
114+
activities=[env_override_activity],
115+
max_cached_workflows=0,
116+
) as worker:
117+
handle = await temporal_client.start_workflow(
118+
EnvOverrideWorkflow.run,
119+
id=f"env-override-no-temporal-{uuid.uuid4()}",
120+
task_queue=worker.task_queue,
121+
)
122+
result = await handle.result()
123+
124+
assert result == "response to: hello"
125+
assert len(collector.runs) == 0, (
126+
f"Expected zero LangSmith runs when LANGSMITH_TRACING=false, "
127+
f"but got {len(collector.runs)}: "
128+
f"{[r.name for r in collector.runs]}"
129+
)
130+
131+
async def test_no_runs_when_langchain_tracing_v2_disabled(
132+
self,
133+
client: Client,
134+
env: WorkflowEnvironment, # type:ignore[reportUnusedParameter]
135+
monkeypatch: pytest.MonkeyPatch,
136+
) -> None:
137+
"""LANGCHAIN_TRACING_V2=false also suppresses runs (legacy env var)."""
138+
monkeypatch.setenv("LANGCHAIN_TRACING_V2", "false")
139+
monkeypatch.delenv("LANGSMITH_TRACING", raising=False)
140+
141+
temporal_client, collector, _ = _make_client_and_collector(
142+
client, add_temporal_runs=True
143+
)
144+
145+
async with new_worker(
146+
temporal_client,
147+
EnvOverrideWorkflow,
148+
activities=[env_override_activity],
149+
max_cached_workflows=0,
150+
) as worker:
151+
handle = await temporal_client.start_workflow(
152+
EnvOverrideWorkflow.run,
153+
id=f"env-override-v2-{uuid.uuid4()}",
154+
task_queue=worker.task_queue,
155+
)
156+
result = await handle.result()
157+
158+
assert result == "response to: hello"
159+
assert len(collector.runs) == 0, (
160+
f"Expected zero LangSmith runs when LANGCHAIN_TRACING_V2=false, "
161+
f"but got {len(collector.runs)}: "
162+
f"{[r.name for r in collector.runs]}"
163+
)
164+
165+
async def test_no_runs_when_tracing_disabled_for_nexus_start(
166+
self,
167+
client: Client,
168+
env: WorkflowEnvironment,
169+
monkeypatch: pytest.MonkeyPatch,
170+
) -> None:
171+
"""With LANGSMITH_TRACING=false, nexus start handler emits no runs."""
172+
if env.supports_time_skipping:
173+
pytest.skip("Time-skipping server doesn't persist headers.")
174+
175+
monkeypatch.setenv("LANGSMITH_TRACING", "false")
176+
monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False)
177+
178+
temporal_client, collector, _ = _make_client_and_collector(
179+
client, add_temporal_runs=True
180+
)
181+
182+
task_queue = f"env-override-nexus-{uuid.uuid4()}"
183+
async with new_worker(
184+
temporal_client,
185+
NexusDirectTraceableWorkflow,
186+
nexus_service_handlers=[DirectTraceableNexusService()],
187+
task_queue=task_queue,
188+
max_cached_workflows=0,
189+
) as worker:
190+
await env.create_nexus_endpoint(
191+
make_nexus_endpoint_name(worker.task_queue),
192+
worker.task_queue,
193+
)
194+
handle = await temporal_client.start_workflow(
195+
NexusDirectTraceableWorkflow.run,
196+
id=f"env-override-nexus-{uuid.uuid4()}",
197+
task_queue=worker.task_queue,
198+
)
199+
result = await handle.result()
200+
201+
assert result == "response to: nexus-input"
202+
assert len(collector.runs) == 0, (
203+
f"Expected zero LangSmith runs when LANGSMITH_TRACING=false "
204+
f"(nexus start path), but got {len(collector.runs)}: "
205+
f"{[r.name for r in collector.runs]}"
206+
)
207+
208+
# NOTE: test_no_runs_when_tracing_disabled_for_nexus_cancel is not
209+
# included — cancelling an in-flight nexus operation requires non-trivial
210+
# orchestration (long-running handler + external cancel signal). Flagged
211+
# for a follow-up ticket.
212+
213+
async def test_runs_emitted_when_tracing_enabled(
214+
self,
215+
client: Client,
216+
env: WorkflowEnvironment, # type:ignore[reportUnusedParameter]
217+
monkeypatch: pytest.MonkeyPatch,
218+
) -> None:
219+
"""Positive control: with LANGSMITH_TRACING=true, runs ARE emitted."""
220+
monkeypatch.setenv("LANGSMITH_TRACING", "true")
221+
monkeypatch.delenv("LANGCHAIN_TRACING_V2", raising=False)
222+
223+
temporal_client, collector, _ = _make_client_and_collector(
224+
client, add_temporal_runs=True
225+
)
226+
227+
async with new_worker(
228+
temporal_client,
229+
EnvOverrideWorkflow,
230+
activities=[env_override_activity],
231+
max_cached_workflows=0,
232+
) as worker:
233+
handle = await temporal_client.start_workflow(
234+
EnvOverrideWorkflow.run,
235+
id=f"env-enabled-{uuid.uuid4()}",
236+
task_queue=worker.task_queue,
237+
)
238+
result = await handle.result()
239+
240+
assert result == "response to: hello"
241+
assert (
242+
len(collector.runs) > 0
243+
), "Expected LangSmith runs when LANGSMITH_TRACING=true, but got none"

0 commit comments

Comments
 (0)