Skip to main content
Glama

img.png

recall.select

Минималистичная агентная система памяти — передайте один URL любому агенту, и он получит долговременную память с почти нулевой настройкой. Построена на Qdrant + FastMCP + FastAPI/Bootstrap.

Полное описание дизайна и поэтапный план сборки см. в docs/specs/initial_specification.md, а историю заметных изменений — в docs/specs/changelog.md.

Как это работает

Память хранится в виде векторов. Каждое хранилище памяти — это коллекция Qdrant, сопоставленная один к одному с парой (user, project). Метаданные вокруг этих векторов — пользователи, API-ключи, проекты и статистика использования/лимитов по коллекциям — хранятся в MongoDB.

flowchart LR
    agent[Agent] --> web[FastAPI / MCP]
    web <-->qdrant[Qdrant]
    web <--> mongo[MongoDB]
    web <--> embed[Embedding API]

Коллекции Qdrant создаются лениво: ничего не обращается к Qdrant, пока первое воспоминание не будет сохранено в пару (user, project).

Related MCP server: Firmament Open Brain

Архитектура

  • app/main.py — приложение FastAPI. Обслуживает Bootstrap-лендинг и при запуске проверяет существование индексов Mongo (устойчиво к холодной/удаленной БД).

  • app/mcp_server.py — MCP-сервер, стоящий за ссылкой на память. MCP-клиент агента указывает на {PUBLIC_BASE_URL}/m/{key} (Streamable HTTP, без состояния, JSON-ответы); API-ключ в пути является единственным учетным данным и ограничивает инструменты проектом по умолчанию владельца ключа. Базовые инструменты: store_memory / recall_memory / delete_memory. Инструменты семантического слоя (см. vector_semantics.py): link_memories / unlink_memories / annotate_memory / memory_connections / recall_connected — подключенный агент выполняет рассуждения о связях на стороне клиента (только по явному запросу), а эти инструменты принимают или обходят результат. Тот же ключ можно отправить как Authorization: Bearer к конечной точке /mcp без ключа, чтобы скрыть секрет из URL/логов. {...}/m/{key}.md (в app/api/connect.py) обслуживает соответствующие инструкции по настройке (обе формы).

  • app/dependencies.py — основной DI-контейнер (injector). Создает общие синглтоны (клиент Qdrant, клиент/БД Mongo, удаленный эмбеддер). Зависимости FastAPI (app/api/deps.py) и код запуска разрешаются из app_container, а не создают клиенты самостоятельно.

  • app/services/ — сервисный слой (без HTTP/кода маршрутов, только ввод-вывод):

    • qdrant_store.py — клиент Qdrant + ensure_collection/upsert_memory/search/delete_memory, а также примитивы уровня точек, необходимые семантическому слою (neighbors, scroll_points, retrieve_points, set_payload).

    • vector_semantics.py — слой утилит векторной памяти: рассматривает хранилище как граф смыслов. Зарезервированное пространство имен _semantics в полезной нагрузке каждой точки содержит дейктические якоря (владелец, время сохранения; записываются при сохранении), извлеченные клиентом сущности и объявленные клиентом типизированные отношения (upsert_relations проверяет и сохраняет их — никаких LLM-вызовов на стороне сервера). Объявленные отношения имеют две качественные оговорки: confidence (0-1], масштабирует силу обхода ребра) и valid_till (ISO 8601; просроченные ребра игнорируются всеми путями чтения, поэтому устаревшая структура самоуничтожается). Гигиена: remove_relations удаляет неверные ребра (корректирующий двойник upsert_relations), а memory.delete_memory вызывает prune_relations_to, чтобы после удаления памяти не осталось висящих ребер. Подключаемые линзы (topical/temporal/entity/declared) выводят типизированные ребра; поверх них работают semantic_graph (мультиграф), spreading_activation (извлечение по связям), concept_clusters (эмерджентная онтология) и infer_relation (сначала объявленная истина, затем геометрические эвристики). Заметка о производительности: поиск входящих ребер (relations_of(include_incoming=True)) сегодня представляет собой ограниченный scroll-and-scan. Если обратный обход станет горячим путем, исправлением будет индекс полезной нагрузки Qdrant на _semantics.relations[].target (create_payload_index, схема ключевого слова) и фильтрованный запрос вместо сканирования — то же хранилище, только индекс; ничего в схеме не меняется.

    • mongo.py — клиент Mongo, get_db() и ensure_indexes() (обеспечивает правило «один к одному» (user, project) с помощью уникального составного индекса).

    • users.py — add_user, get_user, get_user_by_email, update_user.

    • api_keys.py — ключи, привязанные к пользователю, хранятся в виде хеша SHA-256 (открытый текст возвращается один раз, из add_api_key, и никогда не сохраняется): add_api_key, delete_api_key, delete_user_keys, list_api_keys, get_labeled_key, get_by_key (хеширует представленный токен и сопоставляет с дайджестом; record_use=True на шлюзе аутентификации MCP штампует last_used_at). В состоянии покоя каждый ключ также хранит несекретные подсказки для отображения — key_prefix + key_last4, отображаемые masked() как rs_ab12…wxyz — чтобы ключи можно было перечислить и различить, не раскрывая секрет повторно.

    • projects.py — add_project, get_project, list_projects, update_project, delete_project.

    • collections.py — реестр соответствия (user, project) ↔ коллекция Qdrant. collection_name(user_id, project_id) — внутренний стандарт именования (rs_{user}_{project}); отслеживает points_count/calls_count для лимитов и статистики.

    • collection_provisioning.py — двухсторонний шаг create_collection / destroy_collection. Коллекция существует только тогда, когда существуют и ее строка в реестре Mongo, и ее резервная коллекция Qdrant; это объединяет реестр collections с qdrant_store в одну атомарную, идемпотентную операцию, чтобы два хранилища никогда не рассинхронизировались. Создание лениво, поэтому его единственный вызывающий — первая запись памяти (memory.store_memory); API коллекции для удаления использует destroy_collection.

    • embeddings.py — абстракция Embedder; embeddings_remote.py — конкретный бэкенд текст→вектор (удаленное API эмбеддингов, например DeepInfra).

    • monobank.py — минимальный клиент эквайринга Monobank (create_invoice, fetch_invoice_status) плюс аутентификация вебхука (fetch_pubkey / verify_signature, ECDSA-SHA256 по сырому телу). Использует тот же мерчант-токен mcp-api.net; recall.select владеет собственными счетом/редиректом/вебхуком.

    • billing.py — каталог тарифов и запись об оплате, привязанная к invoiceId Monobank. record_pending при оформлении заказа; apply_webhook переключает tier покупателя один раз при success (идемпотентно к повторным попыткам/дубликатам); reconcile обрабатывает то, что пропустил вебхук (см. ниже). Тариф ограничен по времени: grant_tier — единственное место, где когда-либо выдается право (оплаченный счет или добрая воля владельца), записывая tier_expires_at плюс строку аудита в tier_grants; effective_tier(user) — то, что должна читать каждая проверка, так как сохраненный paid_2x, срок которого истек, означает бесплатную учетную запись. Также единственный источник истины для разрешений по тарифам: call_allowance(tier) / project_allowance(tier) (None = безлимитно; неизвестные тарифы возвращаются к бесплатному).

    • usage.py — ежемесячный счетчик вызовов и шлюз ценовой модели. Каждый принятый store/recall/delete учитывается в строке usage для (user, календарный-месяц); check_call_allowed отклоняет вызов, когда израсходован месячный call_allowance тарифа, вызывая QuotaExceeded. Применяется в memory.py (так что охвачены как MCP-инструменты, так и HTTP API памяти) и преобразуется в HTTP 429 в app/main.py; транспорт MCP отображает это как ошибку инструмента. Отдельно от общего collections.calls_count.

    • account.py — доступный только для чтения снимок, который показывает страница /account для вошедшего пользователя (тариф, ежемесячное использование, количество сохраненных элементов по проектам и список API-ключей в замаскированной форме с датами создания/последнего использования), составленный из billing/usage/projects/collections/api_keys.

    • docs.py — контент для публичных руководств по интеграции /docs. Собирает конфигурацию MCP-клиента в одном месте (mcp_config / mcp_config_json), повторно используемую как страницами документации, так и app/api/connect.py для .md по ключу, так что они никогда не расходятся. INTEGRATIONS — реестр руководств (добавьте страницу, добавив запись).

Публичные страницы (обслуживаются из app/main.py, Bootstrap + Jinja, i18n через app/translations/*.yml): лендинг /, /plans, /account (для вошедших) и руководства /docs/integrations. Встроенная документация API FastAPI перемещена с /docs на /api/docs (/api/redoc, /api/openapi.json), чтобы публичный сайт владел /docs.

Платежи работают через HTTP-слой в app/api/payments.py: POST /api/me/checkout (для вошедших) создает счет и возвращает pay_url Monobank; проверенный POST /webhooks/monobank предоставляет тариф; GET /payment/success|fail — это косметические страницы возврата в браузере (право предоставляется через вебхук, никогда через них).

Право не зависит только от вебхука. Monobank отправляет каждое изменение статуса один раз и никогда не пересылает его, поэтому обратный вызов, потерянный из-за перезапуска или сбоя прокси, оставил бы платящего клиента на его старом тарифе, и на нашей стороне не было бы механизма это заметить. Поэтому приложение также выполняет опрос: каждые PAYMENT_RECONCILE_MINUTES фоновый обход (billing.reconcile_with_monobank, запускаемый в жизненном цикле app/main.py) получает реальный статус каждого платежа, все еще находящегося в обработке после пяти минут, и пропускает его через тот же переход apply_webhook. Push и pull идемпотентны друг к другу — какой бы ни сработал первым, предоставляет тариф, другой является no-op. Строка в payments записывается, когда оформление начинается, поэтому created означает «открыл страницу оплаты», а не «оплатил»; /admin/payments показывает это различие явно.

Покупка дает один месяц (SUBSCRIPTION_DAYS), не навсегда: grant_tier штампует tier_expires_at, тот же фоновый цикл возвращает просроченные учетные записи на бесплатный тариф (downgrade_expired), а страница учетной записи показывает дату окончания тарифа. Ничто не продлевается автоматически — пользователь покупает снова, и покупка, пока еще есть кредит, продлевает окно, а не перезапускает его. Право читается через effective_tier, поэтому истекший грант перестает действовать немедленно, даже до того, как обход перезапишет сохраненное поле.

Поскольку ничто не продлевается само по себе, приложение спрашивает: billing.renewal_state(user) отображает подсказку на /account — предупреждение с кнопкой «Продлить» в один клик в последние RENEWAL_WARNING_DAYS (7) действия тарифа и подсказку «ваш тариф закончился, продлите его» в течение LAPSED_PROMPT_DAYS (30) после этого (обход понижения записывает lapsed_tier / tier_lapsed_at, чтобы страница все еще могла показать, что закончилось). Продление отправляет POST на тот же /api/me/checkout, который использует страница тарифов, с предустановленным тарифом, который был у пользователя. Пока нет email — подсказка доходит только до пользователей, посещающих сайт.

Каждая CRUD-функция принимает необязательный аргумент db=/client=, чтобы ее можно было использовать в тестах без работающего бэкенда.

Конфигурация

Задается через окружение (локальный .env загружается автоматически; никогда не коммитьте его — см. .env.example):

Переменная

По умолчанию

Назначение

MONGODB_URI

(обязательно)

Строка подключения к удаленной управляемой MongoDB.

MONGODB_DB

recall_select

Имя базы данных.

QDRANT_URL

http://qdrant:6333

Конечная точка Qdrant (внутренняя сеть Compose).

QDRANT_API_KEY

(нет локально; обязательно в продакшене)

Общий секрет между приложением и Qdrant. Compose устанавливает QDRANT__SERVICE__API_KEY Qdrant из него, и приложение отправляет его в каждом запросе. Это единственный шлюз на панели управления qdrant.recall.select, у которой нет собственной аутентификации.

VECTOR_SIZE

768

Размерность вектора для каждой коллекции. У удаленного эмбеддера запрашивается (через параметр API dimensions) возвращать векторы именно этого размера, чтобы они оставались синхронизированными.

EMBEDDING_API_KEY

(обязательно)

Ключ API для удаленного API эмбеддинга.

EMBEDDING_BASE_URL

https://api.deepinfra.com/v1

Базовый URL API эмбеддингов, совместимого с OpenAI.

GOOGLE_CLIENT_ID

(обязательно для входа)

Идентификатор веб-клиента Google OAuth 2.0.

GOOGLE_CLIENT_SECRET

(обязательно для входа)

Секрет клиента Google OAuth 2.0.

SESSION_SECRET

(запасной вариант для разработки)

Подписывает cookie сессии. Установите стабильное значение в продакшене.

PUBLIC_BASE_URL

http://localhost:8000

Публичный источник; формирует ссылку на память + URI перенаправления OAuth.

FORWARDED_ALLOW_IPS

172.25.0.0/16 (compose) / 127.0.0.1 (uvicorn)

Пиры, чьим X-Forwarded-Proto/-For доверяет uvicorn. Compose устанавливает значение по умолчанию на подсеть caddy_net, чтобы перенаправления сохраняли схему https, а логи видели реальный IP клиента; проверьте с помощью docker network inspect caddy_net, если эта сеть была пересоздана.

MONOBANK_API_KEY

(обязательно для платежей)

Токен мерчанта Monobank. Общий с платформой mcp-api.net - один мерчант, один аккаунт; счета различаются по reference.

MONOBANK_REDIRECT_URL

{PUBLIC_BASE_URL}/payment/success

Куда браузер покупателя возвращается после оплаты.

MONOBANK_WEBHOOK_URL

{PUBLIC_BASE_URL}/webhooks/monobank

Серверный обратный вызов, который предоставляет уровень доступа. Должен быть общедоступным.

MONOBANK_WEBHOOK_VERIFY

1

Проверять X-Sign вебхука по публичному ключу мерчанта. Оставляйте включенным везде, где движутся деньги; 0 только для локальной разработки.

PAYMENT_RECONCILE_MINUTES

15

Как часто запрашивать реальный статус текущих платежей у Monobank, чтобы потерянный вебхук не оставил платящего клиента в подвешенном состоянии. 0 отключает проверку.

ADMIN_SECRET

(не задан - область отключена)

Разблокирует область администратора владельца по адресу /admin. Если не задан, каждый маршрут /admin возвращает 404.

ADMIN_SESSION_HOURS

12

Как долго длится разблокированная сессия администратора, прежде чем она снова заблокируется.

Область администратора владельца (/admin)

Окно только для чтения в личную область любого пользователя для поддержки и просмотра того, что видит пользователь. Установите ADMIN_SECRET (сгенерировать: python -c "import secrets; print(secrets.token_urlsafe(32))"), пересоздайте веб-контейнер, затем откройте {PUBLIC_BASE_URL}/admin и введите ключ один раз за сессию. /admin/users выводит список всех учетных записей - доступен поиск по email, имени или ID пользователя - и каждая строка открывает тарифный план этого пользователя, использование за текущий период, проекты с количеством записей памяти и ссылки на память в замаскированной форме.

Границы продуманы: ключ отправляется через POST (никогда не параметр URL, поэтому он не попадает в историю и журналы доступа), повторные неверные попытки блокируют клиента на пять минут, сессия повторно блокируется через ADMIN_SESSION_HOURS, и ни один маршрут здесь ничего не записывает и не раскрывает текст памяти или секреты ключей - владелец видит структуру учетной записи, а не ее содержимое. Если ADMIN_SECRET не задан, область вообще не существует.

Аутентификация (вход через Google)

Вход открывает доступ к ссылке на память: пользователь входит через Google, затем нажимает Copy memory link, чтобы подготовить свой проект по умолчанию + коллекцию + ключ API и получить URL для передачи агенту. Секрет показывается ровно один раз (сохраняется только его хэш): после этого целевая страница показывает ссылку замаскированной (через GET /api/me/link), а кнопка превращается в явное подтвержденное действие "получить новую ссылку" - перегенерация аннулирует старую ссылку, никогда не делая этого молча. Ключами можно управлять на странице /account: замаскированный список, даты создания/последнего использования, создание с меткой (однократное отображение) и отзыв. Чтобы настроить учетные данные Google:

  1. Google Cloud Console → APIs & Services → OAuth consent screen - настройте его (External; добавьте свой email как тестового пользователя, пока приложение не проверено).

  2. Credentials → Create credentials → OAuth client ID → Web application.

  3. Добавьте Authorized redirect URI: {PUBLIC_BASE_URL}/auth/callback - например, http://localhost:8000/auth/callback для локальной разработки и https://recall.select/auth/callback для продакшена (добавьте оба, если тестируете локально).

  4. Скопируйте Client ID и Client secret в .env (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET) и установите стабильный SESSION_SECRET (python -c "import secrets; print(secrets.token_urlsafe(48))").

Локальный запуск

Полный стек (веб + Qdrant) через Docker Compose:

cp .env.example .env   # then fill in MONGODB_URI
docker compose up --build
# open http://localhost:8000

Или только приложение, используя свой собственный Qdrant/Mongo:

pip install -e ".[dev]"
uvicorn app.main:app --reload

Тесты

pip install -e ".[dev]"
pytest

CRUD-тесты запускаются с MongoDB в памяти (mongomock), а клиенты Qdrant/эмбеддинга имитируются - не требуются работающие бэкенды.

Развертывание

./deploy/deploy.sh

Одна и та же команда работает из двух мест - она определяет, где запущена:

  • С машины разработчика (или из агента): отправляет локальные коммиты, затем запускает развертывание на сервере через SSH-алиас recall-server.

  • На самом сервере (setti@setti-server:~/recall_select$ ./deploy/deploy.sh): развертывает на месте, без SSH-перехода.

Оба пути запускают один и тот же скрипт - deploy/_server_deploy.sh: синхронизация master через git, пересборка стека Compose (FastAPI web + Qdrant), перезагрузка общего прокси Caddy (автоматический HTTPS для recall.select), удаление старых образов. MongoDB является удаленной/управляемой, поэтому переменная окружения auth/MONGODB_URI (см. .env) должна присутствовать на сервере.

Тот, кто запускает это на сервере, должен иметь доступ на чтение к репозиторию GitHub (авторизованный SSH-ключ в своем ~/.ssh) и быть членом группы docker - оба условия верны для claude-agent и setti. Скрипт автоматически регистрирует репозиторий как safe.directory в git, чтобы развертывающий, не являющийся владельцем репозитория, не был заблокирован ошибкой "dubious ownership".

Автоматические развертывания (CI)

Каждый push в master автоматически развертывается через GitHub Actions (.github/workflows/deploy.yml) - тот же процесс, что и выше, только запускаемый CI, а не человеком. Задача подключается по SSH к серверу и передает deploy/_server_deploy.sh через stdin, таким образом выполняя логику развертывания отправленного коммита. Развертывания сериализованы (concurrency), а кнопка Run workflow (workflow_dispatch) позволяет развернуть по требованию.

Одноразовая настройка - добавьте в Settings → Secrets and variables → Actions:

Секрет

Обязательно

Назначение

DEPLOY_SSH_KEY

да

Приватный ключ, чья публичная половина находится в ~/.ssh/authorized_keys пользователя развертывания.

DEPLOY_HOST / DEPLOY_USER

да

Адрес сервера и пользователь SSH для развертывания.

DEPLOY_PORT

нет

Порт SSH (по умолчанию 22).

DEPLOY_KNOWN_HOSTS

нет

Зафиксировать хост-ключ сервера; если не задано, CI доверяет ему при первом использовании через ssh-keyscan.

Секреты приложения (MONGODB_URI, OAuth и т.д.) остаются в .env на сервере - CI их никогда не видит.

Лицензия

Лицензировано в соответствии с GNU Affero General Public License v3.0. Если вы запускаете модифицированную версию в качестве сетевого сервиса, AGPL требует предоставить ее исходный код вашим пользователям. Copyright © 2026 Sergii Setti.

Maintenance

ActivityStale
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables each user to have a private, per-user memory store that persists across AI tools, allowing them to store, recall, search, list, and delete memories via MCP.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to store and semantically retrieve durable memories across sessions via MCP or REST, with tools for remembering, recalling, asking, updating, and forgetting memories.
    6 npm
    MIT