🤖 AI-агенты для бизнеса

19. Виджет и каналы ввода/вывода

🔵 Как пользователи и внешние системы общаются с агентами. Это не «API» в программном смысле, а каналы — готовые точки входа/выхода, которые агент получает из конфигурации.

Канал Где настраивается Характер
Виджет (веб) server.yaml → widget + встраивание widget.js встраиваемый чат на сайте
Telegram-бот agent.yaml → telegram (на агента) long-polling, личные чаты
VK-бот сообщества agent.yaml → vk (на агента) Long Poll, сообщения сообщества
Яндекс Мессенджер agent.yaml → yandex_messenger (на агента) polling getUpdates, личные и групповые чаты
Email (IMAP→SMTP) agent.yaml → email (на агента) входящие письма + ответ
Storage (S3/WebDAV/FTP) agent.yaml → storage (на агента) новые файлы из бакета/папки → агент
HTTP API (SSE) без конфига POST /v1/chat/completions (§8)
Входящий webhook server.yaml → inbound POST /v1/inbound/:template (§8.8)
Sink'и (выход) notify в cron/email/workflow log/webhook/telegram/vk/yandex_messenger/messages_db/agent/email/workflow/storage (§11)

19.1 Виджет (веб)

Встраиваемый чат-интерфейс, который общается с агентом через /v1/chat/completions (SSE) и закрывает сессию через POST /v1/sessions/:id.

Встраивание

На любую страницу сайта:

<script src="http://127.0.0.1:3000/widget.js"></script>

Сервируемые маршруты: /widget (страница), /widget.js (лоадер), /dist/* (ассеты).

Какой агент открывается

Виджет разрешает модель в таком порядке (последний выигрывает):

  1. widget.default_model (из server.yaml);
  2. window.AGENTOS_MODEL — переменная родительского фрейма;
  3. ?model= в URL (/widget?model=felix).

Конфигурация в server.yaml:

widget:
  default_model: "drom-agent"   # агент по умолчанию

Базовый URL виджет берёт из window.AGENTOS_BASE_URL (пусто = тот же origin) — нужно при хостинге виджета на другом домене.

Готовые демо-страницы

URL Что
/widget?model=felix виджет с конкретным агентом
/demo/overlay виджет поверх живых сайтов (вкладки из config/demo.yaml)
/demo демо виджета

Вкладки /demo/overlay задаются в config/demo.yaml (tabs: [{label, url, model}], hot-reload'ится на лету).

Визуальные блоки в виджете

Инструменты могут вернуть визуальный слой (графики chart, таблицы table, текст) — виджет рендерит их inline (Chart.js + таблицы). Формат блоков и пагинация — общие поля.

Вложения в виджете

Когда у шаблона задано attachments.enabled: true (§4.3), в виджете появляется кнопка 📎 с выбором источника: файл (загрузка), скриншот (getDisplayMedia), фото (камера; на мобильных — нативный capture). Файлы уходят в POST /v1/uploads, а в /v1/chat/completions передаются полем attachments — агент получает сообщение с их именами в scratch-каталоге сессии.

Кроме того, агент может сам запросить вложение тулом request_attachment: виджет покажет карточку-подсказку с кнопкой, а после прикрепления ответит через POST /v1/input-response (§8.2.1).

Вход в виджете

Если у шаблона задано login.enabled: true (§4.3) и настроены провайдеры auth.oidc, в шапке виджета появляется кнопка «Войти». Клик открывает popup с OIDC-логином (GET /v1/login/widget/:provider); после входа callback выдаёт подписанный x-user-token (HMAC), который виджет кэширует в localStorage и шлёт заголовком x-user-token в последующих запросах. Browser-id при этом автоматически сливается с OIDC-личностью (Origin::Oidc в user-registry) — один пользователь во всех каналах. Кнопка меняется на «Выйти» (сброс локального токена).

login:
  enabled: true
  providers: [yandex]           # пусто = все включённые провайдеры
  prompt: "Войти через Яндекс"  # текст кнопки (опционально)

Обязательный вход (login.required)

login.required: true делает вход обязательным: анонимный пользователь не может отправить сообщение агенту. Виджет показывает полноэкранный экран входа вместо поля ввода, а сервер отвечает 401 на анонимные POST /v1/chat/completions. Требование удовлетворяется одним из способов:

login:
  required: true
  providers: [yandex]     # вход через Яндекс
  allow_users: true       # ИЛИ логин/пароль из auth.users
  prompt: "Войдите, чтобы продолжить"
Поле По умолч. Описание
enabled false показывать кнопку «Войти» (опциональный вход)
required false требовать вход; анонимные запросы отклоняются (401)
providers [] OIDC-провайдеры (auth.oidc.providers); пусто = все
allow_users false разрешить вход логином/паролем из auth.users
prompt пусто текст кнопки/заголовка экрана входа

Вход в Android-приложении

Android-клиент поддерживает те же способы входа. Если у выбранного агента login.required, приложение показывает экран входа вместо чата:

Полученный токен хранится в настройках приложения (x-user-token) и отправляется вместе с x-user-id; кнопка «Выйти» в шапке чата сбрасывает личность.

Динамический allowlist отправителей

У мессенджер-каналов (Telegram / VK / Яндекс Мессенджер / email) список разрешённых отправителей (allowed_user_ids / allowed_logins / allowed_senders) задаётся в agent.yaml и требует передеплоя. Есть динамическая альтернатива — таблица allowed_senders в logs/allowed_senders.db, которая меняется через API без рестарта/редеплоя.

Запись — это идентичность канала:

Записи привязаны к шаблону агента (или * — глобально). Когда в таблице есть записи для шаблона (или глобальные *), таблица приоритетна: статический allowlist из agent.yaml игнорируется. Если таблица пуста — действует статический allowlist как раньше.

# разрешить tg:12345 и ym:ivan агенту my-agent (без редеплоя)
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"identity":"tg:12345","note":"Pavel"}'
curl -X POST http://127.0.0.1:3000/v1/allowed-senders/my-agent \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"identity":"ym:ivan"}'

# список
curl http://127.0.0.1:3000/v1/allowed-senders/my-agent -H "Authorization: Bearer $ADMIN_TOKEN"

# удалить
curl -X DELETE http://127.0.0.1:3000/v1/allowed-senders/my-agent/tg:12345 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

Эндпоинты — в 08-http-api.md (§ «Allowed senders»).


19.2 Telegram

Один long-polling бот на шаблон; запускается при старте сервера, если задан token_env. Токен читается из env (никогда не в YAML).

telegram:
  token_env: MY_TG_BOT_TOKEN      # имя env с токеном бота
  allowed_user_ids: []            # пусто = любой; список = только эти Telegram user id
  show_typing: true               # индикатор «печатает…» во время работы
  show_tool_calls: true           # компактные строки статуса tool-вызовов
Поле По умолч. Описание
token_env пусто имя env с токеном; пусто = бот выключен
allowed_user_ids [] ограничение отправителей по Telegram user id
show_typing true показывать typing-индикатор
show_tool_calls true эхо tool-вызовов в чат

Визуальные блоки на Telegram: график → PNG (sendPhoto), таблица → markdown-строки.

Входящие фото и документы пользователя (при attachments.enabled: true у шаблона) скачиваются через getFile и сохраняются в scratch сессии; агент получает сообщение с именами файлов. Так же резолвится агентский запрос request_attachment: бот шлёт подсказку «пришлите файл/фото», ответ пользователя с медиа продолжает ход.


19.3 VK (сообщество)

Бот сообщества на Long Poll API — не нужен публичный HTTPS. Один бот на шаблон; запускается при старте сервера, если задан token_env. Токен сообщества читается из env (никогда не в YAML).

vk:
  token_env: VK_GROUP_TOKEN     # имя env с group access token
  group_id: 123456789           # id сообщества (для longpoll и сообщений от имени группы)
  allowed_user_ids: []          # пусто = любой; список = только эти VK user id
  show_typing: true             # индикатор «печатает…» (messages.setActivity)
  show_tool_calls: true         # эхо tool-вызовов в чат
  poll_wait: 25                 # wait для longpoll (сек, 1..25)
Поле По умолч. Описание
token_env пусто имя env с токеном; пусто = бот выключен
group_id 0 id сообщества; 0 = вывести из токена
allowed_user_ids [] ограничение отправителей по VK user id
show_typing true показывать typing-индикатор
show_tool_calls true эхо tool-вызовов в чат
poll_wait 25 интервал ожидания longpoll-запроса (сек)

Сессия — vk:{template}:{peer_id} (для беседы peer_id = 2000000000 + chat_id). Права токена: messagesphotos/docs для загрузки медиа). Визуальные блоки: график → PNG через photos.getMessagesUploadServer, таблица → текстовые строки, файл → документ через docs.getMessagesUploadServer.

Входящие фото и документы пользователя (при attachments.enabled: true у шаблона) скачиваются и сохраняются в scratch сессии; агент получает сообщение с именами файлов. Агентский запрос request_attachment резолвится так же: бот шлёт подсказку, ответ пользователя с медиа продолжает ход.

Как sink результата (у cron/email/monitor) VK задаётся типом vk:

notify:
  - type: vk
    peer_id: 41848219        # peer_id получателя (для беседы 2000000000 + chat_id)
    # token_env: VK_GROUP_TOKEN  # опц.; по умолчанию берётся токен из vk-конфига шаблона

Настройка сообщества

Чтобы бот получал сообщения, в сообществе нужно сделать две вещи:

  1. Включить Long Poll API (Управление → API → Long Poll API → «Включён»).
  2. Включить тип события «Новые сообщения» — по умолчанию VK включает Long Poll, но все типы событий выключены (message_new: 0), поэтому бот не получает ничего. В UI — галочка «Новые сообщения» в Long Poll API; или через API:
curl -X POST "https://api.vk.com/method/groups.setLongPollSettings" \
  -d "group_id=<id>&access_token=<token>&v=5.199" \
  -d "message_new=1&message_reply=1&message_edit=1&message_allow=1&message_deny=1&message_typing_state=1"

Проверить текущие настройки: groups.getLongPollSettings (поле events.message_new).


19.4 Яндекс Мессенджер (Яндекс 360)

Бот на Bot API Яндекса — опрос bot/v1/messages/getUpdates/ с курсором offset (не нужен публичный HTTPS). Один бот на шаблон; запускается при старте сервера, если задан token_env. OAuth-токен бота читается из env (никогда не в YAML). Создаётся бот в Яндекс 360 для бизнеса («Боты в Мессенджере»).

yandex_messenger:
  token_env: YANDEX_MESSENGER_BOT_TOKEN  # имя env с OAuth-токеном бота
  allowed_logins: []                     # пусто = любой; список = только эти логины
  show_typing: true                      # индикатор «печатает…» (sendTyping)
  show_tool_calls: false                 # эхо tool-вызовов (по умолчанию выключено)
  poll_secs: 2                           # пауза между опросами getUpdates (сек)
Поле По умолч. Описание
token_env пусто имя env с OAuth-токеном; пусто = бот выключен
allowed_logins [] ограничение отправителей по логину Яндекса
show_typing true показывать typing-индикатор
show_tool_calls false эхо tool-вызовов (отдельные сообщения — шумно, т.к. Мессенджер не умеет их заменять)
poll_secs 2 пауза между опросами getUpdates (сек)

Сессия — ym:{template}:user:{login} (личный чат) или ym:{template}:chat:{chat_id} (группа/канал). Ответ в личный чат идёт по login, в группу/канал — по chat_id. Форматирование — markdown-подмножество Яндекса (**жирный**, __курсив__, ~~зачёркнутый~~, [ссылка](url), код); полный Markdown агента перерисовывается через comrak. Визуальные блоки: график → PNG (sendImage), таблица → текстовые строки, файл → документ (sendFile).

Входящие файлы и изображения пользователя (при attachments.enabled: true у шаблона) скачиваются через getFile и сохраняются в scratch сессии; агентский запрос request_attachment резолвится так же.

Как sink результата (у cron/email/monitor) Яндекс Мессенджер задаётся типом yandex_messenger; поле chat_id принимает либо id чата/канала (значение со /, напр. 0/0/4f24b544-…), либо логин пользователя для личного чата:

notify:
  - type: yandex_messenger
    chat_id: "0/0/4f24b544-697c-4e18-a9c1-b39432ee9bf9"   # или логин пользователя
    # token_env: YANDEX_MESSENGER_BOT_TOKEN  # опц.; по умолчанию токен из yandex_messenger-конфига

Этот канал — только про Мессенджер. Вход через Яндекс (OAuth-логин на вебе) и инструменты Яндекс Диска / других сервисов — отдельные интеграции.

Как протестировать

  1. В Яндекс 360 → Боты в Мессенджере создай бота и скопируй его OAuth-токен.

  2. Положи токен в .env (см. .env.example):

    YANDEX_MESSENGER_BOT_TOKEN=At...
    
  3. Добавь в agent.yaml шаблона секцию yandex_messenger (пример уже есть у drom-agent), перезапусти сервер. В логе появится yandex messenger bot authenticated (или предупреждение об ошибке токена).

  4. Напиши боту в личные сообщения или добавь его в групповой чат.

Быстрая проверка токена без запуска сервера — smoke-тест:

cargo test -p agent_web_bot --test yandex_messenger_live -- --nocapture

Он вызывает self/get и getUpdates и печатает логин бота; без токена тест молча пропускается.


19.4 Email

Опрос IMAP-ящика: каждое новое письмо запускает one-shot ход и отвечает по SMTP. Контекст между письмами — через долгую память (хук memory), ключ — канонический user id отправителя.

email:
  imap_host: imap.example.com
  imap_port: 993
  imap_username: support@example.com
  imap_password_env: EMAIL_PASSWORD   # пароль из env, не в YAML
  allowed_senders: []                 # пусто = любой; "@domain" = весь домен
  smtp:                               # reply-настройки (опусти = только обработка)
    host: smtp.example.com
    username: support@example.com
    password_env: SMTP_PASSWORD
    from: support@example.com
  notify: []                          # доп. sink'и (как у cron)
  poll_secs: 60
Поле По умолч. Описание
imap_host пусто IMAP-хост; пусто = канал выключен
imap_port 993 порт IMAP (implicit TLS)
imap_username пусто логин (обычно адрес ящика)
imap_password_env пусто имя env с паролем IMAP
allowed_senders [] разрешённые отправители (точный адрес или @домен)
smtp нет SMTP для ответа: host, username, password_env, from, tls, port
notify [] доп. sink'и результата
poll_secs 60 интервал опроса ящика

Вложения письма (при attachments.enabled: true у шаблона) сохраняются в scratch сессии и передаются агенту (base64/quoted-printable декодируются).


19.4 Storage (S3/WebDAV/FTP)

Опрос внешнего хранилища: каждый новый файл под префиксом запускает one-shot ход агента (текст файла — в сообщении). Похоже на email-канал, но источник — файлы в бакете/папке.

storage:
  storage:                           # подключение (как у storage_tool)
    backend: s3
    endpoint: https://s3.example.com
    bucket: inbox
    region: us-east-1
    access_key_env: S3_ACCESS_KEY
    secret_key_env: S3_SECRET_KEY
  prefix: inbox/                     # папка для опроса
  poll_secs: 60                      # интервал опроса
  include_suffix: ".txt"             # опц.: обрабатывать только *.txt
  move_after: processed/             # опц.: переместить файл после обработки
  # delete_after: true               # или: удалить после обработки
Поле По умолч. Описание
storage нет подключение (backend: s3/webdav/ftp); пусто = канал выключен
prefix "" папка/префикс для опроса
poll_secs 60 интервал опроса
include_suffix "" фильтр по суффиксу имени файла (пусто = все)
move_after "" переместить обработанный файл под этот префикс
delete_after false удалить обработанный файл (взаимоисключает move_after)

Dedup — в памяти (обработанные ключи); для переживания рестарта используй move_after/delete_after, чтобы файл не обрабатывался повторно.

Storage-синк (выход)

Доставка результата в хранилище — синк storage в notify (как у cron/email):

notify:
  - type: storage
    storage:
      backend: s3
      endpoint: https://s3.example.com
      bucket: reports
      region: us-east-1
      access_key_env: S3_ACCESS_KEY
      secret_key_env: S3_SECRET_KEY
    key: "runs/{source}/{ts}.txt"   # {source} и {ts} подставляются

19.5 Программные каналы (указатели)