Sitelet https://github.com/xlmax/mediator-bot/blob/master/README.md
Skip to content

Latest commit

 

History

History
482 lines (361 loc) · 37.7 KB

File metadata and controls

482 lines (361 loc) · 37.7 KB

MediatorBot

CI

MediatorBot — экспериментальный приватный посредник для общения двух участников. Каждый участник пишет боту в личном Telegram-чате, а модель решает, нужно ли ответить автору, второму участнику или обоим.

Проект является MVP для технического тестирования идеи. Он не заменяет психолога, семейного терапевта или экстренную помощь.

Поведенческая цель

Бот — тёплый и деятельный посредник, а не пассивный комментатор или постоянное место для накопления претензий. Для значимой незавершённой темы хороший ответ должен не только показать, что человек понят, но и внести движение: отделить факт от интерпретации, вернуть участнику его зону влияния, предложить вариант, первую фразу, ограниченную паузу или конкретный посреднический шаг.

Privacy boundary обеспечивается прежде всего выбором действия и содержания. Бот не проговаривает ограничения без повода; краткое объяснение появляется только при прямом запросе чужих слов или состояния, отказе в передаче либо открытии consent-based запроса. Простая просьба помочь сформулировать сообщение не считается разрешением немедленно отправить его партнёру.

Проактивность не является календарным check-in. Контакт оправдан, только когда он облегчает конкретный следующий шаг: безопасное начало разговора, короткий repair, проверку готовности к определённому действию или ограниченный посреднический процесс. Напоминание о конфликте без ожидаемого движения остаётся внутренней переоценкой и не отправляется.

Возможности

  • одна mediator session с двумя заранее разрешёнными Telegram-аккаунтами;
  • приватная обработка сообщений через Telegram long polling;
  • нативный индикатор typing на время ожидания и обработки сообщения;
  • Fake runtime для локальной проверки без внешней модели;
  • OpenAI Chat Completions и совместимые API, включая собственный endpoint;
  • обязательный ответ модели через один из tool calls;
  • зашифрованная SQLite-база на SQLCipher;
  • восстановление истории после перезапуска;
  • rolling compaction с фиксированным размером памяти и удалением только успешно сжатых сообщений;
  • durable FIFO-обработка Telegram updates одной session по UpdateId с восстановлением после перезапуска;
  • ограниченные повторные попытки и quarantine неисправимых turns без блокировки следующих сообщений;
  • сохранение результата модели до выполнения действий и durable delivery outbox по получателям и частям;
  • уведомление участника только когда его сообщение действительно ожидает compaction;
  • параллельная обработка разных session внутри одного процесса;
  • lossless-разбиение длинных ответов на Telegram-сообщения до 4000 символов;
  • запись подтверждённых частей под единым logical message и пропуск уже доставленных частей при восстановлении;
  • graceful shutdown с прекращением приёма новой работы и ограниченным временем ожидания текущего turn;
  • опциональный live heartbeat, который переоценивает уместность инициативы без фиктивного пользовательского сообщения;
  • отдельные durable initiative decisions и outbox с повторной проверкой свежести перед отправкой;
  • тихие часы 22:00–09:00 МСК, opt-out и жёсткий лимит инициативных контактов;
  • технические логи без текстов переписки, токенов и tool arguments.

Структура решения

Проект Назначение
MediatorBot.Core Доменная модель, application services и абстракции. Не зависит от Telegram и конкретного хранилища.
MediatorBot.Infrastructure SQLCipher/SQLite persistence, Fake runtime и OpenAI-compatible runtime.
MediatorBot.Telegram Telegram UI и composition root на базе TeleFlow.
MediatorBot.Console Консольный стенд для локальной проверки mediation flow.
MediatorBot.BehaviorScenarios Live-harness фиксированных disclosure- и proactive initiative-сценариев с Markdown-transcript.
MediatorBot.Tests Unit- и integration-тесты Core, persistence, OpenAI protocol и Telegram adapter.

Как обрабатывается сообщение

  1. Telegram allowlist проверяет UserId отправителя.
  2. Аккаунт сопоставляется с Participant A или Participant B.
  3. Update атомарно регистрируется и сохраняется в SQLCipher-таблицу PendingTurns; queued updates выбираются по UpdateId.
  4. Между turns проверяется необходимость compaction. Если она уже выполняется, ожидающий участник получает одно нейтральное уведомление; без ожидающих сообщений compaction незаметна.
  5. Для текущей SessionId захватывается process-local lock.
  6. Входящее сообщение сохраняется в зашифрованной базе.
  7. Контекст строится из фиксированной сжатой памяти, свежей общей истории, открытых mediated requests и текущего сообщения.
  8. Модель должна вызвать ровно один инструмент:
    • send_to_participant;
    • send_to_both;
    • open_mediated_request;
    • resolve_mediated_request;
    • cancel_mediated_request;
    • no_action. Для отправки она также обязана классифицировать решение как PrivateResponse, MediatorDisclosure, ExplicitTransfer или SafetyDisclosure; no_action получает NoAction автоматически.
  9. Версионированный результат модели сохраняется в PendingTurns до выполнения его действий и повторно используется после перезапуска.
  10. Для каждого получателя и каждой Unicode-безопасной части до 4000 символов создаётся durable delivery plan.
  11. Части последовательно доставляются в Telegram и подтверждаются в outbox только после успешной доставки. В истории части одного ответа объединяются по LogicalMessageId.
  12. Неисправимый turn после ограниченного числа попыток помещается в quarantine и больше не блокирует FIFO.

Если при send_to_both доставка первому участнику успешна, а второму — нет, подтверждение первого сохраняется. При восстановлении первый получатель пропускается, а отправка продолжается со второго.

Требования

  • .NET SDK 10;
  • Telegram-бот, созданный через BotFather;
  • Telegram UserId двух участников;
  • API-ключ только при использовании OpenAI-compatible runtime.

Быстрый запуск Telegram-бота с Fake runtime

1. Создать development-конфигурацию

Создайте MediatorBot.Telegram/appsettings.Development.json:

{
  "ModelRuntime": "Fake",
  "Storage": {
    "DatabaseKey": "длинный-случайный-локальный-ключ"
  },
  "Telegram": {
    "BotToken": "токен-от-BotFather",
    "SessionId": "00000000-0000-0000-0000-000000000000",
    "ParticipantAUserId": 111111111,
    "ParticipantADisplayName": "Имя A",
    "ParticipantBUserId": 222222222,
    "ParticipantBDisplayName": "Имя B"
  }
}

Замените все значения-заглушки. Для каждой независимой тестовой сессии используйте новый непустой GUID.

PowerShell:

[guid]::NewGuid()

Linux/macOS:

uuidgen

Файлы appsettings.Development.json игнорируются Git и не публикуются.

2. Запретить добавление бота в группы

В BotFather выполните:

/setjoingroups

и выберите Disable. Если бот всё же окажется в группе, супергруппе или канале, приложение попытается автоматически покинуть чат.

3. Запустить

dotnet run --project MediatorBot.Telegram

Оба разрешённых участника могут проверить подключение и состояние командами:

  • /start;
  • /status;
  • /retry_failed — повторить обработку только собственных quarantined-сообщений;
  • /help.

Fake runtime отправляет тестовый ответ автору сообщения и не обращается к внешнему API.

OpenAI-compatible runtime

Измените development-конфигурацию:

{
  "ModelRuntime": "OpenAI",
  "Storage": {
    "DatabaseKey": "длинный-случайный-локальный-ключ"
  },
  "OpenAI": {
    "ApiKey": "ключ-провайдера",
    "Endpoint": "https://api.openai.com/v1",
    "Model": "gpt-4.1-mini",
    "MaxOutputTokens": 1500,
    "MaxAttempts": 3,
    "RequestTimeoutSeconds": 120,
    "RetryBaseDelayMilliseconds": 1000,
    "RetryMaxDelaySeconds": 30
  },
  "Telegram": {
    "BotToken": "токен-от-BotFather",
    "SessionId": "00000000-0000-0000-0000-000000000000",
    "ParticipantAUserId": 111111111,
    "ParticipantADisplayName": "Имя A",
    "ParticipantBUserId": 222222222,
    "ParticipantBDisplayName": "Имя B"
  }
}

Для OpenRouter:

{
  "OpenAI": {
    "ApiKey": "ключ-OpenRouter",
    "Endpoint": "https://openrouter.ai/api/v1",
    "Model": "openai/gpt-4.1-mini",
    "MaxOutputTokens": 1500,
    "MaxAttempts": 3,
    "RequestTimeoutSeconds": 120
  }
}

Выбранные endpoint и модель должны поддерживать:

  • Chat Completions;
  • function tools;
  • tool_choice: required;
  • параметр ограничения выходных токенов max_tokens.

Модель не может возвращать обычный assistant text вместо действия. Такой ответ, пустой choices, некорректные arguments или несколько tool calls считаются ошибкой протокола и не доставляются участникам.

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

Основные параметры:

Ключ Описание
ModelRuntime Fake или OpenAI.
Storage:DatabasePath Путь к SQLite/SQLCipher-файлу.
Storage:DatabaseKey Ключ шифрования базы; обязателен.
Storage:MaxHistoryMessages Аварийный максимум свежих сообщений, передаваемых модели после compaction.
TurnProcessing:MaxAttempts Максимальное число автоматических попыток turn до помещения в quarantine.
TurnProcessing:RetryDelaySeconds Задержка между автоматическими попытками обработки turn.
Compaction:Enabled Включает rolling compaction для OpenAI runtime.
Compaction:TriggerMessageCount Порог количества свежих сообщений.
Compaction:TriggerHistoryCharacters Альтернативный порог суммарного количества символов.
Compaction:RetainRecentMessageCount Максимум недавних сообщений, сохраняемых дословно после compaction.
Compaction:RetainRecentCharacters Целевой предел символов в сохраняемом свежем хвосте.
Compaction:MaxSummaryCharacters Жёсткий общий предел четырёх секций сжатой памяти.
Compaction:MaxOutputTokens Лимит ответа модели при создании сжатой памяти.
Compaction:OperationTimeoutSeconds Общий timeout одной compaction.
Compaction:RetryDelaySeconds Задержка до новой попытки после ошибки compaction.
Initiative:Enabled Включает heartbeat проактивной оценки; по умолчанию false.
Initiative:ShadowMode Сохраняет решения, но не отправляет их. Live-режим используется при false.
Initiative:HeartbeatMinutes Частота дешёвой технической проверки; модель вызывается только при наступившей оценке.
Initiative:MinimumQuietMinutes Минимальная пауза после последнего сообщения участника.
Initiative:MaxHistoryMessages Максимум свежих сообщений в initiative-контексте.
Initiative:RecentDecisionCount Максимум предыдущих initiative decisions в контексте.
Initiative:MinimumReevaluationMinutes Нижняя граница назначаемой моделью повторной оценки.
Initiative:MaximumReevaluationMinutes Верхняя граница повторной оценки; по умолчанию семь дней.
Initiative:MaxContactsPerParticipantPer24Hours Жёсткий лимит доставленных инициатив одному участнику; по умолчанию 2.
Initiative:MaxDeliveryAttempts Максимум попыток initiative delivery.
Initiative:TimeZoneId Часовой пояс quiet hours; по умолчанию Europe/Moscow.
Initiative:QuietHoursStartHour / EndHour Тихий интервал; по умолчанию 22:00–09:00.
OpenAI:ApiKey API-ключ выбранного провайдера.
OpenAI:Endpoint Базовый URL OpenAI-compatible API.
OpenAI:Model Идентификатор модели у выбранного провайдера.
OpenAI:MaxOutputTokens Ограничение выходных токенов.
OpenAI:MaxAttempts Общее количество попыток обращения к модели.
OpenAI:RequestTimeoutSeconds Общий timeout всех попыток и пауз между ними.
OpenAI:RetryBaseDelayMilliseconds Начальная задержка exponential backoff.
OpenAI:RetryMaxDelaySeconds Максимальная задержка, включая Retry-After.
Telegram:BotToken Токен Telegram-бота.
Telegram:SessionId Идентификатор активной session.
Telegram:ParticipantAUserId Telegram UserId первого участника.
Telegram:ParticipantADisplayName Имя, по которому медиатор обращается к первому участнику.
Telegram:ParticipantBUserId Telegram UserId второго участника.
Telegram:ParticipantBDisplayName Имя, по которому медиатор обращается ко второму участнику.
Telegram:DeliveryTimeoutSeconds Timeout доставки сообщения в Telegram.
Telegram:DeliveryRecordingTimeoutSeconds Независимый timeout фиксации успешно доставленного сообщения.

Поддерживаются стандартные источники .NET Configuration:

  1. appsettings.json;
  2. appsettings.{Environment}.json;
  3. .NET User Secrets;
  4. environment variables;
  5. аргументы командной строки.

В environment variables разделитель : заменяется на __, например:

Storage__DatabaseKey
Telegram__BotToken
OpenAI__ApiKey

Секреты нельзя добавлять в appsettings.json или фиксировать в Git.

Сжатие истории

Для OpenAI runtime после завершённого turn Telegram host проверяет два порога: количество свежих сообщений и их суммарный размер. При достижении любого порога старая часть истории вместе с предыдущей памятью преобразуется отдельным обязательным tool call в полную замену четырёх секций:

  • приватный контекст от Participant A;
  • приватный контекст от Participant B;
  • контекст и договорённости, уже известные обоим участникам;
  • границы и безопасность.

Compactor обязан забывать разовые бытовые раздражения, Vent, повторы и обвинительные списки; утверждения сторон не превращаются в факты, а memory не создаёт разрешения на disclosure. Результат принимается только при корректном tool protocol и соблюдении MaxSummaryCharacters.

Замена memory и удаление охваченных сообщений выполняются одной SQLCipher-транзакцией. При ошибке предыдущая memory и все сообщения остаются неизменными, а повтор откладывается. Открытые mediated requests хранятся независимо и не удаляются. SQLite повторно использует страницы удалённых строк, поэтому файл базы может не уменьшиться сразу, но его рост стабилизируется.

Compaction выполняется между turns. Если во время неё приходят сообщения, они остаются в FIFO и каждый ожидающий участник получает одно уведомление без упоминания активности партнёра. После завершения уведомлённый участник получает нейтральное подтверждение, затем queued turns обрабатываются по UpdateId. Эти maintenance-уведомления не входят в model context.

Очередь PendingTurns переживает перезапуск: при старте Telegram host возобновляет сохранённые turns по UpdateId, а строка удаляется только после завершённой обработки. Регистрация ExternalUpdate и сохранение payload выполняются одной транзакцией, поэтому повторная доставка Telegram не создаёт второй turn. Входящее сообщение записывается идемпотентно с устойчивым turn.Id; сохранённый результат модели не вычисляется повторно, а подтверждённые outbox-части не отправляются повторно. Legacy-turn из schema v6 при первом восстановлении связывается с уже существующим входящим сообщением вместо создания дубликата. Переходы mediated requests также идемпотентны.

Каждая попытка turn фиксируется до model/delivery side effects. После TurnProcessing:MaxAttempts неисправимый turn получает статус Failed и исключается из рабочего FIFO, поэтому следующие сообщения продолжают обрабатываться. Участник видит количество только собственных failed turns через /status и может явно повторить их командой /retry_failed. Тексты ошибок, payload и provider diagnostics в командах и логах не раскрываются.

Абсолютная exactly-once доставка через Telegram Bot API недостижима: если процесс завершится после принятия сообщения Telegram, но до локальной фиксации подтверждения, часть останется в состоянии Attempting и может быть отправлена повторно с риском дубля. Автоматические повторы теперь ограничены общим лимитом попыток turn; ручной /retry_failed начинает новый ограниченный цикл. При штатной остановке host прекращает запуск новых turns, возвращает ещё не запущенным handler’ам контролируемый статус Deferred и в пределах shutdown timeout ожидает текущую операцию; незавершённые строки остаются для следующего запуска. Для нескольких экземпляров по-прежнему потребуются распределённый lock или lease.

Проактивный режим

Heartbeat относится к той же session и общей памяти пары, но использует отдельный контекст без текущего автора и фиктивного входящего сообщения. Каждое осмысленное пробуждение создаёт InitiativeDecision: фазу, уверенность, действие, адресата, краткую operational rationale, предполагаемый текст и NextEvaluationAt. Решения и доставки хранятся отдельно от пользовательских turns.

Технический heartbeat каждые 30 минут не обязательно вызывает модель. Оценка выполняется после новой participant activity либо при наступившем NextEvaluationAt; даже спокойное состояние получает дальнюю повторную оценку. Во время пользовательского turn, quiet hours или минимальной паузы модель не вызывается. LLM-запрос выполняется без удержания session lock, а перед сохранением и перед доставкой система повторно проверяет отсутствие новых сообщений и pending turns.

Будущий план никогда не является запланированной отправкой: в назначенное время ситуация оценивается заново. Live-доставка использует отдельный durable outbox и записывает сообщение в общую историю только после подтверждения Telegram. Частично доставленный ContactBoth восстанавливается по получателям и чанкам. Остаточный риск дубля после Telegram delivery и до локального checkpoint такой же, как у обычных turns.

Участник управляет инициативами командами /proactive_status, /proactive_off и /proactive_on. Обычные ответы на его сообщения продолжают работать при отключённой инициативе. По умолчанию весь режим выключен конфигурацией; включение production требует Initiative__Enabled=true.

Консольный стенд

MediatorBot.Console создаёт новую session или продолжает существующую и по очереди принимает сообщения от Participant A и Participant B.

Минимальная локальная конфигурация:

{
  "ModelRuntime": "Fake",
  "Storage": {
    "DatabaseKey": "длинный-случайный-локальный-ключ"
  }
}

Запуск новой session:

dotnet run --project MediatorBot.Console

Продолжение существующей session:

dotnet run --project MediatorBot.Console -- --session <SessionId>

Команда exit завершает работу.

Локальный HTML-аудит initiative decisions из зашифрованной БД создаётся явной командой:

dotnet run --project MediatorBot.Console -- \
  --session <SessionId> \
  --initiative-report ./private/initiative-report.html

Отчёт содержит внутренние rationale и предложенные тексты, поэтому его нельзя сохранять в публичные artifacts, логи или репозиторий. На Linux файл получает mode 600.

Персистентные посреднические запросы

Если участник просит что-либо уточнить у партнёра, модель может открыть open_mediated_request. Запрос сохраняется в SQLCipher отдельно от ограниченного окна истории и проходит состояния:

PendingDelivery → AwaitingResponse → Answered / Declined / NoShareableAnswer / Cancelled

Адресат получает безопасно переформулированный вопрос с явным правом отказаться, а инициатор — подтверждение, что ответ зависит от согласия адресата. Последующее сообщение адресата обрабатывается в новом Telegram turn. Содержательный разрешённый ответ закрывает запрос через Answered; отказ или приватная реакция закрывают ожидание инициатора нейтральным сообщением без пересказа формулировки и настроения адресата.

Это event-driven workflow: обработчик и session lock не удерживаются во время ожидания человека. Автоматическое завершение по таймауту пока не реализовано.

Behavioural disclosure scenarios

MediatorBot.BehaviorScenarios прогоняет изолированные синтетические disclosure-сценарии, включая privacy, mediation-first bridges, safety и anti-rumination. Отдельный initiative-режим проверяет активный конфликт, cooling down, просьбу дать пространство, проигнорированный check-in, позитивную реакцию и взаимную готовность к примирению.

Пример запуска через environment variables в PowerShell:

$env:OpenAI__ApiKey = "ключ-провайдера"
$env:OpenAI__Endpoint = "https://openrouter.ai/api/v1"
$env:OpenAI__Model = "идентификатор-модели"
dotnet run --project MediatorBot.BehaviorScenarios

Harness не использует рабочую базу и Telegram. Для каждого сценария создаётся отдельный in-memory контекст. Transcript с синтетическими входами, ответами, адресатами и DisclosureDecision сохраняется в artifacts/behavioral/ для ручной оценки. API key и tool arguments в него не записываются. Отдельный сценарий можно запустить так:

dotnet run --project MediatorBot.BehaviorScenarios -- --Behavior:ScenarioNumber 8

Проактивные сценарии запускаются отдельно и никогда не используют Telegram transport:

dotnet run --project MediatorBot.BehaviorScenarios -- --Behavior:Mode Initiative

Docker Compose

Контейнер собирается multi-stage Dockerfile: перед публикацией Telegram host внутри Linux SDK-образа выполняются тесты. Runtime запускается не от root, с read-only filesystem и без опубликованных портов. SQLCipher-база хранится в именованном volume mediator-bot-data.

Подготовка конфигурации в WSL/Linux:

cp deploy/mediator-bot.env.example deploy/mediator-bot.env
chmod 600 deploy/mediator-bot.env

Заполните deploy/mediator-bot.env реальными значениями. Файл исключён из Git. Перед запуском контейнера остановите другие экземпляры этого Telegram-бота, чтобы два long-polling процесса не читали updates одновременно.

docker compose build
docker compose up -d
docker compose logs -f --tail=100

Обновление после изменения исходников:

docker compose build
docker compose up -d

Остановка без удаления истории:

docker compose down

Не используйте docker compose down -v, если volume с базой не сохранён отдельно: параметр -v удаляет mediator-bot-data вместе с историей.

Сборка и тесты

Публичный GitHub Actions workflow .github/workflows/ci.yml на каждый push в master и pull request выполняет restore, Release build, весь тестовый набор, проверку Compose и сборку deployment-образа без секретов и без запуска Telegram. Dependabot еженедельно проверяет NuGet, GitHub Actions и Docker base images. Live behavioural scenarios остаются ручными.

dotnet restore
dotnet build MediatorBot.slnx
dotnet test MediatorBot.slnx

Хранение и безопасность

  • История хранится локально в SQLite, зашифрованной SQLCipher.
  • Ключ базы не сохраняется рядом с базой автоматически.
  • Тексты сообщений, API-ключи, bot token и tool arguments не должны попадать в технические логи.
  • Неизвестные Telegram UserId отсекаются фильтром и повторной проверкой application layer.
  • Core и Infrastructure не зависят от Telegram API.
  • Telegram update и его payload атомарно сохраняются в PendingTurns до начала обработки; само входящее сообщение сохраняется в истории до обращения к модели.
  • Исходящий ответ сохраняется только после подтверждённой доставки.
  • После подтверждённой доставки фиксация выполняется независимо от отмены исходного Telegram update и ограничивается собственным timeout.
  • Предложенные, но не отправленные initiative messages не попадают в общую conversation history.
  • Initiative delivery повторно проверяет opt-out, временную паузу, quiet hours, лимит и свежесть контекста.
  • Operational rationale и proposed messages хранятся только в SQLCipher и не выводятся в application logs.

Защита базы зависит от стойкости Storage:DatabaseKey и безопасности среды, в которой запущен процесс.

Диагностика моделей

Логи различают ошибки провайдера и нарушения модельного протокола. Для успешного действия фиксируется только техническая классификация без текста и tool arguments:

DisclosureDecision=PrivateResponse

Возможные решения: PrivateResponse, MediatorDisclosure, ExplicitTransfer, SafetyDisclosure, NoAction.

Для provider failure выводятся только безопасные метаданные:

ProviderCode=429 ProviderErrorType=rate_limit_exceeded ProviderName=...

Для protocol failure выводится причина без содержимого ответа:

ProtocolReason=OutputTokenLimit

Типичные причины:

  • OutputTokenLimit — модели не хватило выходных токенов;
  • UnexpectedAssistantText — модель вернула текст вместо tool call;
  • MissingToolCall — tool call отсутствует;
  • MultipleToolCalls — модель вызвала несколько инструментов;
  • InvalidToolCall — неверное имя или arguments;
  • NoChoices — провайдер вернул ответ без вариантов completion.

План дальнейших работ

Актуальные задачи, критерии возврата к отложенным решениям и эксплуатационный backlog ведутся в TODO.md.

Ограничения текущей версии

  • Telegram host рассчитан на одну настроенную session и двух участников.
  • Блокировка turn хранится в памяти процесса и не подходит для нескольких одновременно запущенных экземпляров приложения.
  • Fallback между моделями и distributed lock не реализованы.
  • Открытые посреднические запросы не завершаются автоматически, если адресат вообще не отвечает.
  • Реакция на инициативу определяется из последующей истории вероятностно; отсутствие ответа не считается доказательством состояния отношений.
  • Proactive heartbeat, как и Telegram host, пока рассчитан на один процесс без distributed lease.
  • Успех зависит от того, насколько выбранная модель соблюдает обязательный tool protocol.
  • Используется alpha-версия TeleFlow, закреплённая в файле проекта.
  • Проект пока предназначен для контролируемого тестирования, а не для production-развёртывания.