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-07-24 22:05:15
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langsmith (Old)
and /work/SRC/openSUSE:Factory/.python-langsmith.new.2004 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langsmith"
Fri Jul 24 22:05:15 2026 rev:14 rq:1367619 version:0.10.10
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-langsmith/python-langsmith.changes
2026-07-22 19:08:42.980479020 +0200
+++
/work/SRC/openSUSE:Factory/.python-langsmith.new.2004/python-langsmith.changes
2026-07-24 22:05:21.649747222 +0200
@@ -1,0 +2,9 @@
+Thu Jul 23 19:49:50 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.10.10:
+ * Require a project to be specified when calling get_run_stats()
+ * Add session_id and start_time to feedback generated from
+ evaluations
+ * Merge sandbox request headers case-insensitively
+
+-------------------------------------------------------------------
Old:
----
langsmith-0.10.9.tar.gz
New:
----
langsmith-0.10.10.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langsmith.spec ++++++
--- /var/tmp/diff_new_pack.pfJRDm/_old 2026-07-24 22:05:23.021795136 +0200
+++ /var/tmp/diff_new_pack.pfJRDm/_new 2026-07-24 22:05:23.025795276 +0200
@@ -17,7 +17,7 @@
Name: python-langsmith
-Version: 0.10.9
+Version: 0.10.10
Release: 0
Summary: Client library for the LangSmith LLM tracing and evaluation
platform
License: MIT
++++++ langsmith-0.10.9.tar.gz -> langsmith-0.10.10.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/.bumpversion.cfg
new/langsmith-0.10.10/.bumpversion.cfg
--- old/langsmith-0.10.9/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.10/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.10.9
+current_version = 0.10.10
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.9/PKG-INFO
new/langsmith-0.10.10/PKG-INFO
--- old/langsmith-0.10.9/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: langsmith
-Version: 0.10.9
+Version: 0.10.10
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.9/langsmith/__init__.py
new/langsmith-0.10.10/langsmith/__init__.py
--- old/langsmith-0.10.9/langsmith/__init__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.10/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.9"
+__version__ = "0.10.10"
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.9/langsmith/_internal/_v2_migration_utils.py
new/langsmith-0.10.10/langsmith/_internal/_v2_migration_utils.py
--- old/langsmith-0.10.9/langsmith/_internal/_v2_migration_utils.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/langsmith/_internal/_v2_migration_utils.py
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,84 @@
+"""Utilities for migrating functionality to the v2 LangSmith API."""
+
+from __future__ import annotations
+
+import datetime
+from typing import TYPE_CHECKING, Any, Optional
+
+from langsmith import schemas
+
+if TYPE_CHECKING:
+ from langsmith.client import Client
+
+
+# Fields for `/v2/runs/query` (RunSelectField enum); omitting selects returns
only id.
+_V2_RUN_SELECTS = [
+ "ID",
+ "NAME",
+ "RUN_TYPE",
+ "STATUS",
+ "START_TIME",
+ "END_TIME",
+ "INPUTS",
+ "OUTPUTS",
+ "PARENT_RUN_IDS",
+ "PROJECT_ID",
+ "TRACE_ID",
+ "DOTTED_ORDER",
+ "REFERENCE_EXAMPLE_ID",
+ "ERROR",
+]
+
+
+def _load_traces_v2(
+ project: schemas.TracerSession,
+ client: Client,
+ *,
+ is_root: Optional[bool],
+) -> list[schemas.Run]:
+ """List an experiment's runs from v2.
+
+ `query_v2` defaults `min_start_time` to ~24h, so bound the window to the
session
+ explicitly or older experiments drop.
+ """
+ now = datetime.datetime.now(datetime.timezone.utc)
+ kwargs: dict[str, Any] = {
+ "project_ids": [str(project.id)],
+ "min_start_time": project.start_time,
+ "max_start_time": project.end_time or now,
+ "selects": _V2_RUN_SELECTS,
+ }
+ if is_root is not None:
+ kwargs["is_root"] = is_root
+ pager = client._get_langsmith_api_sync().runs.query_v2(**kwargs)
+ return [_v2_run_to_schema(run) for run in pager]
+
+
+def _v2_run_to_schema(run: Any) -> schemas.Run:
+ """Map a v2 `Run` to `schemas.Run`.
+
+ `project_id`→`session_id`, `parent_run_ids[-1]`→`parent_run_id`; drop
`None` so
+ schema defaults apply (e.g. `dotted_order`).
+ """
+ parent_run_ids = getattr(run, "parent_run_ids", None)
+ fields = {
+ "id": run.id,
+ "name": run.name,
+ "run_type": run.run_type,
+ "start_time": run.start_time,
+ "end_time": getattr(run, "end_time", None),
+ "trace_id": run.trace_id,
+ "session_id": getattr(run, "project_id", None),
+ "parent_run_id": parent_run_ids[-1] if parent_run_ids else None,
+ "dotted_order": getattr(run, "dotted_order", None),
+ "reference_example_id": getattr(run, "reference_example_id", None),
+ "inputs": getattr(run, "inputs", None) or {},
+ "outputs": getattr(run, "outputs", None),
+ "error": getattr(run, "error", None),
+ "status": (
+ run.status.lower() if getattr(run, "status", None) is not None
else None
+ ),
+ }
+ return schemas.Run(
+ **{key: value for key, value in fields.items() if value is not None}
+ )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/langsmith/client.py
new/langsmith-0.10.10/langsmith/client.py
--- old/langsmith-0.10.9/langsmith/client.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.10/langsmith/client.py 2020-02-02 01:00:00.000000000
+0100
@@ -113,6 +113,7 @@
from langsmith._internal._serde import dumps_json as _dumps_json
from langsmith._internal._uuid import uuid7
from langsmith._openapi_client import AsyncLangsmith as LangsmithOpenAPIClient
+from langsmith._openapi_client import Langsmith as SyncLangsmithOpenAPIClient
from langsmith.prompt_cache import PromptCache, prompt_cache_singleton
from langsmith.schemas import AttachmentInfo, ExampleWithRuns
@@ -917,6 +918,7 @@
"_profile_auth",
"_profile_auth_headers",
"_langsmith_api",
+ "_langsmith_api_sync",
]
_api_key: Optional[str]
@@ -928,6 +930,7 @@
_profile_auth: Optional[_profiles.ProfileAuth]
_profile_auth_headers: dict[str, str]
_langsmith_api: Optional[LangsmithOpenAPIClient]
+ _langsmith_api_sync: Optional[SyncLangsmithOpenAPIClient]
def __init__(
self,
@@ -1467,6 +1470,7 @@
self._failed_traces_max_bytes = 100 * 1024 * 1024
self._langsmith_api = None
+ self._langsmith_api_sync = None
# ------------------------------------------------------------------
# Stainless v2 resource accessors
@@ -1491,6 +1495,22 @@
)
return self._langsmith_api
+ def _get_langsmith_api_sync(self) -> SyncLangsmithOpenAPIClient:
+ if self._langsmith_api_sync is None:
+ self._langsmith_api_sync = SyncLangsmithOpenAPIClient(
+ api_key=self._api_key,
+ tenant_id=str(self._workspace_id) if self._workspace_id else
None,
+ base_url=self.api_url,
+ timeout=_httpx.Timeout(
+ connect=self._timeout[0],
+ read=self._timeout[1],
+ write=self._timeout[1],
+ pool=self._timeout[0],
+ ),
+ default_headers=self._headers or None,
+ )
+ return self._langsmith_api_sync
+
@property
def runs(self) -> AsyncRunsResource:
"""Access the runs resource."""
@@ -4468,6 +4488,10 @@
]
for future in as_completed(futures):
project_ids.append(future.result().id)
+ if not project_ids:
+ raise ValueError(
+ "At least one of project_names or project_ids must be
provided."
+ )
payload = {
"id": id,
"trace": trace,
@@ -7604,10 +7628,10 @@
Optional[ls_schemas.FeedbackConfig], res.feedback_config
),
feedback_source_type=ls_schemas.FeedbackSourceType.MODEL,
- project_id=project_id,
+ project_id=project_id if run is None else None,
extra=res.extra,
trace_id=run.trace_id if run else None,
- session_id=run.session_id if run else None,
+ session_id=run.session_id if run and run.session_id else
project_id,
start_time=run.start_time if run else None,
error=error,
)
@@ -7731,8 +7755,7 @@
The number of times to retry the request before giving up.
project_id (Optional[Union[UUID, str]]):
The ID of the project (or experiment) to provide feedback on.
This is
- used for creating summary metrics for experiments. Cannot
specify
- run_id or trace_id if project_id is specified, and vice versa.
+ used for creating summary metrics for experiments.
comparative_experiment_id (Optional[Union[UUID, str]]):
If this feedback was logged as a part of a comparative
experiment, this
associates the feedback with that experiment.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/langsmith/evaluation/_arunner.py
new/langsmith-0.10.10/langsmith/evaluation/_arunner.py
--- old/langsmith-0.10.9/langsmith/evaluation/_arunner.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/langsmith/evaluation/_arunner.py 2020-02-02
01:00:00.000000000 +0100
@@ -42,7 +42,7 @@
_load_examples_map,
_load_experiment,
_load_tqdm,
- _load_traces,
+ _load_traces_for_experiment,
_resolve_data,
_resolve_evaluators,
_resolve_experiment,
@@ -447,8 +447,8 @@
)
runs = await aitertools.aio_to_thread(
contextvars.copy_context(),
- _load_traces,
- experiment,
+ _load_traces_for_experiment,
+ project,
client,
load_nested=load_nested,
)
@@ -1021,7 +1021,10 @@
if self._upload_results:
self.client._log_evaluation_feedback(
- evaluator_response, run=run,
_executor=feedback_executor
+ evaluator_response,
+ run=run,
+ project_id=self._get_experiment().id,
+ _executor=feedback_executor,
)
return selected_results
except Exception as e:
@@ -1044,7 +1047,10 @@
)
if self._upload_results:
self.client._log_evaluation_feedback(
- error_response, run=run,
_executor=feedback_executor
+ error_response,
+ run=run,
+ project_id=self._get_experiment().id,
+ _executor=feedback_executor,
)
return selected_results
except Exception as e2:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/langsmith/evaluation/_runner.py
new/langsmith-0.10.10/langsmith/evaluation/_runner.py
--- old/langsmith-0.10.9/langsmith/evaluation/_runner.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/langsmith/evaluation/_runner.py 2020-02-02
01:00:00.000000000 +0100
@@ -43,6 +43,7 @@
from langsmith import run_trees as rt
from langsmith import schemas
from langsmith import utils as ls_utils
+from langsmith._internal import _v2_migration_utils
from langsmith._internal._beta_decorator import _warn_once
from langsmith.evaluation.evaluator import (
SUMMARY_EVALUATOR_T,
@@ -505,7 +506,7 @@
""" # noqa: E501
client = client or rt.get_cached_client(timeout_ms=(20_000, 90_001))
project = _load_experiment(experiment, client)
- runs = _load_traces(experiment, client, load_nested=load_nested)
+ runs = _load_traces_for_experiment(project, client,
load_nested=load_nested)
data_map = _load_examples_map(client, project)
data = [data_map[cast(uuid.UUID, run.reference_example_id)] for run in
runs]
return _evaluate(
@@ -893,8 +894,8 @@
comparison_url = _build_comparative_url(experiments_tuple,
comparative_experiment)
_print_comparative_experiment_start(comparison_url)
runs = [
- _load_traces(experiment, client, load_nested=load_nested)
- for experiment in experiments
+ _load_traces_for_experiment(project, client, load_nested=load_nested)
+ for project in projects
]
# Only check intersections for the experiments
examples_intersection = None
@@ -1160,14 +1161,20 @@
return client.read_project(project_name=project)
-def _load_traces(
+def _load_traces_for_experiment(
project: Union[str, uuid.UUID, schemas.TracerSession],
client: langsmith.Client,
load_nested: bool = False,
) -> list[schemas.Run]:
"""Load nested traces for a given project."""
is_root = None if load_nested else True
- if isinstance(project, schemas.TracerSession):
+ runs: Iterable[schemas.Run]
+ # v1 `/runs/query` returns 501 on SmithDB-only backends; use v2 when it's
available.
+ if (client.info.instance_flags or {}).get("sdb_query_enabled"):
+ if not isinstance(project, schemas.TracerSession):
+ project = _load_experiment(project, client)
+ runs = _v2_migration_utils._load_traces_v2(project, client,
is_root=is_root)
+ elif isinstance(project, schemas.TracerSession):
runs = client.list_runs(project_id=project.id, is_root=is_root)
elif isinstance(project, uuid.UUID) or _is_uuid(project):
runs = client.list_runs(project_id=project, is_root=is_root)
@@ -1681,7 +1688,10 @@
if self._upload_results:
# TODO: This is a hack
self.client._log_evaluation_feedback(
- evaluator_response, run=run, _executor=executor
+ evaluator_response,
+ run=run,
+ project_id=self._get_experiment().id,
+ _executor=executor,
)
except Exception as e:
try:
@@ -1704,7 +1714,10 @@
if self._upload_results:
# TODO: This is a hack
self.client._log_evaluation_feedback(
- error_response, run=run, _executor=executor
+ error_response,
+ run=run,
+ project_id=self._get_experiment().id,
+ _executor=executor,
)
except Exception as e2:
logger.debug(f"Error parsing feedback keys: {e2}")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/langsmith/sandbox/_helpers.py
new/langsmith-0.10.10/langsmith/sandbox/_helpers.py
--- old/langsmith-0.10.9/langsmith/sandbox/_helpers.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/langsmith/sandbox/_helpers.py 2020-02-02
01:00:00.000000000 +0100
@@ -34,10 +34,17 @@
base_headers: Optional[Mapping[str, str]] = None,
override_headers: Optional[Mapping[str, str]] = None,
) -> dict[str, str]:
- """Merge request headers, giving precedence to overrides."""
- merged: dict[str, str] = dict(base_headers or {})
- if override_headers:
- merged.update(override_headers)
+ """Merge request headers, giving precedence to overrides.
+
+ Names are normalized to lowercase so an override replaces a base header
that
+ differs only in casing. HTTP header names are case-insensitive, so keeping
+ both would be ambiguous — a server reading the first value would see the
+ base header instead of the override.
+ """
+ merged: dict[str, str] = {}
+ for headers in (base_headers, override_headers):
+ for name, value in (headers or {}).items():
+ merged[name.lower()] = value
return merged
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/langsmith/testing/_internal.py
new/langsmith-0.10.10/langsmith/testing/_internal.py
--- old/langsmith-0.10.9/langsmith/testing/_internal.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/langsmith/testing/_internal.py 2020-02-02
01:00:00.000000000 +0100
@@ -628,6 +628,7 @@
def submit_result(
self,
run_id: uuid.UUID,
+ start_time: datetime.datetime,
error: Optional[str] = None,
skipped: bool = False,
pytest_plugin: Any = None,
@@ -644,12 +645,24 @@
status = "passed"
if pytest_plugin and pytest_nodeid:
pytest_plugin.update_process_status(pytest_nodeid, {"status":
status})
- self._executor.submit(self._submit_result, run_id, score)
+ self._executor.submit(self._submit_result, run_id, start_time, score)
- def _submit_result(self, run_id: uuid.UUID, score: Optional[int]) -> None:
+ def _submit_result(
+ self,
+ run_id: uuid.UUID,
+ start_time: datetime.datetime,
+ score: Optional[int],
+ ) -> None:
# trace_id will always be run_id here because the feedback is on the
root
# test run
- self.client.create_feedback(run_id, key="pass", score=score,
trace_id=run_id)
+ self.client.create_feedback(
+ run_id,
+ key="pass",
+ score=score,
+ trace_id=run_id,
+ session_id=self.experiment_id,
+ start_time=start_time,
+ )
def sync_example(
self,
@@ -726,10 +739,14 @@
self,
run_id: ID_TYPE,
feedback: Union[dict, list],
+ *,
+ start_time: datetime.datetime,
pytest_plugin: Any = None,
pytest_nodeid: Any = None,
**kwargs: Any,
):
+ kwargs["session_id"] = self.experiment_id
+ kwargs["start_time"] = start_time
feedback = feedback if isinstance(feedback, list) else [feedback]
for fb in feedback:
if pytest_plugin and pytest_nodeid:
@@ -819,6 +836,7 @@
self.pytest_nodeid = pytest_nodeid
self.inputs = inputs
self.reference_outputs = reference_outputs
+ self.run_start_time: Optional[datetime.datetime] = None
self._logged_reference_outputs: Optional[dict] = None
self._logged_outputs: Optional[dict] = None
@@ -832,11 +850,14 @@
self.log_reference_outputs(reference_outputs)
def submit_feedback(self, *args, **kwargs: Any):
+ if self.run_start_time is None:
+ raise ValueError("Cannot submit feedback before the test run has
started.")
self.test_suite._submit_feedback(
*args,
**{
**kwargs,
**dict(
+ start_time=self.run_start_time,
pytest_plugin=self.pytest_plugin,
pytest_nodeid=self.pytest_nodeid,
),
@@ -869,8 +890,13 @@
error: Optional[str] = None,
skipped: bool = False,
) -> None:
+ if self.run_start_time is None:
+ raise ValueError(
+ "Cannot submit a test result before the test run has started."
+ )
return self.test_suite.submit_result(
self.run_id,
+ self.run_start_time,
error=error,
skipped=skipped,
pytest_plugin=self.pytest_plugin,
@@ -1035,6 +1061,7 @@
exceptions_to_handle=(SkipException,),
_end_on_exit=False,
) as run_tree:
+ test_case.run_start_time = run_tree.start_time
try:
result = func(*test_args, **test_kwargs)
except SkipException as e:
@@ -1115,6 +1142,7 @@
exceptions_to_handle=(SkipException,),
_end_on_exit=False,
) as run_tree:
+ test_case.run_start_time = run_tree.start_time
try:
result = await func(*test_args, **test_kwargs)
except SkipException as e:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/integration_tests/test_client.py
new/langsmith-0.10.10/tests/integration_tests/test_client.py
--- old/langsmith-0.10.9/tests/integration_tests/test_client.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/integration_tests/test_client.py
2020-02-02 01:00:00.000000000 +0100
@@ -4156,92 +4156,6 @@
assert insights_job.status in ["queued", "running", "success"]
-def test_feedback_formula_crud_flow(langchain_client: Client) -> None:
- dataset_name = f"feedback-formula-crud-{uuid7().hex}"
- feedback_key = f"overall-quality-{uuid7().hex[:8]}"
- initial_parts = [
- {"part_type": "weighted_key", "weight": 0.6, "key": "accuracy"},
- {"part_type": "weighted_key", "weight": 0.4, "key": "helpfulness"},
- ]
- updated_parts = [
- {"part_type": "weighted_key", "weight": 0.25, "key": "coverage"},
- {"part_type": "weighted_key", "weight": 0.75, "key": "relevance"},
- ]
-
- dataset = None
- feedback_formula_id = None
- try:
- dataset = langchain_client.create_dataset(dataset_name)
- created_formula = langchain_client.create_feedback_formula(
- feedback_key=feedback_key,
- aggregation_type="sum",
- formula_parts=initial_parts,
- dataset_id=dataset.id,
- )
- feedback_formula_id = created_formula.id
-
- assert created_formula.dataset_id == dataset.id
- assert created_formula.feedback_key == feedback_key
- assert [part.key for part in created_formula.formula_parts] == [
- part["key"] for part in initial_parts
- ]
-
- formulas =
list(langchain_client.list_feedback_formulas(dataset_id=dataset.id))
- assert any(formula.id == feedback_formula_id for formula in formulas)
-
- updated_feedback_key = f"{feedback_key}-updated"
- updated_formula = langchain_client.update_feedback_formula(
- feedback_formula_id,
- feedback_key=updated_feedback_key,
- aggregation_type="avg",
- formula_parts=updated_parts,
- )
- assert updated_formula.id == feedback_formula_id
- assert updated_formula.feedback_key == updated_feedback_key
- assert updated_formula.aggregation_type == "avg"
- assert [part.key for part in updated_formula.formula_parts] == [
- part["key"] for part in updated_parts
- ]
- assert [part.weight for part in updated_formula.formula_parts] == [
- part["weight"] for part in updated_parts
- ]
-
- fetched_formula = langchain_client.get_feedback_formula_by_id(
- feedback_formula_id
- )
- assert fetched_formula.feedback_key == updated_feedback_key
- assert fetched_formula.aggregation_type == "avg"
- assert [part.key for part in fetched_formula.formula_parts] == [
- part["key"] for part in updated_parts
- ]
-
- langchain_client.delete_feedback_formula(feedback_formula_id)
- deleted_formula_id = feedback_formula_id
- feedback_formula_id = None
-
- wait_for(
- lambda: (
- deleted_formula_id
- not in {
- formula.id
- for formula in langchain_client.list_feedback_formulas(
- dataset_id=dataset.id
- )
- }
- ),
- max_sleep_time=30,
- sleep_time=1,
- )
- finally:
- if feedback_formula_id is not None:
- try:
- langchain_client.delete_feedback_formula(feedback_formula_id)
- except Exception:
- pass
- if dataset is not None:
- safe_delete_dataset(langchain_client, dataset_id=dataset.id)
-
-
# ---------------------------------------------------------------------------
# v2 resource tests (runs.retrieve / runs.query via AsyncRunsResource)
# ---------------------------------------------------------------------------
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/unit_tests/evaluation/test_runner.py
new/langsmith-0.10.10/tests/unit_tests/evaluation/test_runner.py
--- old/langsmith-0.10.9/tests/unit_tests/evaluation/test_runner.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/evaluation/test_runner.py
2020-02-02 01:00:00.000000000 +0100
@@ -12,6 +12,7 @@
import uuid
from datetime import datetime, timezone
from threading import Lock
+from types import SimpleNamespace
from typing import Any, Callable, Dict, List, Tuple
from unittest import mock
from unittest.mock import MagicMock
@@ -21,11 +22,13 @@
from langsmith import Client, aevaluate, evaluate
from langsmith import schemas as ls_schemas
+from langsmith._internal._v2_migration_utils import _v2_run_to_schema
from langsmith.evaluation._runner import (
ComparativeExperimentResults,
_build_comparative_url,
_collect_evaluator_keys,
_get_target_args,
+ _load_traces_for_experiment,
)
from langsmith.evaluation.evaluator import (
DynamicRunEvaluator,
@@ -44,6 +47,7 @@
def __init__(self, ds_id, ds_name, ds_examples, tenant_id):
self.created_session = None
self.runs = {}
+ self.feedbacks = []
self.should_fail = False
self.ds_id = ds_id
self.ds_name = ds_name
@@ -116,6 +120,7 @@
}
return res
elif endpoint == "http://localhost:1984/feedback":
+ self.feedbacks.append(json.loads(kwargs["data"]))
response = MagicMock()
response.json.return_value = {}
return response
@@ -178,6 +183,79 @@
}
+def _fake_client_for_examples(
+ examples: list[dict[str, Any]], ds_id: str, ds_name: str
+) -> tuple[Client, FakeRequest]:
+ tenant_id = str(uuid.uuid4())
+ session = mock.Mock()
+ fake_request = FakeRequest(ds_id, ds_name, examples, tenant_id)
+ session.request = fake_request.request
+ client = Client(api_url="http://localhost:1984", api_key="123",
session=session)
+ client._tenant_id = uuid.UUID(tenant_id)
+ return client, fake_request
+
+
+def test_evaluate_feedback_includes_experiment_session_id_and_run_start_time()
-> None:
+ example, example_payload = _create_example(0)
+ client, fake_request = _fake_client_for_examples(
+ [example_payload], str(example.dataset_id), "my-dataset"
+ )
+
+ def predict(inputs: dict) -> dict:
+ return {"output": inputs["in"] + 1}
+
+ def score(run, example):
+ return {"key": "quality", "score": 1}
+
+ results = evaluate(
+ predict,
+ data=[example],
+ evaluators=[score],
+ client=client,
+ blocking=True,
+ max_concurrency=0,
+ )
+ assert len(list(results)) == 1
+ _wait_until(lambda: len(fake_request.feedbacks) == 1)
+
+ feedback = fake_request.feedbacks[0]
+ _wait_until(lambda: feedback["run_id"] in fake_request.runs)
+ run = fake_request.runs[feedback["run_id"]]
+ assert feedback["session_id"] == fake_request.created_session["id"]
+ assert feedback["start_time"] == run["start_time"]
+
+
[email protected]
+async def test_aevaluate_feedback_includes_experiment_id_and_start_time() ->
None:
+ example, example_payload = _create_example(0)
+ client, fake_request = _fake_client_for_examples(
+ [example_payload], str(example.dataset_id), "my-dataset"
+ )
+
+ async def predict(inputs: dict) -> dict:
+ return {"output": inputs["in"] + 1}
+
+ async def score(run, example):
+ return {"key": "quality", "score": 1}
+
+ results = await aevaluate(
+ predict,
+ data=[example],
+ evaluators=[score],
+ client=client,
+ blocking=True,
+ max_concurrency=0,
+ )
+ assert len([row async for row in results]) == 1
+ _wait_until(lambda: len(fake_request.feedbacks) == 1)
+
+ feedback = fake_request.feedbacks[0]
+ _wait_until(lambda: feedback["run_id"] in fake_request.runs)
+ run = fake_request.runs[feedback["run_id"]]
+ assert feedback["session_id"] == fake_request.created_session["id"]
+ assert feedback["start_time"] == run["start_time"]
+
+
@pytest.mark.skipif(sys.version_info < (3, 9), reason="requires python3.9 or
higher")
@pytest.mark.parametrize("blocking", [False, True])
@pytest.mark.parametrize("as_runnable", [False, True])
@@ -1568,3 +1646,130 @@
legacy = ComparativeExperimentResults(results={}, examples={})
assert legacy.url is None
assert legacy.comparative_experiment is None
+
+
+def test_v2_run_to_schema_maps_generated_fields() -> None:
+ """Generated v2 Run (project_id, parent_run_ids) → ls_schemas.Run."""
+ run_id = uuid.uuid4()
+ trace_id = uuid.uuid4()
+ project_id = uuid.uuid4()
+ parent_id = uuid.uuid4()
+ example_id = uuid.uuid4()
+ start = datetime.now(timezone.utc)
+ # SimpleNamespace, not Mock: `name` is a reserved Mock kwarg.
+ generated = SimpleNamespace(
+ id=str(run_id),
+ name="root",
+ run_type="chain",
+ start_time=start,
+ end_time=None,
+ trace_id=str(trace_id),
+ project_id=str(project_id),
+ parent_run_ids=[str(uuid.uuid4()), str(parent_id)],
+ dotted_order="20240101T000000000000Z" + str(run_id),
+ reference_example_id=str(example_id),
+ inputs={"a": 1},
+ outputs={"b": 2},
+ error=None,
+ status="SUCCESS",
+ )
+
+ run = _v2_run_to_schema(generated)
+
+ assert run.id == run_id
+ assert run.trace_id == trace_id
+ # project_id → session_id, last parent_run_ids entry → parent_run_id
+ assert run.session_id == project_id
+ assert run.parent_run_id == parent_id
+ assert run.reference_example_id == example_id
+ assert run.outputs == {"b": 2}
+ assert run.status == "success"
+
+
+def test_v2_run_to_schema_handles_root_run() -> None:
+ """A root run (no parents) maps to parent_run_id=None."""
+ generated = SimpleNamespace(
+ id=str(uuid.uuid4()),
+ name="root",
+ run_type="chain",
+ start_time=datetime.now(timezone.utc),
+ end_time=None,
+ trace_id=str(uuid.uuid4()),
+ project_id=str(uuid.uuid4()),
+ parent_run_ids=[],
+ dotted_order=None,
+ reference_example_id=None,
+ inputs=None,
+ outputs=None,
+ error=None,
+ status=None,
+ )
+ run = _v2_run_to_schema(generated)
+ assert run.parent_run_id is None
+ assert run.inputs == {}
+
+
+def _make_generated_run() -> SimpleNamespace:
+ return SimpleNamespace(
+ id=str(uuid.uuid4()),
+ name="root",
+ run_type="chain",
+ start_time=datetime.now(timezone.utc),
+ end_time=None,
+ trace_id=str(uuid.uuid4()),
+ project_id=str(uuid.uuid4()),
+ parent_run_ids=[],
+ dotted_order=None,
+ reference_example_id=str(uuid.uuid4()),
+ inputs={},
+ outputs={},
+ error=None,
+ status="SUCCESS",
+ )
+
+
+def _experiment_session() -> ls_schemas.TracerSession:
+ return ls_schemas.TracerSession(
+ id=uuid.uuid4(),
+ name="exp",
+ tenant_id=uuid.uuid4(),
+ reference_dataset_id=uuid.uuid4(),
+ start_time=datetime(2020, 1, 1, tzinfo=timezone.utc),
+ )
+
+
+def test_load_traces_uses_v2_when_sdb_query_enabled() -> None:
+ """sdb_query_enabled → sync query_v2 (/v2/runs/query), never v1
list_runs."""
+ project = _experiment_session()
+ gen_runs = [_make_generated_run(), _make_generated_run()]
+
+ def _query_v2(**kwargs):
+ # Verify the query is scoped to this experiment, root-only, with a
window.
+ assert kwargs["project_ids"] == [str(project.id)]
+ assert kwargs["is_root"] is True
+ assert kwargs["min_start_time"] == project.start_time
+ assert "selects" in kwargs
+ return iter(gen_runs) # sync paginator auto-iterates with a plain
for-loop
+
+ client = mock.Mock()
+ client.info.instance_flags = {"sdb_query_enabled": True}
+ client._get_langsmith_api_sync.return_value.runs.query_v2 = _query_v2
+
+ runs = _load_traces_for_experiment(project, client, load_nested=False)
+
+ assert len(runs) == 2
+ assert all(isinstance(r, ls_schemas.Run) for r in runs)
+ client.list_runs.assert_not_called()
+
+
+def test_load_traces_uses_v1_when_sdb_query_disabled() -> None:
+ """Without the flag, keep the existing v1 client.list_runs path
unchanged."""
+ project = _experiment_session()
+ client = mock.Mock()
+ client.info.instance_flags = {} # sdb_query_enabled absent
+ client.list_runs.return_value = iter([])
+
+ _load_traces_for_experiment(project, client, load_nested=False)
+
+ client.list_runs.assert_called_once_with(project_id=project.id,
is_root=True)
+ client._get_langsmith_api_sync.assert_not_called()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/unit_tests/sandbox/test_async_client.py
new/langsmith-0.10.10/tests/unit_tests/sandbox/test_async_client.py
--- old/langsmith-0.10.9/tests/unit_tests/sandbox/test_async_client.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/sandbox/test_async_client.py
2020-02-02 01:00:00.000000000 +0100
@@ -116,7 +116,8 @@
assert client._http.headers.get("X-Service-Key") == "svc-jwt"
assert client._http.headers.get("X-Api-Key") == "api-key"
assert client._default_headers == {"X-Service-Key": "svc-jwt"}
- assert client._ws_default_headers(None) == {"X-Service-Key":
"svc-jwt"}
+ # merge_headers normalizes names to lowercase.
+ assert client._ws_default_headers(None) == {"x-service-key":
"svc-jwt"}
await client.aclose()
async def test_max_retries_default(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/unit_tests/sandbox/test_client.py
new/langsmith-0.10.10/tests/unit_tests/sandbox/test_client.py
--- old/langsmith-0.10.9/tests/unit_tests/sandbox/test_client.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/sandbox/test_client.py
2020-02-02 01:00:00.000000000 +0100
@@ -133,13 +133,14 @@
api_key="api-key",
headers={"X-Service-Key": "svc-jwt"},
)
- assert client._ws_default_headers(None) == {"X-Service-Key": "svc-jwt"}
+ # merge_headers normalizes names to lowercase.
+ assert client._ws_default_headers(None) == {"x-service-key": "svc-jwt"}
assert client._ws_default_headers({"X-Test": "v"}) == {
- "X-Service-Key": "svc-jwt",
- "X-Test": "v",
+ "x-service-key": "svc-jwt",
+ "x-test": "v",
}
assert client._ws_default_headers({"X-Service-Key": "override"}) == {
- "X-Service-Key": "override"
+ "x-service-key": "override"
}
client.close()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/unit_tests/sandbox/test_helpers.py
new/langsmith-0.10.10/tests/unit_tests/sandbox/test_helpers.py
--- old/langsmith-0.10.9/tests/unit_tests/sandbox/test_helpers.py
1970-01-01 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/sandbox/test_helpers.py
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,39 @@
+"""Unit tests for langsmith.sandbox._helpers."""
+
+import httpx
+
+from langsmith.sandbox._helpers import merge_headers
+
+
+def test_merge_headers_override_wins() -> None:
+ # Names are normalized to lowercase and the override wins.
+ merged = merge_headers({"X-Service-Key": "base"}, {"X-Service-Key":
"override"})
+ assert merged == {"x-service-key": "override"}
+
+
+def test_merge_headers_preserves_non_overridden_base() -> None:
+ merged = merge_headers({"a": "1", "b": "2"}, {"b": "3"})
+ assert merged == {"a": "1", "b": "3"}
+
+
+def test_merge_headers_none_inputs() -> None:
+ assert merge_headers(None, None) == {}
+ assert merge_headers({"a": "1"}, None) == {"a": "1"}
+ assert merge_headers(None, {"a": "1"}) == {"a": "1"}
+
+
+def test_merge_headers_override_replaces_across_casing() -> None:
+ # httpx.Headers normalizes names to lowercase; a Title-Case override must
+ # still replace the base rather than produce a second, duplicate header.
+ base = httpx.Headers({"X-Service-Key": "base", "X-Api-Key": ""})
+ merged = merge_headers(base, {"X-Service-Key": "override"})
+
+ assert [v for k, v in merged.items() if k.lower() == "x-service-key"] == [
+ "override"
+ ]
+ # And only one X-Service-Key actually goes on the wire.
+ request = httpx.Request("GET", "https://example.com", headers=merged)
+ on_wire = [
+ v.decode() for k, v in request.headers.raw if k.lower() ==
b"x-service-key"
+ ]
+ assert on_wire == ["override"]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.9/tests/unit_tests/sandbox/test_ws_execute.py
new/langsmith-0.10.10/tests/unit_tests/sandbox/test_ws_execute.py
--- old/langsmith-0.10.9/tests/unit_tests/sandbox/test_ws_execute.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/sandbox/test_ws_execute.py
2020-02-02 01:00:00.000000000 +0100
@@ -74,8 +74,9 @@
class TestBuildAuthHeaders:
def test_builds_header(self):
+ # Header names are normalized to lowercase.
headers = _build_auth_headers("my-key")
- assert headers == {"X-Api-Key": "my-key"}
+ assert headers == {"x-api-key": "my-key"}
def test_none_key_returns_empty(self):
headers = _build_auth_headers(None)
@@ -87,8 +88,8 @@
{"X-Api-Key": "override-key", "X-Test-Header": "ws-value"},
)
assert headers == {
- "X-Api-Key": "override-key",
- "X-Test-Header": "ws-value",
+ "x-api-key": "override-key",
+ "x-test-header": "ws-value",
}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/tests/unit_tests/test_client.py
new/langsmith-0.10.10/tests/unit_tests/test_client.py
--- old/langsmith-0.10.9/tests/unit_tests/test_client.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/test_client.py 2020-02-02
01:00:00.000000000 +0100
@@ -6734,6 +6734,13 @@
)
+def test_get_run_stats_requires_project() -> None:
+ """get_run_stats must raise ValueError when no project is specified."""
+ client = Client(api_key="test", api_url="http://localhost")
+ with pytest.raises(ValueError, match="project_names or project_ids"):
+ client.get_run_stats()
+
+
def test_removed_sdk_methods_absent() -> None:
"""Verify de-publicized methods were removed from the generated SDK in PR
#28358."""
from langsmith._openapi_client.resources.datasets.datasets import
DatasetsResource
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.9/tests/unit_tests/test_testing.py
new/langsmith-0.10.10/tests/unit_tests/test_testing.py
--- old/langsmith-0.10.9/tests/unit_tests/test_testing.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.10/tests/unit_tests/test_testing.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,8 +1,10 @@
+import datetime
import uuid
from unittest.mock import MagicMock
from langsmith.testing._internal import (
_get_example_id,
+ _LangSmithTestSuite,
_serde_example_values,
_TestCase,
)
@@ -76,3 +78,29 @@
actual_id = _get_example_id(dataset_id, test_case.inputs or {})
assert actual_id == expected_id
assert actual_id != fixture_id
+
+
+def test_pytest_feedback_includes_experiment_id_and_run_start_time():
+ experiment_id = uuid.uuid4()
+ run_id = uuid.uuid4()
+ start_time = datetime.datetime.now(datetime.timezone.utc)
+ test_suite = object.__new__(_LangSmithTestSuite)
+ test_suite.client = MagicMock()
+ test_suite._experiment = MagicMock(id=experiment_id)
+ test_suite._executor = MagicMock()
+ test_suite._executor.submit.side_effect = lambda fn, *args, **kwargs: fn(
+ *args, **kwargs
+ )
+
+ test_suite._submit_result(run_id, start_time, 1)
+ test_suite._submit_feedback(
+ run_id,
+ {"key": "quality", "score": 0.75},
+ start_time=start_time,
+ )
+
+ assert test_suite.client.create_feedback.call_count == 2
+ for call in test_suite.client.create_feedback.call_args_list:
+ assert call.args[0] == run_id
+ assert call.kwargs["session_id"] == experiment_id
+ assert call.kwargs["start_time"] == start_time