Files
agentik/standalone/README.md
T
subochev 24212a1003 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-таблицей,
описанием поведения и инструкцией по включению.
2026-10-05 23:33:57 +03:00

214 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `: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-таргеты
публикует артефакты.