# Интеграция Shopping Assistant по API

Shopping Assistant - это AI-помощник для покупателей. Он ведет диалог, уточняет потребность, помогает выбрать подходящие товары из каталога и возвращает результат в структурированном формате для отображения в клиентском интерфейсе.

Этот вариант интеграции подходит, когда сайт, приложение, backend или другой клиентский канал самостоятельно реализует UI/UX чата и напрямую вызывает API Gravity:

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

API не возвращает готовый экран чата. В ответе приходят текстовые блоки, товарные карточки, кнопки действий и быстрые ответы. Клиентская сторона отвечает за отображение этих блоков, пользовательские сценарии вокруг чата и локальную историю сообщений в интерфейсе.

---

## Что нужно реализовать на стороне клиента

1. Создать чат-интерфейс: поле ввода, список сообщений, состояние загрузки, обработку ошибок и повтор запроса.
2. Генерировать стабильный `threadId` на одну чат-сессию. Все запросы одного диалога должны отправляться с тем же `threadId`.
3. Передавать стабильный `resourceId` - это `uid` пользователя Gravity Field. Удобный способ получить его заранее: [`POST /user`](/Integration/api_integration/v2/personalization/user.md).
4. В `messages` передавать текущее сообщение пользователя. Историю диалога повторно отправлять не нужно: контекст сохраняется на стороне Shopping Assistant по `threadId` и `resourceId`.
5. Рендерить блоки из `response.contents`:
   - `message` как текстовое сообщение ассистента;
   - `products` как список товарных карточек; если в блоке есть `title`, отображать его как заголовок рекомендательного блока;
   - `button` как действие, например переход в поддержку.
6. Рендерить `response.ui.suggests` как быстрые ответы. При нажатии быстрый ответ отправляется как обычное новое сообщение пользователя.
7. Для товарных карточек использовать `sku` как основной идентификатор товара. Открытие PDP, добавление в корзину, получение изображения и актуальной цены остаются на стороне клиентского канала или клиентского каталога.
8. Передавать события показа и взаимодействия: `events` верхнего уровня относятся ко всему рекомендательному блоку, а `events` внутри товара - к конкретной карточке.

---

## Типовой поток

1. Пользователь открывает чат, клиентская сторона создает `threadId`.
2. Если у клиентской стороны еще нет `uid` Gravity Field, она получает или создает пользователя через [`POST /user`](/Integration/api_integration/v2/personalization/user.md) и использует `user.uid` как `resourceId`.
3. Пользователь отправляет сообщение.
4. Клиентская сторона вызывает `/shopping/generate` с текущим сообщением пользователя в `messages`.
5. API возвращает структурированный ответ и сохраняет ход диалога в memory по `threadId` и `resourceId`.
6. Клиентская сторона отображает ответ и сохраняет его в локальной истории интерфейса.
7. На следующем ходе клиентская сторона снова отправляет только новое сообщение пользователя с тем же `threadId` и `resourceId`.

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

---

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

```ts
type ShoppingGenerateRequest = {
  threadId: string;
  resourceId: string;
  trafficType?: "real" | "test";
  messages: Array<{
    role: "user";
    content: string;
  }>;
  runtimeContext: {
    tenant_id: string;
    ctx?: {
      type?: string;
      data?: unknown;
      lng?: string | null;
      location?: string | null;
    };
  };
};
```

### Поля запроса

| Поле | Обязательное | Назначение |
| :--- | :--- | :--- |
| `threadId` | Да | ID диалога. Должен оставаться одинаковым для всех сообщений одной чат-сессии. |
| `resourceId` | Да | `uid` пользователя Gravity Field. Его можно получить через [`POST /user`](/Integration/api_integration/v2/personalization/user.md). |
| `trafficType` | Нет | `real` для боевого трафика или `test` для тестовых сценариев. |
| `messages` | Да | Текущее сообщение пользователя. Обычно передается массив с одним элементом `{ "role": "user", "content": "..." }`. |
| `runtimeContext.tenant_id` | Да | ID секции Gravity Field. |
| `runtimeContext.ctx` | Нет | Контекст текущего экрана или страницы. |

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

`ctx` использует ту же модель контекста, что и API V2. Основные типы: `HOMEPAGE`, `SEARCH`, `PRODUCT`, `CATEGORY`, `CART`, `OTHER`.

- Общие правила и список типов: [API V2: общий контекст](/Integration/api_integration/v2/getting_started.md#общий-контекст).
- Если нужно заранее создать пользователя вместе с контекстом: [`POST /user`](/Integration/api_integration/v2/personalization/user.md).
- Для старых V1/server-side интеграций с `context.page`: [Передача контекста страницы](/Integration/api_integration/personalization/page_context.md).

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

```json
{
  "runtimeContext": {
    "tenant_id": "670ccaae56afcafaf808c146",
    "ctx": {
      "type": "PRODUCT",
      "data": ["1066109"]
    }
  }
}
```

---

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

### Запрос

```bash
curl --request POST \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/generate' \
  --header 'Content-Type: application/json' \
  --data '{
    "threadId": "thread-8f2b1c",
    "resourceId": "665f0a000000000000000001",
    "trafficType": "test",
    "messages": [
      {
        "role": "user",
        "content": "Помогите выбрать корм для взрослой кошки"
      }
    ],
    "runtimeContext": {
      "tenant_id": "670ccaae56afcafaf808c146"
    }
  }'
```

### Ответ

```json
{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Подберу корм, но сначала уточню пару деталей. Кошка стерилизована и есть ли особенности по здоровью?"
      }
    ],
    "ui": {
      "suggests": [
        "Стерилизована, без особенностей",
        "Не стерилизована, чувствительное пищеварение",
        "Пожилая кошка, нужен мягкий корм"
      ]
    }
  }
}
```

---

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

```json
{
  "threadId": "thread-8f2b1c",
  "resourceId": "665f0a000000000000000001",
  "response": {
    "contents": [
      {
        "type": "message",
        "value": "Вот несколько подходящих вариантов. Смотрите на возраст, стерилизацию и чувствительность пищеварения."
      },
      {
        "type": "products",
        "title": "Подходящие корма для взрослых кошек",
        "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?..."
      ]
    }
  ]
}
```

---

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

```ts
type ShoppingGenerateResponse = {
  threadId: string;
  resourceId: string;
  response: {
    contents: Array<
      | { type: "message"; value: string }
      | { type: "products"; value: Product[]; title?: string }
      | { type: "button"; value: string; action: string }
    >;
    ui?: {
      suggests: string[];
    };
  };
  events?: EngagementEvent[];
};

type Product = {
  sku?: 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[];
};
```

Для кнопок клиентская сторона должна обработать `action`. Например, `open_support_chat` можно связать с открытием клиентского чата поддержки.

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

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

---

## Передача показов и кликов

В ответе Shopping Assistant tracking-события могут находиться на двух уровнях:

| Уровень | Где искать | Когда использовать |
| :--- | :--- | :--- |
| Рекомендательный блок | `events[]` в корне ответа | Для событий всего блока, например видимого показа `WRIMP`. |
| Товарная карточка | `response.contents[type="products"].value[].events[]` | Для `visible_impression` и `click` конкретного товара. |

При наступлении события обработайте **все** элементы `events[]` с соответствующим `type`. Для каждого найденного элемента выполните `GET`-запрос по **каждому** URL из `urls[]`. Не ограничивайтесь первым элементом `events[]` или первым URL: один факт взаимодействия может требовать нескольких tracking-запросов.

```js
async function sendTrackingEvents(events, type) {
  const urls = events
    .filter((event) => event.type === type)
    .flatMap((event) => event.urls);

  await Promise.allSettled(
    urls.map((url) => fetch(url, { method: "GET", keepalive: true }))
  );
}

// Весь рекомендательный блок стал видимым.
sendTrackingEvents(result.events ?? [], "WRIMP");

// Стала видимой конкретная карточка товара.
sendTrackingEvents(product.events ?? [], "visible_impression");
```

Отправляйте событие видимого показа один раз для одного фактического показа блока или карточки. Ошибка tracking-запроса не должна блокировать отображение рекомендаций или действие пользователя. Общие правила работы с tracking URL описаны в разделе [Передача взаимодействий пользователя с кампаниями](/Integration/api_integration/v2/personalization/engagement.md).

---

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

| HTTP-статус | Причина | Что делать клиенту |
| :--- | :--- | :--- |
| `400` | Некорректный JSON или тело запроса не соответствует контракту. | Проверить payload перед повторной отправкой. |
| `500` | Техническая ошибка генерации ответа. | Показать ошибку и предложить повторить запрос. |
| `502` | Ответ ассистента не прошел структурную валидацию. | Показать сообщение вроде "Не удалось получить ответ, попробуйте еще раз". |

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