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

Отправьте новое сообщение пользователя и отобразите блоки 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 API V2. Историю диалога повторно отправлять не нужно: Shopping Assistant сохраняет контекст по этим идентификаторам.

Для обычной интеграции используйте роль 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 Да uid пользователя Gravity Field, сохраненный на этапе API-интеграции.
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...",
  "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...",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Подберу корм, но сначала уточню пару деталей. Кошка стерилизована и есть ли особенности по здоровью?"
      }
    ],
    "ui": {
      "suggests": [
        "Стерилизована, без особенностей",
        "Не стерилизована, чувствительное пищеварение",
        "Пожилая кошка, нужен мягкий корм"
      ]
    }
  }
}

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

{
  "threadId": "thread-8f2b1c",
  "resourceId": "demo-user",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Вот несколько подходящих вариантов. Смотрите на возраст, стерилизацию и чувствительность пищеварения."
      },
      {
        "type": "products",
        "title": "Подходящие корма для взрослых кошек",
        "categories": [
          "Зоотовары",
          "Корма для кошек"
        ],
        "value": [
          {
            "sku": "1066109",
            "name": "Сухой корм для взрослых кошек",
            "price": 1299,
            "features": "Для взрослых кошек; повседневный рацион",
            "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": "Корм для стерилизованных кошек",
            "price": 1599,
            "features": "Для стерилизованных кошек; контроль веса",
            "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 ShoppingGenerateResponse = {
  threadId: string;
  resourceId: string;
  assistantMessageId?: string;
  messageFormat?: "plain_text" | "markdown";
  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 передаётся обработчику действий приложения.

Типы событий 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.
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 повтор после потери ответа может создать второе сообщение.

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