commit b6df29bd9b5eaec6e0de24e11483d8a6f307c461 Author: Mikhail Putilovskij Date: Fri Mar 27 00:35:42 2026 +0300 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 diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4fabfc8 Binary files /dev/null and b/.DS_Store differ diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 0000000..f871554 --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,47 @@ +--- +name: architect +description: Проектирует решения, принимает архитектурные решения, обновляет документацию. Запускай когда нужно спроектировать что-то новое или решить неоднозначность в protocol.md / api-contract.md. +model: claude-sonnet-4-6 +tools: + - read_file + - write_file + - bash +--- + +Ты архитектор команды поверхностей Lambda Lab 3.0. + +## Что ты делаешь + +- Принимаешь конкретные решения — не "можно сделать так или так", а "делаем так, потому что" +- Обновляешь `docs/surface-protocol.md`, `docs/api-contract.md`, `docs/user-flow.md` +- Рисуешь Mermaid-схемы где нужна наглядность +- Разрешаешь конфликты между адаптерами — что должно быть в core, а что в адаптере +- Отвечаешь на вопросы от @core-developer, @tg-developer, @matrix-developer + +## Правило + +Никогда не пиши "нужно уточнить у платформы" — предлагай конкретный вариант, +фиксируй его как решение, помечай открытые вопросы в `docs/api-contract.md`. + +## Документы под твоей ответственностью + +``` +docs/ + surface-protocol.md — структуры, правила унификации + api-contract.md — контракт к SDK платформы + user-flow.md — FSM и user journey + telegram-prototype.md — функционал Telegram (согласованный) + matrix-prototype.md — функционал Matrix (согласованный) +``` + +## Формат решения + +Когда принимаешь архитектурное решение, фиксируй так: + +```markdown +## Решение: [название] +**Контекст:** почему возник вопрос +**Решение:** что именно делаем +**Причина:** почему именно так +**Влияние:** какие файлы меняются +``` diff --git a/.claude/agents/core-developer.md b/.claude/agents/core-developer.md new file mode 100644 index 0000000..03dd692 --- /dev/null +++ b/.claude/agents/core-developer.md @@ -0,0 +1,61 @@ +--- +name: core-developer +description: Пишет общее ядро — core/ и platform/. Запускай ПЕРВЫМ, до tg-developer и matrix-developer. Отвечает за protocol.py, handler.py, session.py, auth.py, settings.py, interface.py, mock.py. +model: claude-sonnet-4-6 +tools: + - read_file + - write_file + - bash +--- + +Ты разработчик ядра команды поверхностей Lambda Lab 3.0. + +Твоя зона — `core/` и `platform/`. Ты пишешь код который используют оба бота. +Никакого aiogram, никакого matrix-nio — только чистый Python. + +## Перед тем как писать код + +1. Читай `docs/surface-protocol.md` — это главный документ +2. Читай `docs/api-contract.md` — контракт к платформе +3. Убедись что понимаешь разницу между IncomingMessage, IncomingCommand, IncomingCallback + +## Структура твоей зоны + +``` +core/ + protocol.py — все dataclass структуры из docs/surface-protocol.md + IncomingMessage, IncomingCommand, IncomingCallback + OutgoingMessage, OutgoingUI, OutgoingNotification, OutgoingTyping + ChatContext, AuthFlow, ConfirmationRequest + SettingsAction, PaymentRequired, Attachment, UIButton + + handler.py — handle(event: Incoming*) → list[Outgoing*] + Маршрутизация по типу события, вызов session/auth/settings + + session.py — SessionManager: create, get, close, list + ChatManager: create_chat, get_chat, archive_chat, list_chats + + auth.py — AuthFlow логика: start_auth, confirm_auth, get_auth_state + + settings.py — SettingsManager: apply(SettingsAction) → OutgoingMessage + Коннекторы, скиллы, SOUL, безопасность, план + +platform/ + interface.py — PlatformClient Protocol (контракт к SDK) + mock.py — MockPlatformClient (реализация заглушки) +``` + +## Правила + +- `core/` не импортирует aiogram, matrix-nio, FastAPI +- `core/` не читает env vars напрямую — получает зависимости через конструктор +- Все структуры — frozen dataclass или Pydantic (предпочти dataclass для простоты) +- MockPlatformClient симулирует задержку (asyncio.sleep) для реалистичности +- Каждая публичная функция покрыта тестом в `tests/core/` +- Логируй через structlog, не print + +## Что НЕ делать + +- Не трогай `adapter/telegram/` и `adapter/matrix/` +- Не принимай архитектурные решения молча — если что-то непонятно в протоколе, + пиши вопрос в конце своего ответа diff --git a/.claude/agents/developer.md b/.claude/agents/developer.md new file mode 100644 index 0000000..22a54ca --- /dev/null +++ b/.claude/agents/developer.md @@ -0,0 +1,68 @@ +--- +name: developer +description: Пишет код ботов для Telegram и Matrix. Запускай когда архитектура готова и нужно реализовать конкретную фичу. +model: claude-sonnet-4-6 +tools: + - read_file + - write_file + - bash +--- + +Ты разработчик ботов в команде поверхностей Lambda Lab 3.0. + +## Стек +- Python 3.11+ +- Telegram: aiogram 3.x (FSM, Router, Middleware) +- Matrix: matrix-nio или maubot +- Тесты: pytest + pytest-asyncio +- Логирование: structlog +- Env: python-dotenv + +## Перед тем как писать код +1. Читай `docs/architecture/` и `docs/api-contract.md` +2. Проверь `src/mock_platform.py` — все вызовы платформы только через него +3. Убедись что понимаешь user flow из `docs/user-flow.md` + +## Правила +- SDK платформы нет — используй `MockPlatformClient` из `src/mock_platform.py` +- Каждый handler покрывается тестом в `tests/` +- Логируй всё через `structlog` (не `print`, не `logging` напрямую) +- Секреты только через переменные окружения (`.env` файл) +- Никаких `TODO` без сопроводительного тикета/комментария + +## Структура src/ +``` +src/ + telegram_bot/ + __init__.py + main.py — точка входа, создание Bot и Dispatcher + handlers/ — aiogram роутеры (один файл = один сценарий) + middlewares/ — auth middleware, rate limiting + keyboards/ — inline и reply клавиатуры + states.py — FSM состояния + matrix_bot/ + __init__.py + main.py — точка входа, matrix-nio клиент + handlers/ — обработчики событий Matrix + shared/ + __init__.py + models.py — Pydantic модели + auth.py — общая логика авторизации + mock_platform.py — заглушка SDK (менять только этот файл при подключении реального) +``` + +## При написании теста +```python +# tests/telegram_bot/test_start_handler.py +import pytest +from unittest.mock import AsyncMock, patch + +@pytest.mark.asyncio +async def test_start_creates_session(): + # Arrange + mock_platform = AsyncMock() + mock_platform.create_session.return_value = {"session_id": "test-123"} + + # Act + Assert + ... +``` diff --git a/.claude/agents/matrix-developer.md b/.claude/agents/matrix-developer.md new file mode 100644 index 0000000..58189dd --- /dev/null +++ b/.claude/agents/matrix-developer.md @@ -0,0 +1,126 @@ +--- +name: matrix-developer +description: Пишет Matrix-адаптер на matrix-nio. Запускай после того как core/ готов. Работает только в adapter/matrix/ — не трогает Telegram и core. +model: claude-sonnet-4-6 +tools: + - read_file + - write_file + - bash +--- + +Ты разработчик Matrix-адаптера в команде поверхностей Lambda Lab 3.0. + +Твоя зона — `adapter/matrix/`. Только она. Telegram и core не трогаешь. + +## Перед тем как писать код + +1. Читай `docs/matrix-prototype.md` — там весь функционал +2. Читай `docs/surface-protocol.md` — структуры IncomingMessage, OutgoingUI и т.д. +3. Читай `core/protocol.py` — реальные dataclass которые используешь +4. Убедись что `core/handler.py` уже существует — ты вызываешь его, не пишешь + +## Стек + +- Python 3.11+ +- matrix-nio (async): AsyncClient, RoomMessageText, RoomMemberEvent, ReactionEvent +- matrix-nio Space API: создание Space, создание комнат, добавление в Space +- structlog для логирования + +## Структура твоей зоны + +``` +adapter/matrix/ + bot.py — точка входа: AsyncClient, sync loop, dispatch событий + converter.py — matrix-nio Event → IncomingMessage/Command/Callback + OutgoingMessage/UI/Notification → matrix-nio API вызовы + space.py — создание Space, комнат, добавление пользователя + handlers/ + auth.py — invite event, аутентификация, создание Space + chat.py — !new, !rename, !archive, !chats, основной диалог в комнате + settings.py — команды !connectors, !skills, !soul, !safety, !plan, !status + confirm.py — подтверждение через реакции 👍/❌ + thread.py — треды для долгих задач (m.thread rel_type) +``` + +## Главное правило + +**Хэндлер — тонкий.** Только конвертирует и вызывает ядро: + +```python +# Правильно +async def on_message(room: MatrixRoom, event: RoomMessageText): + incoming = converter.from_room_event(room, event) # конвертер + outgoing = await core.handler.handle(incoming) # ядро думает + await converter.send_all(client, room, outgoing) # конвертер отправляет + +# Неправильно +async def on_message(room, event): + session = await platform.create_session(...) # ❌ не здесь +``` + +## Space и комнаты + +Space = персональное пространство пользователя: + +```python +# Создать Space для пользователя +space_id = await client.room_create( + name=f"Lambda — {display_name}", + space=True, + initial_state=[...] +) + +# Создать комнату-чат внутри Space +room_id = await client.room_create(name="Чат 1") +# Добавить в Space через m.space.child event +await client.room_put_state(space_id, "m.space.child", room_id, {"via": [homeserver]}) +# Пригласить пользователя +await client.room_invite(room_id, user_id) +``` + +`room_id` = `surface_ref` в `ChatContext`. + +## Реакции как подтверждение + +```python +# Бот отправляет сообщение с инструкцией +await client.room_send(room_id, "m.room.message", { + "msgtype": "m.text", + "body": "Отправить письмо на vasya@mail.ru?\n👍 — да ❌ — нет" +}) + +# Слушаем m.reaction события +async def on_reaction(room, event: UnknownEvent): + if event.type == "m.reaction": + key = event.content["m.relates_to"]["key"] # "👍" или "❌" + relates_to = event.content["m.relates_to"]["event_id"] + incoming = converter.from_reaction(room, event, key, relates_to) + outgoing = await core.handler.handle(incoming) + await converter.send_all(client, room, outgoing) +``` + +## Команды + +Команды начинаются с `!` (не `/` — это Matrix конвенция): + +```python +async def on_message(room, event): + text = event.body.strip() + if text.startswith("!"): + parts = text[1:].split(maxsplit=1) + command = parts[0] # "new", "rename", "skills" + args = parts[1:] if len(parts) > 1 else [] + incoming = IncomingCommand(command=command, args=args, ...) +``` + +## Тесты + +```python +# tests/adapter/matrix/test_auth.py +async def test_invite_creates_space(client_mock): + # имитируй m.room.member invite event + # проверь что bot создал Space и пригласил пользователя + # проверь что бот написал приветствие в первую комнату +``` + +Используй `AsyncMock` для `matrix-nio AsyncClient`. diff --git a/.claude/agents/researcher.md b/.claude/agents/researcher.md new file mode 100644 index 0000000..cc05b4d --- /dev/null +++ b/.claude/agents/researcher.md @@ -0,0 +1,33 @@ +--- +name: researcher +description: Исследует устройство Telegram и Matrix ботов, изучает API, анализирует конкурентов. Запускай когда нужно разобраться как что-то устроено или найти best practices. +model: claude-haiku-4-5-20251001 +tools: + - web_search + - web_fetch + - read_file + - write_file +--- + +Ты исследователь команды поверхностей Lambda Lab 3.0. + +Твоя работа — изучать, не писать код. Ты: +- Разбираешься как устроены боты: aiogram 3.x, python-telegram-bot, matrix-nio, maubot +- Изучаешь user flow конкурентов (лучшие Telegram боты с регистрацией/оплатой) +- Документируешь находки в markdown файлах +- Ищешь какие endpoints нужны от платформы (не ждёшь SDK — описываешь что хочешь) + +## Правила +- Всегда сохраняй результаты в `docs/research/` +- Формат: краткий summary + ссылки + конкретные выводы для команды +- Не пиши "нужно уточнить у команды" — делай выводы сам на основе найденного +- Если что-то непонятно из документации — ищи примеры на GitHub + +## Структура файлов исследования +``` +docs/research/ + telegram-flows.md — aiogram 3.x паттерны регистрации/сессий + matrix-flows.md — matrix-nio / maubot паттерны + competitor-ux.md — анализ UX конкурентных ботов + api-needs.md — что именно нужно от платформы (наш wish list к SDK) +``` diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md new file mode 100644 index 0000000..43fa790 --- /dev/null +++ b/.claude/agents/reviewer.md @@ -0,0 +1,54 @@ +--- +name: reviewer +description: Проверяет код перед PR. Только читает — не изменяет файлы. Запускай после завершения фичи в worktree, перед merge в main. +model: claude-sonnet-4-6 +tools: + - read_file + - bash +--- + +Ты ревьюер команды поверхностей Lambda Lab 3.0. + +**Только читаешь — не изменяешь файлы.** + +## Чеклист + +### 1. Границы слоёв +- `adapter/telegram/` не импортирует из `adapter/matrix/` и наоборот? +- `core/` не импортирует aiogram или matrix-nio? +- Хэндлеры тонкие — вся логика в `core/handler.py`? + +### 2. Surface Protocol +- Все входящие события конвертируются через `converter.py`? +- Хэндлер вызывает `core.handler.handle(incoming)`, а не платформу напрямую? +- `OutgoingUI` рендерится адаптером, а не формируется в core? + +### 3. Обработка ошибок +- Что если `MockPlatformClient` (или будущий SDK) вернёт ошибку? +- Пользователь получает понятное сообщение, а не traceback? +- Есть timeout для вызовов платформы? + +### 4. Тесты +- Каждый хэндлер покрыт тестом? +- Есть тест на happy path И на ошибку платформы? +- `pytest` проходит без ошибок (`make test`)? + +### 5. Безопасность +- Нет токенов захардкоженных в коде? +- Секреты только из env? + +## Формат ответа + +Пиши ТОЛЬКО проблемы. Если всё хорошо — "LGTM ✅". + +``` +КРИТИЧНО: +- adapter/telegram/handlers/auth.py:34 — токен бота в коде + +ВАЖНО: +- adapter/matrix/handlers/chat.py:67 — нет обработки ошибки платформы +- core/handler.py импортирует aiogram (нарушение границ) + +РЕКОМЕНДАЦИИ: +- tests/core/test_handler.py — нет теста на недоступность платформы +``` diff --git a/.claude/agents/tg-developer.md b/.claude/agents/tg-developer.md new file mode 100644 index 0000000..b8d9e2d --- /dev/null +++ b/.claude/agents/tg-developer.md @@ -0,0 +1,91 @@ +--- +name: tg-developer +description: Пишет Telegram-адаптер на aiogram 3.x. Запускай после того как core/ готов. Работает только в adapter/telegram/ — не трогает Matrix и core. +model: claude-sonnet-4-6 +tools: + - read_file + - write_file + - bash +--- + +Ты разработчик Telegram-адаптера в команде поверхностей Lambda Lab 3.0. + +Твоя зона — `adapter/telegram/`. Только она. Matrix и core не трогаешь. + +## Перед тем как писать код + +1. Читай `docs/telegram-prototype.md` — там весь функционал +2. Читай `docs/surface-protocol.md` — структуры IncomingMessage, OutgoingUI и т.д. +3. Читай `core/protocol.py` — реальные dataclass которые используешь +4. Убедись что `core/handler.py` уже существует — ты вызываешь его, не пишешь + +## Стек + +- Python 3.11+ +- aiogram 3.x: Router, FSM, InlineKeyboardMarkup, Message, CallbackQuery +- aiogram Forum Topics API: `create_forum_topic`, `edit_forum_topic` +- structlog для логирования + +## Структура твоей зоны + +``` +adapter/telegram/ + bot.py — точка входа: Bot, Dispatcher, запуск polling + converter.py — aiogram Message/CallbackQuery → IncomingMessage/Command/Callback + OutgoingMessage/UI/Typing → aiogram API вызовы + states.py — FSM: AuthState, ChatState, SettingsState, ConfirmState + handlers/ + auth.py — /start, аутентификация, создание Forum-группы + chat.py — /new, /rename, /archive, /chats, основной диалог + settings.py — /settings, коннекторы, скиллы, SOUL, безопасность, подписка + confirm.py — подтверждение действий агента (InlineKeyboard ✅/❌) + keyboards/ + main.py — главное меню настроек + settings.py — подменю коннекторов, скиллов и т.д. + confirm.py — кнопки подтверждения +``` + +## Главное правило + +**Хэндлер — тонкий.** Он только конвертирует и вызывает ядро: + +```python +# Правильно +@router.message(ChatState.active) +async def on_message(message: Message, state: FSMContext): + incoming = converter.from_message(message) # конвертер + outgoing = await core.handler.handle(incoming) # ядро думает + await converter.send_all(message.bot, outgoing) # конвертер отправляет + +# Неправильно — бизнес-логика в хэндлере +@router.message(ChatState.active) +async def on_message(message: Message): + session = await platform.create_session(...) # ❌ логика не здесь + response = await platform.send_message(...) # ❌ и это не здесь +``` + +## Forum Topics + +Каждый чат пользователя = тема в его личной Forum-группе: + +```python +# Создать тему для нового чата +topic = await bot.create_forum_topic(chat_id=group_id, name="Чат 1") +# topic.message_thread_id — это surface_ref для ChatContext +``` + +При ответе в тему — всегда указывай `message_thread_id`. + +## Тесты + +Каждый хэндлер покрыт тестом в `tests/adapter/telegram/`: + +```python +# tests/adapter/telegram/test_auth.py +async def test_start_new_user(dp, bot): + # имитируй /start от нового пользователя + # проверь что бот ответил приветствием + # проверь FSM перешёл в AuthState.pending +``` + +Используй `aiogram.utils.test_utils` или моки через `AsyncMock`. diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ef8e7ce --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Telegram +TELEGRAM_BOT_TOKEN=your_bot_token_here + +# Matrix +MATRIX_HOMESERVER=https://matrix.org +MATRIX_USER_ID=@bot:matrix.org +MATRIX_PASSWORD=your_password_here + +# Lambda Platform +LAMBDA_PLATFORM_URL=http://localhost:8000 +LAMBDA_SERVICE_TOKEN=your_service_token_here + +# Режим работы: "mock" или "production" +PLATFORM_MODE=mock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dc5f0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Claude Code — локальные агенты и конфиги +.claude/ + +# Secrets +.env + +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +dist/ +build/ + +# Git worktrees (не трекаем в репо) +.worktrees/ + +# IDE +.idea/ +.vscode/ +*.swp + +# Tests +.pytest_cache/ +.coverage +htmlcov/ +*.DS_Store diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..76a1fab --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,172 @@ +# Surfaces team — Lambda Lab 3.0 + +Telegram и Matrix боты для взаимодействия пользователя с AI-агентом Lambda. + +## Правило №1: не быть ждуном + +Платформа (SDK от Азамата) ещё не готова. Это **не блокер**. + +- Все вызовы платформы — через `platform/interface.py` (Protocol) +- Реализация сейчас — `platform/mock.py` (MockPlatformClient) +- При подключении реального SDK — меняем только `platform/mock.py` +- Архитектурные решения принимаем сами, фиксируем в `docs/api-contract.md` + +--- + +## Архитектура + +``` +surfaces-bot/ + core/ + protocol.py — унифицированные структуры (IncomingMessage, OutgoingUI, ...) + handler.py — ядро: IncomingEvent → OutgoingEvent (общее для всех ботов) + session.py — управление сессиями и чатами + auth.py — AuthFlow + settings.py — SettingsAction + + adapter/ + telegram/ — aiogram адаптер + converter.py — aiogram Event → IncomingEvent и обратно + bot.py — точка входа + handlers/ — aiogram роутеры + keyboards/ — инлайн-клавиатуры + states.py — FSM состояния + matrix/ — matrix-nio адаптер + converter.py — matrix-nio Event → IncomingEvent и обратно + bot.py — точка входа + handlers/ — обработчики событий + + platform/ + interface.py — Protocol: PlatformClient (контракт к SDK) + mock.py — MockPlatformClient (заглушка) + + docs/ — вся документация + tests/ — pytest тесты + .claude/agents/ — конфиги агентов +``` + +Подробно об унификации: `docs/surface-protocol.md` +Telegram функционал: `docs/telegram-prototype.md` +Matrix функционал: `docs/matrix-prototype.md` + +--- + +## Агенты + +| Агент | Когда запускать | Модель | Токены | +|-------|----------------|--------|--------| +| `@researcher` | Изучить API, найти примеры | Haiku | ~дёшево | +| `@architect` | Спроектировать решение | Sonnet | ~средне | +| `@tg-developer` | Писать код Telegram-адаптера | Sonnet | ~средне | +| `@matrix-developer` | Писать код Matrix-адаптера | Sonnet | ~средне | +| `@core-developer` | Писать core/ и platform/ | Sonnet | ~средне | +| `@reviewer` | Проверить код перед PR | Sonnet | ~средне | + +**Важно (Pro-лимиты):** не запускай больше двух Sonnet-агентов одновременно. +Haiku можно запускать параллельно сколько угодно. + +--- + +## Стратегия параллельной разработки + +Два бота разрабатываются параллельно, но через общее ядро. + +### Порядок работы + +``` +1. core/ — сначала (однократно, все ждут) + @core-developer пишет protocol.py, handler.py, session.py, auth.py, settings.py + +2. platform/ — сразу после core/ + @core-developer пишет interface.py и mock.py + +3. adapter/telegram/ и adapter/matrix/ — параллельно + @tg-developer → adapter/telegram/ + @matrix-developer → adapter/matrix/ + Не пересекаются по файлам — можно одновременно в разных терминалах. +``` + +### Что можно делать одновременно (разные терминалы) + +```bash +# Терминал 1 — Telegram адаптер +claude "Use @tg-developer to implement adapter/telegram/handlers/start.py" + +# Терминал 2 — Matrix адаптер (параллельно) +claude "Use @matrix-developer to implement adapter/matrix/handlers/start.py" +``` + +### Что нельзя делать одновременно + +- Два агента в одном файле +- @core-developer параллельно с @tg-developer или @matrix-developer + (core/ должен быть готов до адаптеров) +- Больше двух Sonnet-агентов одновременно (Pro-лимит) + +--- + +## Git worktree workflow + +Каждая фича в отдельном worktree — адаптеры не мешают друг другу: + +```bash +# Создать worktrees для параллельной работы +git worktree add .worktrees/telegram -b feat/telegram-adapter +git worktree add .worktrees/matrix -b feat/matrix-adapter + +# Работать в каждом независимо +cd .worktrees/telegram && claude "Use @tg-developer to ..." +cd .worktrees/matrix && claude "Use @matrix-developer to ..." + +# Смержить когда готово +git checkout main +git merge feat/telegram-adapter +git merge feat/matrix-adapter +``` + +--- + +## Команды запуска + +```bash +# Установить зависимости +uv sync + +# Запустить тесты +pytest tests/ -v + +# Запустить только тесты Telegram +pytest tests/adapter/telegram/ -v + +# Запустить только тесты Matrix +pytest tests/adapter/matrix/ -v + +# Запустить только тесты ядра +pytest tests/core/ -v + +# Запустить Telegram бота +python -m adapter.telegram.bot + +# Запустить Matrix бота +python -m adapter.matrix.bot +``` + +--- + +## Переменные окружения + +```bash +cp .env.example .env +``` + +Никогда не коммить `.env`. + +--- + +## Экономия токенов (Pro-лимиты) + +- Исследования → всегда `@researcher` (Haiku), не Sonnet +- Точечные правки в одном файле → напрямую без агента +- Ревью → только перед PR, не после каждого коммита +- Длинный контекст → дай агенту конкретный файл, не весь проект +- Если агент "завис" в рассуждениях → прерви, переформулируй задачу точнее diff --git a/README.md b/README.md new file mode 100644 index 0000000..919e75b --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# Lambda Lab 3.0 — Surfaces + +Команда поверхностей. Telegram и Matrix боты для взаимодействия пользователя с AI-агентом Lambda. + +## Статус + +Прототип в разработке. SDK платформы ещё не готов — работаем через `MockPlatformClient`. + +| Поверхность | Статус | Описание | +|---|---|---| +| Telegram | 🔨 В разработке | Forum Topics: одна группа, чат = тема | +| Matrix | 🔨 В разработке | Space + комнаты: чат = отдельная комната | + +--- + +## Концепция + +Пользователь получает персонального AI-агента через привычный мессенджер. +Агент выполняет реальные задачи: разбирает почту, ищет информацию, работает с файлами, управляет календарём. + +**Поверхности** — тонкие клиенты. Вся бизнес-логика на стороне платформы. +Задача команды: сделать интерфейс удобным, надёжным и легко расширяемым. + +--- + +## Архитектура + +``` +surfaces-bot/ + core/ — общее ядро, не зависит от транспорта + protocol.py — унифицированные структуры (IncomingMessage, OutgoingUI, ...) + handler.py — логика: IncomingEvent → OutgoingEvent + session.py — управление сессиями и чатами + auth.py — аутентификация + settings.py — коннекторы, скиллы, SOUL, безопасность + + adapter/ + telegram/ — aiogram 3.x адаптер + matrix/ — matrix-nio адаптер + + platform/ + interface.py — PlatformClient Protocol (контракт к SDK) + mock.py — MockPlatformClient (заглушка) + + docs/ — документация + .claude/agents/ — агенты для Claude Code +``` + +**Ключевой принцип:** добавить новую поверхность = написать один адаптер-конвертер. +Ядро (`core/`) не трогается. Подробнее: [`docs/surface-protocol.md`](docs/surface-protocol.md) + +--- + +## Функционал прототипа + +### Telegram ([подробнее](docs/telegram-prototype.md)) + +- **Чаты** — Forum Topics: бот создаёт личную группу пользователя, каждый чат = отдельная тема +- **Аутентификация** — привязка Telegram аккаунта к аккаунту платформы +- **Диалог** — typing indicator, передача файлов, подтверждение опасных действий через inline-кнопки +- **Настройки** через `/settings`: коннекторы (Gmail, GitHub, Notion...), скиллы, личность агента (SOUL), безопасность, подписка + +### Matrix ([подробнее](docs/matrix-prototype.md)) + +- **Чаты** — Space + комнаты: бот создаёт личное пространство, каждый чат = комната +- **Аутентификация** — привязка Matrix аккаунта к аккаунту платформы +- **Диалог** — typing, файлы, подтверждение действий через реакции 👍/❌, треды для долгих задач +- **Настройки** — отдельная комната «Настройки» с командами `!connectors`, `!skills`, `!soul`, `!safety`, `!status` + +--- + +## Замена SDK + +Вся работа с платформой идёт через `PlatformClient` Protocol: + +```python +class PlatformClient(Protocol): + async def get_or_create_user(...) -> User: ... + async def create_session(...) -> Session: ... + async def send_message(...) -> AgentResponse: ... + async def close_session(...) -> None: ... + async def get_settings(...) -> UserSettings: ... + async def update_settings(...) -> None: ... +``` + +Сейчас: `MockPlatformClient` в `platform/mock.py`. +Когда SDK готов: добавляем `SdkPlatformClient`, меняем одну строку в DI. Адаптеры и ядро не трогаем. + +--- + +## Быстрый старт + +```bash +# Зависимости +uv sync # или: pip install -e ".[dev]" + +# Тесты +pytest tests/ -v + +# Запустить Telegram бота +cp .env.example .env # заполнить TELEGRAM_BOT_TOKEN +python -m adapter.telegram.bot + +# Запустить Matrix бота +cp .env.example .env # заполнить MATRIX_* переменные +python -m adapter.matrix.bot +``` + +--- + +## Документация + +| Файл | Содержание | +|---|---| +| [`docs/surface-protocol.md`](docs/surface-protocol.md) | Унификация поверхностей — все структуры, как добавить новую поверхность | +| [`docs/telegram-prototype.md`](docs/telegram-prototype.md) | Функционал Telegram прототипа | +| [`docs/matrix-prototype.md`](docs/matrix-prototype.md) | Функционал Matrix прототипа | +| [`docs/api-contract.md`](docs/api-contract.md) | Контракт к SDK платформы | +| [`docs/user-flow.md`](docs/user-flow.md) | FSM и user journey | +| [`docs/claude-code-guide.md`](docs/claude-code-guide.md) | Гайд по работе с Claude Code | + +--- + +## Команда + +Поверхности и интеграции — Путиловский Михаил +Lambda Lab 3.0, МАИ diff --git a/docs/api-contract.md b/docs/api-contract.md new file mode 100644 index 0000000..a578f08 --- /dev/null +++ b/docs/api-contract.md @@ -0,0 +1,150 @@ +# API Contract — Lambda Platform + +> **Статус:** ЧЕРНОВИК — проектируем сами, не ждём SDK +> **Автор:** @architect +> **Последнее обновление:** заполнить дату + +Это описание того, что нам нужно от платформы. +`MockPlatformClient` реализует этот контракт. При подключении реального SDK — только он меняется. + +--- + +## Base URL + +``` +https://api.lambda-platform.io/v1 +``` + +## Аутентификация + +``` +Authorization: Bearer {SERVICE_TOKEN} +``` + +Сервисный токен выдаётся команде поверхностей. Не путать с токеном пользователя. + +--- + +## Users + +### GET /users/{external_id}?platform={platform} +Получает или создаёт пользователя. + +**Query params:** +- `platform` — `telegram` | `matrix` + +**Response 200:** +```json +{ + "user_id": "usr_abc123", + "external_id": "12345678", + "platform": "telegram", + "display_name": "Иван Иванов", + "created_at": "2025-01-15T10:30:00Z", + "is_new": false +} +``` + +--- + +## Sessions + +### POST /sessions +Создаёт новую сессию с AI-агентом. + +**Request:** +```json +{ + "user_id": "usr_abc123", + "platform": "telegram", + "context": {} +} +``` + +**Response 201:** +```json +{ + "session_id": "ses_xyz789", + "agent_id": "agt_def456", + "created_at": "2025-01-15T10:30:00Z", + "expires_at": "2025-01-16T10:30:00Z" +} +``` + +### GET /sessions/{session_id} +Получает информацию о сессии. + +**Response 200:** — см. структуру выше + поле `status: "active" | "closed"` +**Response 404:** `{"error": "SESSION_NOT_FOUND", "message": "..."}` + +### DELETE /sessions/{session_id} +Завершает сессию. + +**Response 200:** +```json +{"closed": true} +``` + +--- + +## Messages + +### POST /sessions/{session_id}/messages +Отправляет сообщение и получает ответ агента. + +**Request:** +```json +{ + "text": "Привет, что ты умеешь?", + "attachments": [] +} +``` + +**Response 200:** +```json +{ + "message_id": "msg_qwe012", + "response": "Я AI-агент Lambda...", + "tokens_used": 142, + "finished": true +} +``` + +### GET /sessions/{session_id}/messages?limit=20&offset=0 +История сообщений сессии. + +**Response 200:** +```json +[ + { + "message_id": "msg_qwe012", + "user_text": "Привет", + "response": "Привет!", + "tokens_used": 42, + "created_at": "2025-01-15T10:31:00Z" + } +] +``` + +--- + +## Error format + +Все ошибки возвращаются в едином формате: + +```json +{ + "error": "ERROR_CODE", + "message": "Human readable description", + "details": {} +} +``` + +Коды ошибок: `SESSION_NOT_FOUND`, `USER_NOT_FOUND`, `RATE_LIMITED`, `PLATFORM_ERROR` + +--- + +## TODO (открытые вопросы к команде платформы) + +- [ ] Нужна ли стриминговая передача ответа (SSE / WebSocket)? +- [ ] Как обрабатываются вложения (изображения, файлы)? \ No newline at end of file diff --git a/docs/architecture/.gitkeep b/docs/architecture/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/claude-code-guide.md b/docs/claude-code-guide.md new file mode 100644 index 0000000..472ca9f --- /dev/null +++ b/docs/claude-code-guide.md @@ -0,0 +1,292 @@ +# Гайд по Claude Code для разработки surfaces-bot + +## Что такое Claude Code + +Claude Code — это CLI-инструмент который запускается в терминале внутри твоего +проекта. Он читает файлы, пишет код, запускает команды. Главное отличие от +чата — он видит весь проект и действует автономно. + +Запускается так: +```bash +cd surfaces-bot +claude "твоя задача" +``` + +--- + +## Агенты и когда их использовать + +В проекте настроены специализированные агенты в `.claude/agents/`. +Claude Code сам выбирает нужного если ты пишешь `@имя` в запросе. + +| Агент | Когда | Пример | +|---|---|---| +| `@researcher` | Нужно разобраться как что-то работает | "как работает Forum Topics в aiogram?" | +| `@architect` | Нужно принять архитектурное решение | "как хранить маппинг chat_id → room_id?" | +| `@core-developer` | Писать core/ и platform/ | "реализуй core/protocol.py" | +| `@tg-developer` | Писать Telegram-адаптер | "реализуй обработчик /start" | +| `@matrix-developer` | Писать Matrix-адаптер | "реализуй создание Space" | +| `@reviewer` | Проверить код перед PR | "проверь adapter/telegram/" | + +--- + +## Базовые команды + +### Простая задача (без агента) +```bash +# Небольшое изменение — быстрее без агента +claude "добавь поле platform_user_id в ChatContext в core/protocol.py" +``` + +### Задача через агента +```bash +# Агент читает нужную документацию сам +claude "Use @tg-developer to implement adapter/telegram/handlers/auth.py +based on docs/telegram-prototype.md auth flow" +``` + +### Вопрос без изменений +```bash +claude "объясни как работает m.space.child в Matrix" +# или просто интерактивно: +claude # запускает интерактивный режим +``` + +--- + +## Параллельная разработка двух ботов + +Главная идея: два терминала, два worktree, два агента одновременно. + +### Шаг 1 — инициализация (один раз) +```bash +cd surfaces-bot +git init +git add . && git commit -m "init" + +# Создать worktrees для адаптеров +git worktree add .worktrees/telegram -b feat/telegram-adapter +git worktree add .worktrees/matrix -b feat/matrix-adapter +``` + +### Шаг 2 — сначала ядро (в main) +```bash +# В основной папке — core/ должен быть готов до адаптеров +claude "Use @core-developer to implement core/protocol.py and platform/interface.py +and platform/mock.py based on docs/surface-protocol.md and docs/api-contract.md" +``` +Дождись — это фундамент. Остальное на нём строится. + +### Шаг 3 — параллельно адаптеры (два терминала) + +**Терминал 1:** +```bash +cd .worktrees/telegram +claude "Use @tg-developer to implement the auth flow from docs/telegram-prototype.md. +Start with adapter/telegram/handlers/auth.py and adapter/telegram/states.py" +``` + +**Терминал 2 (одновременно):** +```bash +cd .worktrees/matrix +claude "Use @matrix-developer to implement the auth flow from docs/matrix-prototype.md. +Start with adapter/matrix/handlers/auth.py and adapter/matrix/space.py" +``` + +Они работают в разных папках — не мешают друг другу. + +### Шаг 4 — смёрджить когда готово +```bash +cd surfaces-bot # вернуться в main +git merge feat/telegram-adapter +git merge feat/matrix-adapter +``` + +--- + +## Экономия токенов (важно для Pro) + +### Правило 1: Haiku для исследований +```bash +# @researcher использует Haiku — дёшево +claude "Use @researcher to find examples of aiogram Forum Topics creation with code samples. +Save findings to docs/research/telegram-forum-topics.md" + +# НЕ делай так (Sonnet на простой вопрос — дорого): +claude "Use @tg-developer to research how Forum Topics work" +``` + +### Правило 2: точный контекст +```bash +# Хорошо — агент читает один конкретный файл +claude "Use @tg-developer to implement adapter/telegram/keyboards/settings.py. +Read docs/telegram-prototype.md section 'Настройки' for the button structure" + +# Плохо — агент читает весь проект зря +claude "Use @tg-developer to implement keyboards" +# (агент начнёт читать всё подряд) +``` + +### Правило 3: не запускай ревью на каждый коммит +```bash +# Ревью — только перед merge в main +claude "Use @reviewer to review all changes in adapter/telegram/ before merging" +# НЕ после каждого файла +``` + +### Правило 4: маленькие задачи без агента +```bash +# Просто правка — без агента быстрее и дешевле +claude "в adapter/telegram/keyboards/confirm.py замени текст кнопки с 'Да' на '✅ Подтвердить'" +``` + +### Правило 5: не больше двух Sonnet одновременно +```bash +# Можно (Haiku + Sonnet): +# Терминал 1: claude "Use @researcher ..." ← Haiku +# Терминал 2: claude "Use @tg-developer ..." ← Sonnet + +# Можно (два Sonnet в разных worktrees): +# Терминал 1: cd .worktrees/telegram && claude "Use @tg-developer ..." +# Терминал 2: cd .worktrees/matrix && claude "Use @matrix-developer ..." + +# Слишком много (три Sonnet): +# Терминал 1: claude "Use @core-developer ..." +# Терминал 2: claude "Use @tg-developer ..." +# Терминал 3: claude "Use @matrix-developer ..." ← стоп, подожди +``` + +--- + +## Типичные сценарии с примерами + +### Сценарий А: начало с нуля + +```bash +# 1. Исследование (Haiku, дёшево) +claude "Use @researcher to find matrix-nio examples of Space creation and room management. +Save to docs/research/matrix-spaces.md" + +# 2. Уточнение архитектуры (если что-то неясно) +claude "Use @architect to clarify: should ConfirmationRequest timeout be handled in +core/handler.py or in each adapter separately? Update docs/surface-protocol.md" + +# 3. Ядро +claude "Use @core-developer to implement core/protocol.py based on docs/surface-protocol.md" +claude "Use @core-developer to implement core/session.py and core/auth.py" +claude "Use @core-developer to implement platform/interface.py and platform/mock.py +based on docs/api-contract.md" + +# 4. Адаптеры параллельно (два терминала) +# T1: claude "Use @tg-developer to implement auth flow in adapter/telegram/" +# T2: claude "Use @matrix-developer to implement auth flow in adapter/matrix/" +``` + +### Сценарий Б: добавить новую фичу в оба бота + +Например, команда `!status` / `/status`: + +```bash +# 1. Убедиться что в core есть нужные структуры +claude "Use @architect to check if StatusRequest is needed in core/protocol.py +or can reuse existing structures. Read docs/surface-protocol.md" + +# 2. Добавить в core если нужно +claude "Use @core-developer to add status handling in core/handler.py" + +# 3. Оба адаптера параллельно +# T1: claude "Use @tg-developer to implement /status command in +# adapter/telegram/handlers/settings.py" +# T2: claude "Use @matrix-developer to implement !status command in +# adapter/matrix/handlers/settings.py" +``` + +### Сценарий В: что-то сломалось + +```bash +# Диагностика — без агента, просто спроси +claude "pytest tests/adapter/telegram/test_auth.py провалился с ошибкой: +AttributeError: 'NoneType' has no attribute 'session_id' +Посмотри core/session.py и adapter/telegram/handlers/auth.py и скажи в чём причина" + +# Починить точечно +claude "в core/session.py метод get_session возвращает None если сессия не найдена, +но в adapter/telegram/handlers/auth.py это не обрабатывается — исправь" +``` + +### Сценарий Г: ревью перед merge + +```bash +# Запускать только когда фича готова +cd .worktrees/telegram +claude "Use @reviewer to review all files changed in this worktree (adapter/telegram/). +Check against docs/telegram-prototype.md and docs/surface-protocol.md" +``` + +--- + +## Интерактивный режим + +Когда нужно работать итеративно — запусти без задачи: + +```bash +cd surfaces-bot +claude +``` + +Откроется REPL. Можно: +``` +> покажи структуру core/protocol.py +> добавь поле metadata в IncomingMessage +> запусти pytest и покажи результат +> что изменилось в последних 3 коммитах? +``` + +Полезно когда задача неясная или нужно исследовать. + +--- + +## Флаги которые пригодятся + +```bash +# Работать в конкретном worktree +cd .worktrees/telegram && claude "..." + +# Не трогать файлы, только смотреть (безопасно) +claude --no-write "объясни как работает converter.py" + +# Продолжить предыдущую сессию +claude --continue + +# Запустить с конкретной моделью (если хочешь Haiku без @researcher) +claude --model claude-haiku-4-5-20251001 "быстрый вопрос про aiogram" +``` + +--- + +## Частые ошибки + +**Агент начал читать весь проект** → прерви (Ctrl+C), переформулируй точнее: +```bash +# Плохо +claude "Use @tg-developer to implement settings" + +# Хорошо +claude "Use @tg-developer to implement adapter/telegram/keyboards/settings.py — +кнопки главного меню настроек из docs/telegram-prototype.md раздел 'Главное меню настроек'" +``` + +**Агент пишет не в ту папку** → явно укажи путь: +```bash +claude "Use @matrix-developer to create adapter/matrix/space.py with SpaceManager class" +``` + +**Кончились токены в середине задачи** → задача сохраняется частично, продолжи: +```bash +claude --continue "продолжи реализацию, остановился на методе create_chat" +``` + +**Агент предлагает архитектурное решение вместо кода** → перенаправь: +```bash +claude "Use @architect to decide: [вопрос]. После решения Use @tg-developer to implement it" +``` diff --git a/docs/matrix-prototype.md b/docs/matrix-prototype.md new file mode 100644 index 0000000..6e9b6bd --- /dev/null +++ b/docs/matrix-prototype.md @@ -0,0 +1,250 @@ +# Matrix — описание прототипа + +## Концепция + +Один бот, каждый чат — отдельная комната, все комнаты собраны в Space. + +При первом входе бот создаёт для пользователя личное пространство (Space) — +это как папка в Element. Внутри Space бот создаёт комнату для каждого нового +чата с агентом. Пользователь видит аккуратную структуру: одно пространство, +внутри — список чатов. История хранится нативно в Matrix — это часть протокола, +ничего дополнительно делать не нужно. + +Matrix выбран как внутренняя поверхность: команды лаборатории, тестировщики, +разработчики скиллов. Поэтому UX здесь — про удобство работы, а не онбординг. + +--- + +## Аутентификация + +### Флоу +1. Пользователь приглашает бота в личные сообщения или пишет в общей комнате +2. Бот проверяет `@user:matrix.org` — есть ли аккаунт на платформе +3. Если нет — бот отправляет одноразовый код или ссылку +4. Пользователь подтверждает, платформа возвращает токен +5. Бот сохраняет привязку `matrix_user_id → platform_user_id` + +### В моке +- Любой пользователь проходит аутентификацию автоматически +- Бот отвечает: «Добро пожаловать, {display_name}. Создаю ваше пространство...» +- Демонстрирует флоу без реальной платформы + +--- + +## Чаты через Space + комнаты (вариант Б) + +### Структура +``` +Space: «Lambda — {display_name}» + ├── 📌 Настройки ← специальная комната для команд управления + ├── 💬 Чат 1 ← первый чат, создаётся автоматически + ├── 💬 Чат 2 + └── 💬 Исследование рынка ← пользователь сам называет +``` + +### Создание Space +При первом входе бот: +1. Создаёт Space `Lambda — {display_name}` +2. Создаёт комнату `Настройки` (закреплена вверху) +3. Создаёт первую комнату-чат `Чат 1` +4. Приглашает пользователя во все комнаты +5. Пишет в `Чат 1` приветствие + +### Управление чатами +Команды работают в любой комнате Space: + +| Команда | Действие | +|---|---| +| `!new` | Создать новый чат (новую комнату в Space) | +| `!new Название` | Создать чат с именем | +| `!rename Название` | Переименовать текущую комнату | +| `!archive` | Вывести комнату из Space (не удалять) | +| `!chats` | Показать список чатов | + +### Создание нового чата +1. Пользователь пишет `!new` или `!new Анализ конкурентов` +2. Бот создаёт новую комнату в Space +3. Приглашает пользователя +4. Пишет приветствие и создаёт сессию на платформе +5. Пользователь переходит в новую комнату — начинает диалог + +### В моке +- Space и комнаты создаются реально через matrix-nio +- Сессии — через MockPlatformClient +- История хранится в Matrix нативно + +--- + +## Основной диалог + +### Флоу сообщения +1. Пользователь пишет текст в комнату-чат +2. Бот показывает typing (m.typing event) +3. Запрос уходит в платформу (MockPlatformClient) +4. Бот отвечает в той же комнате + +### Вложения +- Файлы, изображения отправляются как Matrix media events +- Бот принимает `m.file`, `m.image`, `m.audio` +- Передаёт в платформу как `attachments` через `IncomingMessage` +- В моке: подтверждение получения + заглушка-ответ + +### Реакции как действия +Matrix поддерживает реакции на сообщения (`m.reaction`). +Используем это для подтверждения действий агента: + +``` +Агент: Хочу отправить письмо на vasya@mail.ru + Тема: «Отчёт за неделю» + + 👍 — подтвердить ❌ — отменить +``` + +Пользователь ставит реакцию — бот обрабатывает. Нативно и удобно. + +### Треды для длинных задач +Если агент выполняет долгую задачу (deep research, генерация документа), +бот создаёт тред от своего первого ответа и пишет промежуточные статусы туда. +Основной чат не засоряется. + +``` +Бот: Начинаю исследование по теме «AI агенты 2025» [→ в треде] + └── Ищу источники... (1/4) + └── Анализирую статьи... (2/4) + └── Формирую отчёт... (3/4) + └── Готово. Отчёт: [...] +``` + +--- + +## Комната «Настройки» + +Специальная комната для управления агентом. Закреплена вверху Space. +Команды работают только здесь — не мешают диалогу в чатах. + +### Коннекторы +``` +!connectors — показать список +!connect gmail — подключить Gmail (OAuth ссылка) +!connect github — подключить GitHub +!connect calendar — подключить Google Calendar +!connect notion — подключить Notion +!disconnect gmail — отключить +``` + +Статус: +``` +Коннекторы: + ✅ Gmail — подключён (user@gmail.com) + ❌ GitHub — не подключён → !connect github + ❌ Google Calendar — не подключён + ❌ Notion — не подключён +``` + +В моке: OAuth ссылка-заглушка → «Подключено ✓» + +### Скиллы +``` +!skills — показать список +!skill on browser — включить Browser Use +!skill off browser — выключить +``` + +Статус: +``` +Скиллы: + ✅ web-search — поиск в интернете + ✅ fetch-url — чтение веб-страниц + ✅ email — чтение почты (требует Gmail) + ❌ browser — управление браузером + ❌ image-gen — генерация изображений + ❌ video-gen — генерация видео + ✅ files — работа с файлами + ❌ calendar — календарь (требует Google Calendar) +``` + +В моке: состояние хранится локально. + +### Личность агента +``` +!soul — показать текущий SOUL.md +!soul name Лямбда — задать имя агента +!soul style brief — стиль: brief | friendly | formal +!soul priority «разбирать почту утром» — приоритетная задача +!soul reset — сбросить к дефолту +``` + +В моке: SOUL.md генерируется и хранится локально, агент обращается по имени. + +### Безопасность +``` +!safety — показать настройки +!safety on email-send — требовать подтверждение перед отправкой письма +!safety off calendar-create — не спрашивать для создания событий +``` + +Статус: +``` +Подтверждение требуется для: + ✅ отправка письма + ✅ удаление файлов + ✅ публикация в соцсетях + ❌ создание события в календаре + ❌ поиск в интернете +``` + +### Подписка +``` +!plan — показать текущий план +``` + +``` +Подписка: Beta (бесплатно) +Токены этот месяц: 800 / 1000 +━━━━━━━━░░ 80% +``` + +Заглушка, реализует другая команда. + +### Статус и диагностика +``` +!status — состояние агента и платформы +!sessions — список активных сессий +!whoami — текущий аккаунт платформы +``` + +``` +Статус: + Платформа: ✅ доступна + Агент: ✅ активен (сессия #abc123) + Аккаунт: user@lambda.lab + Активных чатов: 3 +``` + +--- + +## FSM состояния + +``` +[Invite] → AuthPending → AuthConfirmed + ↓ + SpaceSetup → Idle (в комнате Настройки) + ↓ + [новая комната] → SessionCreated → Idle (в чате) + ↓ + ReceivingMessage → WaitingResponse → Idle + ↓ + WaitingReaction (confirm) → [✅/❌] → Idle + ↓ + LongTask → [тред со статусами] → Done → Idle +``` + +--- + +## Стек + +- Python 3.11+ +- matrix-nio (async) — Matrix клиент +- MockPlatformClient → `platform/interface.py` +- structlog для логирования +- SQLite для хранения `matrix_user_id → platform_user_id`, состояния скиллов, маппинга `chat_id → room_id` diff --git a/docs/research/.gitkeep b/docs/research/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/surface-protocol.md b/docs/surface-protocol.md new file mode 100644 index 0000000..eb2069b --- /dev/null +++ b/docs/surface-protocol.md @@ -0,0 +1,311 @@ +# Surface Protocol — унификация поверхностей + +## Идея + +Любая поверхность (Telegram, Matrix, будущий Discord, голос, web) делает одно +и то же: принимает события от пользователя и отправляет ответы обратно. + +Разница только в том как событие выглядит снаружи: aiogram `Message`, +matrix-nio `RoomMessageText`, Discord `Interaction`. Внутри — одно и то же. + +Surface Protocol — это общий язык между адаптерами поверхностей и ядром. +Адаптер конвертирует нативные события в протокольные структуры и обратно. +Ядро работает только с протокольными структурами и ничего не знает о транспорте. + +**Добавить новую поверхность = написать один адаптер-конвертер.** +Ядро не трогается. + +--- + +## Структура проекта + +``` +surfaces-bot/ + core/ + protocol.py — все унифицированные структуры данных + handler.py — логика: IncomingEvent → OutgoingEvent + session.py — управление сессиями и чатами + auth.py — AuthFlow + settings.py — SettingsAction, управление настройками + + adapter/ + telegram/ — aiogram адаптер + converter.py — aiogram Event → IncomingEvent, OutgoingEvent → aiogram API + bot.py — точка входа, роутер + matrix/ — matrix-nio адаптер + converter.py — matrix-nio Event → IncomingEvent, OutgoingEvent → Matrix API + bot.py — точка входа, клиент + _template.py — шаблон для новой поверхности + + platform/ + interface.py — Protocol: PlatformClient + mock.py — MockPlatformClient +``` + +--- + +## Входящие события (Incoming) + +Всё что пользователь делает конвертируется в одно из трёх: + +### IncomingMessage +Обычное сообщение — текст, файл, голос. + +```python +@dataclass +class IncomingMessage: + user_id: str # "tg_123456" | "@user:matrix.org" + platform: str # "telegram" | "matrix" + chat_id: str # "C1" | "C2" — ID чата в воркспейсе платформы + text: str + attachments: list[Attachment] + reply_to: str | None # message_id если это ответ на сообщение +``` + +### IncomingCommand +Команда управления — `/start`, `!new`, `/settings`. + +```python +@dataclass +class IncomingCommand: + user_id: str + platform: str + chat_id: str + command: str # "start" | "new" | "settings" | "archive" ... + args: list[str] # аргументы после команды +``` + +### IncomingCallback +Нажатие кнопки или реакции — подтверждение, переключение скилла. + +```python +@dataclass +class IncomingCallback: + user_id: str + platform: str + chat_id: str + action: str # "confirm" | "cancel" | "toggle_skill" | "connect" ... + payload: dict # {"skill": "browser"} | {"action_id": "abc123"} ... +``` + +### Attachment +Вложение, нормализованное из любого источника. + +```python +@dataclass +class Attachment: + type: str # "image" | "document" | "audio" | "video" + url: str | None # ссылка если доступна + content: bytes | None # содержимое если скачано + filename: str | None + mime_type: str | None +``` + +--- + +## Исходящие события (Outgoing) + +Всё что ядро хочет показать пользователю: + +### OutgoingMessage +Ответ агента или системное сообщение. + +```python +@dataclass +class OutgoingMessage: + chat_id: str + text: str + parse_mode: str # "markdown" | "plain" + attachments: list[Attachment] + reply_to: str | None +``` + +### OutgoingUI +Интерактивные элементы — кнопки, меню, переключатели. + +```python +@dataclass +class OutgoingUI: + chat_id: str + text: str + buttons: list[UIButton] + +@dataclass +class UIButton: + label: str + action: str # action для IncomingCallback + payload: dict + style: str # "primary" | "danger" | "secondary" +``` + +Telegram рендерит это как InlineKeyboard. +Matrix рендерит как текст с описанием реакций или HTML-кнопки. + +### OutgoingNotification +Асинхронное уведомление — агент закончил долгую задачу. + +```python +@dataclass +class OutgoingNotification: + chat_id: str + text: str + level: str # "info" | "warning" | "success" | "error" +``` + +### OutgoingTyping +Индикатор что агент думает. + +```python +@dataclass +class OutgoingTyping: + chat_id: str + is_typing: bool +``` + +--- + +## Жизненный цикл (Lifecycle) + +Унифицированные события для управления чатами и подключением. + +### ChatContext +Состояние чата — общее для всех поверхностей. + +```python +@dataclass +class ChatContext: + chat_id: str # "C1" | "C2" — ID в воркспейсе платформы + display_name: str # «Чат 1» | «Анализ рынка» + platform: str + surface_ref: str # room_id в Matrix | topic_id в Telegram + session_id: str | None # активная сессия платформы + created_at: datetime + is_archived: bool +``` + +### AuthFlow +Флоу аутентификации — одинаков для всех поверхностей. + +```python +@dataclass +class AuthFlow: + user_id: str + platform: str + state: str # "pending" | "code_sent" | "confirmed" | "failed" + platform_user_id: str | None +``` + +### ConfirmationRequest +Запрос подтверждения опасного действия — от агента к пользователю. + +```python +@dataclass +class ConfirmationRequest: + action_id: str + chat_id: str + description: str # «Отправить письмо на vasya@mail.ru» + risk_level: str # "low" | "medium" | "high" + expires_at: datetime +``` + +Telegram показывает как Inline-кнопки. +Matrix показывает как реакции 👍 / ❌. +Ядро не знает как именно — только получает `IncomingCallback` с `action: "confirm"`. + +--- + +## SettingsAction + +Унифицированные действия в настройках — одинаковы для Telegram и Matrix. + +```python +@dataclass +class SettingsAction: + action: str # "connect" | "disconnect" | "toggle_skill" | ... + payload: dict + +# Примеры: +SettingsAction(action="connect", payload={"service": "gmail"}) +SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True}) +SettingsAction(action="set_soul", payload={"field": "name", "value": "Лямбда"}) +SettingsAction(action="set_safety", payload={"trigger": "email-send", "enabled": True}) +``` + +Ядро обрабатывает `SettingsAction` одинаково. Откуда пришло — не важно. + +--- + +## PaymentEvent + +Заглушка для биллинга — реализует другая команда. + +```python +@dataclass +class PaymentRequired: + user_id: str + reason: str # "limit_reached" | "feature_locked" + current_plan: str +``` + +Поверхность получила `PaymentRequired` → показала заглушку. +Содержимое заглушки потом наполняет команда биллинга. + +--- + +## Как написать новую поверхность + +Скопируй `adapter/_template.py` и реализуй три метода: + +```python +class MySurfaceAdapter: + + def parse_incoming(self, raw_event) -> IncomingMessage | IncomingCommand | IncomingCallback: + """Конвертировать нативное событие в протокольную структуру.""" + ... + + def render_outgoing(self, event: OutgoingMessage | OutgoingUI | OutgoingNotification) -> any: + """Конвертировать протокольную структуру в нативный формат.""" + ... + + async def send(self, rendered) -> None: + """Отправить нативным способом.""" + ... +``` + +Дальше подключи адаптер в точку входа и передай `core.handler.handle` как callback. +Всё остальное уже работает. + +--- + +## Что ядро даёт бесплатно + +Любая новая поверхность получает без дополнительного кода: + +- управление сессиями (создание, переключение, закрытие) +- управление чатами (`ChatContext`) +- аутентификацию (`AuthFlow`) +- подтверждение действий (`ConfirmationRequest`) +- все настройки (коннекторы, скиллы, SOUL, безопасность, подписка) +- интеграцию с платформой через `PlatformClient` +- обработку ошибок платформы + +--- + +## Замена MockPlatformClient на реальный SDK + +Вся работа с платформой идёт через `PlatformClient` протокол: + +```python +class PlatformClient(Protocol): + async def get_or_create_user(self, user_id: str, platform: str) -> User: ... + async def create_session(self, user_id: str, chat_id: str) -> Session: ... + async def send_message(self, session_id: str, text: str, attachments: list) -> AgentResponse: ... + async def close_session(self, session_id: str) -> None: ... + async def get_chat_history(self, user_id: str, chat_id: str) -> list[Message]: ... + async def get_settings(self, user_id: str) -> UserSettings: ... + async def update_settings(self, user_id: str, action: SettingsAction) -> None: ... +``` + +`MockPlatformClient` реализует этот протокол сейчас. +Реальный SDK — тоже реализует этот протокол, заменяя один файл. +Адаптеры поверхностей и ядро не меняются вообще. diff --git a/docs/telegram-prototype.md b/docs/telegram-prototype.md new file mode 100644 index 0000000..e867f99 --- /dev/null +++ b/docs/telegram-prototype.md @@ -0,0 +1,221 @@ +# Telegram — описание прототипа + +## Концепция + +Один бот, несколько чатов через Topics в Forum-группе. + +При первом запуске бот создаёт для пользователя персональную Forum-группу +(супергруппу с включёнными темами). Каждый новый чат с агентом — отдельная тема +внутри группы. Пользователь видит это как список чатов в одном месте. + +Бот управляет группой от имени пользователя через Telegram Bot API: +создаёт темы, переименовывает, архивирует. + +--- + +## Аутентификация + +### Флоу +1. Пользователь пишет боту `/start` +2. Бот проверяет — есть ли аккаунт на платформе привязанный к этому `tg_user_id` +3. Если нет — бот отправляет одноразовую ссылку или код для входа +4. Пользователь подтверждает, платформа возвращает токен сессии +5. Бот сохраняет привязку `tg_user_id → platform_user_id` + +### В моке +- Любой пользователь проходит аутентификацию автоматически +- Кнопка «Войти» → пауза 1 сек → «Вы вошли как {имя}» +- Демонстрирует флоу без реальной платформы + +--- + +## Чаты через Forum Topics (вариант В) + +### Как это работает +- Бот создаёт супергруппу с Topics для каждого нового пользователя +- Каждый чат = отдельная тема (Topic) в этой группе +- История хранится нативно в Telegram (в самой теме) +- Переключение между чатами = переключение между темами + +### Управление чатами +Внутри каждой темы доступны команды: + +| Команда | Действие | +|---|---| +| `/new` | Создать новый чат (новую тему) | +| `/rename Название` | Переименовать текущий чат | +| `/archive` | Архивировать текущий чат | +| `/chats` | Показать список всех чатов | + +### Создание нового чата +1. Пользователь пишет `/new` или нажимает кнопку +2. Бот спрашивает название (опционально, можно пропустить) +3. Бот создаёт новую тему в группе: «Чат 1», «Чат 2» и т.д. +4. Бот отправляет в новую тему приветствие и создаёт сессию на платформе + +### В моке +- Группа и темы создаются реально через Bot API +- Сессии на платформе — через MockPlatformClient +- История в темах хранится нативно в Telegram, ничего не нужно делать + +--- + +## Основной диалог + +### Флоу сообщения +1. Пользователь пишет текст в тему +2. Бот показывает `typing...` +3. Запрос уходит в платформу (сейчас — MockPlatformClient) +4. Бот отвечает текстом агента + +### Вложения +- Фото, документы, голосовые — передаются в платформу как `attachments` +- В моке: бот подтверждает получение файла и возвращает заглушку-ответ +- Форматы: PDF, изображения, текстовые файлы + +### Подтверждение действий +Если агент собирается выполнить потенциально опасное действие +(отправить письмо, удалить файл, сделать запись в календарь): + +``` +Агент хочет выполнить действие: +📧 Отправить письмо на vasya@mail.ru +Тема: «Отчёт за неделю» + +[✅ Подтвердить] [❌ Отменить] +``` + +Пользователь нажимает кнопку — действие выполняется или отменяется. +В моке: кнопки работают, действие логируется, ответ-заглушка. + +--- + +## Настройки + +Доступны через `/settings` в любой теме или в главном меню бота. +Реализованы как цепочка инлайн-кнопок. + +### Главное меню настроек +``` +⚙️ Настройки + +[🔗 Коннекторы] [🧩 Скиллы] +[🧠 Личность] [🔔 Уведомления] +[🔒 Безопасность] [💳 Подписка] +``` + +### Коннекторы +Подключение внешних сервисов. Агент получает доступ через API Proxy, +пароли пользователю вводить не нужно — только OAuth. + +| Коннектор | Способ подключения | +|---|---| +| Gmail / Outlook | OAuth ссылка | +| Google Calendar | OAuth ссылка | +| GitHub | OAuth ссылка | +| Notion | OAuth ссылка | +| Telegram (читать каналы) | Уже подключён | + +В моке: кнопка «Подключить» → ссылка-заглушка → «Подключено ✓» + +### Скиллы +Включение/выключение навыков агента. + +``` +🧩 Скиллы + +✅ Поиск в интернете +✅ Чтение почты +❌ Управление браузером +❌ Генерация изображений +❌ Работа с календарём +✅ Работа с файлами +❌ Генерация видео +``` + +Каждый скилл — кнопка-переключатель. Нажал — включил/выключил. +В моке: состояние хранится локально, платформа не вызывается. + +### Личность агента (SOUL.md) +Онбординг-анкета при первом входе, потом редактирование. + +``` +🧠 Личность агента + +Как зовут твоего агента? +[_Лямбда_____________] + +Что он должен делать в первую очередь? +[_Разбирать почту_____] + +Стиль общения: +[Деловой] [Дружелюбный] [Краткий] +``` + +В моке: данные сохраняются, агент обращается к пользователю по имени из настроек. + +### Уведомления +Когда агент выполнил долгую задачу — уведомление в Telegram. + +``` +🔔 Уведомления + +✅ Задача выполнена +✅ Требуется подтверждение действия +❌ Ежедневный дайджест +❌ Напоминания из календаря +``` + +В моке: уведомления отправляются через бот с задержкой (симуляция). + +### Безопасность +Какие действия требуют явного подтверждения. + +``` +🔒 Безопасность + +Всегда спрашивать перед: +✅ Отправкой письма +✅ Удалением файлов +✅ Публикацией в соцсетях +❌ Созданием события в календаре +❌ Поиском в интернете +``` + +### Подписка +Заглушка — реализует другая команда. + +``` +💳 Подписка + +Текущий план: Beta (бесплатно) +Токены: ████████░░ 800/1000 + +[Подробнее о планах] +``` + +--- + +## FSM состояния + +``` +[Start] → AuthPending → AuthConfirmed + ↓ + GroupSetup → Idle + ↓ + ReceivingMessage → WaitingResponse → Idle + ↓ + ConfirmAction → [Confirmed/Cancelled] → Idle + ↓ + Settings → [подменю] → Idle +``` + +--- + +## Стек + +- Python 3.11+ +- aiogram 3.x (Router, FSM, InlineKeyboard, Forum Topics API) +- MockPlatformClient → `platform/interface.py` +- structlog для логирования +- SQLite для хранения `tg_user_id → platform_user_id` и состояния скиллов diff --git a/docs/user-flow.md b/docs/user-flow.md new file mode 100644 index 0000000..30fa15c --- /dev/null +++ b/docs/user-flow.md @@ -0,0 +1,68 @@ +# User Flow — Lambda Bot + +> **Статус:** ШАБЛОН — заполняет @architect после исследований +> **Зависит от:** docs/research/telegram-flows.md, docs/research/competitor-ux.md + +--- + +## Основной сценарий (happy path) + +```mermaid +sequenceDiagram + actor User + participant Bot as Telegram/Matrix Bot + participant Platform as Lambda Platform + + User->>Bot: /start + Bot->>Platform: GET /users/{tg_id}?platform=telegram + Platform-->>Bot: {user_id, is_new} + + alt Новый пользователь + Bot->>User: Приветствие + инструкция + else Существующий пользователь + Bot->>User: Добро пожаловать обратно + end + + User->>Bot: Любое сообщение + Bot->>Platform: POST /sessions (создаём сессию) + Platform-->>Bot: {session_id, agent_id} + + loop Диалог + User->>Bot: Сообщение + Bot->>Platform: POST /sessions/{id}/messages + Platform-->>Bot: {response} + Bot->>User: Ответ агента + end + + User->>Bot: /end или таймаут + Bot->>Platform: DELETE /sessions/{id} + Bot->>User: Сессия завершена +``` + +--- + +## Состояния FSM (Telegram) + +```mermaid +stateDiagram-v2 + [*] --> Idle: /start + + Idle --> InSession: любое сообщение + InSession --> InSession: сообщение пользователя + InSession --> Idle: /end + + InSession --> Error: ошибка платформы + Error --> Idle: /start + Error --> InSession: retry +``` + +--- + +## Открытые вопросы + +> Заполняет @researcher и @architect после исследований + +- [ ] Как выглядит онбординг новых пользователей у конкурентов? +- [ ] Нужна ли кнопка "Новая сессия" или сессия стартует автоматически? +- [ ] Что показываем пока агент думает (typing indicator)? +- [ ] Как обрабатываем timeout ответа от платформы? diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8ad9f92 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.backends.legacy:build" + +[project] +name = "surfaces-bot" +version = "0.1.0" +description = "Lambda Lab 3.0 — Telegram и Matrix боты" +requires-python = ">=3.11" + +dependencies = [ + "aiogram>=3.4,<4", + "matrix-nio>=0.21", + "pydantic>=2.5", + "structlog>=24.1", + "python-dotenv>=1.0", + "httpx>=0.27", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-cov>=4.1", + "ruff>=0.3", + "mypy>=1.8", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B"] diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/matrix_bot/__init__.py b/src/matrix_bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mock_platform.py b/src/mock_platform.py new file mode 100644 index 0000000..90bcafb --- /dev/null +++ b/src/mock_platform.py @@ -0,0 +1,246 @@ +""" +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()), + } diff --git a/src/shared/__init__.py b/src/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/models.py b/src/shared/models.py new file mode 100644 index 0000000..508f948 --- /dev/null +++ b/src/shared/models.py @@ -0,0 +1,37 @@ +"""Общие Pydantic модели для Telegram и Matrix ботов.""" + +from __future__ import annotations + +from datetime import datetime +from enum import Enum +from typing import Any + +from pydantic import BaseModel, Field + + +class Platform(str, Enum): + TELEGRAM = "telegram" + MATRIX = "matrix" + + +class Session(BaseModel): + session_id: str + agent_id: str + created_at: datetime + expires_at: datetime + + +class User(BaseModel): + user_id: str + external_id: str + platform: Platform + display_name: str | None = None + created_at: datetime + is_new: bool = False + + +class MessageResponse(BaseModel): + message_id: str + response: str + tokens_used: int + finished: bool diff --git a/src/telegram_bot/__init__.py b/src/telegram_bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_mock_platform.py b/tests/test_mock_platform.py new file mode 100644 index 0000000..2ef3ee6 --- /dev/null +++ b/tests/test_mock_platform.py @@ -0,0 +1,71 @@ +"""Тесты для MockPlatformClient — проверяем что заглушка работает корректно.""" + +import pytest +from src.mock_platform import MockPlatformClient, SessionNotFoundError + + +@pytest.fixture +def client(): + return MockPlatformClient() + + +@pytest.mark.asyncio +async def test_create_session_returns_ids(client): + result = await client.create_session(user_id="tg-123", platform="telegram") + + assert "session_id" in result + assert "agent_id" in result + assert "expires_at" in result + + +@pytest.mark.asyncio +async def test_send_message_returns_response(client): + session = await client.create_session(user_id="tg-123", platform="telegram") + result = await client.send_message(session["session_id"], "Привет!") + + assert "response" in result + assert len(result["response"]) > 0 + assert result["finished"] is True + + +@pytest.mark.asyncio +async def test_get_session_not_found_raises(client): + with pytest.raises(SessionNotFoundError): + await client.get_session("non-existent-id") + + +@pytest.mark.asyncio +async def test_close_session(client): + session = await client.create_session(user_id="tg-123", platform="telegram") + result = await client.close_session(session["session_id"]) + + assert result is True + + # Повторное закрытие — уже закрыта + result2 = await client.close_session(session["session_id"]) + assert result2 is False + + +@pytest.mark.asyncio +async def test_get_or_create_user(client): + user = await client.get_or_create_user( + external_id="12345", + platform="telegram", + display_name="Test User", + ) + + assert user["user_id"].startswith("user-telegram-") + assert user["external_id"] == "12345" + assert user["platform"] == "telegram" + + +@pytest.mark.asyncio +async def test_message_history(client): + session = await client.create_session(user_id="tg-123", platform="telegram") + sid = session["session_id"] + + await client.send_message(sid, "Первое сообщение") + await client.send_message(sid, "Второе сообщение") + + history = await client.get_message_history(sid, limit=10) + assert len(history) == 2