diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md index 0cc6c4c..05f7a7f 100644 --- a/.planning/codebase/ARCHITECTURE.md +++ b/.planning/codebase/ARCHITECTURE.md @@ -1,134 +1,14 @@ -# Architecture +# Архитектура (ARCHITECTURE.md) -**Analysis Date:** 2026-04-01 +## Паттерн "Thin Adapter" (Тонкая поверхность) -## Pattern Overview +Система разделена на три логических слоя: +1. **Транспортный слой (Adapter)**: Подключается к внешней платформе (Matrix). Занимается конвертацией нативных событий (`room.message`) во внутренние структуры (`IncomingMessage`). +2. **Ядро (Core)**: Предоставляет единый протокол (`core/protocol.py`), не зависящий от конкретной реализации (Matrix, Telegram и т.д.). +3. **Платформенный слой (SDK)**: `RealPlatformClient` инкапсулирует подключение по WebSocket к реальным агентам (AgentApi). -**Overall:** Hexagonal / Ports-and-Adapters +## Routing & Registry +Бот может обслуживать множество агентов (multi-tenant). Маршрутизация настраивается статически через `config/matrix-agents.yaml`. Каждый пользователь (`@user:server`) привязан к конкретному `agent_id`, у которого есть свой HTTP URL и свой изолированный `workspace_path` (например, `/agents/1/`). -**Key Characteristics:** -- A platform-neutral `core/` defines all business logic and unified event types -- Adapters (`adapter/telegram/`, `adapter/matrix/`) translate platform-specific events into core types and back -- The AI platform SDK is hidden behind a `PlatformClient` Protocol; the current implementation (`sdk/mock.py`) is swappable without touching core or adapters -- All state is stored through a `StateStore` Protocol, with `InMemoryStore` for tests and `SQLiteStore` for production - -## Layers - -**Protocol Layer:** -- Purpose: Defines every data structure crossing layer boundaries -- Location: `core/protocol.py` -- Contains: `IncomingMessage`, `IncomingCommand`, `IncomingCallback`, `OutgoingMessage`, `OutgoingUI`, `OutgoingNotification`, `OutgoingTyping`, `ChatContext`, `AuthFlow`, `SettingsAction`, type aliases `IncomingEvent` and `OutgoingEvent` -- Depends on: Python stdlib only -- Used by: All other layers - -**Core / Business Logic Layer:** -- Purpose: Handles all domain logic independent of any platform -- Location: `core/` -- Contains: - - `core/handler.py` — `EventDispatcher`: routes `IncomingEvent` to registered handler functions; returns `list[OutgoingEvent]` - - `core/handlers/` — one module per event category (`start`, `message`, `chat`, `settings`, `callback`) - - `core/store.py` — `StateStore` Protocol + `InMemoryStore` + `SQLiteStore` - - `core/chat.py` — `ChatManager`: creates/renames/archives chat workspaces (C1/C2/C3); persists via `StateStore` - - `core/auth.py` — `AuthManager`: tracks auth flow state (`pending` → `confirmed`); persists via `StateStore` - - `core/settings.py` — `SettingsManager`: fetches/caches user settings from SDK; invalidates on write -- Depends on: `core/protocol.py`, `sdk/interface.py` (Protocol only), `core/store.py` -- Used by: Adapters - -**SDK / Platform Layer:** -- Purpose: Wraps the external Lambda AI platform; isolated behind a Protocol -- Location: `sdk/` -- Contains: - - `sdk/interface.py` — `PlatformClient` Protocol: `get_or_create_user`, `send_message`, `stream_message`, `get_settings`, `update_settings`; also `WebhookReceiver` Protocol, Pydantic models (`User`, `MessageResponse`, `MessageChunk`, `UserSettings`, `AgentEvent`) - - `sdk/mock.py` — `MockPlatformClient`: full in-memory implementation with simulated latency; supports both sync (`send_message`) and streaming (`stream_message`, currently returns single chunk); includes webhook simulation via `simulate_agent_event()` -- Depends on: `sdk/interface.py` -- Used by: `core/` managers, adapters during bot startup - -**Adapter Layer:** -- Purpose: Translates platform-native events into `IncomingEvent` and `OutgoingEvent` back to platform-native calls -- Location: `adapter/matrix/`, adapter/telegram/ (in `.worktrees/telegram/`) -- Contains per adapter: `bot.py` (entry point + send logic), `converter.py` (native event → protocol), `handlers/` (adapter-specific handler overrides registered on top of core handlers), optional `store.py` / `room_router.py` / `reactions.py` for adapter state -- Depends on: `core/`, `sdk/`, platform SDK (aiogram or matrix-nio) -- Used by: `__main__` / `asyncio.run(main())` - -## Data Flow - -**Incoming Message (Matrix example):** - -1. `matrix-nio` fires `RoomMessageText` callback → `MatrixBot.on_room_message()` in `adapter/matrix/bot.py` -2. `resolve_chat_id()` in `adapter/matrix/room_router.py` maps `room_id` → logical `chat_id` (e.g. `C1`), persisted in `StateStore` -3. `from_room_event()` in `adapter/matrix/converter.py` converts the nio event to `IncomingMessage` or `IncomingCommand` -4. `EventDispatcher.dispatch(incoming)` in `core/handler.py` selects the handler by routing key (command name, callback action, or `"*"` for messages) -5. Handler (e.g. `core/handlers/message.py:handle_message`) calls `platform.send_message()` on `MockPlatformClient`, receives `MessageResponse` -6. Handler returns `list[OutgoingEvent]` (e.g. `[OutgoingTyping(..., False), OutgoingMessage(...)]`) -7. `MatrixBot._send_all()` iterates the list; `send_outgoing()` converts each to a `client.room_send()` / `client.room_typing()` call - -**Incoming Reaction (Matrix):** - -1. `ReactionEvent` callback → `MatrixBot.on_reaction()` -2. `from_reaction()` maps emoji key to `IncomingCallback` with `action="confirm"`, `"cancel"`, or `"toggle_skill"` -3. Dispatch → `core/handlers/callback.py` - -**Command Routing:** - -The `EventDispatcher` uses a routing key per event type: -- `IncomingCommand` → `event.command` (e.g. `"start"`, `"new"`, `"settings"`) -- `IncomingCallback` → `event.action` (e.g. `"confirm"`, `"toggle_skill"`) -- `IncomingMessage` → `"*"` (catch-all), or `event.attachments[0].type` if attachments present - -Adapters call `register_all(dispatcher)` first (core handlers), then `register_matrix_handlers(dispatcher, ...)` to override or add platform-specific variants (e.g. `!new` creates a real Matrix room via the nio client). - -**State Management:** -- All persistent state goes through `StateStore` (key-value, async interface) -- Key namespaces: `chat:{user_id}:{chat_id}`, `auth:{user_id}`, `settings:{user_id}`, `matrix_room:{room_id}`, `matrix_user:{matrix_user_id}`, `matrix_state:{room_id}`, `matrix_skills_msg:{room_id}` -- Production uses `SQLiteStore` (row-per-key, JSON-serialised values); tests use `InMemoryStore` - -## Key Abstractions - -**EventDispatcher (`core/handler.py`):** -- Purpose: Single dispatch table for all event types; decouples handler logic from transport -- Pattern: Registry (map of `event_type → {key → HandlerFn}`); wildcard `"*"` as fallback -- Handler signature: `async def handler(event, chat_mgr, auth_mgr, settings_mgr, platform) → list[OutgoingEvent]` - -**StateStore Protocol (`core/store.py`):** -- Purpose: Pluggable persistence behind a minimal `get/set/delete/keys` interface -- Implementations: `InMemoryStore` (tests/dev), `SQLiteStore` (production) -- Key pattern: `"{namespace}:{discriminator}"` - -**PlatformClient Protocol (`sdk/interface.py`):** -- Purpose: Contracts the entire surface of the Lambda AI SDK -- Current implementation: `MockPlatformClient` in `sdk/mock.py` -- Swap path: Replace `sdk/mock.py` with a real SDK client; no changes needed elsewhere - -**Converter functions (`adapter/matrix/converter.py`):** -- Purpose: One-way transformation from platform-native event to `IncomingEvent` -- Always produce canonical protocol types; adapters never pass raw library objects to core - -## Entry Points - -**Matrix Bot:** -- Location: `adapter/matrix/bot.py:main()` -- Run: `python -m adapter.matrix.bot` -- Startup sequence: load `.env` → build `AsyncClient` → `build_runtime()` → register callbacks → `client.sync_forever()` - -**Telegram Bot:** -- Location: `.worktrees/telegram/adapter/telegram/bot.py` (feature branch, not merged to main yet) -- Run: `python -m adapter.telegram.bot` - -## Error Handling - -**Strategy:** Errors propagate up to the adapter's event callback. The adapter logs and drops the event; the bot keeps running. - -**Patterns:** -- `EventDispatcher.dispatch()` returns `[]` (empty list) when no handler is found and logs a warning -- `AuthManager` and `ChatManager` raise `ValueError` for not-found entities; callers are responsible for catching -- `MockPlatformClient` raises `PlatformError` (defined in `sdk/interface.py`) on unexpected states - -## Cross-Cutting Concerns - -**Logging:** `structlog` throughout; all managers and the dispatcher use `structlog.get_logger(__name__)` -**Validation:** Pydantic models in `sdk/interface.py` for SDK responses; plain dataclasses in `core/protocol.py` for internal events -**Authentication:** `AuthManager.is_authenticated()` is checked in `handle_message` before forwarding to platform; unauthenticated users receive a prompt to run `!start` / `/start` - ---- - -*Architecture analysis: 2026-04-01* +## Файловый контракт +Файлы не передаются агенту в base64. Бот сохраняет вложение напрямую в локальную директорию (общий volume), и передает агенту только относительный путь (`workspace_path`). diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md index 473d257..5848135 100644 --- a/.planning/codebase/CONCERNS.md +++ b/.planning/codebase/CONCERNS.md @@ -1,235 +1,6 @@ -# Codebase Concerns +# Известные проблемы (CONCERNS.md) -**Analysis Date:** 2026-04-01 - ---- - -## Tech Debt - -### Telegram adapter not merged to main - -- Issue: The entire `adapter/telegram/` directory exists only in the `feat/telegram-adapter` branch (worktree at `.worktrees/telegram/`). `main` has no Telegram adapter at all. -- Files: `.worktrees/telegram/adapter/telegram/` and remote branch `origin/feat/telegram-adapter` -- Impact: Running `python -m adapter.telegram.bot` from `main` fails with ImportError. Tests referencing `adapter/telegram/` (e.g., `tests/adapter/test_forum_db.py`) only exist in the worktree and are absent from `main`. -- Fix approach: Merge `feat/telegram-adapter` into `main` after final manual QA pass. The branch is ahead of main by 5 commits (`a1b7a14` being the most recent). - -### Divergent core/handlers between main and feat/telegram-adapter - -- Issue: `feat/telegram-adapter` removed platform-awareness from `core/handlers/chat.py` and `core/handlers/message.py` — the `_command()` and `_start_command()` helpers that format Matrix `!cmd` vs Telegram `/cmd` prompts were deleted. The branch hardcodes `/start` everywhere. -- Files: `core/handlers/chat.py`, `core/handlers/message.py` (differ between branches) -- Impact: If the Matrix adapter relies on these platform-aware helpers being in `main`'s version of core, merging `feat/telegram-adapter` will break Matrix `!start` prompt text for unauthenticated users. -- Fix approach: Before merging, decide which version of `core/handlers/` is canonical. The Matrix adapter in `main` currently passes because `main` still has the platform-aware helpers. - -### SQLiteStore uses blocking I/O in async context - -- Issue: `core/store.py` `SQLiteStore` methods are declared `async` but perform synchronous blocking `sqlite3.connect()` calls without `asyncio.to_thread` or `aiosqlite`. -- Files: `core/store.py` lines 46–73 -- Impact: Each database call blocks the asyncio event loop. Under any concurrent load (e.g., two Matrix users sending messages simultaneously) this will cause visible latency spikes and potential event loop starvation. -- Fix approach: Replace `sqlite3` calls with `aiosqlite` or wrap each call in `asyncio.to_thread()`. - -### Telegram adapter has its own separate SQLite database layer - -- Issue: `adapter/telegram/db.py` is a fully independent SQLite database (file: `lambda_bot.db`) with its own schema (`tg_users`, `chats`). Meanwhile, `core/store.py` has `SQLiteStore` with a KV schema (`lambda_matrix.db` for Matrix). The two stores are incompatible and do not share data. -- Files: `.worktrees/telegram/adapter/telegram/db.py`, `core/store.py` -- Impact: There is no unified storage layer. Chat state is split across two databases. A user's Telegram chats cannot be seen from Matrix and vice versa (even conceptually). Violates the "single core" architecture principle from CLAUDE.md. -- Fix approach: This is a fundamental design gap. Either extend `StateStore` to support the Telegram-specific data model, or accept separate stores as intentional for the prototype stage and document the constraint. - -### MockPlatformClient hardcoded throughout — no production path wired - -- Issue: Both `adapter/matrix/bot.py` and `.worktrees/telegram/adapter/telegram/bot.py` instantiate `MockPlatformClient()` directly. `PLATFORM_MODE` is defined in `.env.example` but is never read or acted upon anywhere in the codebase. -- Files: `adapter/matrix/bot.py` line 71, `sdk/mock.py`, `sdk/interface.py` -- Impact: There is no runtime switch to connect a real SDK. Switching to production requires code changes, not configuration. -- Fix approach: Add a factory function in `sdk/` that reads `PLATFORM_MODE` and returns either `MockPlatformClient` or a real `PlatformClient`. Both bot entrypoints should use this factory. - -### MatrixRuntime type annotation leaks MockPlatformClient - -- Issue: `adapter/matrix/bot.py` `MatrixRuntime.platform` is typed as `MockPlatformClient` (not `PlatformClient`). `build_event_dispatcher` and `build_runtime` signatures also use `MockPlatformClient` as the parameter type. -- Files: `adapter/matrix/bot.py` lines 46, 54, 67 -- Impact: The isolation promise ("replace only `sdk/mock.py` when real SDK arrives") is broken — the bot layer is coupled to the mock concrete type, not the Protocol. -- Fix approach: Change type annotations to `PlatformClient` from `sdk.interface`. - ---- - -## Known Bugs / Open Issues - -### Telegram forum: global commands visible inside topic context - -- Issue: Telegram shows the full bot command menu (including `/chats`, `/new`, `/settings`) even when the user is inside a forum topic. The code blocks `switch` and `new_chat` callbacks inside topics but the commands themselves still appear in the UI. -- Files: `.worktrees/telegram/adapter/telegram/handlers/forum.py`, `.worktrees/telegram/adapter/telegram/bot.py` -- Impact: Users can tap `/settings` or `/chats` inside a topic and get confusing behavior. -- Tracked: Issue `#15` — `Telegram forum topics: remaining UX and synchronization gaps` - -### Telegram forum: `/new ` inside linked topic does not rename the Telegram topic - -- Issue: Running `/new ` inside a forum topic that is already linked to a chat renames the internal chat record but does not call `edit_forum_topic` to rename the actual Telegram topic. -- Files: `.worktrees/telegram/adapter/telegram/handlers/forum.py` -- Impact: Topic name in Telegram goes out of sync with internal chat name. -- Tracked: Issue `#15` - -### Matrix: `handle_invite` hardcodes `chat_id = "C1"` for all new rooms - -- Issue: `adapter/matrix/handlers/auth.py` `handle_invite()` always assigns `chat_id = "C1"` regardless of how many rooms the user already has. If a user invites the bot into a second room before using `!new`, both rooms get `C1`. -- Files: `adapter/matrix/handlers/auth.py` line 26 -- Impact: Two rooms mapped to the same `chat_id` causes routing collisions. -- Fix approach: Call `next_chat_id(store, user_id)` here instead of hardcoding `"C1"`. - -### Matrix: `remove_reaction` uses non-standard `undo` field - -- Issue: `adapter/matrix/reactions.py` `remove_reaction()` sends a `"undo": True` field in the reaction event body. This is not part of the Matrix spec for reaction redaction. The correct approach is to redact the original reaction event via `client.room_redact()`. -- Files: `adapter/matrix/reactions.py` lines 56–68 -- Impact: Reaction "undo" will silently fail on compliant homeservers. - -### Matrix: E2EE not supported (blocked by `python-olm`) - -- Issue: `matrix-nio` E2EE requires `python-olm`, which fails to build on macOS/ARM. No encrypted DM support. -- Files: `adapter/matrix/bot.py` -- Impact: The bot cannot operate in encrypted rooms. Users who have DM encryption enforced cannot use the Matrix bot. -- Status: Documented as a known infrastructure constraint in `docs/reports/2026-04-01-surfaces-progress-report.md`. Needs a separate infrastructure task. - ---- - -## Security Considerations - -### SQLite database files not in .gitignore - -- Risk: `lambda_bot.db` and `lambda_matrix.db` are present in the working tree (shown in `git status`) but not listed in `.gitignore`. These files may contain user data including chat content and display names. -- Files: `lambda_bot.db`, `lambda_matrix.db`, `.gitignore` -- Current mitigation: Files are currently untracked (not yet staged) but nothing prevents them from being accidentally committed. -- Recommendation: Add `*.db` or specific filenames to `.gitignore` immediately. - -### Auth flow is auto-confirmed in mock — no real validation exists - -- Issue: `core/auth.py` `confirm()` automatically sets `state = "confirmed"` and generates a fake `platform_user_id`. There is no real verification step, no code exchange, no token validation. -- Files: `core/auth.py` lines 39–48 -- Impact: The auth layer is decorative for the prototype. Any user who sends `!start` or `/start` is immediately authenticated. If the real SDK auth requires a different flow (e.g., OAuth, code), the current `AuthManager` interface may not match. -- Current mitigation: Acceptable for mock stage. Must be re-evaluated before production use. - -### Matrix room metadata stored without access control - -- Issue: `adapter/matrix/store.py` stores room metadata keyed by `room_id`. Any call that can supply an arbitrary `room_id` can read or overwrite another user's room metadata. -- Files: `adapter/matrix/store.py`, `adapter/matrix/room_router.py` -- Impact: In the current single-process bot this is not exploitable. If the store is ever shared across processes or users, room metadata can be poisoned. - ---- - -## Fragile Areas - -### `core/chat.py` scan-by-suffix fallback is O(N) and collision-prone - -- Issue: `ChatManager.get()` when called without `user_id` scans all `chat:*` keys and matches by suffix (e.g., `":C1"`). If two users both have a chat named `C1` (which is always the case), this returns the first one found, non-deterministically. -- Files: `core/chat.py` lines 76–82 -- Impact: Functions like `rename` and `archive` that call `chat_mgr.get(chat_id)` without `user_id` will operate on the wrong user's chat in a multi-user scenario. -- Fix approach: Audit all callers and always pass `user_id`. The scan-by-suffix fallback should be removed or explicitly guarded. - -### `adapter/matrix/handlers/chat.py` chat_id counter races under concurrency - -- Issue: `make_handle_new_chat` calls `chat_mgr.list_active()` and uses `len(chats) + 1` to compute a new `chat_id`. This is not atomic. Two concurrent `!new` commands from the same user can produce the same `chat_id`. -- Files: `adapter/matrix/handlers/chat.py` line 17 -- Impact: Duplicate `chat_id` values (`C2`, `C2`) for the same user, leading to state corruption. -- Fix approach: Use `next_chat_id()` from `adapter/matrix/store.py` which increments an atomic counter in the store. The `next_chat_id()` function already exists but is not used here. - -### `conftest.py` contains a fragile stdlib `platform` module workaround - -- Issue: `conftest.py` patches `sys.modules` to remove the Python stdlib `platform` module so local `platform/` (which no longer exists — renamed to `sdk/`) doesn't shadow it. The comment still refers to `platform/` but the directory was renamed to `sdk/` in commit `41660fe`. -- Files: `conftest.py` lines 1–13 -- Impact: The workaround is now a no-op (there is no `platform/` package to shadow) but adds confusion. The comment is incorrect. If someone creates a `platform/` directory again, unexpected behavior can return. -- Fix approach: Remove the `sys.modules` patching entirely since `sdk/` does not conflict with stdlib. Update the comment. - -### Forum onboarding `chat_shared` constructs a fake `Chat` object - -- Issue: `adapter/telegram/handlers/forum.py` handles `chat_shared` by constructing `Chat(id=..., type="supergroup", is_forum=True)` and passing it to `_complete_group_link()`. The `is_forum=True` is hardcoded — the real value from Telegram is not verified. This means the check `if getattr(forwarded_chat, "is_forum", None) is False` in the forwarding fallback path is bypassed entirely. -- Files: `.worktrees/telegram/adapter/telegram/handlers/forum.py` lines 162–168 -- Impact: A user could link a regular supergroup (without Topics enabled) via `chat_shared`, which would succeed in linking but fail when the bot tries to create forum topics. - ---- - -## Gaps Between CLAUDE.md and Actual Code - -### CLAUDE.md says `platform/` — code uses `sdk/` - -- CLAUDE.md architecture diagram shows `platform/interface.py` and `platform/mock.py` -- Actual code uses `sdk/interface.py` and `sdk/mock.py` (renamed in commit `41660fe`) -- Files: `CLAUDE.md` (project instructions), `sdk/interface.py`, `sdk/mock.py` -- Also: Agent config files at `.claude/agents/core-developer.md` still reference `platform/` throughout -- Impact: New contributors reading CLAUDE.md will look for a `platform/` directory that does not exist. - -### CLAUDE.md lists `core/handlers/` sub-handlers that partially do not exist - -- CLAUDE.md lists handler modules but the actual `core/handlers/` only has: `start.py`, `message.py`, `chat.py`, `settings.py`, `callback.py` -- No `voice.py` handler exists; voice is handled as a fallback inside `core/handlers/message.py` (returns stub response) -- No `payment.py` handler exists; `PaymentRequired` dataclass is defined in `core/protocol.py` but never dispatched -- Files: `core/protocol.py` (PaymentRequired defined), `core/handlers/` (no payment or voice handlers) - -### CLAUDE.md workflow describes `@reviewer` agent but agent file references old patterns - -- `.claude/agents/core-developer.md` still says "Твоя зона — `core/` и `platform/`" -- The old Haiku/Sonnet researcher-developer workflow is captured in `docs/workflow-backup-2026-04-01.md`, but `.claude/agents/` configs were not updated to match - -### `tests/adapter/test_forum_db.py` is untracked on main - -- This test file exists in the working tree (visible in `git status`) but is not committed to `main`. It tests `adapter/telegram/db.py` which also does not exist on `main`. -- Files: `tests/adapter/test_forum_db.py` -- Impact: Running `pytest tests/` from main currently includes this test, which imports `adapter.telegram.db`. This import succeeds only because the test auto-reloads the module from an untracked file. This is fragile — if the file is deleted, tests silently pass with fewer tests counted. - ---- - -## Missing Critical Features - -### No streaming response support in adapters - -- Both adapters use `platform.send_message()` (sync) not `platform.stream_message()` (streaming) -- `sdk/interface.py` defines `stream_message` returning `AsyncIterator[MessageChunk]` -- No adapter sends a typing indicator before the response arrives and then streams chunks -- Impact: User experience with slow AI responses will show nothing until the full response is ready -- Files: `core/handlers/message.py` line 28, `sdk/interface.py` lines 83–88 - -### No webhook/push notification handling - -- `sdk/interface.py` defines `WebhookReceiver` Protocol with `on_agent_event()` -- `sdk/mock.py` has `register_webhook_receiver()` and `simulate_agent_event()` -- Neither bot entrypoint registers a `WebhookReceiver` -- Impact: Push notifications from the platform (task completions, background jobs) cannot reach the user -- Files: `sdk/interface.py` lines 95–97, `adapter/matrix/bot.py`, no registration present - -### Telegram adapter uses InMemoryStore for core state - -- `.worktrees/telegram/adapter/telegram/bot.py` calls `InMemoryStore()` for the `EventDispatcher`'s state -- All `core/` state (auth, chat metadata in the KV layer) is lost on bot restart -- `adapter/telegram/db.py` SQLite is used only for Telegram-specific data -- Impact: On restart, authenticated users are logged out; core chat context is wiped -- Files: `.worktrees/telegram/adapter/telegram/bot.py` line 46 - -### No multi-user isolation in Matrix store - -- `adapter/matrix/store.py` keys are global (`matrix_room:ROOMID`, `matrix_user:USERID`) -- There is no namespace or tenant isolation -- Impact: At scale, any key collision would corrupt state. For a single-user prototype this is acceptable, but it is an architectural constraint to document before expanding scope. - ---- - -## Test Coverage Gaps - -### No tests for `adapter/telegram/` in main test suite - -- `tests/adapter/` on main only contains `matrix/` tests and the untracked `test_forum_db.py` -- All Telegram adapter tests live in the worktree at `.worktrees/telegram/tests/` -- Files: `tests/adapter/` (missing `telegram/` subdirectory on main) -- Risk: Merging `feat/telegram-adapter` without also merging its tests leaves Telegram untested on main -- Priority: High - -### No tests for `core/handlers/callback.py` confirm/cancel real behavior - -- `core/handlers/callback.py` `handle_confirm` and `handle_cancel` return stub text with `action_id` -- No test verifies that a real confirmation flow (dispatch → confirm → side effect) works end to end -- Files: `core/handlers/callback.py`, `tests/core/test_dispatcher.py` -- Priority: Medium - -### No tests for `adapter/matrix/handlers/auth.py` multi-room invite scenario - -- The hardcoded `C1` bug (see Known Bugs section) is not caught by any test -- Files: `adapter/matrix/handlers/auth.py`, `tests/adapter/matrix/test_dispatcher.py` -- Priority: Medium - ---- - -*Concerns audit: 2026-04-01* +- **Отсутствие E2E шифрования в Matrix**: На данный момент бот не поддерживает зашифрованные комнаты, так как библиотека `matrix-nio` требует нативной сборки `python-olm`, что усложняет кросс-платформенный деплой. +- **Потеря стейта агентов**: Так как текущий `platform-agent` часто работает с `MemorySaver`, его стейт теряется при перезапусках. Это проблема внешнего агента, но она напрямую влияет на UX поверхности. +- **Общий том (Shared Volume)**: Контракт обязывает бота и агента запускаться на одном физическом хосте (или иметь распределенный сетевой диск), что может стать бутылочным горлышком при сильном масштабировании. +- **Hardcoded роутинг**: `matrix-agents.yaml` требует ручного редактирования и перезапуска бота при добавлении новых агентов. Желательно вынести этот процесс в динамическую БД или API. diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md index 04c7f6a..36a4ed5 100644 --- a/.planning/codebase/CONVENTIONS.md +++ b/.planning/codebase/CONVENTIONS.md @@ -1,195 +1,7 @@ -# Coding Conventions +# Конвенции (CONVENTIONS.md) -**Analysis Date:** 2026-04-01 - -## Linting and Formatting - -**Tool:** ruff (configured in `pyproject.toml`) - -**Settings:** -- Line length: 100 characters -- Target: Python 3.11 -- Active rule sets: `E` (pycodestyle errors), `F` (pyflakes), `I` (isort), `UP` (pyupgrade), `B` (bugbear) - -**Type checking:** mypy (available as dev dependency; not enforced in CI at this time) - -Run linting: -```bash -ruff check . -ruff format . -``` - -## File Naming - -- Module files: `snake_case.py` (e.g., `room_router.py`, `test_dispatcher.py`) -- Each module starts with a comment declaring its path: `# core/handler.py` -- Test files: `test_.py` (e.g., `test_store.py`, `test_converter.py`) -- No index/barrel files except `__init__.py` for package registration - -## Class Naming - -- `PascalCase` for all classes (e.g., `EventDispatcher`, `MockPlatformClient`, `MatrixBot`) -- Protocol/interface classes named after the capability: `StateStore`, `PlatformClient`, `WebhookReceiver` -- Manager classes suffixed with `Manager`: `ChatManager`, `AuthManager`, `SettingsManager` -- Dataclasses follow the same `PascalCase` rule: `IncomingMessage`, `OutgoingUI`, `MatrixRuntime` - -## Function and Method Naming - -- `snake_case` for all functions and methods -- Private helpers prefixed with single underscore: `_to_dict`, `_from_dict`, `_routing_key`, `_latency` -- Handler functions named `handle_`: `handle_start`, `handle_message`, `handle_new_chat` -- Builder functions named `build_`: `build_runtime`, `build_event_dispatcher`, `build_skills_text` -- Converter functions named `from_`: `from_room_event`, `from_command`, `from_reaction` -- Predicate functions named `is_`: `is_authenticated`, `is_new` - -## Variable Naming - -- `snake_case` for all variables and parameters -- Internal state attributes prefixed with `_`: `self._store`, `self._platform`, `self._handlers` -- Store key prefixes are module-level constants in `UPPER_SNAKE_CASE`: - ```python - ROOM_META_PREFIX = "matrix_room:" - USER_META_PREFIX = "matrix_user:" - ``` -- Constants for reaction strings are module-level: `CONFIRM_REACTION = "👍"`, `PLATFORM = "matrix"` - -## Type Annotations - -All files use `from __future__ import annotations` at the top for deferred evaluation. - -**Annotation style:** -- Use built-in generics (`list[str]`, `dict[str, Any]`) — not `List`, `Dict` from `typing` -- Union types written with `|`: `str | None`, `IncomingCallback | None` -- Type aliases at module level: `IncomingEvent = IncomingMessage | IncomingCommand | IncomingCallback` -- Callable types use `typing.Callable` and `typing.Awaitable`: - ```python - HandlerFn = Callable[..., Awaitable[list[OutgoingEvent]]] - ``` -- Handler functions use loose `list` return type without generics (consistent across `core/handlers/`) -- Protocol classes use `...` as body for abstract methods: - ```python - async def get(self, key: str) -> dict | None: ... - ``` - -**Pydantic vs dataclasses:** -- `core/protocol.py` — plain `@dataclass` with `field(default_factory=...)` for mutable defaults -- `sdk/interface.py` — Pydantic `BaseModel` for all SDK-facing models (`User`, `MessageResponse`, `UserSettings`) -- Choose `@dataclass` for internal protocol structs, `BaseModel` for SDK boundary models - -## Import Organization - -Order (enforced by ruff `I` rules): -1. `from __future__ import annotations` -2. Standard library imports (grouped) -3. Third-party imports (grouped) -4. Local imports from project packages (grouped) - -Example from `adapter/matrix/bot.py`: -```python -from __future__ import annotations - -import asyncio -import os -from dataclasses import dataclass -from pathlib import Path - -import structlog -from nio import AsyncClient, ... -from dotenv import load_dotenv - -from adapter.matrix.converter import from_reaction, from_room_event -from core.auth import AuthManager -from core.protocol import OutgoingEvent, ... -from sdk.mock import MockPlatformClient -``` - -No relative imports; all imports use absolute package paths from the project root. - -## Async Patterns - -All I/O methods are `async def`. There are no sync wrappers around async code. - -**Handler signature pattern** (used uniformly across `core/handlers/`): -```python -async def handle_(event: IncomingEvent, auth_mgr, platform, chat_mgr, settings_mgr) -> list: -``` -Note: manager parameters are untyped in handler signatures (accepted as `**kwargs` at call site in `EventDispatcher.dispatch`). - -**Awaiting store calls:** -```python -stored = await self._store.get(f"auth:{user_id}") -await self._store.set(f"auth:{user_id}", _to_dict(flow)) -``` - -**SQLiteStore uses sync sqlite3** inside `async def` methods — blocking I/O is not off-loaded to a thread executor. This is a known limitation (see CONCERNS.md). - -**Mock latency simulation:** -```python -await self._latency(200, 600) # min_ms, max_ms -``` - -## Logging - -**Library:** `structlog` - -**Pattern:** -```python -import structlog -logger = structlog.get_logger(__name__) - -logger.info("Chat created", chat_id=chat_id, user_id=user_id) -logger.warning("No handler registered", event_type=event_type.__name__, key=key) -``` - -- Always pass structured keyword arguments — never use f-strings in log calls -- Logger created at module level with `structlog.get_logger(__name__)` - -## Error Handling - -- Raise `ValueError` for invalid domain state (e.g., chat not found in `ChatManager.rename`) -- `sdk/interface.py` defines `PlatformError(Exception)` with a `code` field for SDK-level errors -- Handler functions never raise — they return `[]` or a fallback `OutgoingMessage` -- No `try/except` blocks in core handlers; errors from the platform are expected to propagate - -## Comments - -- Module-level comment declaring file path at top: `# core/handler.py` -- Docstrings for classes with non-obvious behavior: - ```python - class MockPlatformClient: - """ - Заглушка SDK платформы Lambda. - ... - """ - ``` -- Inline comments for non-obvious blocks: - ```python - # Scan by chat_id suffix when user_id unknown (slower) - ``` -- Comments in Russian are normal and acceptable throughout the codebase - -## Serialization Pattern - -Dataclasses are serialized/deserialized via private module-level functions, not class methods: - -```python -def _to_dict(ctx: ChatContext) -> dict: - return { "chat_id": ctx.chat_id, ... } - -def _from_dict(d: dict) -> ChatContext: - return ChatContext(chat_id=d["chat_id"], ...) -``` - -This pattern is used in `core/auth.py` and `core/chat.py`. Follow this pattern for any new manager that persists to `StateStore`. - -## Module Design - -- No barrel `__init__.py` exports except `core/handlers/__init__.py` which exposes `register_all` -- Manager classes take `(platform, store)` as constructor args; `platform` is often stored as `object` or not stored at all if unused -- `@dataclass` is preferred for plain data containers, not NamedTuple or TypedDict -- Store key namespacing follows `::` pattern: - `"chat:u1:C1"`, `"auth:u1"`, `"matrix_room:!r:m.org"` - ---- - -*Convention analysis: 2026-04-01* +- **Асинхронность**: Весь код бота асинхронный (`asyncio`). Вызовы SDK и Matrix-клиента выполняются через `await`. Блокирующие вызовы (если они есть) должны выноситься в тредпул. +- **Обработка ошибок**: Бот не должен падать из-за ошибок отдельного агента. Ошибки SDK (например, `PlatformError`) отлавливаются в боте и возвращаются пользователю в виде системных сообщений или уведомлений. +- **Стейтлесс-подход**: Поверхность хранит минимальный стейт (только локальный SQLite для связки `room_id` <-> `platform_chat_id`). Вся история сообщений и память лежат на стороне агентов. +- **Переменные окружения**: Бот полностью конфигурируется через `.env` (префиксы `MATRIX_` и `SURFACES_`). +- **Добавление новой поверхности**: Новая поверхность должна быть самостоятельной папкой в `adapter/`, реализовывать `converter.py`, и переиспользовать `sdk/real.py` и `core/protocol.py`. diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md index 3cdae98..cd771d1 100644 --- a/.planning/codebase/INTEGRATIONS.md +++ b/.planning/codebase/INTEGRATIONS.md @@ -1,173 +1,15 @@ -# External Integrations +# Интеграции (INTEGRATIONS.md) -**Analysis Date:** 2026-04-01 +## Platform Agent API +- **Тип**: WebSocket (через `AgentApi` SDK) +- **Назначение**: Связь между Matrix-адаптером и внешней LLM-платформой. +- **Контракт**: Surface выступает "тупым" клиентом. Он отправляет `platform_chat_id` и `user_id` вместе с сообщениями. Платформа/Агент отвечает текстом и вложениями. Контейнерами агентов бот не управляет. -## Bot Platform APIs +## Matrix Homeserver +- **Тип**: HTTP/HTTPS API (via `matrix-nio`) +- **Назначение**: Пользовательский интерфейс и транспорт сообщений для бота. +- **Ограничения**: Поддерживается только нешифрованное (unencrypted) взаимодействие. -**Telegram Bot API:** -- Purpose: Primary messaging surface for user ↔ Lambda agent interaction -- Client library: `aiogram` 3.26.0 (async, wraps Telegram Bot API v7+) -- Authentication: Bot token via `TELEGRAM_BOT_TOKEN` -- Entry point: `adapter/telegram/bot.py` (planned; aiogram worktree branch `feat/telegram-adapter`) -- Transport: Long-polling or webhook (aiogram supports both; mode not yet locked in) -- Bot API docs: https://core.telegram.org/bots/api - -**Matrix Client-Server API:** -- Purpose: Secondary messaging surface (Matrix/Element clients) -- Client library: `matrix-nio` 0.25.2 (async) -- Authentication: password login or pre-existing access token (`MATRIX_ACCESS_TOKEN`) -- Login flow in `adapter/matrix/bot.py` `main()`: - - If `MATRIX_ACCESS_TOKEN` is set → assigned directly to `client.access_token` - - Else if `MATRIX_PASSWORD` is set → `client.login(password=..., device_name="surfaces-bot")` -- Sync method: `client.sync_forever(timeout=30000)` (30-second long-poll) -- E2EE store: nio file-based store at path from `MATRIX_STORE_PATH` (default: `"matrix_store"`) -- Matrix C-S API docs: https://spec.matrix.org/latest/client-server-api/ - -### Matrix Room Model - -Rooms are mapped to Lambda chat slots (C1, C2, C3…) via `adapter/matrix/room_router.py`: -- First message in a room → assigns next chat ID (C1, C2, …) and persists mapping to store -- Room metadata stored under key `matrix_room:` in `StateStore` -- User metadata (next chat index) stored under `matrix_user:` - -### Matrix Event Types Handled - -| nio Event Class | Handler | Action | -|--------------------|-----------------------------|-------------------------------| -| `RoomMessageText` | `MatrixBot.on_room_message` | Dispatch to `EventDispatcher` | -| `ReactionEvent` | `MatrixBot.on_reaction` | Button confirmation / skill toggle | -| `InviteMemberEvent`| `MatrixBot.on_member` | Accept room invite | -| `RoomMemberEvent` | `MatrixBot.on_member` | Membership change handling | - -## Lambda Platform (Internal SDK) - -**Purpose:** AI agent backend — processes user messages, manages user accounts, returns responses - -**Interface:** `sdk/interface.py` — `PlatformClient` Protocol - -**Current Implementation:** `sdk/mock.py` — `MockPlatformClient` -- Simulates network latency (10–80 ms default, 200–600 ms for message calls) -- In-process in-memory state (users, messages, settings dicts) -- Supports webhook simulation via `simulate_agent_event()` - -**Production Integration (future):** -- URL: `LAMBDA_PLATFORM_URL` (default: `http://localhost:8000`) -- Auth: `LAMBDA_SERVICE_TOKEN` (bearer token) -- Mode switch: `PLATFORM_MODE=mock` vs `PLATFORM_MODE=production` -- Swap path: replace `sdk/mock.py` only; no changes to `core/` or `adapter/` - -**Platform API Methods (from `sdk/interface.py`):** - -```python -async def get_or_create_user(external_id, platform, display_name) -> User -async def send_message(user_id, chat_id, text, attachments) -> MessageResponse -async def stream_message(user_id, chat_id, text, attachments) -> AsyncIterator[MessageChunk] -async def get_settings(user_id) -> UserSettings -async def update_settings(user_id, action) -> None -``` - -**Webhook / Push (outbound from platform → bot):** -- Interface: `WebhookReceiver` Protocol (`sdk/interface.py`) -- Registration: `MockPlatformClient.register_webhook_receiver(receiver)` -- Event types: `task_done`, `task_error`, `task_progress` (modelled in `AgentEvent`) -- Production implementation not yet wired; mock supports `simulate_agent_event()` for testing - -## Data Storage - -**Databases:** - -*SQLite (primary persistence):* -- Client: stdlib `sqlite3` (synchronous, called from async code without `asyncio.to_thread`) -- Schema: single key-value table: `kv (key TEXT PRIMARY KEY, value TEXT NOT NULL)` -- JSON serialization for values (`json.dumps` / `json.loads`) -- Matrix bot DB path: `MATRIX_DB_PATH` (default: `"lambda_matrix.db"`) -- Telegram bot DB path: implicit `"lambda_bot.db"` (file present in repo root — development artifact) -- Implementation: `core/store.py` `SQLiteStore` - -*In-Memory (testing / development):* -- `InMemoryStore` — plain Python dict, no persistence across restarts -- `MockPlatformClient` internal state — also in-memory dicts - -**File Storage:** -- Matrix nio E2EE store: local filesystem directory at `MATRIX_STORE_PATH` (default: `"matrix_store/"`) -- No object storage (S3/GCS/etc.) currently; mock client has `attachment_mode` flag (`"url"` | `"binary"` | `"s3"`) reserved for future real SDK - -**Caching:** -- None — no Redis or external cache layer - -## Authentication & Identity - -**Telegram Auth:** -- Bot token → passed to aiogram dispatcher at startup -- User identity: Telegram user ID mapped to platform `external_id` - -**Matrix Auth:** -- Password or access token (see above) -- User identity: Matrix user ID (e.g. `@user:matrix.org`) mapped to platform `external_id` - -**Lambda Platform User Identity:** -- `get_or_create_user(external_id, platform)` → returns `User` with internal `user_id` -- External IDs are platform-prefixed in mock: `"{platform}:{external_id}"` - -## Monitoring & Observability - -**Logging:** -- `structlog` 25.5.0 — structured logging (key=value pairs) -- Logger instantiation: `structlog.get_logger(__name__)` in each module -- Log calls use keyword arguments: `logger.info("event_name", key=value, ...)` -- No log shipping / aggregation configured (local stdout only) - -**Error Tracking:** -- None — no Sentry, Datadog, or similar integration - -**Metrics:** -- None — `MockPlatformClient.get_stats()` returns basic in-memory counters (not exported) - -## CI/CD & Deployment - -**Hosting:** -- Not specified — no Dockerfile, docker-compose, or cloud config files present - -**CI Pipeline:** -- None detected — no `.github/workflows/`, `.gitlab-ci.yml`, etc. - -## Environment Configuration - -**Required variables (from `.env.example`):** - -| Variable | Required | Default | Purpose | -|-----------------------|----------|--------------------|--------------------------------------| -| `TELEGRAM_BOT_TOKEN` | Yes* | — | Telegram Bot API token | -| `MATRIX_HOMESERVER` | Yes* | — | Matrix homeserver URL (e.g. `https://matrix.org`) | -| `MATRIX_USER_ID` | Yes* | — | Bot's Matrix user ID | -| `MATRIX_PASSWORD` | Cond. | — | Login password (if no access token) | -| `MATRIX_ACCESS_TOKEN` | Cond. | — | Pre-issued access token (preferred) | -| `MATRIX_DEVICE_ID` | No | `""` | Matrix device ID | -| `MATRIX_DB_PATH` | No | `"lambda_matrix.db"` | SQLite DB file path (Matrix bot) | -| `MATRIX_STORE_PATH` | No | `"matrix_store"` | nio E2EE store directory | -| `LAMBDA_PLATFORM_URL` | No** | `http://localhost:8000` | Lambda platform base URL | -| `LAMBDA_SERVICE_TOKEN`| No** | — | Service auth token for Lambda API | -| `PLATFORM_MODE` | No | `"mock"` | `"mock"` or `"production"` | - -\* Required for the respective bot to function. -\*\* Only required when `PLATFORM_MODE=production`. - -**Secrets location:** -- `.env` file (gitignored) -- Never committed — `.env.example` provides template -- Loaded via `python-dotenv` at module import in each `bot.py` entry point - -## Webhooks & Callbacks - -**Incoming (platform → bot):** -- `WebhookReceiver.on_agent_event(event: AgentEvent)` — receives async task completion notifications -- Not yet wired to an HTTP endpoint; `MockPlatformClient.simulate_agent_event()` used for testing - -**Outgoing (bot → external):** -- Telegram: all via `aiogram` polling or webhook (no direct outbound HTTP beyond Telegram API) -- Matrix: all via `matrix-nio` `AsyncClient.room_send()`, `room_typing()`, etc. -- Platform: via `PlatformClient` send/stream methods - ---- - -*Integration audit: 2026-04-01* +## Файловая система (Shared Volume) +- **Тип**: Docker Shared Volume (`/agents/`) +- **Назначение**: Прямая передача файлов между поверхностью и агентами в обход сети. Поверхность пишет файлы в поддиректорию конкретного агента, агент их читает, и наоборот. diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md index 708a4bf..b40772d 100644 --- a/.planning/codebase/STACK.md +++ b/.planning/codebase/STACK.md @@ -1,113 +1,14 @@ -# Technology Stack +# Технологический стек (STACK.md) -**Analysis Date:** 2026-04-01 +## Язык и Runtime +- **Python**: 3.11-slim (используется в Docker-образах) +- **Пакетный менеджер**: `uv` (используется для быстрой и строгой установки зависимостей, frozen lockfiles). -## Languages +## Ключевые библиотеки +- **matrix-nio**: Асинхронный клиент для Matrix (события, синхронизация, отправка). +- **pydantic**: Для валидации структур данных (события из AgentApi). +- **structlog**: Структурированное логирование (json/console). -**Primary:** -- Python 3.11+ — all application code (enforced via `pyproject.toml` `requires-python = ">=3.11"`) - -**Type Annotations:** -- Full `from __future__ import annotations` usage throughout -- `typing.Protocol` used for dependency inversion (`core/store.py`, `sdk/interface.py`) - -## Runtime - -**Environment:** -- CPython — runtime (development host currently runs 3.14.3) -- Minimum: Python 3.11 (uses `match`-compatible union syntax, `Self`, `X | Y` type hints) - -**Package Manager:** -- `uv` 0.9.30 (Homebrew) -- Lockfile: `uv.lock` present and committed -- Install: `uv sync` - -## Frameworks - -**Telegram Bot:** -- `aiogram` 3.26.0 — async Telegram Bot API framework - - Used in `adapter/telegram/` (planned; directory not yet present in main branch) - - Brings in `aiohttp` 3.13.3 as its HTTP transport - -**Matrix Bot:** -- `matrix-nio` 0.25.2 — async Matrix Client-Server API client - - Used in `adapter/matrix/bot.py` - - Key classes: `AsyncClient`, `AsyncClientConfig`, `RoomMessageText`, `ReactionEvent`, `InviteMemberEvent`, `RoomMemberEvent`, `MatrixRoom` - - Long-polling via `client.sync_forever(timeout=30000)` - -**Data Validation:** -- `pydantic` 2.12.5 — data models in `sdk/interface.py` - - `User`, `Attachment`, `MessageResponse`, `MessageChunk`, `UserSettings`, `AgentEvent`, `PlatformError` - - Core protocol structs (`core/protocol.py`) use plain `dataclasses` instead - -**Build/Dev:** -- `setuptools` ≥68 + `setuptools-scm` + `wheel` — build backend (`pyproject.toml`) -- `ruff` 0.15.8 — linting and import sorting (`line-length = 100`, `target-version = "py311"`, rules: E, F, I, UP, B) -- `mypy` 1.19.1 — static type checking - -## Key Dependencies - -**Critical:** -- `aiogram>=3.4,<4` (resolved: 3.26.0) — Telegram adapter; pin avoids breaking v4 API -- `matrix-nio>=0.21` (resolved: 0.25.2) — Matrix adapter; async-only client -- `pydantic>=2.5` (resolved: 2.12.5) — SDK interface models; v2 required (v1 incompatible) - -**Infrastructure:** -- `structlog` 25.5.0 — structured logging throughout; used via `structlog.get_logger(__name__)` -- `python-dotenv` 1.2.2 — loads `.env` at bot startup (`load_dotenv(Path(...) / ".env")`) -- `httpx` 0.28.1 — available for HTTP calls (future SDK integration, not yet used in core logic) - -**Async I/O:** -- `aiohttp` 3.13.3 — transitive via aiogram; provides HTTP session to Telegram Bot API -- `asyncio` — stdlib; used directly in `sdk/mock.py` (`asyncio.sleep` for latency simulation) and all bot entry points (`asyncio.run(main())`) - -## Testing - -**Runner:** -- `pytest` 9.0.2 -- `pytest-asyncio` 1.3.0 — `asyncio_mode = "auto"` (set in `pyproject.toml`) -- `pytest-cov` 7.1.0 — coverage reporting - -**Configuration:** -- `pyproject.toml` `[tool.pytest.ini_options]`: `testpaths = ["tests"]`, `pythonpath = ["."]` -- `conftest.py` at project root - -## Internal Module Structure - -**Core (no external deps except stdlib + pydantic via sdk):** -- `core/protocol.py` — `dataclasses`-based unified event types -- `core/store.py` — `StateStore` Protocol + `InMemoryStore` (dict) + `SQLiteStore` (stdlib `sqlite3`) -- `core/handler.py` — `EventDispatcher` -- `core/auth.py`, `core/chat.py`, `core/settings.py` — domain managers - -**SDK Layer:** -- `sdk/interface.py` — `PlatformClient` Protocol (pydantic models) -- `sdk/mock.py` — `MockPlatformClient` in-process stub; simulates latency via `asyncio.sleep` - -**Adapters:** -- `adapter/matrix/` — matrix-nio integration (active) -- `adapter/telegram/` — aiogram integration (referenced in deps, worktree branch exists) - -## Configuration - -**Environment:** -- Loaded from `.env` via `python-dotenv` at startup -- See `INTEGRATIONS.md` for full variable list - -**Build:** -- `pyproject.toml` — single source of truth for deps, build, lint, test config - -## Platform Requirements - -**Development:** -- Python ≥3.11 -- `uv` for dependency management - -**Production:** -- Any environment with Python ≥3.11 -- Matrix bot: requires writable filesystem path for `matrix_store/` (nio E2EE store) and SQLite DB -- Telegram bot: stateless beyond env vars (or optionally SQLite for persistence) - ---- - -*Stack analysis: 2026-04-01* +## Инфраструктура +- **Docker / Docker Compose**: Используется для локального (fullstack) и продакшн развертывания. +- **SQLite**: Легковесная локальная база данных для хранения маппингов пользователей/комнат (`adapter/matrix/store.py`). diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md index 08896a5..9ea8a18 100644 --- a/.planning/codebase/STRUCTURE.md +++ b/.planning/codebase/STRUCTURE.md @@ -1,210 +1,18 @@ -# Codebase Structure +# Структура (STRUCTURE.md) -**Analysis Date:** 2026-04-01 - -## Directory Layout - -``` -surfaces-bot/ -├── adapter/ -│ ├── __init__.py -│ └── matrix/ # matrix-nio adapter (merged to main) -│ ├── __init__.py -│ ├── bot.py # Entry point, MatrixBot class, send_outgoing() -│ ├── converter.py # nio Event → IncomingEvent -│ ├── reactions.py # Emoji constants, skills text builder -│ ├── room_router.py # room_id → chat_id resolution -│ ├── store.py # Matrix-specific StateStore helpers (room meta, user meta) -│ └── handlers/ -│ ├── __init__.py # register_matrix_handlers() -│ ├── auth.py # handle_invite (invite member event) -│ ├── chat.py # Chat creation (creates real Matrix rooms) -│ ├── confirm.py # Confirmation flow callbacks -│ └── settings.py # Settings sub-commands and toggle_skill -├── core/ -│ ├── auth.py # AuthManager: start_flow, confirm, is_authenticated -│ ├── chat.py # ChatManager: get_or_create, list_active, rename, archive -│ ├── handler.py # EventDispatcher: register, dispatch, _routing_key -│ ├── protocol.py # All shared dataclasses and type aliases -│ ├── settings.py # SettingsManager: get (cached), apply (invalidates cache) -│ ├── store.py # StateStore Protocol, InMemoryStore, SQLiteStore -│ └── handlers/ -│ ├── __init__.py # register_all() — binds all core handlers to dispatcher -│ ├── callback.py # handle_confirm, handle_cancel, handle_toggle_skill -│ ├── chat.py # handle_new_chat, handle_rename, handle_archive, handle_list_chats -│ ├── message.py # handle_message — auth guard + platform.send_message -│ ├── settings.py # handle_settings — displays settings menu -│ └── start.py # handle_start — get_or_create_user + welcome message -├── sdk/ -│ ├── __init__.py -│ ├── interface.py # PlatformClient Protocol, WebhookReceiver Protocol, Pydantic models -│ └── mock.py # MockPlatformClient — full in-memory implementation -├── tests/ -│ ├── __init__.py -│ ├── conftest.py # (root conftest — sys.path fix for local sdk/ shadowing stdlib) -│ ├── adapter/ -│ │ ├── __init__.py -│ │ ├── matrix/ -│ │ │ ├── __init__.py -│ │ │ ├── test_converter.py -│ │ │ ├── test_dispatcher.py -│ │ │ ├── test_reactions.py -│ │ │ └── test_store.py -│ │ └── test_forum_db.py # untracked — forum DB exploration -│ ├── core/ -│ │ ├── test_auth.py -│ │ ├── test_chat.py -│ │ ├── test_dispatcher.py -│ │ ├── test_integration.py -│ │ ├── test_protocol.py -│ │ ├── test_settings.py -│ │ ├── test_store.py -│ │ └── test_voice_slot.py -│ └── platform/ -│ └── test_mock.py -├── docs/ # All human documentation -├── .planning/ # GSD planning artefacts -│ └── codebase/ # Codebase map documents (this directory) -├── .claude/ -│ └── agents/ # Agent configuration files -├── .worktrees/ -│ └── telegram/ # Telegram adapter on feat/telegram-adapter branch -│ └── ... # Mirrors main layout; merged separately -├── conftest.py # Root pytest conftest: sys.path hack for local sdk/ -├── pyproject.toml # Project metadata, dependencies, ruff + pytest config -├── uv.lock # Lockfile (uv) -├── lambda_matrix.db # SQLite DB written by Matrix bot (gitignored) -└── .env.example # Environment variable template -``` - -## Directory Purposes - -**`core/`:** -- Purpose: Platform-neutral business logic. Never imports from `adapter/`. -- Key files: `protocol.py` (all shared types), `handler.py` (dispatcher), `store.py` (persistence interface) -- Add new domain logic here; keep it free of aiogram/matrix-nio imports - -**`core/handlers/`:** -- Purpose: One async function per command/callback/message type. Each returns `list[OutgoingEvent]`. -- Registration: `register_all()` in `core/handlers/__init__.py` binds them to the dispatcher -- Adapters can override any key by calling `dispatcher.register(event_type, key, fn)` after `register_all()` - -**`sdk/`:** -- Purpose: Contract (`interface.py`) and mock (`mock.py`) for the Lambda AI platform SDK -- Note: The directory is named `sdk/` in actual code (not `platform/` as CLAUDE.md describes); `handler.py` imports from `sdk.interface` -- When real SDK arrives: replace `sdk/mock.py` only; `sdk/interface.py` must not change unless the contract changes - -**`adapter/matrix/`:** -- Purpose: Everything matrix-nio-specific. Translates between nio and core protocol. -- `bot.py` owns `MatrixBot`, `build_runtime()`, `send_outgoing()`, and `main()` -- `store.py` provides key-namespaced helpers on top of `StateStore` (not a separate store implementation) -- `room_router.py` maintains the `room_id → chat_id` mapping persisted in `StateStore` - -**`adapter/telegram/`:** -- Purpose: aiogram 3.x adapter. Lives in `.worktrees/telegram/` on `feat/telegram-adapter` branch. -- Uses aiogram FSM states (`states.py`) and inline keyboards (`keyboards/`) -- Not yet merged to `main` - -**`tests/`:** -- Purpose: pytest test suite mirroring the source tree -- `tests/core/` — unit tests for each core module -- `tests/adapter/matrix/` — Matrix adapter tests (converter, dispatcher, reactions, store) -- `tests/platform/` — MockPlatformClient tests - -**`docs/`:** -- Purpose: Human-readable design documents; not consumed by code -- Key docs: `docs/surface-protocol.md` (unification rationale), `docs/api-contract.md` (SDK contract), `docs/telegram-prototype.md`, `docs/matrix-prototype.md` - -## Key File Locations - -**Entry Points:** -- `adapter/matrix/bot.py` — Matrix bot `main()`, run via `python -m adapter.matrix.bot` -- `.worktrees/telegram/adapter/telegram/bot.py` — Telegram bot entry (feature branch) - -**Shared Protocol:** -- `core/protocol.py` — single source of truth for all inter-layer data types - -**SDK Contract:** -- `sdk/interface.py` — `PlatformClient` Protocol; defines the API surface for the real SDK -- `sdk/mock.py` — `MockPlatformClient`; current runtime implementation - -**Dispatcher Registration:** -- `core/handlers/__init__.py` — `register_all()` for platform-agnostic handlers -- `adapter/matrix/handlers/__init__.py` — `register_matrix_handlers()` for Matrix overrides - -**Persistence:** -- `core/store.py` — `StateStore` Protocol, `InMemoryStore`, `SQLiteStore` -- `adapter/matrix/store.py` — Matrix-specific store helper functions (not a store implementation) - -**Configuration:** -- `pyproject.toml` — dependencies, pytest config (`asyncio_mode = "auto"`, `pythonpath = ["."]`), ruff config -- `conftest.py` — `sys.path` insert so local `sdk/` shadows stdlib `platform` module - -## Naming Conventions - -**Files:** -- Modules: `snake_case.py` -- Entry points: `bot.py` per adapter -- Converter: `converter.py` per adapter -- Handlers directory: `handlers/` per layer - -**Classes:** -- Managers: `{Domain}Manager` (e.g. `ChatManager`, `AuthManager`, `SettingsManager`) -- Bot runtime: `{Platform}Bot` (e.g. `MatrixBot`) -- Protocol types: PascalCase dataclasses (e.g. `IncomingMessage`, `OutgoingUI`) -- SDK types: PascalCase Pydantic models (e.g. `MessageResponse`, `UserSettings`) - -**Handler functions:** -- `handle_{command}` for command handlers (e.g. `handle_start`, `handle_new_chat`) -- `make_handle_{command}` for factory functions that close over adapter state (e.g. `make_handle_new_chat(client, store)`) - -**State keys:** -- `"{namespace}:{discriminator}"` — always use the prefix constants defined in `adapter/matrix/store.py` - -## Where to Add New Code - -**New core command handler:** -1. Add `async def handle_{cmd}(event, chat_mgr, auth_mgr, settings_mgr, platform) -> list` in `core/handlers/{category}.py` -2. Register it in `core/handlers/__init__.py:register_all()` with `dispatcher.register(IncomingCommand, "{cmd}", handle_{cmd})` -3. Write tests in `tests/core/test_dispatcher.py` or a dedicated `tests/core/test_{category}.py` - -**New Matrix-specific handler (needs nio client or matrix store):** -1. Add handler in `adapter/matrix/handlers/{category}.py` -2. Register in `adapter/matrix/handlers/__init__.py:register_matrix_handlers()` — this overrides the core handler for that key - -**New protocol type:** -- Add dataclass to `core/protocol.py`; update `IncomingEvent` or `OutgoingEvent` union aliases if it crosses layer boundaries -- Update `EventDispatcher._routing_key()` if it requires a new dispatch strategy - -**New StateStore key namespace:** -- Add prefix constant and helper functions in `adapter/matrix/store.py` (for Matrix-specific state) or directly in the relevant manager (for core state) - -**New test:** -- Unit tests for core logic: `tests/core/test_{module}.py` -- Adapter tests: `tests/adapter/matrix/test_{module}.py` -- Use `InMemoryStore` as the store; use `MockPlatformClient` as the platform client - -## Special Directories - -**`.worktrees/telegram/`:** -- Purpose: Git worktree for `feat/telegram-adapter` branch; full copy of the repo root -- Generated: Yes (via `git worktree add`) -- Committed: No (worktrees are local) - -**`.planning/`:** -- Purpose: GSD planning artefacts — phase plans and codebase maps -- Generated: Yes (by `/gsd:` commands) -- Committed: Yes (tracked with the repo) - -**`.claude/agents/`:** -- Purpose: Agent role configuration files for the multi-agent workflow -- Committed: Yes - -**`src/`:** -- Purpose: Contains only `surfaces_bot.egg-info/` (setuptools build artefact); no source code -- Generated: Yes -- Committed: No - ---- - -*Structure analysis: 2026-04-01* +- `core/`: + - `protocol.py` — Унифицированные структуры данных (сообщения, файлы, UI). +- `adapter/matrix/`: + - `bot.py` — Главный event-loop Matrix. + - `converter.py` — Конвертация событий Matrix-nio ⇄ `core/protocol.py`. + - `agent_registry.py` — Парсинг `matrix-agents.yaml`. + - `files.py` — Работа с вложениями и shared volume. + - `store.py` — SQLite база для маппинга чатов Matrix и `platform_chat_id`. + - `routed_platform.py` — Динамический роутинг вызовов к нужным агентам на лету. +- `sdk/`: + - `interface.py` — Интерфейс PlatformClient. + - `real.py` — Имплементация WebSocket клиента (`AgentApi`). + - `mock.py` — Мок-клиент для E2E тестов без платформы. +- `config/`: Конфиги маршрутизации (YAML). +- `docs/`: Актуальная документация по развертыванию и архитектуре. +- `docker-compose*.yml`: Продакшн и локальные манифесты для сборки. diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md index f685abc..07311dc 100644 --- a/.planning/codebase/TESTING.md +++ b/.planning/codebase/TESTING.md @@ -1,210 +1,17 @@ -# Testing Patterns +# Тестирование (TESTING.md) -**Analysis Date:** 2026-04-01 +## Unit-тесты +Расположены в `tests/`. Покрытие сфокусировано на логике Matrix адаптера (пока он является основной поверхностью): +- Файловый контракт (`test_files.py`) +- Диспетчер и конвертация (`test_dispatcher.py`) +- Взаимодействие с PlatformClient (`test_routed_platform.py`) +- Работа с контекстными командами бота (`test_context_commands.py`) -## Test Framework +## E2E тестирование +Локально тестируется через запуск контейнеров из `docker-compose.fullstack.yml`, который поднимает один инстанс бота и один локальный `platform-agent`. Это позволяет имитировать полную цепочку взаимодействия (Matrix -> Бот -> Агент) с общим каталогом для файлов. -**Runner:** pytest 8.x -**Config:** `pyproject.toml` `[tool.pytest.ini_options]` - -```toml -[tool.pytest.ini_options] -asyncio_mode = "auto" -testpaths = ["tests"] -pythonpath = ["."] -``` - -**Async support:** pytest-asyncio with `asyncio_mode = "auto"` — all `async def` test functions run automatically without decorators. - -**Coverage:** pytest-cov (available but no minimum threshold configured) - -**Run commands:** +## Запуск тестов ```bash -pytest tests/ -v # all tests -pytest tests/core/ -v # core layer only -pytest tests/adapter/telegram/ -v # telegram adapter only -pytest tests/adapter/matrix/ -v # matrix adapter only -pytest tests/ --cov=. --cov-report=term # with coverage report +# Запуск юнит-тестов (только для Matrix адаптера) +pytest tests/adapter/matrix/ -v ``` - -## Test Directory Structure - -``` -tests/ -├── __init__.py -├── core/ -│ ├── test_auth.py — AuthManager unit tests -│ ├── test_chat.py — ChatManager unit tests -│ ├── test_dispatcher.py — EventDispatcher routing tests -│ ├── test_integration.py — full flow smoke tests (dispatcher + managers + mock) -│ ├── test_protocol.py — dataclass defaults and construction -│ ├── test_settings.py — SettingsManager unit tests -│ ├── test_store.py — InMemoryStore + SQLiteStore tests -│ └── test_voice_slot.py — handle_message() handler unit tests -├── adapter/ -│ ├── __init__.py -│ ├── test_forum_db.py — Telegram SQLite DB helpers (untracked, new) -│ └── matrix/ -│ ├── __init__.py -│ ├── test_converter.py — matrix-nio event → IncomingEvent converter -│ ├── test_dispatcher.py — full Matrix bot integration (build_runtime) -│ ├── test_reactions.py — reaction text builders and emoji mapping -│ └── test_store.py — Matrix store helper functions -└── platform/ - └── test_mock.py — MockPlatformClient behavior -``` - -Tests mirror the source tree. New tests for `adapter/telegram/` go in `tests/adapter/telegram/` (directory exists in `.worktrees/telegram` branch, not yet merged to main). - -## conftest.py - -`conftest.py` at project root (`/Users/a/MAI/sem2/lambda/surfaces-bot/conftest.py`) handles a sys.path conflict: the project has a local `platform/` (now `sdk/`) package that shadows Python's stdlib `platform` module. It inserts the project root at `sys.path[0]` and removes the cached stdlib `platform` module. - -No shared fixtures are defined in `conftest.py`. All fixtures are local to test files. - -## Test Structure - -**Fixture pattern — local to each test file:** -```python -@pytest.fixture -def mgr(): - return AuthManager(MockPlatformClient(), InMemoryStore()) - -@pytest.fixture -def store() -> InMemoryStore: - return InMemoryStore() -``` - -**Async tests require no decorator** (asyncio_mode = "auto"): -```python -async def test_not_authenticated_initially(mgr): - assert await mgr.is_authenticated("u1") is False -``` - -**Sync tests** are used for pure-function tests (protocol dataclass construction, reaction text builders): -```python -def test_incoming_message_defaults(): - msg = IncomingMessage(user_id="u1", platform="telegram", chat_id="C1", text="hi") - assert msg.attachments == [] -``` - -**Integration fixture pattern** — builds full runtime in-process: -```python -@pytest.fixture -def dispatcher(): - platform = MockPlatformClient() - store = InMemoryStore() - d = EventDispatcher( - platform=platform, - chat_mgr=ChatManager(platform, store), - auth_mgr=AuthManager(platform, store), - settings_mgr=SettingsManager(platform, store), - ) - register_all(d) - return d -``` - -## Mocking Strategy - -**Primary mock: `MockPlatformClient`** from `sdk/mock.py` - -All tests use `MockPlatformClient()` directly — it is the real mock for the SDK layer. No unittest.mock patching of `MockPlatformClient` is needed. - -**`unittest.mock.AsyncMock`** is used only when testing integration with external clients (matrix-nio `AsyncClient`): -```python -from unittest.mock import AsyncMock - -client = SimpleNamespace( - room_create=AsyncMock(return_value=SimpleNamespace(room_id="!r2:example")) -) -``` - -**`types.SimpleNamespace`** is used to fabricate matrix-nio event objects without importing the full nio library: -```python -def text_event(body: str, sender: str = "@a:m.org", event_id: str = "$e1"): - return SimpleNamespace( - sender=sender, body=body, event_id=event_id, msgtype="m.text", replyto_event_id=None - ) -``` -This is the pattern for all matrix converter tests — define factory functions at module level that return `SimpleNamespace` objects. - -**`tmp_path` pytest fixture** is used for SQLiteStore tests to get a throwaway database file: -```python -async def test_sqlite_set_and_get(tmp_path): - store = SQLiteStore(str(tmp_path / "test.db")) -``` - -**`monkeypatch.setenv`** is used in `tests/adapter/test_forum_db.py` to inject `DB_PATH` env var and reload the module with a fresh database: -```python -@pytest.fixture(autouse=True) -def fresh_db(tmp_path, monkeypatch): - db_file = str(tmp_path / "test.db") - monkeypatch.setenv("DB_PATH", db_file) - import importlib - import adapter.telegram.db as db_mod - importlib.reload(db_mod) - db_mod.init_db() - return db_mod -``` - -**What NOT to mock:** -- `InMemoryStore` — use it directly; it's a real in-memory implementation -- `MockPlatformClient` — use it directly; patching it defeats the purpose -- Core manager classes (`AuthManager`, `ChatManager`, `SettingsManager`) — always instantiate real ones - -## Test Data Patterns - -**User IDs:** short strings like `"u1"`, `"u2"`, `"tg_123"`, `"@alice:m.org"` - -**Chat IDs:** `"C1"`, `"C2"`, `"C3"` — matches the workspace slot naming - -**Platform strings:** literal `"telegram"` or `"matrix"` - -**Room IDs:** `"!r:m.org"`, `"!dm:example.org"` — valid Matrix room ID format - -No shared factories or fixtures files. Test data is constructed inline or via simple factory functions local to the test module. - -## What Is Tested - -| Area | Status | -|------|--------| -| `core/protocol.py` — dataclass defaults | Covered (`test_protocol.py`) | -| `core/store.py` — InMemoryStore + SQLiteStore | Covered (`test_store.py`) | -| `core/auth.py` — AuthManager | Covered (`test_auth.py`) | -| `core/chat.py` — ChatManager | Covered (`test_chat.py`) | -| `core/settings.py` — SettingsManager | Covered (`test_settings.py`) | -| `core/handler.py` — EventDispatcher routing | Covered (`test_dispatcher.py`) | -| `core/handlers/message.py` — handle_message | Covered (`test_voice_slot.py`) | -| Full dispatcher + all core handlers integration | Covered (`test_integration.py`) | -| `sdk/mock.py` — MockPlatformClient | Covered (`test_mock.py`) | -| `adapter/matrix/converter.py` — event parsing | Covered (`test_converter.py`) | -| `adapter/matrix/store.py` — store helpers | Covered (`test_store.py`) | -| `adapter/matrix/reactions.py` — text builders | Covered (`test_reactions.py`) | -| `adapter/matrix/bot.py` — MatrixBot + build_runtime | Covered (`test_dispatcher.py`) | -| `adapter/telegram/db.py` — SQLite helpers | Covered (`test_forum_db.py`, untracked) | - -## Coverage Gaps - -**Telegram adapter handlers** — `adapter/telegram/handlers/` (`auth.py`, `chat.py`, `confirm.py`, `forum.py`, `settings.py`) have no tests in `main`. Tests exist only in `.worktrees/telegram` branch (not yet merged). - -**Telegram converter** — `adapter/telegram/converter.py` has no tests in `main`. - -**`core/handlers/callback.py` and `core/handlers/settings.py`** — tested indirectly through integration tests but lack dedicated unit tests. - -**`adapter/matrix/room_router.py`** — `resolve_chat_id` has no direct unit tests; exercised only through `MatrixBot.on_room_message` integration path. - -**`adapter/matrix/handlers/`** — individual handler files (`auth.py`, `chat.py`, `confirm.py`, `settings.py`) are tested only via `test_dispatcher.py` integration; no isolated unit tests. - -**`sdk/mock.py` streaming** — `stream_message` is not tested; only `send_message` is covered. - -**Error paths** — `ChatManager.rename` raises `ValueError` when chat not found; no test exercises this path. Same for `ChatManager.archive`. - -## Naming Conventions - -- Test functions: `test_` — descriptive, no abbreviations -- Fixture names match the object they create: `mgr`, `store`, `dispatcher`, `deps` -- Factory functions in converter tests: `text_event()`, `file_event()`, `image_event()`, `reaction_event()` - ---- - -*Testing analysis: 2026-04-01*