docs: matrix adapter design spec
This commit is contained in:
parent
a3449fc864
commit
09919b2463
1 changed files with 263 additions and 0 deletions
263
docs/superpowers/specs/2026-03-31-matrix-adapter-design.md
Normal file
263
docs/superpowers/specs/2026-03-31-matrix-adapter-design.md
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
# Matrix Adapter Design
|
||||||
|
|
||||||
|
**Date:** 2026-03-31
|
||||||
|
**Status:** Approved — ready for implementation
|
||||||
|
**Scope:** `adapter/matrix/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Matrix-адаптер — внутренняя поверхность для команды Lambda Lab: разработчики, тестировщики, авторы скиллов. UX ориентирован на удобство работы, не на онбординг.
|
||||||
|
|
||||||
|
Адаптер конвертирует matrix-nio события в `IncomingEvent` (core protocol) и отправляет `OutgoingEvent` обратно. Бизнес-логика — в `core/`, адаптер только переводит форматы и управляет Matrix API.
|
||||||
|
|
||||||
|
Клиент: Element (web/desktop). Стек: matrix-nio (async), Python 3.11+, SQLite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Онбординг — DM как первый чат (ленивый Space)
|
||||||
|
|
||||||
|
**Решение:** DM-комната с ботом = Чат #1. Space создаётся только при первом `!new`.
|
||||||
|
|
||||||
|
### Флоу
|
||||||
|
|
||||||
|
1. Пользователь инвайтит бота в личные сообщения
|
||||||
|
2. Бот принимает инвайт, регистрирует DM-комнату как `chat_room` с `chat_id = C1`
|
||||||
|
3. Бот пишет приветствие в DM — пользователь сразу пишет
|
||||||
|
4. При первом `!new` — бот создаёт Space `Lambda — {display_name}`, добавляет DM-комнату в Space через `m.space.child`, создаёт новую комнату-чат
|
||||||
|
|
||||||
|
### Почему не Space сразу
|
||||||
|
|
||||||
|
Создание Space при инвайте порождает 3 инвайта подряд (Space + Settings + Чат 1) до первого сообщения. DM-first убирает этот шум, сохраняя такой же UX как Telegram.
|
||||||
|
|
||||||
|
### Приветствие
|
||||||
|
|
||||||
|
```
|
||||||
|
Привет, {display_name}! Пиши — я здесь.
|
||||||
|
|
||||||
|
Команды: !new · !chats · !rename · !archive · !skills
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Архитектура — Room-type routing
|
||||||
|
|
||||||
|
При получении события адаптер сначала определяет тип комнаты (`chat` / `settings`), затем маршрутизирует в соответствующий обработчик.
|
||||||
|
|
||||||
|
```
|
||||||
|
adapter/matrix/
|
||||||
|
bot.py — matrix-nio клиент, sync loop
|
||||||
|
converter.py — RoomEvent → IncomingEvent, OutgoingEvent → Matrix API
|
||||||
|
room_router.py — определяет тип комнаты: chat | settings
|
||||||
|
states.py — FSM состояния (per room_id, SQLite)
|
||||||
|
|
||||||
|
handlers/
|
||||||
|
auth.py — invite → onboarding
|
||||||
|
chat.py — сообщения, !new, !chats, !rename, !archive
|
||||||
|
settings.py — !skills, !connectors, !soul, !safety, !plan, !status, !whoami
|
||||||
|
confirm.py — реакции 👍/❌ и команды !yes / !no
|
||||||
|
|
||||||
|
reactions.py — helpers: add_reaction, remove_reactions, parse_reaction_event
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FSM состояния (per room_id)
|
||||||
|
|
||||||
|
```python
|
||||||
|
class RoomState(StatesGroup):
|
||||||
|
idle = State() # ждём сообщения
|
||||||
|
waiting_response = State() # запрос ушёл на платформу
|
||||||
|
confirm_pending = State() # ждём !yes/!no или реакцию 👍/❌
|
||||||
|
settings_active = State() # Settings-комната (не чат)
|
||||||
|
```
|
||||||
|
|
||||||
|
`room_type` хранится в SQLite. `room_router.py` читает его при каждом событии.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Команды
|
||||||
|
|
||||||
|
Все команды на английском. Работают в любой комнате Space.
|
||||||
|
|
||||||
|
| Команда | Действие |
|
||||||
|
|---------|---------|
|
||||||
|
| `!new [name]` | Создать чат. При первом вызове — создаёт Space, переносит DM |
|
||||||
|
| `!chats` | Список чатов с текущим активным |
|
||||||
|
| `!rename <name>` | Переименовать текущую комнату |
|
||||||
|
| `!archive` | Вывести комнату из Space (не удалять) |
|
||||||
|
| `!skills` | Список скиллов — реакции как тумблеры |
|
||||||
|
| `!connectors` | Коннекторы (OAuth заглушки) |
|
||||||
|
| `!soul` | Личность агента |
|
||||||
|
| `!safety` | Настройки безопасности |
|
||||||
|
| `!plan` | Подписка и токены |
|
||||||
|
| `!status` | Состояние платформы и чатов |
|
||||||
|
| `!whoami` | Текущий аккаунт |
|
||||||
|
| `!yes` / `!no` | Подтверждение / отмена действия агента |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settings room
|
||||||
|
|
||||||
|
Создаётся при первом `!new` вместе со Space. Закреплена вверху Space.
|
||||||
|
|
||||||
|
### Скиллы — реакции как тумблеры
|
||||||
|
|
||||||
|
`!skills` → бот отправляет список. Каждый скилл пронумерован. Реакция 1️⃣–N️⃣ переключает соответствующий скилл (toggle). Несколько скиллов могут быть включены одновременно. Бот редактирует своё сообщение через `m.replace` после каждого переключения.
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ 1 web-search — поиск в интернете
|
||||||
|
✅ 2 fetch-url — чтение веб-страниц
|
||||||
|
✅ 3 email — чтение почты
|
||||||
|
❌ 4 browser — управление браузером
|
||||||
|
❌ 5 image-gen — генерация изображений
|
||||||
|
✅ 6 files — работа с файлами
|
||||||
|
|
||||||
|
Реакция 1️⃣–6️⃣ = переключить скилл
|
||||||
|
```
|
||||||
|
|
||||||
|
### Остальные настройки
|
||||||
|
|
||||||
|
`!connectors`, `!soul`, `!safety`, `!plan`, `!status`, `!whoami` — текстовые ответы, без интерактивных элементов. Поля задаются аргументами команды: `!soul name Lambda`, `!soul style brief`, `!safety on email-send`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Подтверждение действий агента
|
||||||
|
|
||||||
|
Агент запрашивает подтверждение → бот отправляет сообщение с описанием действия. Пользователь подтверждает **реакцией или командой** — оба способа работают.
|
||||||
|
|
||||||
|
```
|
||||||
|
🤖 Lambda:
|
||||||
|
Отправить письмо azamat@lambda.lab?
|
||||||
|
Тема: «Отчёт за неделю»
|
||||||
|
|
||||||
|
👍 подтвердить · ❌ отменить
|
||||||
|
!yes — подтвердить · !no — отменить
|
||||||
|
|
||||||
|
Истекает через 5 минут
|
||||||
|
```
|
||||||
|
|
||||||
|
После ответа: бот убирает реакции с сообщения, редактирует статус (`m.replace`), переходит в `idle`.
|
||||||
|
|
||||||
|
FSM: `waiting_response` → `confirm_pending` → `idle`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Долгие задачи — треды
|
||||||
|
|
||||||
|
Если задача занимает больше одного хода — бот создаёт тред от своего первого сообщения.
|
||||||
|
|
||||||
|
```
|
||||||
|
🤖 Lambda (основной поток):
|
||||||
|
Начинаю исследование «AI агенты 2025» — займёт 2-3 минуты.
|
||||||
|
🧵 Прогресс (в треде):
|
||||||
|
└── ✅ Ищу источники... (12 найдено)
|
||||||
|
└── ✅ Анализирую статьи...
|
||||||
|
└── ⏳ Формирую отчёт...
|
||||||
|
└── ○ Финальная проверка
|
||||||
|
```
|
||||||
|
|
||||||
|
Основной поток не засоряется. Финальный результат — отдельным сообщением в основной поток.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typing indicator
|
||||||
|
|
||||||
|
`m.typing` — отправлять перед запросом к платформе. Если запрос > 5 сек — возобновлять каждые 25 сек (Matrix typing живёт ~30 сек).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Converter
|
||||||
|
|
||||||
|
`adapter/matrix/converter.py` — конвертация в обе стороны.
|
||||||
|
|
||||||
|
### matrix-nio → IncomingEvent
|
||||||
|
|
||||||
|
```python
|
||||||
|
def from_room_message(event: RoomMessageText, room_id: str, chat_id: str) -> IncomingMessage:
|
||||||
|
return IncomingMessage(
|
||||||
|
user_id=event.sender, # @user:matrix.org
|
||||||
|
platform="matrix",
|
||||||
|
chat_id=chat_id, # C1, C2... из rooms таблицы
|
||||||
|
text=event.body,
|
||||||
|
attachments=[],
|
||||||
|
reply_to=event.replyto_event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_reaction(event: ReactionEvent, room_id: str) -> IncomingCallback | None:
|
||||||
|
# Парсит m.reaction → IncomingCallback(action="toggle_skill" | "confirm" | "cancel")
|
||||||
|
...
|
||||||
|
|
||||||
|
def from_command(body: str, sender: str, room_id: str, chat_id: str) -> IncomingCommand | None:
|
||||||
|
# Парсит !new, !skills, !yes, !no и т.д. → IncomingCommand
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### OutgoingEvent → Matrix
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def send_outgoing(client: AsyncClient, room_id: str, event: OutgoingEvent) -> None:
|
||||||
|
if isinstance(event, OutgoingMessage):
|
||||||
|
await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": event.text})
|
||||||
|
|
||||||
|
elif isinstance(event, OutgoingUI):
|
||||||
|
# Confirmation request — текст + подсказка по реакциям/командам
|
||||||
|
body = f"{event.text}\n\n👍 подтвердить · ❌ отменить\n!yes — подтвердить · !no — отменить"
|
||||||
|
await client.room_send(room_id, "m.room.message", {"msgtype": "m.text", "body": body})
|
||||||
|
await client.room_send(room_id, "m.reaction", {...}) # добавить 👍 и ❌ на сообщение
|
||||||
|
|
||||||
|
elif isinstance(event, OutgoingTyping):
|
||||||
|
await client.room_typing(room_id, event.is_typing, timeout=25000)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## БД схема
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE matrix_users (
|
||||||
|
matrix_user_id TEXT PRIMARY KEY, -- @user:matrix.org
|
||||||
|
platform_user_id TEXT NOT NULL, -- из MockPlatformClient
|
||||||
|
display_name TEXT,
|
||||||
|
space_id TEXT, -- NULL до первого !new
|
||||||
|
settings_room_id TEXT, -- NULL до первого !new
|
||||||
|
created_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE rooms (
|
||||||
|
room_id TEXT PRIMARY KEY, -- room_id Matrix
|
||||||
|
matrix_user_id TEXT NOT NULL,
|
||||||
|
room_type TEXT NOT NULL, -- 'chat' | 'settings'
|
||||||
|
chat_id TEXT, -- C1, C2... (NULL для settings)
|
||||||
|
display_name TEXT,
|
||||||
|
created_at TIMESTAMP,
|
||||||
|
archived_at TIMESTAMP,
|
||||||
|
FOREIGN KEY(matrix_user_id) REFERENCES matrix_users(matrix_user_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
`StateStore` из `core/store.py` (`SQLiteStore`) — для FSM per room_id.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Что НЕ реализуем в прототипе
|
||||||
|
|
||||||
|
- Webhook от платформы (используем sync `send_message`)
|
||||||
|
- E2E encryption (nio поддерживает, но усложняет прототип)
|
||||||
|
- Экспорт истории
|
||||||
|
- `!rename`, `!archive` — добавить после основного флоу
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Порядок реализации
|
||||||
|
|
||||||
|
1. `bot.py` — AsyncClient, sync loop, middleware для platform client
|
||||||
|
2. `states.py` — RoomState
|
||||||
|
3. `room_router.py` — определение типа комнаты
|
||||||
|
4. `converter.py` — from_room_message, from_reaction, from_command
|
||||||
|
5. `handlers/auth.py` — invite → onboarding
|
||||||
|
6. `handlers/chat.py` — сообщения + !new + !chats
|
||||||
|
7. `reactions.py` — helpers для работы с реакциями
|
||||||
|
8. `handlers/confirm.py` — реакции 👍/❌ + !yes/!no
|
||||||
|
9. `handlers/settings.py` — !skills с m.replace + остальные команды
|
||||||
Loading…
Add table
Add a link
Reference in a new issue