# core/chat.py from __future__ import annotations from datetime import UTC, datetime import structlog from core.protocol import ChatContext from core.store import StateStore logger = structlog.get_logger(__name__) def _to_dict(ctx: ChatContext) -> dict: return { "chat_id": ctx.chat_id, "display_name": ctx.display_name, "platform": ctx.platform, "surface_ref": ctx.surface_ref, "created_at": ctx.created_at.isoformat(), "is_archived": ctx.is_archived, } def _from_dict(d: dict) -> ChatContext: return ChatContext( chat_id=d["chat_id"], display_name=d["display_name"], platform=d["platform"], surface_ref=d["surface_ref"], created_at=datetime.fromisoformat(d["created_at"]), is_archived=d.get("is_archived", False), ) class ChatManager: """ Управляет метаданными чатов (C1/C2/C3 в workspace пользователя). НЕ управляет lifecycle контейнера — это дело Master'а на стороне платформы. """ def __init__(self, platform: object, store: StateStore) -> None: self._store = store def _key(self, user_id: str, chat_id: str) -> str: return f"chat:{user_id}:{chat_id}" async def get_or_create( self, user_id: str, chat_id: str, platform: str, surface_ref: str, name: str | None = None, ) -> ChatContext: key = self._key(user_id, chat_id) stored = await self._store.get(key) if stored: return _from_dict(stored) ctx = ChatContext( chat_id=chat_id, display_name=name or f"Чат {chat_id}", platform=platform, surface_ref=surface_ref, created_at=datetime.now(UTC), ) await self._store.set(key, _to_dict(ctx)) logger.info("Chat created", chat_id=chat_id, user_id=user_id) return ctx async def get(self, chat_id: str, user_id: str | None = None) -> ChatContext | None: if user_id: stored = await self._store.get(self._key(user_id, chat_id)) return _from_dict(stored) if stored else None # Scan by chat_id suffix when user_id unknown (slower) for key in await self._store.keys("chat:"): if key.endswith(f":{chat_id}"): stored = await self._store.get(key) if stored: return _from_dict(stored) return None async def rename(self, chat_id: str, name: str, user_id: str | None = None) -> ChatContext: ctx = await self.get(chat_id, user_id) if not ctx: raise ValueError(f"Chat {chat_id} not found") ctx.display_name = name for key in await self._store.keys("chat:"): if key.endswith(f":{chat_id}"): await self._store.set(key, _to_dict(ctx)) break return ctx async def archive(self, chat_id: str, user_id: str | None = None) -> None: ctx = await self.get(chat_id, user_id) if not ctx: raise ValueError(f"Chat {chat_id} not found") ctx.is_archived = True for key in await self._store.keys("chat:"): if key.endswith(f":{chat_id}"): await self._store.set(key, _to_dict(ctx)) break async def list_active(self, user_id: str) -> list[ChatContext]: chats = [] for key in await self._store.keys(f"chat:{user_id}:"): stored = await self._store.get(key) if stored and not stored.get("is_archived"): chats.append(_from_dict(stored)) return chats