24212a1003
Новый модуль :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-таблицей,
описанием поведения и инструкцией по включению.
214 lines
12 KiB
Markdown
214 lines
12 KiB
Markdown
# `:standalone` — single-jar HTTP-сервер со всеми транспортами
|
||
|
||
## Что это
|
||
|
||
Главный исполняемый модуль проекта — single-jar HTTP-сервер с:
|
||
|
||
- **A2A** transport на `POST /` (JSON-RPC) + `GET /.well-known/agent-card.json`.
|
||
- **`:proto`** transport на `POST /agentik/*` (HTTP+JSON+SSE) — наш stateful.
|
||
- **Embedded LLM backend**: `GOOGLE` (LiteRT) или `OPENAI`-совместимый
|
||
(vLLM, LiteLLM, OpenAI API).
|
||
- **SQLite persistence** через `:storage-sqlite`.
|
||
- **Memory backend**: `md` (файловый) или `vector` (SQLite+JVector+
|
||
HTTP/SIGLIP-embeddings).
|
||
- **Skills** из `~/.agentik/skills/*.md`.
|
||
- **SOUL** из `~/.agentik/SOUL.md`.
|
||
- **Background подпроцессы**: рефлексия, skill-mining,
|
||
memory-reviewer.
|
||
|
||
Решает: даёт пользователю один JAR (10–250 МБ), который запускается
|
||
через `java -jar agentik-0.1.0-all.jar`, и поднимает сразу все
|
||
транспорты, которые другие системы могут хавать.
|
||
|
||
## Как запустить
|
||
|
||
### Требования
|
||
|
||
- JVM 21+.
|
||
- (Опционально) CUDA-устройство для `:backend=google` (LiteRT).
|
||
- (Опционально) LM через OpenAI-совместимый endpoint (vLLM / Ollama
|
||
/ OpenAI) для `:backend=openai`.
|
||
|
||
### Запуск из готового fatjar
|
||
|
||
```bash
|
||
java --enable-native-access=ALL-UNNAMED \
|
||
-jar agentik-0.1.0-all.jar
|
||
```
|
||
|
||
С дефолтами — встроенный SQLite, OpenAI-compatible backend на
|
||
`http://localhost:8001/v1`, порт 8080.
|
||
|
||
### Запуск через Gradle (dev)
|
||
|
||
```bash
|
||
./gradlew :standalone:run
|
||
```
|
||
|
||
### `pull-model` subcommand (для LiteRT)
|
||
|
||
```bash
|
||
# Сначала скачать модель под LiteRT-Gemma-4-E2B
|
||
AGENTIK_LLM_BACKEND=google \
|
||
AGENTIK_GOOGLE_MODEL_PATH=/root/gemma-4-E2B-it.litertlm \
|
||
java --enable-native-access=ALL-UNNAMED \
|
||
-jar agentik-0.1.0-all.jar pull-model
|
||
```
|
||
|
||
Скачивает `https://static.binom.pw/models/gemma-4-E2B-it.litertlm`
|
||
(2.5 ГБ, с Range-resume). Поддерживает override через
|
||
`AGENTIK_GOOGLE_MODEL_URL` и verify через
|
||
`AGENTIK_GOOGLE_MODEL_SHA256_URL`.
|
||
|
||
## Переменные среды
|
||
|
||
Полный список — общий для всего `:standalone`-процесса:
|
||
|
||
| Env | Default | Что делает |
|
||
|---|---|---|
|
||
| `AGENTIK_PORT` | `8080` | Порт HTTP-сервера |
|
||
| `AGENTIK_DB_PATH` | `./agentik.db` | Путь к SQLite |
|
||
| `AGENTIK_TOKEN` | (пусто) | Bearer-токен для HTTP-фасада `/agentik`. Пусто — авторизация выключена |
|
||
| `AGENTIK_A2A_TOKEN` | (пусто) | Bearer-токен для A2A-фасада `/a2a`. Пусто — авторизация выключена (независим от `AGENTIK_TOKEN`) |
|
||
| `AGENTIK_AGENT_ID` | `agentik` | ID агента (для multi-instance) |
|
||
| `AGENTIK_LLM_BACKEND` | `openai` | `openai` или `google` |
|
||
| `AGENTIK_LLM_MODEL` | (выбирается по backend) | Имя модели |
|
||
| `AGENTIK_LLM_API_URL` | `http://localhost:8001/v1` | Endpoint для OpenAI-compatible |
|
||
| `AGENTIK_LLM_API_KEY` | `no-key-needed` | Auth header |
|
||
| `AGENTIK_LLM_CONTEXT_TOKENS` | `115000` | Сколько токенов остаётся модели |
|
||
| `AGENTIK_GOOGLE_MODEL_PATH` | `/root/gemma-4-E2B-it.litertlm` | Путь к `.litertlm` файлу |
|
||
| `AGENTIK_GOOGLE_MODEL_URL` | `https://static.binom.pw/models/gemma-4-E2B-it.litertlm` | Откуда скачивать |
|
||
| `AGENTIK_GOOGLE_MODEL_SHA256_URL` | — | Если задан — verify по SHA-256 |
|
||
| `AGENTIK_AUTO_DOWNLOAD_MODEL` | `0` | `1` = скачать модель если её нет |
|
||
| `AGENTIK_MEMORY_BACKEND` | `vector` | `md`, `vector` или `off` |
|
||
| `AGENTIK_EMBEDDING_BACKEND` | `http` | `http` или `siglip` (только для `vector`) |
|
||
| `AGENTIK_EMBEDDING_API_URL` | `http://localhost:8001/v1` | Endpoint для эмбеддингов |
|
||
| `AGENTIK_EMBEDDING_MODEL` | `text-embedding-3-small` | Имя embedding-модели |
|
||
| `AGENTIK_SOUL_PATH` | `~/.agentik/SOUL.md` | Путь к SOUL.md |
|
||
| `AGENTIK_SKILLS_DIR` | `~/.agentik/skills/` | Каталог SKILL.md |
|
||
| `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 | Описание |
|
||
|---|---|---|---|
|
||
| `GET` | `/health` | любой | health-check (`{"ok":true}`) |
|
||
| `POST` | `/` | A2A | JSON-RPC `message/send`, `tasks/get`, `tasks/cancel` |
|
||
| `GET` | `/.well-known/agent-card.json` | A2A | Discovery |
|
||
| `POST` | `/agentik/conversations` | :proto | Создать диалог |
|
||
| `GET` | `/agentik/conversations` | :proto | Список диалогов |
|
||
| `GET` | `/agentik/conversations/:id` | :proto | Snapshot |
|
||
| `GET` | `/agentik/conversations/:id/messages` | :proto | История |
|
||
| `POST` | `/agentik/conversations/:id/send` | :proto | Send (SSE) |
|
||
| `GET` | `/agentik/conversations/:id/events` | :proto | Live-events (SSE) |
|
||
| `POST` | `/agentik/conversations/:id/interrupt` | :proto | Прервать |
|
||
| `POST` | `/agentik/conversations/:id/rename` | :proto | Переименовать |
|
||
| `DELETE` | `/agentik/conversations/:id` | :proto | Удалить |
|
||
|
||
## Тесты
|
||
|
||
```
|
||
./gradlew :standalone:jvmTest # unit-тесты
|
||
./gradlew :standalone:integrationTest # integration (Testcontainers)
|
||
./gradlew :standalone:shadowJar # → build/libs/agentik-0.1.0-all.jar
|
||
```
|
||
|
||
## Известные ограничения
|
||
|
||
1. **vLLM не поддерживает cancel-inference** (`interrupt()` только
|
||
закрывает client SSE-socket; бэкенд всё равно генерирует до конца).
|
||
2. **A2A JSON discriminator — `"kind"`** (text/file/data),
|
||
а не `"type"`. См. `A2aJson` в `:standalone`.
|
||
3. **SSE в не-TTY ssh закрывается на default Ktor timeout**.
|
||
|
||
## Текущий статус
|
||
|
||
Production-ready. Все KMP-модули проекта интегрированы. Полный
|
||
manual-test checklist смотрите в [`MANUAL-TESTS.md`](../../MANUAL-TESTS.md)
|
||
или `MANUAL-TESTS.md` в корне.
|
||
|
||
## Где скачать
|
||
|
||
- **Source**: `git clone https://git.binom.pw/subochev/agentik`
|
||
- **Fatjar**: Gitea CI artifacts (через `.gitea/workflows/release.yml`
|
||
на tag `v*`) или собирается через `./gradlew :standalone:shadowJar`.
|
||
|
||
## Версии
|
||
|
||
Все `gradle/libs.versions.toml`. Поднять версию → release через
|
||
`git tag v0.2.0 && git push --tags` → CI собирает все KMP-таргеты
|
||
публикует артефакты.
|