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>
This commit is contained in:
commit
b6df29bd9b
29 changed files with 2504 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
47
.claude/agents/architect.md
Normal file
47
.claude/agents/architect.md
Normal file
|
|
@ -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
|
||||||
|
## Решение: [название]
|
||||||
|
**Контекст:** почему возник вопрос
|
||||||
|
**Решение:** что именно делаем
|
||||||
|
**Причина:** почему именно так
|
||||||
|
**Влияние:** какие файлы меняются
|
||||||
|
```
|
||||||
61
.claude/agents/core-developer.md
Normal file
61
.claude/agents/core-developer.md
Normal file
|
|
@ -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/`
|
||||||
|
- Не принимай архитектурные решения молча — если что-то непонятно в протоколе,
|
||||||
|
пиши вопрос в конце своего ответа
|
||||||
68
.claude/agents/developer.md
Normal file
68
.claude/agents/developer.md
Normal file
|
|
@ -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
|
||||||
|
...
|
||||||
|
```
|
||||||
126
.claude/agents/matrix-developer.md
Normal file
126
.claude/agents/matrix-developer.md
Normal file
|
|
@ -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`.
|
||||||
33
.claude/agents/researcher.md
Normal file
33
.claude/agents/researcher.md
Normal file
|
|
@ -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)
|
||||||
|
```
|
||||||
54
.claude/agents/reviewer.md
Normal file
54
.claude/agents/reviewer.md
Normal file
|
|
@ -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 — нет теста на недоступность платформы
|
||||||
|
```
|
||||||
91
.claude/agents/tg-developer.md
Normal file
91
.claude/agents/tg-developer.md
Normal file
|
|
@ -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`.
|
||||||
14
.env.example
Normal file
14
.env.example
Normal file
|
|
@ -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
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -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
|
||||||
172
CLAUDE.md
Normal file
172
CLAUDE.md
Normal file
|
|
@ -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, не после каждого коммита
|
||||||
|
- Длинный контекст → дай агенту конкретный файл, не весь проект
|
||||||
|
- Если агент "завис" в рассуждениях → прерви, переформулируй задачу точнее
|
||||||
127
README.md
Normal file
127
README.md
Normal file
|
|
@ -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, МАИ
|
||||||
150
docs/api-contract.md
Normal file
150
docs/api-contract.md
Normal file
|
|
@ -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)?
|
||||||
|
- [ ] Как обрабатываются вложения (изображения, файлы)?
|
||||||
0
docs/architecture/.gitkeep
Normal file
0
docs/architecture/.gitkeep
Normal file
292
docs/claude-code-guide.md
Normal file
292
docs/claude-code-guide.md
Normal file
|
|
@ -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"
|
||||||
|
```
|
||||||
250
docs/matrix-prototype.md
Normal file
250
docs/matrix-prototype.md
Normal file
|
|
@ -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`
|
||||||
0
docs/research/.gitkeep
Normal file
0
docs/research/.gitkeep
Normal file
311
docs/surface-protocol.md
Normal file
311
docs/surface-protocol.md
Normal file
|
|
@ -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 — тоже реализует этот протокол, заменяя один файл.
|
||||||
|
Адаптеры поверхностей и ядро не меняются вообще.
|
||||||
221
docs/telegram-prototype.md
Normal file
221
docs/telegram-prototype.md
Normal file
|
|
@ -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` и состояния скиллов
|
||||||
68
docs/user-flow.md
Normal file
68
docs/user-flow.md
Normal file
|
|
@ -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 ответа от платформы?
|
||||||
38
pyproject.toml
Normal file
38
pyproject.toml
Normal file
|
|
@ -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"]
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
0
src/matrix_bot/__init__.py
Normal file
0
src/matrix_bot/__init__.py
Normal file
246
src/mock_platform.py
Normal file
246
src/mock_platform.py
Normal file
|
|
@ -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()),
|
||||||
|
}
|
||||||
0
src/shared/__init__.py
Normal file
0
src/shared/__init__.py
Normal file
37
src/shared/models.py
Normal file
37
src/shared/models.py
Normal file
|
|
@ -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
|
||||||
0
src/telegram_bot/__init__.py
Normal file
0
src/telegram_bot/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
71
tests/test_mock_platform.py
Normal file
71
tests/test_mock_platform.py
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue