# Оценка ответа

Сохраните оценку конкретного сообщения ассистента. GET подходит для готовых ссылок из ответа генерации; PUT также позволяет снять реакцию. Оценки покупателей отображаются отдельно от отзывов команды в личном кабинете.

## `GET /shopping/feedback`

После сохранения ответа генерация добавляет в корневой `events[]` ссылки двух типов:

```json
{
  "events": [
    {
      "type": "like",
      "url": "https://shopping-assistant-api.gravityfield.ai/shopping/feedback?tenant_id=demo-section&resourceId=demo-user&threadId=demo-thread&messageId=demo-answer&reaction=like"
    },
    {
      "type": "dislike",
      "url": "https://shopping-assistant-api.gravityfield.ai/shopping/feedback?tenant_id=demo-section&resourceId=demo-user&threadId=demo-thread&messageId=demo-answer&reaction=dislike"
    }
  ]
}
```

Идентификаторы в примере условные. Используйте готовый `url` из фактического ответа: тело запроса и дополнительный токен не требуются.

1. Найдите событие нужного типа рядом с сохранённым ответом.
2. Вызовите его URL методом GET только при нажатии соответствующей кнопки.
3. После `200` обновите состояние оценки; при ошибке сохраните предыдущее состояние.

```javascript
async function rateAnswer(result, reaction) {
  const event = result.events?.find(item => item.type === reaction);
  if (!event?.url) throw new Error('Для ответа нет ссылки оценки');
  const response = await fetch(event.url, { method: 'GET', cache: 'no-store' });
  if (!response.ok) throw new Error('Не удалось сохранить оценку');
  return response.json();
}
// В обработчике кнопки «Нравится»: rateAnswer(result, 'like').
```

Не вызывайте все URL из `events[]` при получении ответа: `like` и `dislike` меняют состояние одного сообщения. Повторная реакция не создаёт дубликат, противоположная заменяет предыдущую.

Обязательные query-параметры: непустые `tenant_id`, `resourceId`, `threadId`, `messageId` и `reaction: like | dislike`. GET не принимает `null`; для снятия оценки используйте PUT. Все готовые ссылки ведут на production-домен, включая ссылки из другого окружения. Ответ должен существовать в принимающем сервисе.

| HTTP | Результат |
| --- | --- |
| `200` | `{ "reaction": "like", "updatedAt": "2026-10-13T10:00:00.000Z" }` |
| `400` | `Invalid request query`: проверьте параметры. |
| `404` | `Assistant message not found`: нет сохранённого ответа с указанной связкой. |
| `500` | `Failed to save feedback`: предложите повторить позже. |

GET не ограничивается списком разрешённых IP секции. Сервер проверяет принадлежность сообщения указанным секции, пользователю и диалогу; ссылка не подтверждает личность отправителя. Ответы используют `Cache-Control: no-store`.

Debug-ответы не сохраняются и не получают feedback-ссылки. History API не добавляет эти ссылки: для восстановленного ответа используйте PUT и `id` сообщения. Готовый Web-виджет `1.41` также использует PUT.

## `PUT /shopping/feedback`

Чтобы собрать оценку конкретного ответа Shopping Assistant, вызовите:

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

Endpoint сохраняет текущую реакцию пользователя на сообщение ассистента. Повторная отправка того же состояния идемпотентна: она не создает дубликат оценки.

### Запрос

```bash
curl --request PUT \
  --url 'https://shopping-assistant-api.gravityfield.ai/shopping/feedback' \
  --header 'Content-Type: application/json' \
  --data '{
    "tenant_id": "demo-section",
    "resourceId": "demo-user",
    "threadId": "thread-8f2b1c",
    "messageId": "shopping:assistant:8a21...",
    "reaction": "like"
  }'
```

| Поле | Обязательное | Назначение |
| :--- | :--- | :--- |
| `tenant_id` | Да | ID секции Gravity Field. Должен совпадать с `runtimeContext.tenant_id` запроса `/shopping/generate`. |
| `resourceId` | Да | `uid` пользователя Gravity Field, которому принадлежит диалог. |
| `threadId` | Да | ID диалога, в котором был получен ответ. |
| `messageId` | Да | Значение `assistantMessageId` из ответа `/shopping/generate` или `id` сообщения ассистента из `/shopping/history`. |
| `reaction` | Да | `like` для положительной оценки, `dislike` для отрицательной или `null`, чтобы удалить ранее сохраненную оценку. |

API принимает оценку только при точном совпадении `tenant_id`, `resourceId`, `threadId` и `messageId`. Это не позволяет записать реакцию на сообщение из другого диалога или профиля пользователя.

### Ответ

```json
{
  "reaction": "like",
  "updatedAt": "2026-08-07T10:00:00.000Z"
}
```

В `reaction` возвращается сохраненное состояние. Поле `updatedAt` содержит время его последнего изменения.

Чтобы заменить оценку, отправьте тот же запрос с другим значением `reaction`. Чтобы пользователь мог снять выбранную оценку, отправьте `reaction: null`.

### Ошибки отправки оценки

| HTTP-статус | Причина | Что делать клиенту |
| :--- | :--- | :--- |
| `400` | Некорректный JSON, отсутствует обязательное поле, передано пустое значение идентификатора или неподдерживаемая реакция. | Проверить payload перед повторной отправкой. |
| `403` | IP не разрешён для секции. | Проверить условия доступа с командой Gravity Field. |
| `404` | Сообщение ассистента не найдено или связка идентификаторов не совпадает. | Не сохранять оценку локально как отправленную; проверить идентификаторы сообщения и диалога. |
| `500` | Оценку не удалось сохранить. | Вернуть элемент управления в предыдущее состояние и предложить повторить запрос. |

Проверьте положительную, отрицательную и повторную оценку, смену реакции и снятие через PUT. Диагностика: [Оценка не сохраняется](../../troubleshooting.md#оценка-не-сохраняется).

---
