Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-langsmith for
openSUSE:Factory checked in at 2026-08-12 16:12:24
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langsmith (Old)
and /work/SRC/openSUSE:Factory/.python-langsmith.new.17972 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langsmith"
Wed Aug 12 16:12:24 2026 rev:19 rq:1370779 version:0.10.18
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-langsmith/python-langsmith.changes
2026-08-09 21:40:00.203794640 +0200
+++
/work/SRC/openSUSE:Factory/.python-langsmith.new.17972/python-langsmith.changes
2026-08-12 16:13:35.925237595 +0200
@@ -1,0 +2,10 @@
+Wed Aug 12 05:18:46 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.10.18:
+ * Add LANGSMITH_EXCLUDE_INPUTS_ON_PATCH to skip inputs on patch
+ * Support tags when creating datasets and experiments
+ * Do not swallow KeyboardInterrupt/SystemExit in serde
+ * Performance improvements in the JSON serialization path
+ * Documentation clarifications for the serialization helpers
+
+-------------------------------------------------------------------
Old:
----
langsmith-0.10.17.tar.gz
New:
----
langsmith-0.10.18.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langsmith.spec ++++++
--- /var/tmp/diff_new_pack.BBr3hT/_old 2026-08-12 16:13:36.741272063 +0200
+++ /var/tmp/diff_new_pack.BBr3hT/_new 2026-08-12 16:13:36.745272232 +0200
@@ -17,7 +17,7 @@
Name: python-langsmith
-Version: 0.10.17
+Version: 0.10.18
Release: 0
Summary: Client library for the LangSmith LLM tracing and evaluation
platform
License: MIT
++++++ langsmith-0.10.17.tar.gz -> langsmith-0.10.18.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/.bumpversion.cfg
new/langsmith-0.10.18/.bumpversion.cfg
--- old/langsmith-0.10.17/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.18/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.10.17
+current_version = 0.10.18
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch}
search = {current_version}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/PKG-INFO
new/langsmith-0.10.18/PKG-INFO
--- old/langsmith-0.10.17/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: langsmith
-Version: 0.10.17
+Version: 0.10.18
Summary: Client library to connect to the LangSmith Observability and
Evaluation Platform.
Project-URL: Homepage, https://smith.langchain.com/
Project-URL: Documentation, https://docs.smith.langchain.com/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/bench/tracing_client_bench.py
new/langsmith-0.10.18/bench/tracing_client_bench.py
--- old/langsmith-0.10.17/bench/tracing_client_bench.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/bench/tracing_client_bench.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,3 +1,27 @@
+"""Benchmark client-side tracing throughput for run creation and run patching.
+
+The HTTP session is mocked, so this measures only the SDK's own cost - payload
+serialization, compression and tracing-queue handling - with no network or
+LangSmith backend involved.
+
+Run as a script, it reports three phases: a create-only baseline, then a patch
+phase with and without `inputs`, plus a speedup summary. The latter quantifies
+what is saved by omitting inputs from a patch when they were already sent on
the
+create (see `RunTree.patch(exclude_inputs=True)`).
+
+Usage:
+ uv run python bench/tracing_client_bench.py
+ BENCH_SAMPLES=5 uv run python bench/tracing_client_bench.py
+
+`BENCH_SAMPLES` sets the number of timed repetitions (default 1, which reports
a
+zero stdev and is dominated by warmup). Payload size and run count are the
+`json_size` and `num_runs` module-level constants.
+
+Note: `create_run_data` is also imported by `tracing_client_via_pyo3.py` and
+`tracing_rust_client_bench.py`, so keep its signature stable.
+"""
+
+import os
import statistics
import time
from datetime import datetime, timedelta, timezone
@@ -57,12 +81,35 @@
}
-def benchmark_run_creation(num_runs: int, json_size: int, samples: int = 1) ->
Dict:
+def _stats(timings: list) -> Dict:
+ return {
+ "mean": statistics.mean(timings),
+ "median": statistics.median(timings),
+ "stdev": statistics.stdev(timings) if len(timings) > 1 else 0,
+ "min": min(timings),
+ "max": max(timings),
+ }
+
+
+def benchmark_run_creation(
+ num_runs: int,
+ json_size: int,
+ samples: int = 1,
+ *,
+ patch: bool = False,
+ exclude_inputs: bool = False,
+) -> Dict:
"""
- Benchmark run creation with specified parameters.
+ Benchmark run creation (and optionally patching) with specified parameters.
Returns timing statistics.
+
+ Args:
+ patch: Also benchmark a patch (update_run) phase for each created run.
+ exclude_inputs: When patching, omit inputs from the patch (they were
+ already sent on the create). Mirrors
RunTree.patch(exclude_inputs=True).
"""
- timings = []
+ timings: list = []
+ patch_timings: list = []
project_name = "__tracing_client_bench_python" + datetime.now().strftime(
"%Y%m%dT%H%M%S"
@@ -86,38 +133,93 @@
# wait for client.tracing_queue to be empty
client.tracing_queue.join()
- elapsed = time.perf_counter() - start
+ timings.append(time.perf_counter() - start)
- timings.append(elapsed)
+ if patch:
+ patch_start = time.perf_counter()
+ for run in runs:
+ client.update_run(
+ run_id=run["id"],
+ trace_id=run["trace_id"],
+ dotted_order=run["dotted_order"],
+ outputs=run["outputs"],
+ end_time=datetime.now(timezone.utc),
+ inputs=None if exclude_inputs else run["inputs"],
+ )
+ client.tracing_queue.join()
+ patch_timings.append(time.perf_counter() - patch_start)
return {
- "mean": statistics.mean(timings),
- "median": statistics.median(timings),
- "stdev": statistics.stdev(timings) if len(timings) > 1 else 0,
- "min": min(timings),
- "max": max(timings),
+ "create": _stats(timings),
+ "patch": _stats(patch_timings) if patch else None,
}
json_size = 3_000
num_runs = 1000
+samples = int(os.environ.get("BENCH_SAMPLES", "1"))
-def main(json_size: int, num_runs: int):
+def _print_stats(label: str, num: int, stats: Dict, unit: str) -> None:
+ print(f"\n{label}:")
+ print(f"Mean time: {stats['mean']:.4f} seconds")
+ print(f"Median time: {stats['median']:.4f} seconds")
+ print(f"Std Dev: {stats['stdev']:.4f} seconds")
+ print(f"Min time: {stats['min']:.4f} seconds")
+ print(f"Max time: {stats['max']:.4f} seconds")
+ print(f"Throughput: {num / stats['mean']:.2f} {unit}")
+
+
+def main(
+ json_size: int,
+ num_runs: int,
+ samples: int = 1,
+ *,
+ patch: bool = False,
+ exclude_inputs: bool = False,
+) -> Dict:
"""
Run benchmarks with different combinations of parameters and report
results.
"""
- results = benchmark_run_creation(num_runs=num_runs, json_size=json_size)
+ results = benchmark_run_creation(
+ num_runs=num_runs,
+ json_size=json_size,
+ samples=samples,
+ patch=patch,
+ exclude_inputs=exclude_inputs,
+ )
- print(f"\nBenchmark Results for {num_runs} runs with JSON size
{json_size}:")
- print(f"Mean time: {results['mean']:.4f} seconds")
- print(f"Median time: {results['median']:.4f} seconds")
- print(f"Std Dev: {results['stdev']:.4f} seconds")
- print(f"Min time: {results['min']:.4f} seconds")
- print(f"Max time: {results['max']:.4f} seconds")
- print(f"Throughput: {num_runs / results['mean']:.2f} runs/second")
+ _print_stats(
+ f"Create results for {num_runs} runs with JSON size {json_size}",
+ num_runs,
+ results["create"],
+ "runs/second",
+ )
+ if results["patch"] is not None:
+ _print_stats(
+ f"Patch results (exclude_inputs={exclude_inputs})",
+ num_runs,
+ results["patch"],
+ "patches/second",
+ )
+ return results
if __name__ == "__main__":
- main(json_size, num_runs)
+ # Create-only baseline (default behavior), then the patch phase both ways
+ # to show the exclude_inputs optimization side by side.
+ # Set BENCH_SAMPLES>1 for a measurement that is not dominated by warmup.
+ main(json_size, num_runs, samples)
+ off = main(json_size, num_runs, samples, patch=True, exclude_inputs=False)
+ on = main(json_size, num_runs, samples, patch=True, exclude_inputs=True)
+
+ off_mean = off["patch"]["mean"]
+ on_mean = on["patch"]["mean"]
+ saved = 100 * (1 - on_mean / off_mean)
+ print("\nPatch-phase comparison:")
+ print(
+ f"exclude_inputs=False: {num_runs / off_mean:.2f} patches/s
({off_mean:.4f}s)"
+ )
+ print(f"exclude_inputs=True: {num_runs / on_mean:.2f} patches/s
({on_mean:.4f}s)")
+ print(f"speedup: {off_mean / on_mean:.2f}x ({saved:.1f}% faster)")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/langsmith/__init__.py
new/langsmith-0.10.18/langsmith/__init__.py
--- old/langsmith-0.10.17/langsmith/__init__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.18/langsmith/__init__.py 2020-02-02 01:00:00.000000000
+0100
@@ -49,7 +49,7 @@
# Avoid calling into importlib on every call to __version__
-__version__ = "0.10.17"
+__version__ = "0.10.18"
version = __version__ # for backwards compatibility
# Metadata key to hide a traced run from LangSmith's Messages View.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/langsmith/_internal/_serde.py
new/langsmith-0.10.18/langsmith/_internal/_serde.py
--- old/langsmith-0.10.17/langsmith/_internal/_serde.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/langsmith/_internal/_serde.py 2020-02-02
01:00:00.000000000 +0100
@@ -30,12 +30,21 @@
| _orjson.OPT_NON_STR_KEYS
)
_JSON_KEY_TYPES = (str, int, float, bool, type(None))
+# Matches escaped lone UTF-16 surrogates (e.g. b"\\ud800") in ensure_ascii
+# json.dumps output; used to strip them on the stdlib-json fallback path.
+_SURROGATE_RE = re.compile(rb"\\ud[89a-f][0-9a-f]{2}", re.IGNORECASE)
def _simple_default(obj):
try:
# Only need to handle types that orjson doesn't serialize by default
# https://github.com/ijl/orjson#serialize
+ #
+ # datetime/UUID look redundant with orjson's native encoders, but this
+ # function is reached via two paths that bypass them, so keep them:
+ # (a) non-str dict keys normalized through _normalize_json_keys, and
+ # (b) the stdlib json.dumps fallback in dumps_json (surrogate path),
+ # which routes these *values* through this hook.
if isinstance(obj, datetime.datetime):
return obj.isoformat()
elif isinstance(obj, uuid.UUID):
@@ -71,19 +80,20 @@
elif isinstance(obj, (bytes, bytearray)):
return base64.b64encode(obj).decode()
return str(obj)
- except BaseException as e:
+ except Exception as e:
logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}")
return str(obj)
_serialization_methods: list[tuple[str, dict[str, Any]]] = [
- (
- "model_dump",
- {"exclude_none": True, "mode": "json"},
- ), # Pydantic V2 with non-serializable fields
- ("model_dump", {"exclude_none": True}), # Pydantic V2 without json mode
- ("dict", {}), # Pydantic V1 with non-serializable field
- ("to_dict", {}), # dataclasses-json
+ # Pydantic v2 primary: coerce fields to JSON-native types.
+ # Raises on truly non-serializable fields -> the next entry handles those.
+ ("model_dump", {"exclude_none": True, "mode": "json"}),
+ # Pydantic v2 fallback: python-mode dump; leaves non-JSON values as objects
+ # for orjson / _simple_default to serialize.
+ ("model_dump", {"exclude_none": True}),
+ ("dict", {}), # Pydantic v1 .dict()
+ ("to_dict", {}), # dataclasses-json to_dict()
]
@@ -100,14 +110,14 @@
return obj._asdict()
return list(obj)
+ # A class object has no useful instance serialization method
+ if isinstance(obj, type):
+ return _simple_default(obj)
+
for attr, kwargs in _serialization_methods:
- if (
- hasattr(obj, attr)
- and callable(getattr(obj, attr))
- and not isinstance(obj, type)
- ):
+ method = getattr(obj, attr, None)
+ if callable(method):
try:
- method = getattr(obj, attr)
response = method(**kwargs)
if not isinstance(response, dict):
return str(response)
@@ -117,9 +127,8 @@
f"Failed to use {attr} to serialize {type(obj)} to"
f" JSON: {repr(e)}"
)
- pass
return _simple_default(obj)
- except BaseException as e:
+ except Exception as e:
logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}")
return str(obj)
@@ -176,9 +185,7 @@
def _elide_surrogates(s: bytes) -> bytes:
- pattern = re.compile(rb"\\ud[89a-f][0-9a-f]{2}", re.IGNORECASE)
- result = pattern.sub(b"", s)
- return result
+ return _SURROGATE_RE.sub(b"", s)
def dumps_json(obj: Any) -> bytes:
@@ -188,13 +195,11 @@
----------
obj : Any
The object to serialize.
- default : Callable[[Any], Any] or None, default=None
- The default function to use for serialization.
Returns:
-------
- str
- The JSON formatted string.
+ bytes
+ The JSON formatted string, encoded as UTF-8 bytes.
"""
try:
return _orjson.dumps(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/langsmith/client.py
new/langsmith-0.10.18/langsmith/client.py
--- old/langsmith-0.10.17/langsmith/client.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.18/langsmith/client.py 2020-02-02 01:00:00.000000000
+0100
@@ -5200,6 +5200,7 @@
num_examples: Optional[int] = None,
num_repetitions: Optional[int] = None,
evaluator_keys: Optional[list[str]] = None,
+ tag_value_ids: Optional[list[ID_TYPE]] = None,
) -> ls_schemas.TracerSession:
"""Create a project on the LangSmith API.
@@ -5221,6 +5222,8 @@
row-level evaluators that will run against this project. Used
by
the backend to populate per-evaluator experiment progress.
Transport-only.
+ tag_value_ids (Optional[list[Union[UUID, str]]]): IDs of tag
values to
+ apply to the project at creation time.
Returns:
TracerSession: The created project.
@@ -5246,6 +5249,8 @@
body["num_repetitions"] = num_repetitions
if evaluator_keys:
body["evaluator_keys"] = evaluator_keys
+ if tag_value_ids is not None:
+ body["tag_value_ids"] = tag_value_ids
response = self.request_with_retries(
"POST",
endpoint,
@@ -5683,6 +5688,7 @@
outputs_schema: Optional[dict[str, Any]] = None,
transformations: Optional[list[ls_schemas.DatasetTransformation]] =
None,
metadata: Optional[dict] = None,
+ tag_value_ids: Optional[list[ID_TYPE]] = None,
) -> ls_schemas.Dataset:
"""Create a dataset in the LangSmith API.
@@ -5701,6 +5707,8 @@
A list of transformations to apply to the dataset.
metadata (Optional[dict]):
Additional metadata to associate with the dataset.
+ tag_value_ids (Optional[list[Union[UUID, str]]]): IDs of tag
values to
+ apply to the dataset at creation time.
Returns:
Dataset: The created dataset.
@@ -5730,6 +5738,9 @@
if outputs_schema is not None:
dataset["outputs_schema_definition"] = outputs_schema
+ if tag_value_ids is not None:
+ dataset["tag_value_ids"] = [str(tag_id) for tag_id in
tag_value_ids]
+
response = self.request_with_retries(
"POST",
"/datasets",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/langsmith/run_trees.py
new/langsmith-0.10.18/langsmith/run_trees.py
--- old/langsmith-0.10.17/langsmith/run_trees.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/langsmith/run_trees.py 2020-02-02
01:00:00.000000000 +0100
@@ -141,6 +141,20 @@
return cast(WriteReplica, filtered)
[email protected]_cache(maxsize=1)
+def _exclude_inputs_on_patch() -> bool:
+ """Whether `RunTree.patch()` should omit `inputs` by default.
+
+ Controlled by ``LANGSMITH_EXCLUDE_INPUTS_ON_PATCH``; defaults to
``False``, i.e.
+ inputs are re-sent on every patch. Enabling it skips serializing and
uploading
+ the inputs a second time, which is a meaningful saving for large payloads,
but
+ it also means inputs first set *after* `post()` are never persisted.
+
+ Read once per process and cached; call ``.cache_clear()`` to re-read.
+ """
+ return utils.is_truish(utils.get_env_var("EXCLUDE_INPUTS_ON_PATCH"))
+
+
LANGSMITH_PREFIX = "langsmith-"
LANGSMITH_DOTTED_ORDER = sys.intern(f"{LANGSMITH_PREFIX}trace")
LANGSMITH_DOTTED_ORDER_BYTES = LANGSMITH_DOTTED_ORDER.encode("utf-8")
@@ -522,6 +536,10 @@
def add_inputs(self, inputs: dict[str, Any]) -> None:
"""Upsert the given inputs into the run.
+ Note: if `LANGSMITH_EXCLUDE_INPUTS_ON_PATCH` is enabled, inputs added
+ after the initial `post()` are not sent by `patch()`. Call
+ `patch(exclude_inputs=False)` explicitly to persist them.
+
Args:
inputs: A dictionary containing the inputs to be added.
"""
@@ -820,12 +838,18 @@
for child_run in self.child_runs:
child_run.post(exclude_child_runs=False)
- def patch(self, *, exclude_inputs: bool = False) -> None:
+ def patch(self, *, exclude_inputs: Optional[bool] = None) -> None:
"""Patch the run tree to the API in a background thread.
Args:
exclude_inputs: Whether to exclude inputs from the patch request.
+ Defaults to `None`, meaning the value of the
+ `LANGSMITH_EXCLUDE_INPUTS_ON_PATCH` environment variable is
used
+ (itself defaulting to `False`). Pass an explicit `True` or
`False`
+ to override the environment for this call.
"""
+ if exclude_inputs is None:
+ exclude_inputs = _exclude_inputs_on_patch()
if not self.end_time:
self.end()
attachments = {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/tests/unit_tests/test_client.py
new/langsmith-0.10.18/tests/unit_tests/test_client.py
--- old/langsmith-0.10.17/tests/unit_tests/test_client.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/tests/unit_tests/test_client.py 2020-02-02
01:00:00.000000000 +0100
@@ -2420,6 +2420,41 @@
}
+def test__dumps_json_does_not_swallow_keyboard_interrupt():
+ """Serialization must not swallow KeyboardInterrupt/SystemExit.
+
+ The serde error handlers catch ``Exception`` (not ``BaseException``), so a
+ system-exiting signal raised while serializing an object propagates instead
+ of being masked as ``str(obj)``.
+ """
+
+ class _RaisesInterrupt:
+ def model_dump(self, **kwargs):
+ raise KeyboardInterrupt
+
+ with pytest.raises(KeyboardInterrupt):
+ _dumps_json({"x": _RaisesInterrupt()})
+
+
+def test__dumps_json_type_object_serializes_as_str():
+ # A class object has no useful instance serialization method; it must fall
+ # through to _simple_default -> str rather than be probed like an instance.
+ # The probe attribute has to live at the class level (via a metaclass) for
+ # this to distinguish the guarded and unguarded implementations.
+ class Meta(type):
+ def to_dict(cls):
+ return {"meta": "yes"}
+
+ class WithClassLevelToDict(metaclass=Meta):
+ pass
+
+ # Guarded: falls through to _simple_default -> str.
+ # Unguarded: probed like an instance, emits {"meta": "yes"}.
+ assert isinstance(
+ _orjson.loads(_dumps_json({"cls": WithClassLevelToDict}))["cls"], str
+ )
+
+
@patch("langsmith.client.requests.Session", autospec=True)
def test_host_url(_: MagicMock) -> None:
client = Client(api_url="https://api.foobar.com/api", api_key="API_KEY")
@@ -6764,6 +6799,11 @@
{},
{"reference_dataset_id": "DATASET_ID_PLACEHOLDER"},
),
+ (
+ {"tag_value_ids": ["550e8400-e29b-41d4-a716-446655440000"]},
+ {},
+ {"tag_value_ids": ["550e8400-e29b-41d4-a716-446655440000"]},
+ ),
# All parameters together
(
{
@@ -6824,6 +6864,32 @@
assert body[field] == value
+def test_create_dataset_with_tag_value_ids():
+ tag_value_ids = [uuid.uuid4(), uuid.uuid4()]
+ with patch("langsmith.client.requests.Session") as mock_session_cls:
+ mock_session = MagicMock()
+ mock_response = MagicMock()
+ mock_response.status_code = 200
+ mock_response.json.return_value = {
+ "id": str(uuid.uuid4()),
+ "name": "test-dataset",
+ "created_at": "2024-01-01T00:00:00Z",
+ }
+ mock_session.request.return_value = mock_response
+ mock_session_cls.return_value = mock_session
+
+ client = Client(api_key="test", auto_batch_tracing=False)
+ client.create_dataset("test-dataset", tag_value_ids=tag_value_ids)
+
+ create_call = next(
+ call
+ for call in mock_session.request.call_args_list
+ if call.args[0] == "POST" and call.args[1].endswith("/datasets")
+ )
+ body = json.loads(create_call.kwargs["data"])
+ assert body["tag_value_ids"] == [str(tag_id) for tag_id in
tag_value_ids]
+
+
_MULTIPART_HEADERS = {"Content-Type": "multipart/form-data;
boundary=test-boundary"}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.17/tests/unit_tests/test_run_trees.py
new/langsmith-0.10.18/tests/unit_tests/test_run_trees.py
--- old/langsmith-0.10.17/tests/unit_tests/test_run_trees.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.18/tests/unit_tests/test_run_trees.py 2020-02-02
01:00:00.000000000 +0100
@@ -14,6 +14,7 @@
from langsmith import run_trees
from langsmith import schemas as ls_schemas
+from langsmith import utils as ls_utils
from langsmith._internal._uuid import uuid7_deterministic
from langsmith.client import Client
from langsmith.run_trees import RunTree
@@ -744,3 +745,104 @@
rt = RunTree(name="test", run_type="chain", inputs={})
rt.set(outputs=MyOutput(answer="result"))
assert rt.outputs == {"answer": "result"}
+
+
[email protected]
+def _reset_exclude_inputs_cache():
+ """Reset the memoized `LANGSMITH_EXCLUDE_INPUTS_ON_PATCH` lookup."""
+
+ def _clear():
+ run_trees._exclude_inputs_on_patch.cache_clear()
+ ls_utils.get_env_var.cache_clear()
+
+ _clear()
+ yield _clear
+ _clear()
+
+
[email protected](
+ "env_name, env_value, explicit, expect_inputs",
+ [
+ # Unset env var keeps today's behaviour: inputs are re-sent on patch.
+ (None, None, None, True),
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "true", None, False),
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "TRUE", None, False),
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "1", None, False),
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "false", None, True),
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "bogus", None, True),
+ # The LANGCHAIN_ namespace is honoured too.
+ ("LANGCHAIN_EXCLUDE_INPUTS_ON_PATCH", "true", None, False),
+ # An explicit argument always wins over the environment.
+ ("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "true", False, True),
+ (None, None, True, False),
+ ],
+)
+def test_patch_exclude_inputs_env_flag(
+ monkeypatch,
+ _reset_exclude_inputs_cache,
+ env_name,
+ env_value,
+ explicit,
+ expect_inputs,
+):
+ monkeypatch.delenv("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", raising=False)
+ monkeypatch.delenv("LANGCHAIN_EXCLUDE_INPUTS_ON_PATCH", raising=False)
+ if env_name is not None:
+ monkeypatch.setenv(env_name, env_value)
+ _reset_exclude_inputs_cache()
+
+ client = MagicMock()
+ run_tree = RunTree(
+ name="test_run", run_type="chain", inputs={"a": 1}, client=client
+ )
+ run_tree.patch(**({} if explicit is None else {"exclude_inputs":
explicit}))
+
+ sent = client.update_run.call_args.kwargs["inputs"]
+ if expect_inputs:
+ assert sent == {"a": 1}
+ else:
+ assert sent is None
+
+
+def test_patch_exclude_inputs_env_flag_with_replicas(
+ monkeypatch, _reset_exclude_inputs_cache
+):
+ """The replica patch path honours the flag as well."""
+ monkeypatch.setenv("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "true")
+ _reset_exclude_inputs_cache()
+
+ client = MagicMock()
+ run_tree = RunTree(
+ name="test_run",
+ run_type="chain",
+ inputs={"a": 1},
+ client=client,
+ project_name="test-project",
+ replicas=[run_trees.WriteReplica(project_name="replica-project")],
+ )
+ run_tree.patch()
+
+ assert client.update_run.call_args.kwargs["inputs"] is None
+
+
+def test_patch_exclude_inputs_flag_is_cached(monkeypatch,
_reset_exclude_inputs_cache):
+ """The env var is read once per process, not on every patch."""
+ monkeypatch.delenv("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", raising=False)
+ monkeypatch.delenv("LANGCHAIN_EXCLUDE_INPUTS_ON_PATCH", raising=False)
+ _reset_exclude_inputs_cache()
+
+ client = MagicMock()
+ run_tree = RunTree(
+ name="test_run", run_type="chain", inputs={"a": 1}, client=client
+ )
+ run_tree.patch()
+ assert client.update_run.call_args.kwargs["inputs"] == {"a": 1}
+
+ # Flipping the env var mid-process has no effect until the cache is
cleared.
+ monkeypatch.setenv("LANGSMITH_EXCLUDE_INPUTS_ON_PATCH", "true")
+ run_tree.patch()
+ assert client.update_run.call_args.kwargs["inputs"] == {"a": 1}
+
+ _reset_exclude_inputs_cache()
+ run_tree.patch()
+ assert client.update_run.call_args.kwargs["inputs"] is None