Перейти к содержанию

ИИ-чат

Чат учителя с ИИ-ассистентом: вопросы по предмету, методике и планированию урока. Ответ приходит потоком Server-Sent Events и печатается по мере генерации, переписка хранится по тредам. Методы доступны ролям teacher и school_admin; каждый видит только свои треды, чужой отвечает 404, как отсутствующий.

  • Ассистент отвечает на русском, в Markdown; формулы - в LaTeX: $...$ в строке и $$...$$ отдельным блоком, так что нужен рендер формул, например KaTeX.
  • Пока ответ печатается, Markdown бывает незакрытым: рендер должен это переживать.
  • Файлов и ссылок ассистент не открывает и генераций сам не создаёт.

Объект «Тред»

Параметр Тип Описание
id* uuid id треда
title* string Начало первого сообщения, до 80 символов
created_at* datetime Когда создан
updated_at* datetime Когда пришёл последний ответ
{"id": "5f0c2b1a-7d3e-4f6a-9b8c-1d2e3f4a5b6c", "title": "Как объяснить дроби пятиклассникам?", "created_at": "2026-11-10T10:00:00Z", "updated_at": "2026-11-10T10:05:00Z"}

Объект «Сообщение»

Параметр Тип Описание
id* string id сообщения, тот же, что в событиях message_id и ai_message_id
role* string user - учитель, assistant - ассистент
content* string Текст; у ассистента - Markdown
{"id": "a1b2...", "role": "user", "content": "Как объяснить дроби пятиклассникам?"}

Отправка сообщения

Отправляет сообщение ассистенту и возвращает его ответ потоком событий.

Запрос: POST {AI_URL}/v1/ai-chat/stream, авторизация - токен.

Параметр Тип Описание
message* string Сообщение, 1-8000 символов
thread_id uuid Тред, который продолжается; без него или с null начинается новый
curl -N -X POST "$AI_URL/v1/ai-chat/stream" -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" -d '{"message": "Как объяснить дроби пятиклассникам?", "thread_id": null}'

-N выключает буфер curl, иначе ответ появится целиком в конце.

Ответ 200: поток text/event-stream, каждое событие - строка data: {json} и пустая строка за ней.

data: {"type": "metadata", "thread_id": "5f0c2b1a-7d3e-4f6a-9b8c-1d2e3f4a5b6c"}

data: {"type": "message_id", "id": "a1b2..."}

data: {"type": "content", "content": "Начните"}

data: {"type": "content", "content": " с пиццы"}

data: {"type": "ai_message_id", "id": "c3d4..."}

data: {"type": "done"}
type Поля Когда
metadata thread_id Первым: тред ответа, новый или тот же
message_id id id сообщения учителя
content content Очередной кусок ответа; ответ - их склейка по порядку
ai_message_id id id ответа, когда он закончен и сохранён
done Последним, если всё прошло
error content Последним при сбое модели: сообщение учителя сохранено, ответа нет
  • Поток идёт ответом на POST, а EventSource браузера умеет только GET: читайте тело через fetch, как в примере ниже.
  • Следующие сообщения отправляйте с thread_id из события metadata, иначе каждое начнёт новый тред.
  • Ассистент видит последние 30 сообщений треда; для новой темы начните новый тред.
  • Поток, который оборвался без done, ответа не сохраняет: в треде остаётся только сообщение учителя. Повторная отправка того же текста добавит его в тред ещё раз, ассистенту это не мешает.
async function ask(message, threadId, token, onText) {
  const response = await fetch(`${AI_URL}/v1/ai-chat/stream`, {
    method: "POST",
    headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
    body: JSON.stringify({ message, thread_id: threadId }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`); // 401, 404, 422

  const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
  const result = {};
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const events = buffer.split("\n\n");
    buffer = events.pop(); // событие без пустой строки после него ещё не пришло целиком
    for (const raw of events) {
      if (!raw.startsWith("data: ")) continue;
      const event = JSON.parse(raw.slice(6));
      if (event.type === "metadata") result.threadId = event.thread_id;
      else if (event.type === "content") onText(event.content);
      else if (event.type === "ai_message_id") result.messageId = event.id;
      else if (event.type === "error") throw new Error("chat reply failed");
    }
  }
  return result; // без messageId поток оборвался раньше конца ответа
}
Код ошибки Когда
401 Нет токена или он недействителен
404 Треда нет или он чужой
422 Сообщение пустое или длиннее 8000 символов

Ошибки с кодом приходят до начала потока; сбой посреди ответа - событием error.

Список тредов

Возвращает треды владельца токена, последний активный первым.

Запрос: GET {AI_URL}/v1/ai-chat/threads, авторизация - токен.

Параметр Тип Описание
page, page_size integer В строке запроса: пагинация

Ответ 200: страница с items - объектами «Тред».

Сообщения треда

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

Запрос: GET {AI_URL}/v1/ai-chat/threads/{thread_id}/messages, авторизация - токен.

Параметр Тип Описание
page, page_size integer В строке запроса: пагинация

Ответ 200: страница с items - объектами «Сообщение».

Код ошибки Когда
404 Треда нет или он чужой

Удаление треда

Удаляет тред со всеми сообщениями.

Запрос: DELETE {AI_URL}/v1/ai-chat/threads/{thread_id}, авторизация - токен.

Ответ 204 без тела.

Код ошибки Когда
404 Треда нет или он чужой