Генерация ответа

Отправьте новое сообщение пользователя и отобразите блоки response.contents. Для пошагового запуска используйте API-интеграцию; для параметров доступа и идентификаторов — обзор API.

POST /shopping/generate

POST https://shopping-assistant-api.gravityfield.ai/shopping/generate
Content-Type: application/json

Endpoint принимает новое сообщение пользователя и возвращает текстовые блоки, товарные карточки, кнопки действий и быстрые ответы. Готовый UI чата API не возвращает.

Для всех сообщений одного диалога используйте одинаковые threadId и resourceId. В resourceId передавайте user.uid Gravity Field или значение user.custom, если ваша интеграция использует внешний ID. Для гостевой сессии зарегистрируйте тот же resourceId и добавляйте заголовок Authorization: Bearer gst_..., включая debug-режим. Получение токена и общие ошибки описаны на странице Гостевые токены. Примеры ниже без заголовка авторизации относятся к прежнему режиму.

Историю диалога повторно отправлять не нужно: Shopping Assistant сохраняет контекст по этим идентификаторам.

В гостевом режиме диалог и память ассистента принадлежат текущей сессии. После новой регистрации с тем же resourceId используйте новый threadId: токен другой сессии не откроет прежний разговор. Порядок действий описан в разделе восстановления сессии.

Для обычной интеграции используйте роль user.

Контракт запроса

type ShoppingTextContentPart = {
  type: "text";
  text: string;
};

type ShoppingImageContentPart = {
  type: "image";
  source:
    | {
        type: "url";
        url: string;
      }
    | {
        type: "base64";
        mediaType: "image/jpeg" | "image/png" | "image/webp";
        data: string;
      };
};

type ShoppingGenerateRequest = {
  threadId: string;
  resourceId: string;
  trafficType?: "real" | "test" | "debug";
  messages: Array<{
    role: "user";
    content:
      | string
      | Array<ShoppingTextContentPart | ShoppingImageContentPart>;
  }>;
  runtimeContext: {
    tenant_id: string;
    ctx?: {
      type?: string;
      data?: unknown;
      lng?: string | null;
      location?: string | null;
      attributes?: {
        storeId?: string;
        [key: string]: unknown;
      };
    };
  };
};

Поля запроса

Поле Обязательное Назначение
threadId Да ID диалога. Должен оставаться одинаковым для всех сообщений одной чат-сессии.
resourceId Да user.uid Gravity Field или используемый в интеграции user.custom. Для гостевой сессии должен совпадать с ID при регистрации токена.
trafficType Нет real для боевого трафика, test для тестовых сценариев или debug для проверки интеграции на фиксированном ответе без обращения к LLM. Без поля тип трафика остаётся неизвестным.
messages Да Текущее сообщение пользователя. Обычно передается массив с одним элементом. content может быть строкой или массивом блоков text и image.
messages[].id Нет ID нового сообщения пользователя. Для повтора сохраните исходный ID; для нового сообщения создайте новый.
runtimeContext.tenant_id Да ID секции Gravity Field.
runtimeContext.ctx Нет Контекст текущего экрана или страницы.
runtimeContext.ctx.attributes.storeId Для сценария наличия в выбранном магазине Стабильный ID точки из справочника магазинов. Передавайте, если покупатель выбрал магазин в интерфейсе.

Как передать изображение

Чтобы отправить изображение, передайте в content массив блоков. В одном пользовательском сообщении поддерживается только одно изображение. Вместе с ним можно передать текстовый блок с пояснением:

{
  "threadId": "thread-8f2b1c",
  "resourceId": "demo-user",
  "trafficType": "test",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Найдите похожий диван в более светлом цвете"
        },
        {
          "type": "image",
          "source": {
            "type": "base64",
            "mediaType": "image/jpeg",
            "data": "/9j/4AAQSkZJRgABAQ..."
          }
        }
      ]
    }
  ],
  "runtimeContext": {
    "tenant_id": "demo-section"
  }
}

В поле data передавайте только Base64-содержимое файла, без префикса data:image/jpeg;base64,. Значение mediaType должно соответствовать фактическому формату файла.

Изображение также можно передать по URL. Замените блок image в примере выше:

{
  "type": "image",
  "source": {
    "type": "url",
    "url": "https://cdn.example.com/user-photo.webp"
  }
}

Текст необязателен: для запроса только по изображению передайте в content массив с одним блоком image.

Ограничения для изображения

Ограничение Требование
Количество Не более одного изображения в одном пользовательском сообщении.
Форматы JPEG, PNG или WebP: image/jpeg, image/png, image/webp.
Размер файла Не более 5 МиБ до Base64-кодирования.
Разрешение Не более 16 000 000 пикселей: ширина × высота.
Base64 Только корректный raw Base64 без data:-префикса. MIME-тип должен совпадать с содержимым файла.
URL Публичный URL по HTTPS без логина и пароля. URL должен возвращать поддерживаемое изображение с корректным Content-Type; допускается не более трех перенаправлений, таймаут загрузки - 10 секунд.

После проверки Shopping Assistant учитывает EXIF-ориентацию, уменьшает изображение до размера не более 2048 × 2048 пикселей без увеличения и преобразует его в JPEG для обработки.

Как заполнять ctx

runtimeContext.ctx использует модель контекста API V2. Передавайте актуальный контекст, согласованный с /visit и /choose. Поддерживаемые типы и формат data описаны в разделе Контекст API V2.

Если чат открыт на карточке товара, передайте SKU товара в контексте:

{
  "runtimeContext": {
    "tenant_id": "demo-section",
    "ctx": {
      "type": "PRODUCT",
      "data": ["1066109"]
    }
  }
}

Если покупатель выбрал магазин, передавайте его storeId в runtimeContext.ctx.attributes.storeId вместе с каждым запросом /shopping/generate, в том числе после перехода на другую страницу:

{
  "runtimeContext": {
    "tenant_id": "demo-section",
    "ctx": {
      "type": "PRODUCT",
      "data": ["1066109"],
      "attributes": {
        "storeId": "kzn-014"
      }
    }
  }
}

Используйте точный storeId из справочника магазинов. Если магазин не выбран, не передавайте поле с пустой строкой; ассистент уточнит точку по сообщению пользователя или задаст вопрос. ctx.lng обозначает зону обслуживания для фида и поиска и не заменяет storeId. Передача storeId сама по себе не включает проверку наличия: для неё требуется подключить API наличия товаров в магазинах и согласовать обработку этого поля в коннекторе.

Режим отладки

Используйте trafficType: "debug", чтобы проверить вызов endpoint-а и отображение ответа в клиентском интерфейсе без обращения к LLM и расхода токенов. В этом режиме Shopping Assistant возвращает одну и ту же заглушку независимо от текста сообщения и настроек секции.

Запрос по-прежнему должен соответствовать контракту: передайте непустые threadId, resourceId, runtimeContext.tenant_id и хотя бы одно сообщение пользователя.

curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/generate' \
  --header 'Content-Type: application/json' \
  --data '{
    "threadId": "debug-thread-1",
    "resourceId": "debug-user-1",
    "trafficType": "debug",
    "messages": [
      {
        "role": "user",
        "content": "Проверка интеграции"
      }
    ],
    "runtimeContext": {
      "tenant_id": "demo-section"
    }
  }'

Ответ содержит текст, два тестовых товара, три быстрых ответа и тестовое tracking-событие:

{
  "threadId": "debug-thread-1",
  "resourceId": "debug-user-1",
  "assistantMessageId": "shopping:assistant:8a21...",
  "context": null,
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Debug response"
      },
      {
        "type": "products",
        "title": "Debug products",
        "shelf_id": "debug-products",
        "source_query": "debug",
        "value": [
          {
            "sku": "debug-product-1",
            "name": "Debug product 1",
            "brand": "Debug brand",
            "price": 100
          },
          {
            "sku": "debug-product-2",
            "name": "Debug product 2",
            "brand": "Debug brand",
            "price": 200
          }
        ]
      }
    ],
    "ui": {
      "suggests": [
        "Debug suggestion 1",
        "Debug suggestion 2",
        "Debug suggestion 3"
      ]
    }
  },
  "responseLatencyMs": 100,
  "events": [
    {
      "type": "visible_impression",
      "url": "https://runtime.test/widget"
    }
  ]
}

Значения responseLatencyMs и events в этом ответе являются частью заглушки: они не отражают фактическую задержку и не фиксируют аналитику. Debug-запрос не запускает генерацию и обработку изображений, не сохраняет сообщения и не появляется в истории. Не отправляйте оценку такого ответа через /shopping/feedback.

После проверки интеграции замените debug на test или real.


Минимальный пример

Запрос

curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/generate' \
  --header 'Content-Type: application/json' \
  --data '{
    "threadId": "thread-8f2b1c",
    "resourceId": "demo-user",
    "trafficType": "test",
    "messages": [
      {
        "role": "user",
        "content": "Помогите выбрать ноутбук для учёбы"
      }
    ],
    "runtimeContext": {
      "tenant_id": "demo-section"
    }
  }'

Ответ

{
  "threadId": "thread-8f2b1c",
  "resourceId": "demo-user",
  "assistantMessageId": "shopping:assistant:8a21...",
  "context": {
    "intent": "product-selection",
    "scenario": { "id": "laptop-selection", "version": 1 },
    "mode": "clarify"
  },
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Какой у вас бюджет и нужна ли большая диагональ экрана?"
      }
    ],
    "ui": {
      "suggests": [
        "До 60 000 ₽, компактный",
        "До 80 000 ₽, большой экран",
        "Помогите определиться с бюджетом"
      ]
    }
  }
}

Пример ответа с товарами

ID и версия сценария в примерах условные: фактические значения зависят от опубликованных плейбуков секции и выбранного сценария текущего ответа.

{
  "threadId": "thread-8f2b1c",
  "resourceId": "demo-user",
  "context": {
    "intent": "product-selection",
    "scenario": { "id": "laptop-selection", "version": 1 },
    "mode": "answer"
  },
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Вот два варианта для учёбы: компактный ноутбук и модель с большим экраном."
      },
      {
        "type": "products",
        "title": "Ноутбуки для учёбы",
        "categories": [
          "Электроника",
          "Ноутбуки"
        ],
        "value": [
          {
            "sku": "1066109",
            "name": "Ноутбук Demo 14",
            "price": 59990,
            "features": "14 дюймов; 16 ГБ RAM; SSD 512 ГБ",
            "is_stm": false,
            "brand": "Demo Brand",
            "url": "https://example.com/product/1066109",
            "image_url": "https://example.com/product/1066109.jpg",
            "events": [
              {
                "type": "visible_impression",
                "urls": [
                  "https://evs-01.gravityfield.ai/v2/engagement?...",
                  "https://evs-02.gravityfield.ai/v2/engagement?..."
                ]
              },
              {
                "type": "click",
                "urls": [
                  "https://evs-01.gravityfield.ai/v2/engagement?..."
                ]
              }
            ]
          },
          {
            "sku": "1023814",
            "name": "Ноутбук Demo 16",
            "price": 74990,
            "features": "16 дюймов; 16 ГБ RAM; SSD 512 ГБ",
            "is_stm": true
          }
        ]
      },
      {
        "type": "message",
        "value": "Первый вариант удобнее носить на занятия. Второй подойдёт, если важнее большой экран."
      }
    ],
    "ui": {
      "suggests": [
        "Показать варианты дешевле",
        "Нужен ноутбук легче",
        "Сравнить эти модели"
      ]
    }
  },
  "events": [
    {
      "type": "WRIMP",
      "urls": [
        "https://evs-01.gravityfield.ai/v2/engagement?...",
        "https://evs-02.gravityfield.ai/v2/engagement?..."
      ]
    }
  ]
}

Контракт ответа

type ShoppingResponseContext = {
  intent: string;
  scenario: { id: string; version: number } | null;
  mode: "answer" | "clarify" | "handoff" | "refuse";
};

type ShoppingGenerateResponse = {
  threadId: string;
  resourceId: string;
  assistantMessageId?: string;
  messageFormat?: "plain_text" | "markdown";
  context: ShoppingResponseContext | null;
  response: {
    contents: Array<
      | { type: "message"; value: string }
      | { type: "products"; value: Product[]; title?: string; categories?: string[]; shelf_id?: string; source_query?: string }
      | { type: "button"; value: string; action: string; url?: string }
    >;
    ui?: {
      suggests: string[];
    };
  };
  responseLatencyMs?: number;
  events?: EngagementEvent[];
};

type Product = {
  sku?: string;
  category_id?: string;
  name: string;
  price?: number | null;
  features?: string;
  is_stm?: boolean;
  brand?: string;
  url?: string;
  image_url?: string;
  events?: EngagementEvent[];
};

type EngagementEvent =
  | { type: string; urls: string[] }
  | { type: string; url: string };

Поле assistantMessageId содержит ID созданного ответа ассистента. Сохраните его вместе с сообщением в UI: этот ID нужен для отправки оценки через /shopping/feedback.

messageFormat задаёт формат текста. Для plain_text отображайте текст без интерпретации HTML. Для markdown используйте Markdown-рендерер с безопасной обработкой ссылок и HTML; если поле отсутствует, уточните формат подключённой секции. responseLatencyMs — длительность ответа сервиса, а не время отображения на устройстве покупателя.

Кнопка перехода к оператору приходит в массиве response.contents в следующем формате:

{
  "type": "button",
  "value": "Позвать оператора",
  "action": "open_support_chat"
}

Поле value содержит текст кнопки, а action: "open_support_chat" обозначает переход к оператору. Shopping Assistant API только возвращает идентификатор действия. Сам переход выполняется клиентским каналом после нажатия кнопки.

Поле title в блоке products содержит готовый заголовок для этой группы рекомендаций. Если поле пришло, отобразите его над товарными карточками. Если title отсутствует, блок можно отобразить без заголовка.

Поле categories в блоке products содержит путь категории товарного блока: от общей категории к более конкретной. Например, ["Диваны и кресла", "Диваны"]. Поле опционально и появляется в ответе после согласования с командой сопровождения Gravity Field.

Для товарных карточек считайте sku основным идентификатором товара. Поля price, brand, url, image_url и другие данные могут использоваться для быстрого отображения карточки, но актуализация цены, изображения, наличия, открытие PDP и добавление в корзину остаются на стороне клиентского канала или клиентского каталога.

category_id — необязательный идентификатор категории товара. Поле categories у блока products содержит путь категории и не заменяет этот идентификатор. Для кнопки с action: "follow_url" поле url содержит HTTP(S)-адрес перехода; open_support_chat передаётся обработчику действий приложения.

Контекст сценария и интерфейс

Поле context находится на верхнем уровне рядом с response. Оно описывает решение для текущего ответа и позволяет адаптировать собственный UI под задачу покупателя. Например, для сценария подбора ноутбука можно показывать элементы интерфейса для выбора диагонали и бюджета. Такие элементы реализует клиентское приложение.

Поле Значение и использование
intent Намерение текущего запроса, например product-selection. Это открытая строка: клиент должен принимать и незнакомые значения.
scenario.id ID выбранного плейбука. Используйте его для выбора UI в пределах текущей секции.
scenario.version Положительное целое число — версия плейбука, использованная для этого ответа. Это не версия всего ассистента.
mode Режим текущего ответа; значения приведены ниже.
mode Что означает
answer Ответ или подборка товаров. Не означает, что задача покупателя завершена.
clarify Уточнение данных у покупателя.
handoff Передача обращения в поддержку. Переход выполняет клиент по действию из response.contents.
refuse Отказ в выполнении запроса.

ID и текущую версию активного сценария можно посмотреть в личном кабинете: Настройки ассистента → Сценарии. Рядом есть кнопка копирования ID и справка Использование в интерфейсе. ID и версия также показаны в сохранённом плейбуке. Названия сценариев и компоненты вашего UI задаются на стороне приложения.

При обработке ответа соблюдайте следующие правила:

  1. Меняйте UI после получения готового ответа. Отдельного раннего события с выбранным сценарием нет.
  2. При handoff и refuse отдавайте приоритет передаче обращения или отказу, скрывая элементы подбора по сценарию.
  3. Для неизвестного ID, scenario: null, context: null или отсутствующего поля используйте обычный чат и продолжайте отображать response.contents и быстрые ответы.
  4. Используйте контекст текущего ответа. Следующее сообщение может сохранить сценарий при другом intent или получить scenario: null; не переносите прежний сценарий автоматически.
  5. Связывайте ответ с assistantMessageId. Повтор старого запроса не должен переключать UI, который уже показывает более новый ответ.

scenario: null означает, что для текущего ответа плейбук не выбран. context: null означает, что полных корректных метаданных нет: например, для старых сообщений или фиксированного debug-ответа. В старой версии сервера поле может отсутствовать — обрабатывайте это так же, как null.

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

Типы событий EngagementEvent

EngagementEvent содержит готовые tracking URL для учета показов и кликов по рекомендациям. Поле type определяет, при каком действии нужно вызвать эти URL. Само получение ответа API не означает, что пользователь увидел подборку или товар.

Корневой events[] может содержать события рекомендательного блока и ссылки оценки ответа. Массив events[] внутри товара в блоке products относится к конкретной карточке.

type Что означает Когда отправлять
IMP Показ рекомендательного блока: блок отрисован в интерфейсе. После первого фактического рендера блока, если событие вернулось в корневом events[]. Попадание в область видимости для этого события не требуется.
WRIMP Видимый показ рекомендательного блока. Когда блок виден пользователю как минимум на 50% в течение не менее 1 секунды. Используйте событие из корневого events[].
visible_impression Видимый показ конкретной товарной карточки; в корневом events[] — видимый показ всего блока. Когда карточка стала видима как минимум на 50%. Для события всего блока применяйте условие видимости WRIMP.
click Клик по конкретному товару из подборки. Когда пользователь нажал на товарную карточку. Используйте events[] этой карточки.
like Положительная оценка ответа. Только при нажатии «Нравится». Вызовите готовый url методом GET.
dislike Отрицательная оценка ответа. Только при нажатии «Не нравится». Вызовите готовый url методом GET.

Обрабатывайте только события, которые пришли в ответе, и сохраняйте регистр type: WRIMP и visible_impression — разные значения. Поле оставлено строковым в контракте, поскольку набор событий зависит от рекомендательной кампании.

При наступлении нужного действия найдите все события с соответствующим type на нужном уровне. Для каждого события выполните GET-запрос по каждому адресу из urls[]; если вместо массива пришло поле url, вызовите этот единственный адрес. Используйте URL как есть: параметры трекинга уже включены в него. Tracking URL фиксирует событие; для перехода на страницу товара используйте Product.url.

Отправляйте показ и видимый показ один раз для одного фактического показа блока или карточки, без повторов при каждой перерисовке. click отправляйте при каждом клике пользователя. Ошибка tracking-запроса не должна блокировать интерфейс.

Ссылки like и dislike не являются событиями показа. Не отправляйте весь массив при рендеринге. См. Оценка ответа.

Пример обработки events[] и события воронки диалога описаны в разделе Трекинг событий.

Ошибки и повтор запроса

HTTP-статус Причина Что делать клиенту
400 Некорректный JSON, тело запроса не соответствует контракту, передано больше одного изображения или некорректный Base64/URL. Проверить payload перед повторной отправкой.
403 IP не разрешён для секции. Проверить условия доступа с командой Gravity Field.
404 DIALOG_NOT_FOUND — диалог скрыт или принадлежит другому пользователю/секции. Проверка действует также для debug-режима и повтора запроса. Проверить идентификаторы. Скрытый диалог продолжить нельзя; для нового разговора создать новый threadId с прежним пользователем и токеном.
409 MESSAGE_ID_CONFLICT: ID сообщения уже использован для другого запроса. Для повтора передать исходные данные и ID; для нового сообщения создать новый ID.
413 Изображение превышает лимит 5 МиБ или 16 000 000 пикселей. Попросить пользователя выбрать или подготовить изображение меньшего размера.
415 Формат изображения не поддерживается или MIME-тип не соответствует содержимому файла. Разрешить только JPEG, PNG и WebP и проверить MIME-тип.
422 Файл изображения поврежден или его невозможно обработать. Попросить пользователя выбрать другое изображение.
500 Техническая ошибка генерации ответа. Показать ошибку и предложить повторить запрос.
502 Ответ ассистента не прошел структурную валидацию. Показать сообщение вроде "Не удалось получить ответ, попробуйте еще раз".
504 Shopping Assistant не успел загрузить изображение по URL. Проверить доступность URL и повторить запрос.

Для повтора того же сообщения сохраняйте messages[].id, threadId, resourceId и исходные данные запроса. Без стабильного ID повтор после потери ответа может создать второе сообщение.

Повтор возвращает context исходного ответа. Если после него уже получен более новый ответ, не применяйте старый контекст к текущему UI.

Показывайте индикатор загрузки до получения ответа, не создавайте новый threadId при повторе того же пользовательского сообщения и защищайтесь от двойной отправки одинакового сообщения при нестабильной сети.