surfaces/src/mock_platform.py
Mikhail Putilovskij b6df29bd9b init: surfaces-bot — Telegram & Matrix prototype
- Surface Protocol: unified IncomingMessage/OutgoingUI/ChatContext
- Telegram: Forum Topics (group + topics per chat)
- Matrix: Space + rooms per chat
- MockPlatformClient with PlatformClient Protocol
- docs: surface-protocol, telegram/matrix specs, api-contract, claude-code-guide
- project scaffold: src/, tests/, pyproject.toml

Co-Authored-By: Claude Sonnet 4-6 <noreply@anthropic.com>
2026-03-27 00:35:42 +03:00

246 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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()),
}