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

Reply via email to