Skip to content

Commit f034d39

Browse files
authored
Set global configuration (#2002)
It's nice if you can configure stuff (like metadata, etc.) at the module level, especially for scnearios like LGP. ``` import langsmith as ls ls.configure(metadata={"revision": "foo", "bar": "baz}) ``` RIght now if you do .configure() and then do asyncio.run() etc., the contextvar can be lost (need to copycontext()) he expectation for .configure() is that it works like logging configuration and is global. if you want to set something in context you use `tracing_context`. That's the point of that function. This pr also does some cleanup of contextvars definitions and also sets a globally scoped default for metadata, etc. so that metadata doesn't get lost in async tasks.
1 parent a2ad86c commit f034d39

File tree

6 files changed

+88
-56
lines changed

6 files changed

+88
-56
lines changed

python/langsmith/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
# Avoid calling into importlib on every call to __version__
2323

24-
__version__ = "0.4.26"
24+
__version__ = "0.4.27"
2525
version = __version__ # for backwards compatibility
2626

2727

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Shared context (ContextVars and global defaults) that configure tracing."""
2+
3+
import contextvars
4+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
5+
6+
if TYPE_CHECKING:
7+
from langsmith.client import Client
8+
from langsmith.run_trees import RunTree
9+
else:
10+
Client = Any # type: ignore[assignment]
11+
RunTree = Any # type: ignore[assignment]
12+
13+
_PROJECT_NAME = contextvars.ContextVar[Optional[str]]("_PROJECT_NAME", default=None)
14+
_TAGS = contextvars.ContextVar[Optional[list[str]]]("_TAGS", default=None)
15+
_METADATA = contextvars.ContextVar[Optional[dict[str, Any]]]("_METADATA", default=None)
16+
17+
_TRACING_ENABLED = contextvars.ContextVar[Optional[Union[bool, Literal["local"]]]](
18+
"_TRACING_ENABLED", default=None
19+
)
20+
_CLIENT = contextvars.ContextVar[Optional["Client"]]("_CLIENT", default=None)
21+
_PARENT_RUN_TREE = contextvars.ContextVar[Optional["RunTree"]](
22+
"_PARENT_RUN_TREE", default=None
23+
)
24+
# Not thread-local, so you can set this process-wide (before asyncio.run, etc.)
25+
_GLOBAL_PROJECT_NAME: Optional[str] = None
26+
_GLOBAL_TAGS: Optional[list[str]] = None
27+
_GLOBAL_METADATA: Optional[dict[str, Any]] = None
28+
_GLOBAL_TRACING_ENABLED: Optional[Union[bool, Literal["local"]]] = None
29+
_GLOBAL_CLIENT: Optional["Client"] = None

python/langsmith/run_helpers.py

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
from typing_extensions import ParamSpec, TypeGuard, get_args, get_origin
4343

44+
import langsmith._internal._context as _context
4445
from langsmith import client as ls_client
4546
from langsmith import run_trees, schemas, utils
4647
from langsmith._internal import _aiter as aitertools
@@ -53,25 +54,13 @@
5354
from langchain_core.runnables import Runnable
5455

5556
LOGGER = logging.getLogger(__name__)
56-
_PARENT_RUN_TREE = contextvars.ContextVar[Optional[run_trees.RunTree]](
57-
"_PARENT_RUN_TREE", default=None
58-
)
59-
_PROJECT_NAME = contextvars.ContextVar[Optional[str]]("_PROJECT_NAME", default=None)
60-
_TAGS = contextvars.ContextVar[Optional[list[str]]]("_TAGS", default=None)
61-
_METADATA = contextvars.ContextVar[Optional[dict[str, Any]]]("_METADATA", default=None)
62-
63-
64-
_TRACING_ENABLED = contextvars.ContextVar[Optional[Union[bool, Literal["local"]]]](
65-
"_TRACING_ENABLED", default=None
66-
)
67-
_CLIENT = contextvars.ContextVar[Optional[ls_client.Client]]("_CLIENT", default=None)
6857
_CONTEXT_KEYS: dict[str, contextvars.ContextVar] = {
69-
"parent": _PARENT_RUN_TREE,
70-
"project_name": _PROJECT_NAME,
71-
"tags": _TAGS,
72-
"metadata": _METADATA,
73-
"enabled": _TRACING_ENABLED,
74-
"client": _CLIENT,
58+
"parent": _context._PARENT_RUN_TREE,
59+
"project_name": _context._PROJECT_NAME,
60+
"tags": _context._TAGS,
61+
"metadata": _context._METADATA,
62+
"enabled": _context._TRACING_ENABLED,
63+
"client": _context._CLIENT,
7564
"replicas": run_trees._REPLICAS,
7665
"distributed_parent_id": run_trees._DISTRIBUTED_PARENT_ID,
7766
}
@@ -83,7 +72,7 @@
8372

8473
def get_current_run_tree() -> Optional[run_trees.RunTree]:
8574
"""Get the current run tree."""
86-
return _PARENT_RUN_TREE.get()
75+
return _context._PARENT_RUN_TREE.get()
8776

8877

8978
def get_tracing_context(
@@ -92,12 +81,12 @@ def get_tracing_context(
9281
"""Get the current tracing context."""
9382
if context is None:
9483
return {
95-
"parent": _PARENT_RUN_TREE.get(),
96-
"project_name": _PROJECT_NAME.get(),
97-
"tags": _TAGS.get(),
98-
"metadata": _METADATA.get(),
99-
"enabled": _TRACING_ENABLED.get(),
100-
"client": _CLIENT.get(),
84+
"parent": _context._PARENT_RUN_TREE.get(),
85+
"project_name": _context._PROJECT_NAME.get(),
86+
"tags": _context._TAGS.get(),
87+
"metadata": _context._METADATA.get(),
88+
"enabled": _context._TRACING_ENABLED.get(),
89+
"client": _context._CLIENT.get(),
10190
"replicas": run_trees._REPLICAS.get(),
10291
"distributed_parent_id": run_trees._DISTRIBUTED_PARENT_ID.get(),
10392
}
@@ -995,8 +984,8 @@ def _setup(self) -> run_trees.RunTree:
995984
self.old_ctx = get_tracing_context()
996985
enabled = utils.tracing_is_enabled(self.old_ctx)
997986

998-
outer_tags = _TAGS.get()
999-
outer_metadata = _METADATA.get()
987+
outer_tags = _context._TAGS.get() or _context._GLOBAL_TAGS
988+
outer_metadata = _context._METADATA.get() or _context._GLOBAL_METADATA
1000989
client_ = self.client or self.old_ctx.get("client")
1001990
parent_run_ = _get_parent_run(
1002991
{
@@ -1049,11 +1038,11 @@ def _setup(self) -> run_trees.RunTree:
10491038
if enabled is True:
10501039
self.new_run.post()
10511040
if enabled:
1052-
_TAGS.set(tags_)
1053-
_METADATA.set(metadata)
1054-
_PARENT_RUN_TREE.set(self.new_run)
1055-
_PROJECT_NAME.set(project_name_)
1056-
_CLIENT.set(client_)
1041+
_context._TAGS.set(tags_)
1042+
_context._METADATA.set(metadata)
1043+
_context._PARENT_RUN_TREE.set(self.new_run)
1044+
_context._PROJECT_NAME.set(project_name_)
1045+
_context._CLIENT.set(client_)
10571046

10581047
return self.new_run
10591048

@@ -1161,8 +1150,10 @@ def _get_project_name(project_name: Optional[str]) -> Optional[str]:
11611150
prt = _PARENT_RUN_TREE.get()
11621151
return (
11631152
# Maintain tree consistency first
1164-
_PROJECT_NAME.get()
1153+
_context._PROJECT_NAME.get()
11651154
or (prt.session_name if prt else None)
1155+
# Global fallback configured via ls.configure(...)
1156+
or _context._GLOBAL_PROJECT_NAME
11661157
# fallback to the default for the environment
11671158
or utils.get_tracer_project()
11681159
)
@@ -1453,21 +1444,22 @@ def _setup_run(
14531444
dangerously_allow_filesystem = container_input.get(
14541445
"dangerously_allow_filesystem", False
14551446
)
1456-
outer_project = _PROJECT_NAME.get()
1447+
outer_project = _context._PROJECT_NAME.get()
14571448
langsmith_extra = langsmith_extra or LangSmithExtra()
14581449
name = langsmith_extra.get("name") or container_input.get("name")
1459-
client_ = langsmith_extra.get("client", client) or _CLIENT.get()
1450+
client_ = langsmith_extra.get("client", client) or _context._CLIENT.get()
14601451
parent_run_ = _get_parent_run(
14611452
{**langsmith_extra, "client": client_}, kwargs.get("config")
14621453
)
1463-
project_cv = _PROJECT_NAME.get()
1454+
project_cv = _context._PROJECT_NAME.get()
14641455
selected_project = (
14651456
project_cv # From parent trace
14661457
or (
14671458
parent_run_.session_name if parent_run_ else None
14681459
) # from parent run attempt 2 (not managed by traceable)
14691460
or langsmith_extra.get("project_name") # at invocation time
14701461
or container_input["project_name"] # at decorator time
1462+
or _context._GLOBAL_PROJECT_NAME # global fallback from ls.configure
14711463
or utils.get_tracer_project() # default
14721464
)
14731465
reference_example_id = langsmith_extra.get("reference_example_id")
@@ -1492,14 +1484,14 @@ def _setup_run(
14921484
signature = inspect.signature(func)
14931485
name_ = name or utils._get_function_name(func)
14941486
extra_inner = _collect_extra(extra_outer, langsmith_extra)
1495-
outer_metadata = _METADATA.get()
1496-
outer_tags = _TAGS.get()
1487+
outer_metadata = _context._METADATA.get() or _context._GLOBAL_METADATA
1488+
outer_tags = _context._TAGS.get() or _context._GLOBAL_TAGS
14971489
context = copy_context()
14981490
metadata_ = {
14991491
**(langsmith_extra.get("metadata") or {}),
15001492
**(outer_metadata or {}),
15011493
}
1502-
context.run(_METADATA.set, metadata_)
1494+
context.run(_context._METADATA.set, metadata_)
15031495
metadata_.update(metadata or {})
15041496
metadata_["ls_method"] = "traceable"
15051497
extra_inner["metadata"] = metadata_
@@ -1523,7 +1515,7 @@ def _setup_run(
15231515
except BaseException as e:
15241516
LOGGER.error(f"Failed to filter inputs for {name_}: {e}")
15251517
tags_ = (langsmith_extra.get("tags") or []) + (outer_tags or [])
1526-
context.run(_TAGS.set, tags_)
1518+
context.run(_context._TAGS.set, tags_)
15271519
tags_ += tags or []
15281520
if parent_run_ is not None:
15291521
new_run = parent_run_.create_child(
@@ -1568,7 +1560,7 @@ def _setup_run(
15681560
context=context,
15691561
_token_event_logged=False,
15701562
)
1571-
context.run(_PROJECT_NAME.set, response_container["project_name"])
1563+
context.run(_context._PROJECT_NAME.set, response_container["project_name"])
15721564
context.run(_PARENT_RUN_TREE.set, response_container["new_run"])
15731565
return response_container
15741566

@@ -2081,3 +2073,12 @@ def _maybe_create_otel_context(run_tree: Optional[run_trees.RunTree]):
20812073

20822074
non_recording_span = NonRecordingSpan(span_context)
20832075
return use_span(non_recording_span)
2076+
2077+
2078+
# For backwards compatibility
2079+
_PROJECT_NAME = _context._PROJECT_NAME
2080+
_TAGS = _context._TAGS
2081+
_METADATA = _context._METADATA
2082+
_TRACING_ENABLED = _context._TRACING_ENABLED
2083+
_CLIENT = _context._CLIENT
2084+
_PARENT_RUN_TREE = _context._PARENT_RUN_TREE

python/langsmith/run_trees.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import threading
2626
import urllib.parse
2727

28+
import langsmith._internal._context as _context
2829
from langsmith import schemas as ls_schemas
2930
from langsmith import utils
3031
from langsmith.client import ID_TYPE, RUN_TYPE_T, Client, _dumps_json, _ensure_uuid
@@ -48,7 +49,6 @@ class WriteReplica(TypedDict, total=False):
4849
LANGSMITH_REPLICAS = sys.intern(f"{LANGSMITH_PREFIX}replicas")
4950
OVERRIDE_OUTPUTS = sys.intern("__omit_auto_outputs")
5051
NOT_PROVIDED = cast(None, object())
51-
_CLIENT: Optional[Client] = None
5252
_LOCK = threading.Lock()
5353

5454
# Context variables
@@ -147,21 +147,17 @@ def configure(
147147
if client is not _SENTINEL:
148148
_CLIENT = client
149149
if enabled is not _SENTINEL:
150-
from langsmith.run_helpers import _TRACING_ENABLED
151-
152-
_TRACING_ENABLED.set(enabled)
150+
_context._TRACING_ENABLED.set(enabled)
151+
_context._GLOBAL_TRACING_ENABLED = enabled
153152
if project_name is not _SENTINEL:
154-
from langsmith.run_helpers import _PROJECT_NAME
155-
156-
_PROJECT_NAME.set(project_name)
153+
_context._PROJECT_NAME.set(project_name)
154+
_context._GLOBAL_PROJECT_NAME = project_name
157155
if tags is not _SENTINEL:
158-
from langsmith.run_helpers import _TAGS
159-
160-
_TAGS.set(tags)
156+
_context._TAGS.set(tags)
157+
_context._GLOBAL_TAGS = tags
161158
if metadata is not _SENTINEL:
162-
from langsmith.run_helpers import _METADATA
163-
164-
_METADATA.set(metadata)
159+
_context._METADATA.set(metadata)
160+
_context._GLOBAL_METADATA = metadata
165161

166162

167163
def validate_extracted_usage_metadata(
@@ -1129,4 +1125,5 @@ def _create_current_dotted_order(
11291125
return st.strftime("%Y%m%dT%H%M%S%fZ") + str(id_)
11301126

11311127

1128+
_CLIENT: Optional[Client] = _context._GLOBAL_CLIENT
11321129
__all__ = ["RunTree", "RunTree"]

python/langsmith/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ class LangSmithMissingAPIKeyWarning(LangSmithWarning):
9898

9999
def tracing_is_enabled(ctx: Optional[dict] = None) -> Union[bool, Literal["local"]]:
100100
"""Return True if tracing is enabled."""
101+
# Access global fallbacks via context module to avoid stale references.
102+
import langsmith._internal._context as _context
101103
from langsmith.run_helpers import get_current_run_tree, get_tracing_context
102104

103105
tc = ctx or get_tracing_context()
@@ -110,6 +112,9 @@ def tracing_is_enabled(ctx: Optional[dict] = None) -> Union[bool, Literal["local
110112
# Next check if we're mid-trace
111113
if get_current_run_tree():
112114
return True
115+
# If a global fallback was configured, use it next.
116+
if _context._GLOBAL_TRACING_ENABLED is not None:
117+
return _context._GLOBAL_TRACING_ENABLED
113118
# Finally, check the global environment
114119
var_result = get_env_var("TRACING_V2", default=get_env_var("TRACING", default=""))
115120
return var_result == "true"

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "langsmith"
7-
version = "0.4.26"
7+
version = "0.4.27"
88
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
99
authors = [
1010
{name = "LangChain", email = "[email protected]"},

0 commit comments

Comments
 (0)