Compare commits

...

1 Commits

Author SHA1 Message Date
William Fu-Hinthorn
e7c1199ecf chore: Use uuid7 in callback run IDs 2025-11-21 13:32:00 -08:00
5 changed files with 144 additions and 16 deletions

View File

@@ -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,

View File

@@ -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]

View File

@@ -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()

View File

@@ -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"]