Files
agentik/standalone/README.md
T
subochev 88dabd49a3 feat(standalone): :integrations-telegram — bridge stability + markdown rendering
Outbound (DurableEvent-driven):
- typing дёргается ДО conv.send (а не только по online-событиям);
  рефреш по online — каждые ≥4 с, чтобы не получить 429-спам
  (Telegram rate-limit ~250 ms на sendChatAction).
- финальный текст — sendMessage(... ParseMode.HTML) на
  DurableEvent.AssistantMessage, без промежуточных editMessage.
- служебные строки ((empty response), ошибки) идут как plain text
  (без parseMode=HTML), чтобы 400 can't parse entities не выбил
  предупреждение в чат.

Inline markdown → Telegram HTML (MarkdownToTelegram):
- **bold** / __bold__ / *italic* / _italic_ / `code` → <b>/<i>/<code>;
  fenced ``` → <pre>; [text](url) → <a href>.
- markdown-таблицы → Unicode-рамка (┌─┬─┐││├─┼─┤└─┴─┘) внутри <pre>
  (Telegram Bot API <table> не поддерживает).
- escapeOutsideTags теперь ведёт стек открытых тегов: раньше брал
  первый попавшийся закрывающий тег и терял парность (агент вставил
  <i> внутри <code> → Telegram 400 can't find end tag для <code>).
- экранирует только вне наших тегов; внутри — уже подготовленный текст.
- сообщения > 4096 режутся по \n вне тегов.

Видимость ошибок:
- kotlin-logging в build.gradle.kts.
- runCatching'и заменены на try/catch + log.error/warn с контекстом
  (chatId, convId, htmlLen, превью первых ≤120 символов HTML).
  Раньше Telegram 400 на sendMessage с 'can't parse entities' уходил
  молча — теперь видно в логе с полным телом запроса.

Тесты: 23 + 12 + 28 = 63 в :integrations-telegram, 132 в :standalone.
:standalone:assemble собирает fatjar.
2026-10-06 00:51:28 +03:00

236 lines
14 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`, и поднимает сразу все
транспорты, которые другие системы могут хавать.
## Как запустить
### Переменные среды
`AGENTIK_SKILLS_DIR=./agentik/skills;AGENTIK_SYSTEM_PROMPT=Ты полезный ассистент.;OPENAI_BASE_URL=http://192.168.88.135:8001/v1;AGENTIK_DB_PATH=./agentik/db.db;OPENAI_CONTEXT_WINDOW=115000;AGENTIK_MEMORY_DIR=./agentik/memory;AGENTIK_LLM_BACKEND=openai;AGENTIK_PORT=8080;OPENAI_API_KEY=sk-76R2p5nxQxflPkIROr6r2xGiuSYzUCYM;OPENAI_MODEL=/root/.cache/huggingface/Qwen3.8-27B-NVFP4-RTX5090;AGENTIK_SOUL=./agentik/SOUL.md;AGENTIK_TELEGRAM_TOKEN=8857094360:AAFeVYMlutSaHptbhpO4mUryR6Je7OUJsm4`
### Требования
- 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-чат = один диалог с агентом.
- **«печатает…» в чате** через `sendChatAction(TYPING)`:
- одно сразу при получении апдейта (ДО вызова LLM — пользователь
видит статус ровно в момент отправки текста);
- рефреш на каждом online-событии для диалога (Working /
StartReasoning / StartResponse / AppendText / AppendImage / End) —
Telegram гасит статус через ~5 с, поэтому для длинных tool-call'ов
(когда дельт текста нет) мост продолжает рефрешить typing по
`Working`.
- **Финальное сообщение** приходит одним `sendMessage(...)` по
`DurableEvent.AssistantMessage` (целиковый текст из `Content.Text`).
Никакого стриминга через `editMessage` — UX проще, и Telegram
нормально индексирует одиночное сообщение.
- **Markdown → Telegram HTML**: мост конвертирует CommonMark-подобный
markdown агента в `ParseMode.HTML` (`<b>`, `<i>`, `<code>`, `<pre>`,
`<a href>`); остальные символы экранируются. Сообщения длиннее
4096 символов режутся по переводам строк (вне HTML-тегов).
- **Markdown-таблицы** (`| col | col |` + `| --- | --- |`) рендерятся
через Unicode box-drawing (`┌─┬─┐ │ │ │ ├─┼─┤ └─┴─┘`) внутри `<pre>` —
Telegram Bot API не поддерживает `<table>`, поэтому табличный вид
даёт только моноширинная рамка. Markdown/HTML-разметка внутри
ячеек снимается (в `<pre>` inline-форматирование не работает).
- Group-чаты (`group`, `supergroup`) и команда `/new` стартуют **новый
временный** диалог (`ConversationRecord.isTemporal = true`).
- Ошибки агента (`DurableEvent.Error`, исключение из `conv.send`)
приходят одной строкой, 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. Ответ приходит одним сообщением (после завершения хода ассистента).
Пока агент думает / вызывает тулзы / стримит токены — в чате горит
«печатает…».
### 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-таргеты
публикует артефакты.