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/&quot; 
onclick=&quot;alert(1)/o/tenant-id/"
+        "projects/p/project-id?tab=3&amp;clusterJobId=job-id"
+    )
+    expected_name = (
+        
"bad&#x27;);&lt;/a&gt;&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;"
+    )
+    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."""

Reply via email to