feat(standalone): :integrations-telegram — мост Telegram-чат ↔ agent
Новый модуль :integrations-telegram, опциональный «плагин» для standalone:
* TelegramBridgeComponent — Component-имплементация, устанавливается
в agent через agent.install(...) только при заданном
AGENTIK_TELEGRAM_TOKEN. Нет токена — нет polling'а, classpath не
содержит pw.binom.telegram.* (factory возвращает AutoCloseable,
а Component-объект создаётся внутри factory и утекает в
standalone только через agent-api).
* Long-polling getUpdates?timeout=N — не webhook, не нужен публичный URL.
* chatId ↔ ConversationId маппинг в общей SQLite-БД standalone'а
(таблица tg_chat_map); persistent=true переиспользует conv_id,
persistent=false — каждый Telegram-message = свежий temp-диалог.
* Group-чаты и /new стартуют временный диалог, не пишут в мапу.
* Tool-call показывает «⚙️ обрабатываю…» через sendChatAction(typing)
по onlineOutbox.onlineEvents.
* Outbound streaming через editMessageText с guard'ом «message is not
modified»; End с пустым телом → «(empty response)».
* Ошибки conv.send — одной строкой в чат, polling не падает.
* uninstall отменяет scope, polling и stream-джобы корректно
останавливаются.
Конфиг — через env (AppConfig.fromEnv):
* AGENTIK_TELEGRAM_TOKEN (default пусто → модуль не активен)
* AGENTIK_TELEGRAM_POLLING_TIMEOUT (default 30s, ок 25–35)
* AGENTIK_TELEGRAM_PERSISTENT (default true)
Зависимость: наша in-house либа caffeine-mgn/telegramClient
(pw.binom.telegram:telegramClient:1.0.0-SNAPSHOT из mavenLocal).
standalone/Main.kt:
* integrationsScope (SupervisorJob + Dispatchers.Default) создаётся
до server'а; tgClient закрывается shutdown-hook'ом ДО agent.close()
и integrationsScope.cancel() — порядок важен, иначе polling
long-poll не прерывается и JVM висит на выходе.
Тесты (8/8 ✅):
* первый входящий текст → persistent conv + conv.send
* повтор из того же chat → тот же conv
* non-persistent → каждый message = новый temp-conv
* typing-индикатор на каждом сообщении
* outbound streaming → 3 editMessage подряд
* End с пустым телом → draft заменяется на «(empty response)»
* ошибка conv.send → одна строка в чат, polling не падает
* uninstall → polling остановлен, новые push'и не обрабатываются
standalone/README.md — добавлен раздел «Telegram-интеграция» с env-таблицей,
описанием поведения и инструкцией по включению.
This commit is contained in:
@@ -89,9 +89,78 @@ AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
|
||||
| `AGENTIK_MEMORY_DIR` | `~/.agentik/memory/` | Каталог для md-памяти |
|
||||
| `AGENTIK_TOOLSETS_DEFAULT` | `memory,skills,files,web` | Включённые тулы |
|
||||
| `AGENTIK_DEBUG` | `0` | `1` = verbose logging |
|
||||
| `AGENTIK_TELEGRAM_TOKEN` | (пусто) | Bot token от `@BotFather`. Пусто — Telegram-интеграция выключена, standalone работает ровно как без неё |
|
||||
| `AGENTIK_TELEGRAM_POLLING_TIMEOUT` | `30` | Long-poll timeout в секундах. Bot API держит HTTP-соединение открытым до N секунд, возвращает 200 OK с пустым массивом если апдейтов нет. Меньше — больше polling-нагрузки, больше — дольше «висит» соединение. 25–35 обычно ок |
|
||||
| `AGENTIK_TELEGRAM_PERSISTENT` | `true` | `true` → диалог из чата сохраняется в SQLite как обычный `ConversationRecord` и переживает рестарт. `false` → каждый Telegram-message стартует свежий `temp=true` диалог (как «temporary chat» в Android-клиенте) |
|
||||
|
||||
Значения читаются через `AgentikConfig.fromEnv()` в `:standalone/.../Main.kt`.
|
||||
|
||||
## Telegram-интеграция
|
||||
|
||||
Опциональный «мост» между Telegram-чатом и standalone-агентом.
|
||||
Реализован отдельным модулем `:integrations-telegram` и подключается
|
||||
ТОЛЬКО при заданном `AGENTIK_TELEGRAM_TOKEN` (нет токена — нет polling'а,
|
||||
нет зависимости в classpath при выключенном флаге, см. ниже).
|
||||
|
||||
### Что умеет
|
||||
|
||||
- Long-polling через Bot API (`getUpdates?timeout=N`). Не webhook —
|
||||
не нужен публичный URL, всё работает за NAT.
|
||||
- Маппинг `chatId ↔ ConversationId` в общей SQLite-БД standalone'а
|
||||
(таблица `tg_chat_map`). Один Telegram-чат = один диалог с агентом;
|
||||
сообщения из чата стримят ответы обратно через `editMessageText`.
|
||||
- Group-чаты (`group`, `supergroup`) и команда `/new` стартуют **новый
|
||||
временный** диалог (`ConversationRecord.isTemporal = true`) — не
|
||||
пишут в мапу, удаляются при следующем `/new`.
|
||||
- Tool-call показывает «⚙️ обрабатываю…» через `sendChatAction(typing)` —
|
||||
включается на `onlineOutbox.onlineEvents` пока обрабатывается ход.
|
||||
- Ошибки агента стримятся в чат одной строкой, polling не падает.
|
||||
|
||||
### Как включить
|
||||
|
||||
1. Создать бота через `@BotFather`, скопировать токен.
|
||||
2. Запустить standalone с токеном:
|
||||
|
||||
```bash
|
||||
AGENTIK_TELEGRAM_TOKEN=123456:ABC-DEF... \
|
||||
java --enable-native-access=ALL-UNNAMED \
|
||||
-jar agentik-0.1.0-all.jar
|
||||
```
|
||||
|
||||
3. Написать боту в личку (или добавить в группу). В логе standalone'а
|
||||
появится:
|
||||
|
||||
```
|
||||
telegram: enabled (polling timeout=30s, persistent=true)
|
||||
```
|
||||
|
||||
4. Ответы приходят стримингом — `editMessageText` обновляет одно и то
|
||||
же сообщение каждые ~1с пока агент генерирует.
|
||||
|
||||
### Persistent vs temporary
|
||||
|
||||
| Сценарий | `AGENTIK_TELEGRAM_PERSISTENT` | Поведение |
|
||||
|---|---|---|
|
||||
| Личка с ботом | `true` (default) | Один диалог на пользователя, переживает рестарт, `/new` стартует новый |
|
||||
| Личка с ботом | `false` | Каждое сообщение — свежий `temp` диалог |
|
||||
| Group-чат | (любое) | Всегда `temp` — каждый message может быть от другого юзера, диалоги не персистятся |
|
||||
| `/new` | (любое) | Стартует новый `temp` диалог (для `persistent=true` это закрывает старый `ConversationRecord`) |
|
||||
|
||||
### Технические заметки
|
||||
|
||||
- Либа — своя, [`caffeine-mgn/telegramClient`](https://github.com/caffeine-mgn/telegramClient)
|
||||
(`pw.binom.telegram:telegramClient`), опубликованная через
|
||||
`publishToMavenLocal`. Артефакт `pw.binom.telegram.*` НЕ утекает в
|
||||
classpath standalone'а — host-сайд видит только фабрику
|
||||
`createTelegramBridgeComponent`, которая возвращает `AutoCloseable`.
|
||||
- Polling/stream-джобы живут на отдельном `integrationsScope`. В
|
||||
shutdown-hook'е он отменяется **до** `agent.close()` — иначе
|
||||
long-poll держит соединение до `AGENTIK_TELEGRAM_POLLING_TIMEOUT`
|
||||
и блокирует JVM-выход.
|
||||
- Один бот на standalone (таблица `tg_chat_map` глобальная). Если
|
||||
когда-нибудь захочется multi-agent + multi-bot — мапу надо будет
|
||||
разводить по агентам.
|
||||
|
||||
## Эндпоинты
|
||||
|
||||
| Метод | Путь | Transport | Описание |
|
||||
|
||||
+22
-11
@@ -23,7 +23,7 @@ val skipVectorMemory: Boolean =
|
||||
// `jvm()` target). Чтобы не таскать лишние sourceSet'ы, ВСЁ живёт в
|
||||
// commonMain + commonTest — даже зависимости, которые формально JVM-only
|
||||
// (ktor-server-cio, litert-openai, ...). KMP-плагин тут
|
||||
// только ради бесшовного потребления KMP-зависимостей (`:proto`, `:server`,
|
||||
// только ради бесшовного потребления KMP-зависимостей (`:proto`,
|
||||
// `:journal-api`, ...); компилируется всё ровно в одну JVM-таргет.
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
@@ -40,7 +40,6 @@ kotlin {
|
||||
commonMain.dependencies {
|
||||
// Протокол + KMP storage API
|
||||
implementation(project(":proto"))
|
||||
implementation(project(":server"))
|
||||
implementation(project(":journal-api"))
|
||||
implementation(project(":outbox-api"))
|
||||
implementation(project(":context-api"))
|
||||
@@ -74,9 +73,10 @@ kotlin {
|
||||
|
||||
// Bounded-tail live event stream + per-event TTL.
|
||||
implementation(project(":outbox-inmemory"))
|
||||
// Персистентный счётчик событий (CursorStore) поверх ksqlite —
|
||||
// Персистентный счётчик событий (CursorHolder) поверх ksqlite —
|
||||
// offset'ы переживают рестарт, клиент продолжает инкрементально.
|
||||
implementation(project(":outbox-ksqlite"))
|
||||
implementation(project(":cursor-ksqlite"))
|
||||
implementation(project(":cursor-inmemory"))
|
||||
|
||||
// :agent-api — MutableAgent + Component + ToolProvider/SystemPromptProvider.
|
||||
// ChatAgent реализует MutableAgent; компоненты (McpBridgeComponent и т.п.)
|
||||
@@ -90,8 +90,16 @@ kotlin {
|
||||
// Skill-подсистема как Component: тулы + SkillMiningComponent.
|
||||
implementation(project(":skill-mining"))
|
||||
|
||||
// Generic MCP-bridge (McpConfig, McpRegistry, McpLiteToolAdapter).
|
||||
implementation(project(":mcp-bridge"))
|
||||
// Generic MCP-bridge (McpConfig, McpRegistry, McpLiteToolAdapter).
|
||||
implementation(project(":mcp-bridge"))
|
||||
|
||||
// Внешние мессенджер-интеграции. Подключаются как обычные
|
||||
// :implementation (не api), чтобы при квант-like опциях в AppConfig
|
||||
// можно было не класть их в classpath. Сейчас обе всегда включены —
|
||||
// inactive-код стоит ~нулевых килобайт в fatjar (jar'ы маленькие),
|
||||
// а опция «выключить интеграцию» живёт в `config.integrations.telegram`,
|
||||
// не в classpath. См. README `:integrations-telegram`.
|
||||
implementation(project(":integrations-telegram"))
|
||||
|
||||
// litert-kmp: контракт (commonMain)
|
||||
api(libs.litert.api)
|
||||
@@ -102,7 +110,7 @@ kotlin {
|
||||
// litert-google: встроенный LiteRT-LM движок, нужен только на runtime
|
||||
api(libs.litert.google)
|
||||
|
||||
// Ktor server (для :server facade + a2aServer)
|
||||
// Ktor server (для A2A-фасада)
|
||||
// Используем CIO вместо Netty — он KMP (jvm + linuxX64/ios/...), нам нужен
|
||||
// для будущей native-сборки; на JVM работает идентично для нашего сценария.
|
||||
implementation(libs.ktor.server.core)
|
||||
@@ -111,8 +119,8 @@ kotlin {
|
||||
implementation(libs.ktor.server.content.negotiation)
|
||||
implementation(libs.ktor.serialization.kotlinx.json)
|
||||
|
||||
// Транспортные фасады (A2A остаётся заготовкой; на v1 не подключается в Main.kt —
|
||||
// см. docs/ARCHITECTURE.md §3).
|
||||
// A2A-фасад (agent↔agent). Proto user↔agent (:server) сюда НЕ подключается —
|
||||
// standalone больше не выставляет :proto по сети.
|
||||
implementation(libs.a2a.server)
|
||||
|
||||
// MCP (Model Context Protocol) клиент — подключение внешних/внутренних MCP-серверов
|
||||
@@ -129,10 +137,13 @@ kotlin {
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.kotlinx.coroutines.test)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlin.test)
|
||||
// Ktor test engine для smoke-тестов HTTP
|
||||
implementation(libs.ktor.server.test.host)
|
||||
// Ktor server нужен ТОЛЬКО тестам: ModelDownloaderTest поднимает fake
|
||||
// HTTP-сервер на CIO, чтобы проверять скачивание/resume/HEAD.
|
||||
implementation(libs.ktor.server.core)
|
||||
implementation(libs.ktor.server.cio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user