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-10 17:43:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-langsmith (Old)
and /work/SRC/openSUSE:Factory/.python-langsmith.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-langsmith"
Fri Jul 10 17:43:09 2026 rev:7 rq:1364813 version:0.10.1
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-langsmith/python-langsmith.changes
2026-07-09 22:21:09.573761848 +0200
+++
/work/SRC/openSUSE:Factory/.python-langsmith.new.1991/python-langsmith.changes
2026-07-10 17:45:43.190230845 +0200
@@ -1,0 +2,10 @@
+Fri Jul 10 05:15:22 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 0.10.1:
+ * Percent-encode resource names in sandbox client request paths
+ to prevent URL path injection
+ * HTML-escape the insights report repr to avoid unintended
+ markup when rendered in notebooks
+ * Raise the minimum supported LangSmith backend to 0.16.12rc1
+
+-------------------------------------------------------------------
Old:
----
langsmith-0.10.0.tar.gz
New:
----
langsmith-0.10.1.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-langsmith.spec ++++++
--- /var/tmp/diff_new_pack.McwuoV/_old 2026-07-10 17:45:45.378298439 +0200
+++ /var/tmp/diff_new_pack.McwuoV/_new 2026-07-10 17:45:45.378298439 +0200
@@ -17,7 +17,7 @@
Name: python-langsmith
-Version: 0.10.0
+Version: 0.10.1
Release: 0
Summary: Client library for the LangSmith LLM tracing and evaluation
platform
License: MIT
++++++ langsmith-0.10.0.tar.gz -> langsmith-0.10.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.0/.bumpversion.cfg
new/langsmith-0.10.1/.bumpversion.cfg
--- old/langsmith-0.10.0/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.1/.bumpversion.cfg 2020-02-02 01:00:00.000000000
+0100
@@ -1,5 +1,5 @@
[bumpversion]
-current_version = 0.10.0
+current_version = 0.10.1
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.0/PKG-INFO
new/langsmith-0.10.1/PKG-INFO
--- old/langsmith-0.10.0/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: langsmith
-Version: 0.10.0
+Version: 0.10.1
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.0/langsmith/__init__.py
new/langsmith-0.10.1/langsmith/__init__.py
--- old/langsmith-0.10.0/langsmith/__init__.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.1/langsmith/__init__.py 2020-02-02 01:00:00.000000000
+0100
@@ -29,7 +29,7 @@
# Avoid calling into importlib on every call to __version__
-__version__ = "0.10.0"
+__version__ = "0.10.1"
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.0/langsmith/_internal/_constants.py
new/langsmith-0.10.1/langsmith/_internal/_constants.py
--- old/langsmith-0.10.0/langsmith/_internal/_constants.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/_internal/_constants.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
import uuid
-_MIN_BACKEND_VERSION = "0.16.10rc1"
+_MIN_BACKEND_VERSION = "0.16.12rc1"
_SIZE_LIMIT_BYTES = 20_971_520 # 20MB by default
_AUTO_SCALE_UP_QSIZE_TRIGGER = 200
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.0/langsmith/_internal/voice/audio.py
new/langsmith-0.10.1/langsmith/_internal/voice/audio.py
--- old/langsmith-0.10.0/langsmith/_internal/voice/audio.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/_internal/voice/audio.py 2020-02-02
01:00:00.000000000 +0100
@@ -15,6 +15,8 @@
import math
import wave
+DEFAULT_MAX_AUDIO_SECONDS = 10 * 60
+
def pcm_to_wav(pcm: bytes, sample_rate: int, num_channels: int = 1) -> bytes:
"""Wrap raw PCM16 bytes in a WAV container.
@@ -56,12 +58,34 @@
return out
+def _chunk_end(t: float, data: bytes, sample_rate: int) -> float:
+ return t + (len(data) // 2) / sample_rate
+
+
+def session_wav_exceeds_duration_cap(
+ user_chunks: list[tuple[float, bytes]],
+ agent_chunks: list[tuple[float, bytes]],
+ sample_rate: int,
+ max_duration_seconds: float | None,
+) -> bool:
+ """Return whether natural-play WAV layout exceeds the duration cap."""
+ if max_duration_seconds is None:
+ return False
+ user = _layout_chunks_to_play_time(user_chunks, sample_rate)
+ agent = _layout_chunks_to_play_time(agent_chunks, sample_rate)
+ user_end = max((_chunk_end(t, d, sample_rate) for t, d in user),
default=0.0)
+ agent_end = max((_chunk_end(t, d, sample_rate) for t, d in agent),
default=0.0)
+ return max(user_end, agent_end) > max_duration_seconds
+
+
def build_stereo_session_wav(
user_chunks: list[tuple[float, bytes]],
agent_chunks: list[tuple[float, bytes]],
sample_rate: int,
+ *,
+ max_duration_seconds: float | None = DEFAULT_MAX_AUDIO_SECONDS,
) -> bytes:
- """Reconstruct a stereo WAV from timestamped PCM16 chunks.
+ """Reconstruct a duration-capped stereo WAV from timestamped PCM16 chunks.
Left channel = user, right channel = agent. Both channels are laid out at
natural play time (see ``_layout_chunks_to_play_time``). Gaps between
bursts
@@ -74,12 +98,12 @@
user = _layout_chunks_to_play_time(user_chunks, sample_rate)
agent = _layout_chunks_to_play_time(agent_chunks, sample_rate)
- def chunk_end(t: float, data: bytes) -> float:
- return t + (len(data) // 2) / sample_rate
-
- user_end = max((chunk_end(t, d) for t, d in user), default=0.0)
- agent_end = max((chunk_end(t, d) for t, d in agent), default=0.0)
+ user_end = max((_chunk_end(t, d, sample_rate) for t, d in user),
default=0.0)
+ agent_end = max((_chunk_end(t, d, sample_rate) for t, d in agent),
default=0.0)
total_samples = int(math.ceil(max(user_end, agent_end) * sample_rate))
+ if max_duration_seconds is not None:
+ max_samples = int(math.ceil(max_duration_seconds * sample_rate))
+ total_samples = min(total_samples, max_samples)
if total_samples <= 0:
return b""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/langsmith/_internal/voice/session.py
new/langsmith-0.10.1/langsmith/_internal/voice/session.py
--- old/langsmith-0.10.0/langsmith/_internal/voice/session.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/_internal/voice/session.py 2020-02-02
01:00:00.000000000 +0100
@@ -31,7 +31,11 @@
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
from langsmith import RunTree
-from langsmith._internal.voice.audio import build_stereo_session_wav
+from langsmith._internal.voice.audio import (
+ DEFAULT_MAX_AUDIO_SECONDS,
+ build_stereo_session_wav,
+ session_wav_exceeds_duration_cap,
+)
from langsmith._internal.voice.helpers import dump_event, scrub
if TYPE_CHECKING:
@@ -66,6 +70,7 @@
# further chunks on it are dropped (the conversation's start is kept) and
the
# root is flagged ``audio_truncated``. ``None`` disables the cap.
max_audio_bytes: Optional[int] = None
+ max_audio_seconds: Optional[float] = DEFAULT_MAX_AUDIO_SECONDS
# Monotonic clock origin. Everything else stores ``now() - t0``.
t0: float = field(default_factory=time.monotonic)
# Time-stamped audio chunks for the stereo session WAV. Each entry is
@@ -141,14 +146,11 @@
Dropped once the user channel reaches ``max_audio_bytes`` (see that
field) so a long session can't exhaust memory.
"""
- if (
- self.max_audio_bytes is not None
- and self._user_bytes >= self.max_audio_bytes
- ):
- self._note_audio_truncated()
+ chunk = self._bounded_audio_chunk(self._user_bytes, data)
+ if not chunk:
return
- self._user_bytes += len(data)
- self.user_chunks.append((t, data))
+ self._user_bytes += len(chunk)
+ self.user_chunks.append((t, chunk))
def record_agent(self, t: float, data: bytes) -> None:
"""Record a timestamped chunk of agent (playback) PCM16 for the WAV.
@@ -156,24 +158,33 @@
Dropped once the agent channel reaches ``max_audio_bytes`` (see that
field) so a long session can't exhaust memory.
"""
- if (
- self.max_audio_bytes is not None
- and self._agent_bytes >= self.max_audio_bytes
- ):
- self._note_audio_truncated()
+ chunk = self._bounded_audio_chunk(self._agent_bytes, data)
+ if not chunk:
return
- self._agent_bytes += len(data)
- self.agent_chunks.append((t, data))
+ self._agent_bytes += len(chunk)
+ self.agent_chunks.append((t, chunk))
+
+ def _bounded_audio_chunk(self, current_bytes: int, data: bytes) -> bytes:
+ if self.max_audio_bytes is None:
+ return data
+ remaining = self.max_audio_bytes - current_bytes
+ remaining -= remaining % 2
+ if remaining <= 0:
+ self._note_audio_truncated()
+ return b""
+ if len(data) <= remaining:
+ return data
+ self._note_audio_truncated()
+ return data[:remaining]
def _note_audio_truncated(self) -> None:
- """Flag (once) that recorded audio was capped at
``max_audio_bytes``."""
+ """Flag once that recorded audio was capped."""
if self._audio_truncated:
- return # log once per session, not once per dropped chunk
+ return
self._audio_truncated = True
logger.warning(
- "voice tracing: audio capture hit the %d-byte per-channel cap; "
- "further audio for this conversation is dropped from the WAV",
- self.max_audio_bytes,
+ "voice tracing: audio capture hit the configured cap; "
+ "further audio for this conversation is dropped from the WAV"
)
def set_title(self, text: str) -> None:
@@ -377,6 +388,14 @@
run.end(outputs=scrub(outputs) if outputs is not None else {})
run.patch()
+ def _audio_exceeds_duration_cap(self) -> bool:
+ return session_wav_exceeds_duration_cap(
+ self.user_chunks,
+ self.agent_chunks,
+ self.sample_rate,
+ self.max_audio_seconds,
+ )
+
def finalize(self) -> None:
"""Roll up stats, attach the stereo WAV, and close the root span.
@@ -386,6 +405,8 @@
"""
# Close the last open turn (if any) before the root.
self._close_turn()
+ if self._audio_exceeds_duration_cap():
+ self._note_audio_truncated()
extra: dict[str, Any] = self.run.extra or {}
metadata: dict[str, Any] = dict(extra.get("metadata") or {})
metadata["event_count"] = self.event_count
@@ -397,7 +418,10 @@
try:
wav = build_stereo_session_wav(
- self.user_chunks, self.agent_chunks, self.sample_rate
+ self.user_chunks,
+ self.agent_chunks,
+ self.sample_rate,
+ max_duration_seconds=self.max_audio_seconds,
)
except Exception:
# A WAV-build failure (e.g. an oversized buffer) must not lose the
@@ -427,7 +451,7 @@
tags: Optional[list[str]] = None,
metadata: Optional[dict[str, Any]] = None,
name: str = "realtime_session",
- max_audio_seconds: Optional[float] = None,
+ max_audio_seconds: Optional[float] = DEFAULT_MAX_AUDIO_SECONDS,
client: Optional[Client] = None,
replicas: Optional[Sequence[WriteReplica]] = None,
integration: str,
@@ -446,9 +470,10 @@
(see LangSmith's tracing replicas). Set on the root and inherited by child
spans via ``create_child``; ``None`` disables replication.
- ``max_audio_seconds`` caps how much audio per channel is retained for the
- stereo WAV, guarding memory on long-running sessions; ``None`` keeps all
- audio. It is converted to a per-channel byte budget at PCM16 (2
bytes/sample).
+ ``max_audio_seconds`` caps how much audio per channel is retained and how
far
+ into the session the stereo WAV can extend, guarding memory on long-running
+ sessions. The default is bounded; pass ``None`` to keep all audio. It is
+ converted to a per-channel byte budget at PCM16 (2 bytes/sample).
``integration`` / ``integration_version`` stamp ``ls_integration`` and
``ls_integration_version`` on the root (the convention the batch
integrations
@@ -496,4 +521,5 @@
project_name=project_name,
sample_rate=sample_rate,
max_audio_bytes=max_audio_bytes,
+ max_audio_seconds=max_audio_seconds,
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/langsmith/integrations/google_adk_live/_plugin.py
new/langsmith-0.10.1/langsmith/integrations/google_adk_live/_plugin.py
--- old/langsmith-0.10.0/langsmith/integrations/google_adk_live/_plugin.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/integrations/google_adk_live/_plugin.py
2020-02-02 01:00:00.000000000 +0100
@@ -54,7 +54,11 @@
observe_safely,
scrub,
)
-from langsmith._internal.voice.session import EventSession, start_session
+from langsmith._internal.voice.session import (
+ DEFAULT_MAX_AUDIO_SECONDS,
+ EventSession,
+ start_session,
+)
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -362,7 +366,7 @@
project_name: Optional[str] = None,
tags: Optional[list[str]] = None,
metadata: Optional[dict[str, Any]] = None,
- max_audio_seconds: Optional[float] = None,
+ max_audio_seconds: Optional[float] = DEFAULT_MAX_AUDIO_SECONDS,
client: Optional[Client] = None,
replicas: Optional[Sequence[WriteReplica]] = None,
) -> None:
@@ -377,8 +381,8 @@
tags / metadata: attached to the conversation root span.
max_audio_seconds: per-channel cap on audio retained for the WAV,
to
bound memory. Since one shared plugin can trace many (and
- long-running) conversations, set this on a server to keep audio
- buffers from growing unbounded; ``None`` (default) keeps all
audio.
+ long-running) conversations, pass ``None`` only when all audio
+ should be kept.
client: LangSmith ``Client`` for tracing writes; ``None`` (default)
uses the SDK's standard env-based resolution (``LANGSMITH_*``).
replicas: tracing replicas to mirror the conversation trace to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/langsmith/integrations/openai_realtime/_connection.py
new/langsmith-0.10.1/langsmith/integrations/openai_realtime/_connection.py
--- old/langsmith-0.10.0/langsmith/integrations/openai_realtime/_connection.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/integrations/openai_realtime/_connection.py
2020-02-02 01:00:00.000000000 +0100
@@ -33,7 +33,11 @@
from langsmith._internal._package_version import get_package_version
from langsmith._internal._usage import _create_usage_metadata
from langsmith._internal.voice.helpers import observe_safely
-from langsmith._internal.voice.session import EventSession, start_session
+from langsmith._internal.voice.session import (
+ DEFAULT_MAX_AUDIO_SECONDS,
+ EventSession,
+ start_session,
+)
from langsmith.run_helpers import tracing_context
if TYPE_CHECKING:
@@ -466,7 +470,7 @@
tags: Optional[list[str]] = None,
metadata: Optional[dict[str, Any]] = None,
is_agent_speaking: Optional[Callable[[], bool]] = None,
- max_audio_seconds: Optional[float] = None,
+ max_audio_seconds: Optional[float] = DEFAULT_MAX_AUDIO_SECONDS,
client: Optional[Client] = None,
replicas: Optional[Sequence[WriteReplica]] = None,
) -> _RealtimeTracingSession:
@@ -493,7 +497,7 @@
is_agent_speaking: zero-arg callable returning whether the agent is
still
audible, used to flag a barge-in; ``None`` disables that flag.
max_audio_seconds: per-channel cap on audio retained for the WAV, to
- bound memory on long sessions; ``None`` (default) keeps all audio.
+ bound memory on long sessions; pass ``None`` to keep all audio.
client: LangSmith ``Client`` for tracing writes; ``None`` (default)
uses
the SDK's standard env-based resolution (``LANGSMITH_*``).
replicas: tracing replicas to mirror the conversation trace to
additional
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/langsmith/integrations/openai_realtime/_session.py
new/langsmith-0.10.1/langsmith/integrations/openai_realtime/_session.py
--- old/langsmith-0.10.0/langsmith/integrations/openai_realtime/_session.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/integrations/openai_realtime/_session.py
2020-02-02 01:00:00.000000000 +0100
@@ -38,7 +38,11 @@
from langsmith._internal._package_version import get_package_version
from langsmith._internal.voice.helpers import observe_safely
-from langsmith._internal.voice.session import EventSession, start_session
+from langsmith._internal.voice.session import (
+ DEFAULT_MAX_AUDIO_SECONDS,
+ EventSession,
+ start_session,
+)
from langsmith.run_helpers import tracing_context
if TYPE_CHECKING:
@@ -605,7 +609,7 @@
tags: Optional[list[str]] = None,
metadata: Optional[dict[str, Any]] = None,
on_message: Optional[Callable[[str, str], None]] = None,
- max_audio_seconds: Optional[float] = None,
+ max_audio_seconds: Optional[float] = DEFAULT_MAX_AUDIO_SECONDS,
client: Optional[Client] = None,
replicas: Optional[Sequence[WriteReplica]] = None,
) -> _TracedRealtimeSession:
@@ -631,7 +635,7 @@
on_message: optional callback ``(role, text)`` invoked once per
finalized
transcript line (e.g. to print to a console).
max_audio_seconds: per-channel cap on audio retained for the WAV, to
- bound memory on long sessions; ``None`` (default) keeps all audio.
+ bound memory on long sessions; pass ``None`` to keep all audio.
client: LangSmith ``Client`` for tracing writes; ``None`` (default)
uses
the SDK's standard env-based resolution (``LANGSMITH_*``).
replicas: tracing replicas to mirror the conversation trace to
additional
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/langsmith/integrations/pipecat/processor.py
new/langsmith-0.10.1/langsmith/integrations/pipecat/processor.py
--- old/langsmith-0.10.0/langsmith/integrations/pipecat/processor.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/integrations/pipecat/processor.py
2020-02-02 01:00:00.000000000 +0100
@@ -64,6 +64,8 @@
# first). The TTL is the governing limit in practice.
DEFAULT_STATE_MAXSIZE = 100_000
+_WAV_HEADER_BYTES = 44
+
class PipecatLangSmithSpanProcessor(BaseLangSmithSpanProcessor):
"""Enriches Pipecat's OTel spans with LangSmith-compatible attributes."""
@@ -113,8 +115,8 @@
maxsize=DEFAULT_STATE_MAXSIZE, ttl=state_ttl_seconds
)
# Merged PCM accumulated from a Pipecat AudioBufferProcessor (see
- # attach_audio_buffer), keyed by conversation id. Each value is
- # {"pcm": bytearray, "sample_rate": int, "num_channels": int}. Also
+ # attach_audio_buffer), keyed by conversation id. Each value carries
the
+ # PCM bytearray, sample rate, channel count, and truncation flag. Also
# TTL-bounded — these entries hold the raw audio and are the larger
leak.
self._audio_by_conversation: MutableMapping[str, dict[str, Any]] =
TTLCache(
maxsize=DEFAULT_STATE_MAXSIZE, ttl=state_ttl_seconds
@@ -145,6 +147,11 @@
audio_buffer.event_handler("on_audio_data")(_on_audio_data)
+ def _pcm_audio_size_limit_bytes(self) -> Optional[int]:
+ if self.audio_size_limit_bytes is None:
+ return None
+ return max(0, self.audio_size_limit_bytes - _WAV_HEADER_BYTES)
+
def _accumulate_audio(
self, conversation_id: str, audio: bytes, sample_rate: int,
num_channels: int
) -> None:
@@ -154,7 +161,21 @@
"pcm": bytearray(),
"sample_rate": sample_rate,
"num_channels": num_channels,
+ "audio_truncated": False,
}
+ pcm_limit_bytes = self._pcm_audio_size_limit_bytes()
+ if pcm_limit_bytes is not None:
+ remaining = pcm_limit_bytes - len(rec["pcm"])
+ remaining -= remaining % 2
+ if remaining <= 0:
+ rec["audio_truncated"] = True
+ rec["sample_rate"] = sample_rate
+ rec["num_channels"] = num_channels
+ self._audio_by_conversation[conversation_id] = rec
+ return
+ if len(audio) > remaining:
+ rec["audio_truncated"] = True
+ audio = audio[:remaining]
rec["pcm"].extend(audio)
rec["sample_rate"] = sample_rate
rec["num_channels"] = num_channels
@@ -346,6 +367,14 @@
return
if not rec["pcm"]:
return
+ pcm_limit_bytes = self._pcm_audio_size_limit_bytes()
+ if pcm_limit_bytes is not None and len(rec["pcm"]) > pcm_limit_bytes:
+ logger.warning(
+ "langsmith voice: skipped oversize pipecat audio for "
+ "conversation_id=%r",
+ conversation_id,
+ )
+ return
wav = pcm_to_wav(bytes(rec["pcm"]), rec["sample_rate"],
rec["num_channels"])
self._attach_audio(
tspan, name="conversation.wav", data=wav,
mime_type=self._audio_mime_type
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.0/langsmith/sandbox/_client.py
new/langsmith-0.10.1/langsmith/sandbox/_client.py
--- old/langsmith-0.10.0/langsmith/sandbox/_client.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/langsmith/sandbox/_client.py 2020-02-02
01:00:00.000000000 +0100
@@ -70,6 +70,8 @@
def _quote_path_segment(value: str) -> str:
"""Quote a user-controlled value for use as a single URL path segment."""
+ if not value:
+ raise ValueError("URL path segment must be a non-empty string")
return quote(value, safe="")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/langsmith-0.10.0/langsmith/schemas.py
new/langsmith-0.10.1/langsmith/schemas.py
--- old/langsmith-0.10.0/langsmith/schemas.py 2020-02-02 01:00:00.000000000
+0100
+++ new/langsmith-0.10.1/langsmith/schemas.py 2020-02-02 01:00:00.000000000
+0100
@@ -6,6 +6,7 @@
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from enum import Enum
+from html import escape as _html_escape
from pathlib import Path
from typing import (
Annotated,
@@ -1434,7 +1435,7 @@
return
f"{self.host_url}/o/{str(self.tenant_id)}/projects/p/{str(self.project_id)}?tab=3&clusterJobId={str(self.id)}"
def _repr_html_(self) -> str:
- return f'<a href="{self.link}", target="_blank"
rel="noopener">InsightsReport(\'{self.name}\')</a>'
+ return f'<a href="{_html_escape(self.link, quote=True)}",
target="_blank"
rel="noopener">InsightsReport(\'{_html_escape(self.name)}\')</a>'
class InsightsHighlightedTrace(BaseModel):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/integration_tests/test_runs.py
new/langsmith-0.10.1/tests/integration_tests/test_runs.py
--- old/langsmith-0.10.0/tests/integration_tests/test_runs.py 2020-02-02
01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/integration_tests/test_runs.py 2020-02-02
01:00:00.000000000 +0100
@@ -1,4 +1,5 @@
import asyncio
+import logging
import time
import uuid
from collections import defaultdict
@@ -22,6 +23,8 @@
from langsmith.schemas import Attachment
from tests.integration_tests.conftest import skip_if_rate_limited
+logger = logging.getLogger(__name__)
+
@pytest.fixture
def langchain_client() -> Generator[Client, None, None]:
@@ -56,7 +59,7 @@
):
return runs
except ls_utils.LangSmithError:
- pass
+ logger.debug("Error polling runs", exc_info=True)
time.sleep(sleep_time)
retries += 1
raise AssertionError(f"Failed to get {count} runs after {max_retries}
attempts.")
@@ -149,6 +152,7 @@
assert runs[0].session_id != runs[1].session_id
+@skip_if_rate_limited
async def test_nested_async_runs_with_threadpool(langchain_client: Client):
"""Test nested runs with a mix of async and sync functions."""
project_name = (
@@ -206,10 +210,9 @@
)
executor.shutdown(wait=True)
filter_ = f'and(eq(metadata_key, "test_run"), eq(metadata_value,
"{meta}"))'
- poll_runs_until_count(
+ runs = poll_runs_until_count(
langchain_client, project_name, 17, filter_=filter_, max_retries=30
)
- runs = list(langchain_client.list_runs(project_name=project_name,
filter=filter_))
trace_runs = list(
langchain_client.list_runs(
trace_id=runs[0].trace_id, project_name=project_name,
filter=filter_
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/integration_tests/test_v2_threads.py
new/langsmith-0.10.1/tests/integration_tests/test_v2_threads.py
--- old/langsmith-0.10.0/tests/integration_tests/test_v2_threads.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/integration_tests/test_v2_threads.py
2020-02-02 01:00:00.000000000 +0100
@@ -29,7 +29,7 @@
if condition():
return
except Exception:
- pass
+ logger.debug("Error checking sync condition", exc_info=True)
time.sleep(sleep_time)
raise ValueError(f"Condition not met within {max_sleep_time}s")
@@ -47,7 +47,7 @@
if result:
return result
except Exception:
- pass
+ logger.debug("Error checking async condition", exc_info=True)
time.sleep(sleep_time)
raise ValueError(f"Condition not met within {max_sleep_time}s")
@@ -102,7 +102,7 @@
is not None
)
- _wait_for_sync(_runs_indexed, max_sleep_time=30, sleep_time=2)
+ _wait_for_sync(_runs_indexed, max_sleep_time=90, sleep_time=2)
project = langchain_client.read_project(project_name=project_name)
min_start_time = now - timedelta(hours=1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/integration_tests/test_v2_traces.py
new/langsmith-0.10.1/tests/integration_tests/test_v2_traces.py
--- old/langsmith-0.10.0/tests/integration_tests/test_v2_traces.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/integration_tests/test_v2_traces.py
2020-02-02 01:00:00.000000000 +0100
@@ -29,7 +29,7 @@
if condition():
return
except Exception:
- pass
+ logger.debug("Error checking sync condition", exc_info=True)
time.sleep(sleep_time)
raise ValueError(f"Condition not met within {max_sleep_time}s")
@@ -47,7 +47,7 @@
if result:
return result
except Exception:
- pass
+ logger.debug("Error checking async condition", exc_info=True)
time.sleep(sleep_time)
raise ValueError(f"Condition not met within {max_sleep_time}s")
@@ -90,7 +90,7 @@
is not None
)
- _wait_for_sync(_runs_indexed, max_sleep_time=30, sleep_time=2)
+ _wait_for_sync(_runs_indexed, max_sleep_time=90, sleep_time=2)
project = langchain_client.read_project(project_name=project_name)
min_start_time = now - timedelta(hours=1)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/unit_tests/test_insights_report.py
new/langsmith-0.10.1/tests/unit_tests/test_insights_report.py
--- old/langsmith-0.10.0/tests/unit_tests/test_insights_report.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/unit_tests/test_insights_report.py
2020-02-02 01:00:00.000000000 +0100
@@ -127,6 +127,29 @@
assert report.link == expected_link
+def test_insights_report_repr_html_escapes_name_and_link() -> None:
+ report = ls_schemas.InsightsReport(
+ id="job-id",
+ name='bad\');</a><script>alert("x")</script>',
+ status="success",
+ project_id="project-id",
+ host_url='https://smith.langchain.com/" onclick="alert(1)',
+ tenant_id="tenant-id",
+ )
+
+ expected_href = (
+ "https://smith.langchain.com/"
onclick="alert(1)/o/tenant-id/"
+ "projects/p/project-id?tab=3&clusterJobId=job-id"
+ )
+ expected_name = (
+
"bad');</a><script>alert("x")</script>"
+ )
+ assert report._repr_html_() == (
+ f'<a href="{expected_href}", target="_blank" rel="noopener">'
+ f"InsightsReport('{expected_name}')</a>"
+ )
+
+
def test_get_insights_report_with_runs_and_cluster_load_traces() -> None:
report_payload = _make_report_payload()
runs_page_1 = _make_runs_page_payload(offset=0, has_next=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/unit_tests/wrappers/test_pipecat.py
new/langsmith-0.10.1/tests/unit_tests/wrappers/test_pipecat.py
--- old/langsmith-0.10.0/tests/unit_tests/wrappers/test_pipecat.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/unit_tests/wrappers/test_pipecat.py
2020-02-02 01:00:00.000000000 +0100
@@ -298,6 +298,30 @@
assert "langsmith.attachments" not in _exported_attrs(proc)
+ def test_accumulate_audio_truncates_before_extend(self):
+ proc = _processor(audio_size_limit_bytes=50)
+ proc._accumulate_audio("c1", b"\x00\x01" * 2, 16000, 1)
+ proc._accumulate_audio("c1", b"\x02\x03" * 2, 16000, 1)
+
+ rec = proc._audio_by_conversation["c1"]
+ assert bytes(rec["pcm"]) == b"\x00\x01" * 2 + b"\x02\x03"
+ assert rec["audio_truncated"] is True
+
+ def test_oversize_audio_skips_conversion(self):
+ proc = _processor(audio_size_limit_bytes=50)
+ proc._audio_by_conversation["c1"] = {
+ "pcm": bytearray(b"\x00\x01" * 4),
+ "sample_rate": 16000,
+ "num_channels": 1,
+ }
+ span = _make_span("conversation", {"conversation.id": "c1"})
+
+ with patch("langsmith.integrations.pipecat.processor.pcm_to_wav") as
patched:
+ proc.on_end(span)
+
+ patched.assert_not_called()
+ assert "langsmith.attachments" not in _exported_attrs(proc)
+
class TestBaseProcessorHelpers:
"""Shared helpers in BaseLangSmithSpanProcessor."""
@@ -394,6 +418,16 @@
assert wf.getframerate() == 16000
assert wf.getsampwidth() == 2
+ def test_stereo_session_wav_duration_cap_bounds_frames(self):
+ wav = audio_utils.build_stereo_session_wav(
+ [(0.0, b"\x01\x02" * 20), (60.0, b"\x03\x04" * 20)],
+ [],
+ 10,
+ max_duration_seconds=1.0,
+ )
+ with wave.open(io.BytesIO(wav), "rb") as wf:
+ assert wf.getnframes() == 10
+
class TestStateLifecycle:
"""TTL-bounded state and its normal cleanup."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/langsmith-0.10.0/tests/unit_tests/wrappers/test_voice_session.py
new/langsmith-0.10.1/tests/unit_tests/wrappers/test_voice_session.py
--- old/langsmith-0.10.0/tests/unit_tests/wrappers/test_voice_session.py
2020-02-02 01:00:00.000000000 +0100
+++ new/langsmith-0.10.1/tests/unit_tests/wrappers/test_voice_session.py
2020-02-02 01:00:00.000000000 +0100
@@ -43,9 +43,9 @@
)
-def _session(**kwargs) -> EventSession:
+def _session(sample_rate: int = 24_000, **kwargs) -> EventSession:
kwargs.setdefault("integration", "test-integration")
- return start_session(thread_id="t1", sample_rate=24_000, **kwargs)
+ return start_session(thread_id="t1", sample_rate=sample_rate, **kwargs)
class TestAudioBound:
@@ -56,23 +56,28 @@
s = _session(max_audio_seconds=2.0)
assert s.max_audio_bytes == 2 * 24_000 * 2
- def test_no_cap_by_default(self):
+ def test_bounded_cap_by_default(self):
s = _session()
- assert s.max_audio_bytes is None
+ assert s.max_audio_bytes == session_mod.DEFAULT_MAX_AUDIO_SECONDS *
24_000 * 2
for _ in range(5):
s.record_user(0.0, b"\x00\x00" * 1000)
assert len(s.user_chunks) == 5
assert s._audio_truncated is False
+ def test_none_disables_cap(self):
+ s = _session(max_audio_seconds=None)
+ assert s.max_audio_bytes is None
+ assert s.max_audio_seconds is None
+
def test_user_channel_capped_and_flagged_once(self):
s = _session()
- s.max_audio_bytes = 100 # tiny cap for the test
- s.record_user(0.0, b"\x00" * 80) # under cap → kept
- s.record_user(0.1, b"\x00" * 80) # now at/over cap → kept, pushes over
- s.record_user(0.2, b"\x00" * 80) # dropped
- s.record_user(0.3, b"\x00" * 80) # dropped
+ s.max_audio_bytes = 100
+ s.record_user(0.0, b"\x00" * 80)
+ s.record_user(0.1, b"\x00" * 80)
+ s.record_user(0.2, b"\x00" * 80)
assert len(s.user_chunks) == 2
- assert s._user_bytes == 160
+ assert s.user_chunks[1][1] == b"\x00" * 20
+ assert s._user_bytes == 100
assert s._audio_truncated is True
def test_cap_is_per_channel(self):
@@ -88,11 +93,37 @@
s = _session()
s.max_audio_bytes = 10
s.record_user(0.0, b"\x00" * 20)
- s.record_user(0.1, b"\x00" * 20) # dropped → truncated
s.finalize()
meta = (s.run.extra or {}).get("metadata") or {}
assert meta.get("audio_truncated") is True
+ def test_finalize_caps_wav_duration(self, monkeypatch):
+ s = _session(max_audio_seconds=1.0)
+ calls = []
+
+ def fake_build(*args, **kwargs):
+ calls.append(kwargs)
+ return b""
+
+ monkeypatch.setattr(session_mod, "build_stereo_session_wav",
fake_build)
+ s.finalize()
+ assert calls[0]["max_duration_seconds"] == 1.0
+
+ def test_natural_play_duration_cap_marks_audio_truncated(self,
monkeypatch):
+ s = _session(sample_rate=10, max_audio_seconds=600)
+ chunk = b"\x00\x00" * 150
+ s.record_agent(570.0, chunk)
+ s.record_agent(571.0, chunk)
+ s.record_agent(572.0, chunk)
+ monkeypatch.setattr(
+ session_mod, "build_stereo_session_wav", lambda *_, **__: b""
+ )
+
+ s.finalize()
+
+ meta = (s.run.extra or {}).get("metadata") or {}
+ assert meta.get("audio_truncated") is True
+
class TestIntegrationMetadata:
"""``integration`` / ``integration_version`` → ``ls_integration*`` on
root."""