87 lines
2.7 KiB
Python
87 lines
2.7 KiB
Python
|
|
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)
|