""" MockPlatformClient — заглушка SDK платформы Lambda. Единственный файл который нужно заменить при подключении реального SDK. Все обращения к платформе в коде ботов идут только через этот класс. При подключении реального SDK: 1. Скопируй интерфейс (методы и сигнатуры) из этого файла 2. Замени тело методов на реальные API вызовы 3. Обнови импорт в telegram_bot/main.py и matrix_bot/main.py """ from __future__ import annotations import asyncio import uuid from datetime import datetime, timedelta from typing import Any import structlog logger = structlog.get_logger(__name__) class PlatformError(Exception): """Базовый класс ошибок платформы.""" def __init__(self, message: str, code: str = "PLATFORM_ERROR"): super().__init__(message) self.code = code class SessionNotFoundError(PlatformError): def __init__(self, session_id: str): super().__init__(f"Session {session_id} not found", "SESSION_NOT_FOUND") class MockPlatformClient: """ Имитирует поведение SDK платформы Lambda. Хранит состояние в памяти — при перезапуске сбрасывается. Для персистентности в моке используй MockPlatformClient(persistent=True) (тогда сохраняет в /tmp/mock_platform_state.json). """ def __init__(self, base_url: str = "http://localhost:8000", timeout: float = 5.0): self._sessions: dict[str, dict] = {} self._messages: dict[str, list] = {} # session_id -> messages self._base_url = base_url self._timeout = timeout logger.info("MockPlatformClient initialized", base_url=base_url) # ─── Sessions ──────────────────────────────────────────────────────────── async def create_session( self, user_id: str, platform: str, # "telegram" | "matrix" context: dict[str, Any] | None = None, ) -> dict: """ Создаёт новую сессию пользователя с AI-агентом. Returns: { "session_id": str, "agent_id": str, "created_at": ISO8601, "expires_at": ISO8601, } """ await self._simulate_latency() session_id = str(uuid.uuid4()) agent_id = f"agent-{uuid.uuid4().hex[:8]}" now = datetime.utcnow() session = { "session_id": session_id, "agent_id": agent_id, "user_id": user_id, "platform": platform, "context": context or {}, "created_at": now.isoformat() + "Z", "expires_at": (now + timedelta(hours=24)).isoformat() + "Z", "status": "active", } self._sessions[session_id] = session self._messages[session_id] = [] logger.info("Session created", session_id=session_id, user_id=user_id, platform=platform) return {k: v for k, v in session.items() if k not in ("user_id", "platform", "context", "status")} async def get_session(self, session_id: str) -> dict: """ Возвращает информацию о сессии. Raises: SessionNotFoundError: если сессия не существует или истекла """ await self._simulate_latency() session = self._sessions.get(session_id) if not session: raise SessionNotFoundError(session_id) return session async def close_session(self, session_id: str) -> bool: """ Завершает сессию. Returns: True если сессия была закрыта, False если уже была закрыта """ await self._simulate_latency() session = self._sessions.get(session_id) if not session: raise SessionNotFoundError(session_id) was_active = session["status"] == "active" session["status"] = "closed" session["closed_at"] = datetime.utcnow().isoformat() + "Z" logger.info("Session closed", session_id=session_id) return was_active # ─── Messages ───────────────────────────────────────────────────────────── async def send_message( self, session_id: str, text: str, attachments: list[dict] | None = None, ) -> dict: """ Отправляет сообщение пользователя в сессию и получает ответ агента. Returns: { "message_id": str, "response": str, "tokens_used": int, "finished": bool, } """ await self._simulate_latency(min_ms=200, max_ms=800) if session_id not in self._sessions: raise SessionNotFoundError(session_id) message_id = str(uuid.uuid4()) # Mock ответ агента response = f"[MOCK] Ответ на: «{text[:50]}{'...' if len(text) > 50 else ''}»" message = { "message_id": message_id, "session_id": session_id, "user_text": text, "response": response, "tokens_used": len(text.split()) * 2, # грубая оценка "finished": True, "created_at": datetime.utcnow().isoformat() + "Z", } self._messages[session_id].append(message) logger.info("Message sent", session_id=session_id, message_id=message_id) return { "message_id": message["message_id"], "response": message["response"], "tokens_used": message["tokens_used"], "finished": message["finished"], } async def get_message_history( self, session_id: str, limit: int = 20, offset: int = 0, ) -> list[dict]: """ Возвращает историю сообщений сессии. """ await self._simulate_latency() if session_id not in self._sessions: raise SessionNotFoundError(session_id) messages = self._messages.get(session_id, []) return messages[offset : offset + limit] # ─── User ───────────────────────────────────────────────────────────────── async def get_or_create_user( self, external_id: str, platform: str, display_name: str | None = None, ) -> dict: """ Возвращает или создаёт пользователя платформы. Returns: { "user_id": str, "external_id": str, "platform": str, "display_name": str | None, "created_at": ISO8601, "is_new": bool, } """ await self._simulate_latency() # В моке генерируем детерминированный user_id user_id = f"user-{platform}-{external_id}" logger.info("User fetched", user_id=user_id, platform=platform) return { "user_id": user_id, "external_id": external_id, "platform": platform, "display_name": display_name, "created_at": "2025-01-01T00:00:00Z", "is_new": False, } # ─── Internals ──────────────────────────────────────────────────────────── async def _simulate_latency(self, min_ms: int = 10, max_ms: int = 100) -> None: """Имитирует сетевую задержку для реалистичного тестирования.""" import random delay = random.randint(min_ms, max_ms) / 1000 await asyncio.sleep(delay) def get_stats(self) -> dict: """Возвращает статистику мока (для отладки).""" return { "active_sessions": sum(1 for s in self._sessions.values() if s["status"] == "active"), "total_sessions": len(self._sessions), "total_messages": sum(len(msgs) for msgs in self._messages.values()), }