From f3e28c076eece2319551ba28ae51dae3debebde8 Mon Sep 17 00:00:00 2001 From: Maple Xu Date: Thu, 23 Apr 2026 19:58:26 -0400 Subject: [PATCH] Use LangSmith's official runtime override API instead of monkey-patching aio_to_thread LangSmith 0.7.34 added `set_runtime_overrides(aio_to_thread=...)` which provides a supported hook for frameworks with non-standard event loops. This replaces the process-wide monkey-patch of `langsmith._internal._aiter.aio_to_thread` with a call to the official API, making the integration less fragile against LangSmith internal refactors. Co-Authored-By: Claude Opus 4.6 (1M context) --- pyproject.toml | 4 +- temporalio/contrib/langsmith/_interceptor.py | 60 ++++++++++---------- uv.lock | 10 ++-- 3 files changed, 36 insertions(+), 38 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6ea339047..b94b84780 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ opentelemetry = ["opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2"] pydantic = ["pydantic>=2.0.0,<3"] openai-agents = ["openai-agents>=0.3,<0.7", "mcp>=1.9.4, <2"] google-adk = ["google-adk>=1.27.0,<2"] -langsmith = ["langsmith>=0.7.0,<0.8"] +langsmith = ["langsmith>=0.7.34,<0.8"] lambda-worker-otel = [ "opentelemetry-api>=1.11.1,<2", "opentelemetry-sdk>=1.11.1,<2", @@ -79,7 +79,7 @@ dev = [ "pytest-rerunfailures>=16.1", "pytest-xdist>=3.6,<4", "moto[s3,server]>=5", - "langsmith>=0.7.0,<0.8", + "langsmith>=0.7.34,<0.8", "setuptools<82", "opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2", "opentelemetry-semantic-conventions>=0.40b0,<1", diff --git a/temporalio/contrib/langsmith/_interceptor.py b/temporalio/contrib/langsmith/_interceptor.py index 5e020eb4d..91b8c690b 100644 --- a/temporalio/contrib/langsmith/_interceptor.py +++ b/temporalio/contrib/langsmith/_interceptor.py @@ -152,48 +152,46 @@ def _get_current_run_for_propagation() -> RunTree | None: # --------------------------------------------------------------------------- -# Workflow event loop safety: patch @traceable's aio_to_thread +# Workflow event loop safety: override @traceable's aio_to_thread # --------------------------------------------------------------------------- -_aio_to_thread_patched = False +_aio_to_thread_override_installed = False -def _patch_aio_to_thread() -> None: - """Patch langsmith's ``aio_to_thread`` to run synchronously in workflows. +async def _temporal_aio_to_thread( + default_aio_to_thread: Callable[..., Any], + ctx: Any, + func: Callable[..., Any], + /, + *args: Any, + **kwargs: Any, +) -> Any: + """Run LangSmith's ``aio_to_thread`` synchronously inside Temporal workflows. The ``@traceable`` decorator on async functions uses ``aio_to_thread()`` → ``loop.run_in_executor()`` for run setup/teardown. The Temporal workflow - event loop does not support ``run_in_executor``. This patch runs those - functions synchronously on the workflow thread when inside a workflow. - Functions passed here must not perform blocking I/O. + event loop does not support ``run_in_executor``. This override runs those + functions synchronously on the workflow thread when inside a workflow, + and delegates to the default implementation outside workflows. + Registered via ``langsmith.set_runtime_overrides(aio_to_thread=...)``. """ - global _aio_to_thread_patched # noqa: PLW0603 - if _aio_to_thread_patched: - return - - import langsmith._internal._aiter as _aiter + if not temporalio.workflow.in_workflow(): + return await default_aio_to_thread(ctx, func, *args, **kwargs) + with temporalio.workflow.unsafe.sandbox_unrestricted(): + return ctx.run(func, *args, **kwargs) - _original = _aiter.aio_to_thread - import contextvars +def _install_aio_to_thread_override() -> None: + """Install the ``aio_to_thread`` override via LangSmith's official API. - async def _safe_aio_to_thread( - func: Callable[..., Any], - /, - *args: Any, - __ctx: contextvars.Context | None = None, - **kwargs: Any, - ) -> Any: - if not temporalio.workflow.in_workflow(): - return await _original(func, *args, __ctx=__ctx, **kwargs) - with temporalio.workflow.unsafe.sandbox_unrestricted(): - # Run without ctx.run() so context var changes propagate - # to the caller. Safe because workflows are single-threaded. - return func(*args, **kwargs) - - _aiter.aio_to_thread = _safe_aio_to_thread # type: ignore[assignment] - _aio_to_thread_patched = True + Safe to call multiple times; the override is only installed once. + """ + global _aio_to_thread_override_installed # noqa: PLW0603 + if _aio_to_thread_override_installed: + return + langsmith.set_runtime_overrides(aio_to_thread=_temporal_aio_to_thread) + _aio_to_thread_override_installed = True # --------------------------------------------------------------------------- @@ -595,7 +593,7 @@ def workflow_interceptor_class( self, input: temporalio.worker.WorkflowInterceptorClassInput ) -> type[_LangSmithWorkflowInboundInterceptor]: """Return the workflow interceptor class with config bound.""" - _patch_aio_to_thread() + _install_aio_to_thread_override() config = self class InterceptorWithConfig(_LangSmithWorkflowInboundInterceptor): diff --git a/uv.lock b/uv.lock index 6d824cf92..f56baa0f7 100644 --- a/uv.lock +++ b/uv.lock @@ -2473,7 +2473,7 @@ wheels = [ [[package]] name = "langsmith" -version = "0.7.26" +version = "0.7.34" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -2486,9 +2486,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/86/6de4f6f0451a9658f26f633e0bb090552a4dafd7df3f1ae7f0d40558e67e/langsmith-0.7.26.tar.gz", hash = "sha256:a3e06f3d689ce7195717aa6b8f91082319819ec7ea9b9a62cdcd3d9dc25bfc7b", size = 1146118, upload-time = "2026-04-06T15:01:03.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/3c/f5b2444909632fa9f11a0d2a12b960f5fc1edf1f0c9c222e78cb5ef8df18/langsmith-0.7.34.tar.gz", hash = "sha256:1b1fd637e129ae41d5fc8eebf23483816cd1251d61cffb21ce5203858561e70c", size = 4390970, upload-time = "2026-04-23T13:51:43.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/8e/7eb7d65ce62e98e74b9f18f193ea7ac3996d4fbd71fffcc67d0f7ba3103e/langsmith-0.7.26-py3-none-any.whl", hash = "sha256:fe5c877972cea450c1c48251c8fae0f18543c8d19dfdb9ff9a9c4263763dde4e", size = 360160, upload-time = "2026-04-06T15:01:01.516Z" }, + { url = "https://files.pythonhosted.org/packages/e3/94/c011bc7e045e6da6aaab9d2adb2f3cefdb382fc038f09ef0865ac3214978/langsmith-0.7.34-py3-none-any.whl", hash = "sha256:52478123a74403b320230625d90810b5d8b88d3cd275b3eae6d2f696d40053ce", size = 376754, upload-time = "2026-04-23T13:51:41.767Z" }, ] [[package]] @@ -5092,7 +5092,7 @@ requires-dist = [ { name = "aioboto3", marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, { name = "google-adk", marker = "extra == 'google-adk'", specifier = ">=1.27.0,<2" }, { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, - { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.0,<0.8" }, + { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.8" }, { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.9.4,<2" }, { name = "nexus-rpc", specifier = "==1.4.0" }, { name = "openai-agents", marker = "extra == 'openai-agents'", specifier = ">=0.3,<0.7" }, @@ -5119,7 +5119,7 @@ dev = [ { name = "googleapis-common-protos", specifier = "==1.70.0" }, { name = "grpcio-tools", specifier = ">=1.48.2,<2" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "langsmith", specifier = ">=0.7.0,<0.8" }, + { name = "langsmith", specifier = ">=0.7.34,<0.8" }, { name = "litellm", specifier = ">=1.83.0" }, { name = "maturin", specifier = ">=1.8.2" }, { name = "moto", extras = ["s3", "server"], specifier = ">=5" },