session.py 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. """Pure conversation-session identity tracking.
  2. :class:`SessionTracker` mints one id for the first observed utterance and rolls
  3. over to a new id once the quiet gap between consecutive utterances exceeds the
  4. configured threshold. It has no filesystem, config, or daemon dependencies: the
  5. caller supplies the clock (``now``) and may inject the id factory, which keeps
  6. the behavior deterministic and easy to test.
  7. """
  8. import threading
  9. from datetime import datetime
  10. from typing import Callable
  11. def _default_id_factory(now: float) -> str:
  12. """Format an epoch timestamp as a second-resolution session id."""
  13. return datetime.fromtimestamp(now).strftime("%Y-%m-%d-%H-%M-%S")
  14. class SessionTracker:
  15. """Assigns a stable session id and rolls over after a quiet gap.
  16. Args:
  17. gap_s: Seconds of silence between consecutive observations that starts
  18. a new session. Values ``<= 0`` disable rollover after the first id.
  19. id_factory: Builds the id from an epoch timestamp. Defaults to the
  20. local-time ``YYYY-MM-DD-HH-MM-SS`` format; tests inject a counter.
  21. """
  22. def __init__(self, gap_s: float, id_factory: Callable[[float], str] | None = None):
  23. self._gap_s = gap_s
  24. self._id_factory = id_factory or _default_id_factory
  25. self._lock = threading.Lock()
  26. self._current_id: str | None = None
  27. self._last: float = 0.0
  28. def observe(self, now: float) -> str:
  29. """Return the session id for an observation at epoch ``now``.
  30. Mints on the first call, and mints a new id when ``now - last > gap_s``
  31. (only when ``gap_s > 0``); otherwise returns the current id. The
  32. observation time is remembered so the gap is measured between
  33. consecutive observations. Thread-safe.
  34. """
  35. with self._lock:
  36. if self._current_id is None or (
  37. self._gap_s > 0 and now - self._last > self._gap_s
  38. ):
  39. self._current_id = self._id_factory(now)
  40. self._last = now
  41. return self._current_id
  42. @property
  43. def current_id(self) -> str | None:
  44. """The current session id, or ``None`` before the first observation."""
  45. with self._lock:
  46. return self._current_id