from __future__ import annotations import logging from datetime import UTC, datetime logger = logging.getLogger(__name__) THREAD_PARENT_CACHE_MAX = 100 THREAD_PARENT_CACHE_TTL_SECONDS = 300 class ThreadSessionManager: def __init__(self): self._parent_cache: dict[str, tuple[datetime, dict]] = {} def cache_thread_parent(self, thread_id: str, parent_msg: dict) -> None: now = datetime.now(UTC) self._parent_cache[thread_id] = (now, parent_msg) self._evict_expired() def get_thread_parent(self, thread_id: str) -> dict | None: now = datetime.now(UTC) entry = self._parent_cache.get(thread_id) if not entry: return None cached_at, parent = entry if (now - cached_at).total_seconds() > THREAD_PARENT_CACHE_TTL_SECONDS: self._parent_cache.pop(thread_id, None) return None return parent def _evict_expired(self) -> None: if len(self._parent_cache) <= THREAD_PARENT_CACHE_MAX: return now = datetime.now(UTC) expired = [ k for k, (t, _) in self._parent_cache.items() if (now - t).total_seconds() > THREAD_PARENT_CACHE_TTL_SECONDS ] for k in expired: self._parent_cache.pop(k, None) if len(self._parent_cache) > THREAD_PARENT_CACHE_MAX: sorted_keys = sorted( self._parent_cache.keys(), key=lambda k: self._parent_cache[k][0], ) for k in sorted_keys[: len(self._parent_cache) - THREAD_PARENT_CACHE_MAX]: self._parent_cache.pop(k, None) def resolve_msteams_session_key( conversation_id: str, *, is_channel_thread: bool = False, thread_root_message_id: str | None = None, ) -> str: if is_channel_thread and thread_root_message_id: return f"msteams:thread:{conversation_id}:{thread_root_message_id}" return f"msteams:{conversation_id}" def resolve_msteams_thread_id( reply_to_id: str | None, conversation_type: str | None, ) -> str | None: if conversation_type == "channel" and reply_to_id: return reply_to_id return None def format_thread_history(messages: list[dict]) -> str: if not messages: return "" lines = ["[Thread history]"] for msg in reversed(messages): ts = msg.get("timestamp", "") sender = msg.get("from_name", msg.get("from", {}).get("name", "Unknown")) text = msg.get("text", "") ts_display = ts[:19].replace("T", " ") if ts else "" if ts_display: lines.append(f"{ts_display} {sender}: {text}") else: lines.append(f"{sender}: {text}") lines.append("[/Thread history]") return "\n".join(lines)