ИИ-чат¶
Чат учителя с ИИ-ассистентом: вопросы по предмету, методике и планированию урока.
Ответ приходит потоком 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 |
Треда нет или он чужой |