diff --git a/libs/core/langchain_core/callbacks/manager.py b/libs/core/langchain_core/callbacks/manager.py index 7ed53f7400e..732677cd48c 100644 --- a/libs/core/langchain_core/callbacks/manager.py +++ b/libs/core/langchain_core/callbacks/manager.py @@ -6,7 +6,6 @@ import asyncio import atexit import functools import logging -import uuid from abc import ABC, abstractmethod from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor @@ -40,6 +39,7 @@ from langchain_core.tracers.context import ( ) from langchain_core.tracers.langchain import LangChainTracer from langchain_core.tracers.stdout import ConsoleCallbackHandler +from langchain_core.utils import uuid as lc_uuid from langchain_core.utils.env import env_var_is_set if TYPE_CHECKING: @@ -504,7 +504,7 @@ class BaseRunManager(RunManagerMixin): """ return cls( - run_id=uuid.uuid4(), + run_id=lc_uuid.uuid7(), handlers=[], inheritable_handlers=[], tags=[], @@ -1330,7 +1330,7 @@ class CallbackManager(BaseCallbackManager): managers = [] for i, prompt in enumerate(prompts): # Can't have duplicate runs with the same run ID (if provided) - run_id_ = run_id if i == 0 and run_id is not None else uuid.uuid4() + run_id_ = run_id if i == 0 and run_id is not None else lc_uuid.uuid7() handle_event( self.handlers, "on_llm_start", @@ -1384,7 +1384,7 @@ class CallbackManager(BaseCallbackManager): run_id_ = run_id run_id = None else: - run_id_ = uuid.uuid4() + run_id_ = lc_uuid.uuid7() handle_event( self.handlers, "on_chat_model_start", @@ -1433,7 +1433,7 @@ class CallbackManager(BaseCallbackManager): """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() handle_event( self.handlers, "on_chain_start", @@ -1488,7 +1488,7 @@ class CallbackManager(BaseCallbackManager): """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() handle_event( self.handlers, @@ -1537,7 +1537,7 @@ class CallbackManager(BaseCallbackManager): The callback manager for the retriever run. """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() handle_event( self.handlers, @@ -1594,7 +1594,7 @@ class CallbackManager(BaseCallbackManager): ) raise ValueError(msg) if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() handle_event( self.handlers, @@ -1816,7 +1816,7 @@ class AsyncCallbackManager(BaseCallbackManager): run_id_ = run_id run_id = None else: - run_id_ = uuid.uuid4() + run_id_ = lc_uuid.uuid7() if inline_handlers: inline_tasks.append( @@ -1900,7 +1900,7 @@ class AsyncCallbackManager(BaseCallbackManager): run_id_ = run_id run_id = None else: - run_id_ = uuid.uuid4() + run_id_ = lc_uuid.uuid7() for handler in self.handlers: task = ahandle_event( @@ -1962,7 +1962,7 @@ class AsyncCallbackManager(BaseCallbackManager): The async callback manager for the chain run. """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() await ahandle_event( self.handlers, @@ -2010,7 +2010,7 @@ class AsyncCallbackManager(BaseCallbackManager): The async callback manager for the tool run. """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() await ahandle_event( self.handlers, @@ -2060,7 +2060,7 @@ class AsyncCallbackManager(BaseCallbackManager): if not self.handlers: return if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() if kwargs: msg = ( @@ -2102,7 +2102,7 @@ class AsyncCallbackManager(BaseCallbackManager): The async callback manager for the retriever run. """ if run_id is None: - run_id = uuid.uuid4() + run_id = lc_uuid.uuid7() await ahandle_event( self.handlers, diff --git a/libs/core/langchain_core/tracers/event_stream.py b/libs/core/langchain_core/tracers/event_stream.py index acccd979d1c..b3c92c6da9a 100644 --- a/libs/core/langchain_core/tracers/event_stream.py +++ b/libs/core/langchain_core/tracers/event_stream.py @@ -12,7 +12,7 @@ from typing import ( TypeVar, cast, ) -from uuid import UUID, uuid4 +from uuid import UUID from typing_extensions import NotRequired, override @@ -42,6 +42,7 @@ from langchain_core.tracers.log_stream import ( _astream_log_implementation, ) from langchain_core.tracers.memory_stream import _MemoryStream +from langchain_core.utils import uuid as lc_uuid from langchain_core.utils.aiter import aclosing, py_anext if TYPE_CHECKING: @@ -1006,7 +1007,7 @@ async def _astream_events_implementation_v2( # Assign the stream handler to the config config = ensure_config(config) - run_id = cast("UUID", config.setdefault("run_id", uuid4())) + run_id = cast("UUID", config.setdefault("run_id", lc_uuid.uuid7())) callbacks = config.get("callbacks") if callbacks is None: config["callbacks"] = [event_streamer] diff --git a/libs/core/langchain_core/utils/_internal/__init__.py b/libs/core/langchain_core/utils/_internal/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/core/langchain_core/utils/_internal/_uuid.py b/libs/core/langchain_core/utils/_internal/_uuid.py new file mode 100644 index 00000000000..65f5c3ae474 --- /dev/null +++ b/libs/core/langchain_core/utils/_internal/_uuid.py @@ -0,0 +1,108 @@ +"""Private UUID v7 implementation backported from Python 3.14+. + +This module provides UUID v7 generation for Python versions < 3.14. +The implementation is taken directly from CPython's uuid.py: +https://github.com/python/cpython/blob/main/Lib/uuid.py +""" + +from __future__ import annotations + +import os +import time +import uuid +import uuid as uuid_std + +__all__ = ["uuid7"] +# RFC 4122 version 7 flags: version bits (7 << 76) | variant bits (0x8000 << 48) +_RFC_4122_VERSION_7_FLAGS = (7 << 76) | (0x8000 << 48) + +# Global state for monotonic counter +_last_timestamp_v7: int | None = None +_last_counter_v7: int = 0 # 42-bit counter + + +def _uuid7_get_counter_and_tail() -> tuple[int, int]: + """Generate random counter and tail values for UUID v7.""" + rand = int.from_bytes(os.urandom(10), byteorder="big") + # 42-bit counter with MSB set to 0 + counter = (rand >> 32) & 0x1FF_FFFF_FFFF + # 32-bit random data + tail = rand & 0xFFFF_FFFF + return counter, tail + + +def _uuid7(nanoseconds: int | None = None) -> uuid_std.UUID: + """Generate a UUID from a Unix timestamp in milliseconds and random bits. + + UUIDv7 objects feature monotonicity within a millisecond. + + Args: + nanoseconds: Optional ns timestamp. If not provided, uses current time. + """ + # --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 - + # unix_ts_ms | version | counter_hi | variant | counter_lo | random + # + # 'counter = counter_hi | counter_lo' is a 42-bit counter constructed + # with Method 1 of RFC 9562, ยง6.2, and its MSB is set to 0. + # + # 'random' is a 32-bit random value regenerated for every new UUID. + # + # If multiple UUIDs are generated within the same millisecond, the LSB + # of 'counter' is incremented by 1. When overflowing, the timestamp is + # advanced and the counter is reset to a random 42-bit integer with MSB + # set to 0. + + global _last_timestamp_v7 # noqa: PLW0603 + global _last_counter_v7 # noqa: PLW0603 + + if nanoseconds is None: + nanoseconds = time.time_ns() + timestamp_ms = nanoseconds // 1_000_000 + + if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7: + counter, tail = _uuid7_get_counter_and_tail() + else: + if timestamp_ms < _last_timestamp_v7: + timestamp_ms = _last_timestamp_v7 + 1 + # advance the 42-bit counter + counter = _last_counter_v7 + 1 + if counter > 0x3FF_FFFF_FFFF: # noqa: PLR2004 + # advance the 48-bit timestamp + timestamp_ms += 1 + counter, tail = _uuid7_get_counter_and_tail() + else: + # 32-bit random data + tail = int.from_bytes(os.urandom(4), byteorder="big") + + unix_ts_ms = timestamp_ms & 0xFFFF_FFFF_FFFF + counter_msbs = counter >> 30 + # keep 12 counter's MSBs and clear variant bits + counter_hi = counter_msbs & 0x0FFF + # keep 30 counter's LSBs and clear version bits + counter_lo = counter & 0x3FFF_FFFF + # ensure that the tail is always a 32-bit integer + tail &= 0xFFFF_FFFF + + int_uuid_7 = unix_ts_ms << 80 + int_uuid_7 |= counter_hi << 64 + int_uuid_7 |= counter_lo << 32 + int_uuid_7 |= tail + # by construction, the variant and version bits are already cleared + int_uuid_7 |= _RFC_4122_VERSION_7_FLAGS + + # Use the public UUID constructor with int parameter + res = uuid.UUID(int=int_uuid_7) + + # defer global update until all computations are done + _last_timestamp_v7 = timestamp_ms + _last_counter_v7 = counter + return res + + +def uuid7() -> uuid_std.UUID: + """Generate a random UUID v7. + + Returns: + uuid_std.UUID: A random, RFC 9562-compliant UUID v7. + """ + return _uuid7() diff --git a/libs/core/langchain_core/utils/uuid.py b/libs/core/langchain_core/utils/uuid.py new file mode 100644 index 00000000000..9fefd3b92b0 --- /dev/null +++ b/libs/core/langchain_core/utils/uuid.py @@ -0,0 +1,19 @@ +"""UUID utility functions. + +This module exports a uuid7 function to generate monotonic, time-ordered UUIDs +for tracing and similar operations. +""" + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from langchain_core.utils._internal._uuid import uuid7 +try: + # Exported after version 0.4.43. + # When available, re-use to ensure monotonicity between IDs + from langsmith import uuid7 # type: ignore[no-redef] +except ImportError: + from langchain_core.utils._internal._uuid import uuid7 + + +__all__ = ["uuid7"]