Generic selectors

Exact matches only

Search in title

Search in content

Post Type Selectors

Как подключить ИИ-ассистента Альбато к вашему ИИ-агенту по протоколу A2A

Эта инструкция предназначена для разработчиков, которые встраивают Альбато в свой продукт в рамках вайт-лейбл интеграции. Вы узнаете, как подключить своего ИИ-агента к ИИ-ассистенту Альбато по открытому протоколу A2A (Agent2Agent, версия 1.0). После подключения ваш агент сможет поручать ИИ-ассистенту создание автоматизаций, а вы сможете показывать результат пользователю в интерфейсе своего продукта.

1. Что такое ИИ-ассистент (Copilot) по A2A

ИИ-ассистент Альбато (Copilot) — это агент, который от имени пользователя Альбато:

  • собирает, настраивает, проверяет и запускает автоматизации: выбирает приложения, триггеры и действия, подключения, сопоставляет поля;
  • ищет по каталогу Альбато приложения, их триггеры и действия, показывает подключения пользователя;
  • разбирает существующие связки и объясняет, почему они не работают.

По A2A ваш агент отправляет ИИ-ассистенту задачу обычным текстом на языке пользователя, а в ответ получает поток событий: что ИИ-ассистент делает сейчас, какие вопросы у него возникли и итоговый ответ. Технически это JSON-RPC 2.0 поверх HTTPS, поток событий приходит как Server-Sent Events (SSE). Описание протокола и инструменты для разработки доступны на официальном сайте A2A. Для Python используется пакет a2a-sdk.

JSON-RPC 2.0 задаёт формат запросов и ответов. SSE (Server-Sent Events) позволяет серверу передавать события по мере выполнения задачи. DataPart — часть сообщения со структурированными данными.

Основные понятия:

Понятие Что это
Agent Card JSON-описание агента по адресу <COPILOT_URL>/.well-known/agent-card.json: навыки, адрес JSON-RPC, требования к авторизации. SDK (набор инструментов для разработки) читает карточку автоматически
Task Одна задача (один ход диалога). Имеет taskId и состояние: SUBMITTED → WORKING → COMPLETED, FAILED, CANCELED или INPUT_REQUIRED (ИИ-ассистент ждёт ответа)
contextId Идентификатор разговора. Передавайте его во всех следующих сообщениях, и ИИ-ассистент будет помнить, что уже сделано
Artifact Результат задачи. У ИИ-ассистента это артефакт answer: текст ответа и, если создана автоматизация, её bundle_id

2. Что вы получите от менеджера Альбато

  1. Адрес ИИ-ассистента — базовый URL, далее в тексте <COPILOT_URL>.
  2. Партнёрский токен вида cpa2a_… — секрет, который идентифицирует вашего агента. Один на партнёра. Храните его на сервере, как любой API-ключ, и никогда не передавайте в браузер. Если токен утёк, напишите менеджеру: его перевыпустят, старый перестанет работать сразу.

3. Авторизация: два токена в каждом запросе

В каждый запрос к <COPILOT_URL>/a2a добавьте два заголовка:

Заголовок Что это Где получить
X-Copilot-Partner-Token: cpa2a_… Идентификация вашего агента Выдал менеджер Альбато
Authorization: Bearer <JWT> Идентификация пользователя Альбато, от имени которого работает ИИ-ассистент Токен сессии Альбато этого пользователя (см. раздел 4)

И обязательный служебный заголовок протокола: A2A-Version: 1.0 (SDK добавляет его автоматически; при ручных запросах добавьте вручную).

Необязательно: если пользователь работает в рабочем пространстве Альбато Teams, добавьте заголовки WorkspaceId и SpaceId — те же, что ваш интерфейс отправляет в API Альбато.

Важно! Партнёрский токен не заменяет пользовательский: без токена пользователя ИИ-ассистент не сможет ничего сделать в его аккаунте. Анонимного режима нет.

4. Как получить токен пользователя и передать его ИИ-ассистенту

ИИ-ассистент принимает тот же токен сессии Альбато, который вы уже используете, чтобы показать пользователю интерфейс Альбато внутри своего продукта: session_token, который вы передаёте в интерфейс Альбато при открытии (в браузере он затем хранится в cookie authToken_<среда>). Отдельный токен пользователя для ИИ-ассистента не нужен.

Рекомендуемая схема:

1. Браузер пользователя
Пользователь пишет сообщение в чат вашего продукта. Интерфейс отправляет его в ваш API с авторизацией вашего продукта.

2. Ваш бэкенд, на котором работает ИИ-агент
Сервер получает токен сессии Альбато этого пользователя. Это тот токен, который вы получили от Альбато при открытии пользователю интерфейса Альбато.

Сервер отправляет запрос ИИ-ассистенту:

POST <COPILOT_URL>/a2a
X-Copilot-Partner-Token: cpa2a_…
Authorization: Bearer <session_token пользователя>
A2A-Version: 1.0

Партнёрский токен cpa2a_… хранится на сервере и не передаётся в браузер.

3. ИИ-ассистент Альбато
ИИ-ассистент выполняет задачу и возвращает на ваш бэкенд поток событий с ответами и вопросами.

4. Ответ пользователю
Ваш API передаёт события в интерфейс продукта по мере их поступления. Пользователь видит ответы и вопросы ИИ-ассистента в чате.

В этой схеме бэкенд — серверная часть вашего продукта, а фронтенд — интерфейс, который работает в браузере пользователя.

Правила:

  • A2A-клиент работает на вашем сервере. Партнёрский токен нельзя передавать в браузер. Если ваш агент запускается на фронтенде, используйте между ним и ИИ-ассистентом свой прокси-сервер, который добавит партнёрский токен.
  • Токен пользователя хранит ваш бэкенд, а не страница. Ваш фронтенд авторизуется в вашем API как обычно, а токен Альбато бэкенд подставляет сам. Так токен не попадает в JavaScript и логи браузера. Если по архитектуре вы всё же передаёте его с фронтенда, делайте это только в теле или заголовке запроса к вашему API по HTTPS и не добавляйте в URL.
  • Срок жизни токена равен сроку сессии пользователя в Альбато. Если ИИ-ассистент ответил 401 на пользовательский токен, получите новый токен тем же способом, каким получаете его для интерфейса Альбато, и повторите запрос.
  • Один токен пользователя — один пользователь. Не отправляйте задачи одного пользователя с токеном другого: ИИ-ассистент будет действовать в чужом аккаунте.

5. Как отправить задачу и обработать ответ

Шаг 1. Получите карточку агента

curl -s <COPILOT_URL>/.well-known/agent-card.json

Карточка публичная. Из неё нужен адрес JSON-RPC (supportedInterfaces[0].url, это <COPILOT_URL>/a2a) и список навыков. SDK получает эти данные автоматически.

Шаг 2. Отправьте задачу

Рекомендуем потоковый метод SendStreamingMessage: вы сразу видите прогресс, и снижается риск закрытия соединения по таймауту балансировщика нагрузки. Блокирующий SendMessage держит HTTP-соединение до конца задачи (это может занять минуты) и подходит только для коротких запросов.

curl -N -s -X POST <COPILOT_URL>/a2a \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -H "A2A-Version: 1.0" \
  -H "X-Copilot-Partner-Token: $COPILOT_PARTNER_TOKEN" \
  -H "Authorization: Bearer $ALBATO_USER_TOKEN" \
  -d '{
    "jsonrpc": "2.0", "id": "1", "method": "SendStreamingMessage",
    "params": {"message": {"messageId": "m-1", "role": "ROLE_USER",
                           "parts": [{"text": "Какие триггеры есть у Telegram?"}]}}
  }'

messageId — любой уникальный идентификатор с вашей стороны (например, UUID).

Шаг 3. Обработайте поток событий

Каждое SSE-событие — строка data: {...} с JSON-RPC-ответом. В поле result находится одно из следующих событий:

Событие Что делать
task Начало задачи. Сохраните task.id (taskId) и task.contextId
statusUpdate со state: TASK_STATE_WORKING Прогресс. В status.message.parts[0].text короткий текст («Calling search_partners», «step_2: completed …»), во второй части DataPart с деталями. Можно показывать пользователю как индикатор
artifactUpdate Итоговый ответ: artifact.name == "answer", текст в parts[0].text, в DataPart может быть bundle_id созданной автоматизации
statusUpdate со state: TASK_STATE_COMPLETED Задача завершена. Текст ответа продублирован в status.message
statusUpdate со state: TASK_STATE_INPUT_REQUIRED ИИ-ассистент ждёт ответа пользователя — см. шаг 4
statusUpdate со state: TASK_STATE_FAILED Ошибка: текст в status.message, в DataPart error_code и recoverable
statusUpdate со state: TASK_STATE_CANCELED Задача отменена

Поток заканчивается вместе с завершением задачи или на состоянии INPUT_REQUIRED.

Шаг 4. Передайте ответы на вопросы ИИ-ассистента (INPUT_REQUIRED)

ИИ-ассистент часто уточняет: какое действие выбрать, какое подключение использовать, что именно нужно сделать. Тогда задача переходит в TASK_STATE_INPUT_REQUIRED, а в status.message передаются:

  • текст вопроса — parts[0].text;
  • DataPart с деталями — parts[1].data:
    • {"type": "question"} — свободный вопрос, ответьте текстом;
    • {"type": "hitl_request", "hitl_type": "select" | "add_connection" | "connection_actions" | …, "prompt": "…", "options": [{"option_id": "…", "label": "…"}], "connection_url": "…"?} — выбор из вариантов.

Покажите вопрос и варианты пользователю и отправьте его ответ новым сообщением с теми же taskId и contextId:

{"jsonrpc": "2.0", "id": "2", "method": "SendStreamingMessage",
 "params": {"message": {"messageId": "m-2", "role": "ROLE_USER",
                        "taskId": "<taskId>", "contextId": "<contextId>",
                        "parts": [{"text": "My Telegram bot"}]}}}

Чтобы выбрать вариант, передайте option_id, его label (регистр не учитывается) или отдельную часть {"data": {"selected_option_id": "cred_1"}}. Если ответ не совпал ни с одним вариантом, задача останется в INPUT_REQUIRED, а в сообщении будет подсказка со списком вариантов.

Важно! Особый случай — add_connection: пользователю нужно подключить сервис. В DataPart придёт connection_url. Откройте эту ссылку пользователю в новом окне браузера, дождитесь, пока он авторизуется, и ответьте вариантом add_connection. Затем ИИ-ассистент предложит connection_actions с вариантами continue и try_again. Автоматически пройти этот шаг нельзя: его выполняет человек.

Шаг 5. Продолжите разговор

  • Следующее сообщение в тот же разговор: передайте contextId из первого ответа (без taskId). ИИ-ассистент помнит контекст: найденные приложения, созданную автоматизацию, предыдущие ответы.
  • Новое сообщение без taskId, пока предыдущая задача не завершена: текущая задача прерывается, неотвеченный вопрос отменяется, начинается новая задача.
  • Отменить задачу: {"method": "CancelTask", "params": {"id": "<taskId>"}}.
  • Узнать состояние: {"method": "GetTask", "params": {"id": "<taskId>"}}.
  • Сообщение в завершённую задачу отклоняется — начните новую с тем же contextId.

6. Пример на Python (a2a-sdk)

pip install "a2a-sdk>=1.1" httpx
import asyncio
import os

import httpx
from a2a.client import ClientConfig, create_client
from a2a.helpers import (
    get_data_parts, get_stream_response_text,
    new_data_part, new_message, new_text_part,
)
from a2a.types import Role, SendMessageRequest, TaskState

COPILOT_URL = os.environ["COPILOT_URL"]              # прислал менеджер
PARTNER_TOKEN = os.environ["COPILOT_PARTNER_TOKEN"]  # секрет сервера


async def ask_copilot(user_token, text, *, context_id=None, task_id=None,
                      selected_option_id=None):
    http = httpx.AsyncClient(
        headers={
            "X-Copilot-Partner-Token": PARTNER_TOKEN,
            "Authorization": f"Bearer {user_token}",
        },
        timeout=httpx.Timeout(600, connect=10),
    )
    config = ClientConfig(streaming=True, httpx_client=http)
    client = await create_client(COPILOT_URL, client_config=config)

    parts = [new_text_part(text)] if text else []
    if selected_option_id:
        parts.append(new_data_part({"selected_option_id": selected_option_id}))
    message = new_message(parts, context_id=context_id, task_id=task_id,
                          role=Role.ROLE_USER)

    state, answer, question = None, "", None
    async for chunk in client.send_message(SendMessageRequest(message=message)):
        if chunk.HasField("task"):
            task_id, context_id = chunk.task.id, chunk.task.context_id
        elif chunk.HasField("status_update"):
            status = chunk.status_update.status
            state = TaskState.Name(status.state)
            if state == "TASK_STATE_WORKING":
                progress = get_stream_response_text(chunk)
                if progress:
                    print("[copilot]", progress)  # прогресс
            elif state == "TASK_STATE_INPUT_REQUIRED":
                datas = get_data_parts(status.message.parts)
                question = {
                    "text": get_stream_response_text(chunk),
                    "data": datas[0] if datas else None,
                }
        elif chunk.HasField("artifact_update"):
            answer += get_stream_response_text(chunk)

    await client.close()
    await http.aclose()
    return {"state": state, "answer": answer, "question": question,
            "task_id": task_id, "context_id": context_id}


async def main():
    user_token = "<session_token пользователя Albato>"
    r = await ask_copilot(user_token, "Сделай связку: новый лид в amoCRM "
                                      "→ сообщение в Telegram")
    while r["state"] == "TASK_STATE_INPUT_REQUIRED":
        print("Copilot спрашивает:", r["question"]["text"])
        for opt in (r["question"]["data"] or {}).get("options", []):
            print("  -", opt["option_id"], ":", opt["label"])
        reply = input("Ответ пользователя: ")
        r = await ask_copilot(user_token, reply,
                              context_id=r["context_id"], task_id=r["task_id"])
    print(r["state"], r["answer"])


asyncio.run(main())
Важно! В примере выше HTTP-клиент создаётся и закрывается при каждом вызове ask_copilot. При внедрении создавайте его вне этой функции и закрывайте после окончания разговора. Используйте один httpx.AsyncClient на весь разговор пользователя: он хранит cookies и соединение, это ускоряет запросы и позволяет балансировщику держать разговор на одном сервере.

7. Ошибки

HTTP-ошибки приходят до JSON-RPC:

Код Причина Что делать
401 Нет партнёрского токена, либо нет/просрочен токен пользователя (в detail сказано, какой именно) Проверьте заголовки; обновите токен пользователя
403 Партнёрский токен неизвестен или отозван Обратитесь к менеджеру

Ошибки JSON-RPC приходят с HTTP 200 в поле error:

Код Смысл
-32602 Некорректные параметры: пустое сообщение, contextId другого пользователя или идентификатор не в формате UUID, contextId не соответствует задаче, сообщение в завершённую задачу
-32001 Задача не найдена (неизвестный или устаревший taskId) — начните новую задачу с тем же contextId
-32009 Нет заголовка A2A-Version: 1.0
-32603 Внутренняя ошибка ИИ-ассистента — повторите позже

Ошибки самого ИИ-ассистента (нет доступа к API Альбато, исчерпан лимит, сбой агента) приходят как TASK_STATE_FAILED с текстом для пользователя.

8. Ограничения и советы

  • Одна задача выполняется до 15 минут; затем она прерывается и возвращается как FAILED.
  • В одном разговоре (contextId) задачи идут последовательно: дождитесь завершения или INPUT_REQUIRED прежде чем отправлять следующее сообщение.
  • Пишите ИИ-ассистенту на языке пользователя — ответ придёт на том же языке.
  • Чаты A2A-сессий можно будет посмотреть в партнёрском кабинете Альбато.
  • Не записывайте токены в журналы (логи). Партнёрский токен — на сервере, пользовательский — только в памяти запроса.

9. К кому обращаться по вопросам интеграции

По всем вопросам интеграции вы можете обращаться к менеджеру Альбато.