В современном мире, где обучение становится все более сложным, а тесты — настоящим испытанием для студентов и учеников, а также для начинающих специалистов, которые работают в компаниях, где сильно развито грейдирование, мы постоянно ищем эффективные способы облегчить процесс получения знаний. Специально для вас я разбираю мощный плагин для браузера, который использует возможности GPT для решения тестов на любые темы. Этот не инновационный инструмент, но таких примеров разбора я в интернете не нашёл. В этой статье мы расскажем, как работает этот плагин, какие преимущества он предлагает и как вы можете использовать его, чтобы достигать результатов на 100%. Давайте разберемся, как сделать вашу учебу проще и эффективнее с помощью этой уникальной технологии!

В своём примере я использовал браузер Chrome. Ну что же, приступим.

Сам плагин состоит из 2 файлов end.js, в которых прописывается основной функционал плагина и manifest.json, в котором даны инструкции для сайтов и в котором задействуется наш основной файл end.js.

Файл manifest.json содержит:

{
    "name": "Тест",
    "version": "1.0",
    "manifest_version": 3,
    "content_scripts": [
        {
            "matches": ["https://Здесь ссылка на сайт/*"],
            "js": [ "end.js" ]
        }
    ]	
}

В общем и целом, здесь от нас требуется сущий пустяк: указать своё название плагина в name и адрес сайта. С manifest.json я решил не заморачиваться, поскольку это не какой-то грандиозный проект, а обычный помощник в тестах.

Теперь перейдём к самому вкусненькому — end.js. Специально для этой статьи я оставил максимум комментариев к строкам. Первой функцией я решил добавить на сайт кнопку, чтобы потом на неё повесить обращение к gpt.

// Добавление кнопки для отображения вопроса и отправки в GPT
function addButton() {
    const button = document.createElement("button");
    button.innerText = "Правильный ответ";
    button.style.position = "fixed"; // Фиксированное положение
    button.style.top = "10px"; // Позиция от верхнего края
    button.style.right = "60px"; // Позиция от правого края
    button.style.zIndex = 1000; // Чтобы кнопка была сверху
	
    // Создаем индикатор загрузки
    const loadingIndicator = document.createElement("span");
    loadingIndicator.style.position = "fixed"; // Фиксированное положение
    loadingIndicator.style.top = "10px"; // Позиция от верхнего края
    loadingIndicator.style.right = "60px"; // Позиция от правого края
    loadingIndicator.style.zIndex = 1000; // Чтобы кнопка была сверху
    loadingIndicator.innerText = "Загрузка...";
    loadingIndicator.style.display = "none"; // Скрываем индикатор по умолчанию
    loadingIndicator.style.marginLeft = "10px"; // Отступ от кнопки
    loadingIndicator.style.color = "red"; // Цвет текста индикатора

    let intervalId; // Переменная для хранения идентификатора интервала

    // Функция для обновления текста индикатора
    const updateLoadingText = () => {
        if (loadingIndicator.innerText === "Загрузка...") {
            loadingIndicator.innerText = "Загрузка.";
        } else if (loadingIndicator.innerText === "Загрузка.") {
            loadingIndicator.innerText = "Загрузка..";
        } else {
            loadingIndicator.innerText = "Загрузка...";
        }
    };

    // Добавляем обработчик событий для нажатия кнопки
    button.onclick = async () => {
        loadingIndicator.style.display = "inline"; // Показываем индикатор загрузки
        button.style.display = "none"; // Скрываем кнопку
        intervalId = setInterval(updateLoadingText, 500); // Запускаем интервал для обновления текста
        await showMessageFromIframe(); // Ждем завершения обработки
        loadingIndicator.style.display = "none";  // Скрываем индикатор загрузки
        button.style.display = "inline";  // Показываем кнопку
        clearInterval(intervalId); // Очищаем интервал
        loadingIndicator.innerText = "Загрузка..."; // Сбрасываем текст
    };

    // Добавляем индикатор загрузки и кнопку в тело документа
    document.body.appendChild(button);
    document.body.appendChild(loadingIndicator);
    console.log("Кнопка добавлена на страницу.");
}

Собственно говоря, ничего такого грандиозного. Обычная кнопка и сменяющий её индикатор загрузки, видимый, пока не закончит выполняться сама функция обращения к gpt showMessageFromIframe(). Теперь перейдём непосредственно к ней.

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

// Функция для отображения сообщения из iframe
async function showMessageFromIframe() {
    const iframe = document.querySelector("iframe.content_frame"); // Получаем iframe
    if (iframe) {
        const iframeDocument = iframe.contentDocument || iframe.contentWindow.document; // Получаем доступ к документу iframe
        const questionElement = iframeDocument.querySelector("#q_6879ubhfbka7-myuq0b3e1owp > div.quiz-player-skin__main-container > div.quiz-slide-container > div.quiz-session-view > div > div.slide-layout > div > div:nth-child(1) > div > p > span"); // Укажите путь к вопросу
		const variandElement = iframeDocument.querySelector("#q_6879ubhfbka7-myuq0b3e1owp > div.quiz-player-skin__main-container > div.quiz-slide-container > div.quiz-session-view > div > div.slide-layout > div > div:nth-child(2) > div > div"); // Указываем путь к вариантом ответа
		
        if (questionElement && variandElement) {
            const question = questionElement.innerText; // Получаем текст вопроса
			const variants = Array.from(variandElement.querySelectorAll("div")).map(variant => variant.innerText); // Получаем текст вариантов ответов
			
			const questionWithVariants = question + " " + variants.join(" "); // Объединяем вопрос и варианты ответов
			
            await main(questionWithVariants); // Отправляем вопрос в GPT
        } else {
            alert("Вопрос не найден в iframe.");
        }
    } else {
        alert("Iframe не найден.");
    }
}

Вот тут как раз мы получаем сам вопрос и варианты ответов, которые находятся в одном блоке в разных <div>, и собираем их все. После объединяем и получаем единое сообщение для отправки к GPT.

// Основной процесс
async function main(question) {
    const threadId = await createThread(); // Шаг 1: Создаем тему
    if (threadId) {
        await addMessageToThread(threadId, question); // Шаг 2: Добавляем сообщение
        const runId = await createRun(threadId, "asst_код_асисстента", "Тебе даются вопросы с вариантоми ответов и ты на основе базы данных выдаёшь правильный ответ один. Ты выдаёшь только один правильный вариант ответа без каких либо пояснений."); // Шаг 3: Создаем запуск
		
		// Проверяем статус выполнения запуска
        await checkRunStatus(threadId, runId); // Шаг 4: Проверка статуса выполнения

        // Получение сообщений после создания запуска
        await getMessagesFromThread(threadId); // Шаг 4: Получаем сообщения
    }
}

Поскольку мы не пользуемся в этом примере никакими крутыми плюшками, а обращение к GPT будет обычным fetch‑запросом, то пункт с checkRunStatus очень важен. В технической документации о ней не особо говорится, но на форумах нашёл, что при данном подходе она прямо-таки необходима.

Ну что ж, теперь погрузимся в недра GPT.

Начнём всё как по тех. документации от open.ai: создаём тему для нашего ранее уже созданного ассистента в функции createThread().

async function createThread() {
    console.log("Создание темы...");

    try {
        const response = await fetch("https://api.openai.com/v1/threads", {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer уникальный_ключ_выданный_openai",
                "OpenAI-Beta": "assistants=v2"
            },
            body: JSON.stringify({})
        });

        if (!response.ok) {
            throw new Error(`Ошибка HTTP: ${response.status}`);
        }

        const data = await response.json();
        console.log("Тема создана, ID:", data.id);
        return data.id; // Вернуть ID темы для дальнейшего использования
    } catch (error) {
        console.error("Ошибка при создании темы:", error);
    }
}

Специально чтобы всё отслеживать и мониторить на случай ошибок, я также оставил вывод в консоль console.log(«...»); практически во всех функциях. В каждом запросе мы будем использовать «Authorization»: «Bearer уникальный_ключ_выданный_openai» и Bearer не нужно отсюда удалять, как советуют на многих форумах))). Функция возвращает нам уникальный ID, который мы будем использовать в дальнейшем.

Теперь идём к следующей функции addMessageToThread. Наконец-то отправим наше сообщение в GPT.

async function addMessageToThread(threadId, message) {
    console.log("Добавление сообщения в тему...");

    try {
        const response = await fetch(`https://api.openai.com/v1/threads/${threadId}/messages`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer уникальный_ключ_выданный_openai",
                "OpenAI-Beta": "assistants=v2"
            },
            body: JSON.stringify({
                role: "user",
                content: message
            })
        });

        if (!response.ok) {
            throw new Error(`Ошибка HTTP: ${response.status}`);
        }

        const data = await response.json();
        console.log("Сообщение добавлено в тему:", data);
    } catch (error) {
        console.error("Ошибка при добавлении сообщения в тему:", error);
    }
}

Да-да. От строк:

const data = await response.json();
console.log("Сообщение добавлено в тему:", data);

можно избавиться, но всё же мне было интересно, что выдаст open.ai в ответ. Сама функция нам ничего не возвращает, лишь отправляет сообщение в GPT. Здесь интересного мало, так что идём дальше к функции createRun.

async function createRun(threadId, assistantId, instructions) {
    console.log("Создание запуска...");

    try {
        const response = await fetch(`https://api.openai.com/v1/threads/${threadId}/runs`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer уникальный_ключ_выданный_openai",
                "OpenAI-Beta": "assistants=v2"
            },
            body: JSON.stringify({
                assistant_id: assistantId,
                instructions: instructions
            })
        });

        if (!response.ok) {
            throw new Error(`Ошибка HTTP: ${response.status}`);
        }

        const data = await response.json();
        console.log("Запуск создан:", data);
        return data.id; // Вернуть ID созданного запуска для последующего использования
    } catch (error) {
        console.error("Ошибка при создании запуска:", error);
    }
}

Она уже, в свою очередь, как я понял, отправляет нашему ассистенту команду на генерацию долгожданного ответа и ещё выдаёт нам уникальный id, к которому мы будем в дальнейшем обращаться за статусом готовности ответа.

Теперь перейдём к ней... Той самой функции, которая опрашивает периодически, готов ли наш ответ или нет. checkRunStatus

async function checkRunStatus(threadId, runId) {
    let status = '';
    try {
        while (status !== 'completed') {
            const response = await fetch(`https://api.openai.com/v1/threads/${threadId}/runs/${runId}`, {
                method: "GET",
                headers: {
                    "Authorization": "Bearer уникальный_ключ_выданный_openai",
                    "OpenAI-Beta": "assistants=v1"
                }
            });

            if (!response.ok) {
                throw new Error(`Ошибка HTTP: ${response.status}`);
            }

            const data = await response.json();
            status = data.status;
            console.log("Статус запуска:", status);

            // Подождать перед следующей проверкой
            await new Promise(res => setTimeout(res, 2000));
        }
    } catch (error) {
        console.error("Ошибка при проверке статуса запуска:", error);
    }
}

Как только наш ассистент составит ответ, у него изменится идентификатор на completed, и наш ответ будет готов к тому, чтобы мы его забрали.

Я, честно говоря, долго возился и пытался понять, почему мне не приходит ответ после отправки к GPT. Ведь я пытаюсь забрать его, а мне приходит какая-то дичь.

Ну и наконец получаем ответ, который я решил вывести в alert:

// Получение сообщений после завершения выполнения
async function getMessagesFromThread(threadId) {
    console.log("Получение сообщений из темы...");

    try {
        const response = await fetch(`https://api.openai.com/v1/threads/${threadId}/messages`, {
            method: "GET",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer уникальный_ключ_выданный_openai",
                "OpenAI-Beta": "assistants=v2"
            }
        });

        if (!response.ok) {
            throw new Error(`Ошибка HTTP: ${response.status}`);
        }

        const data = await response.json();
        console.log("Полученный ответ:", data);

        // Проверяем, если данные содержат сообщения внутри объекта data
        if (Array.isArray(data.data) && data.data.length > 0) {
            const messages = data.data[0].content; // Получаем содержимое сообщения
			const answer = messages[0].text.value; // Получаем текст ответа
			
			const cleanedAnswer = answer.replace(/【\d+:\d+†source】/g, '').trim(); // Удаляем все вхождения формата 【x:y†source】
			
            alert(`Правильный вариант: ${cleanedAnswer}`);
        } else {
            alert("Ассистент не вернул ответ.");
        }
    } catch (error) {
        console.error("Ошибка при получении сообщений из темы:", error);

При выводе заметил, что у нас после ответа выдаётся идентификатор файлов, к которым обращался ассистент для поиска ответа. Да, я знаю, что это мелочь, но глаза всё равно режет. Поэтому строкой:
const cleanedAnswer = answer.replace(/【\d+:\d+†source】/g, '').trim(); // Удаляем все вхождения формата 【x:y†source】
удаляем всё лишнее после ответа.

У любителей чистого кода прошу прощения за все левые выводы в консоль. Мне нравится лицезреть, что именно у меня работает, и в случае ошибки намного проще выявить проблемный сегмент.

Да, и если решили просто скопировать данные запросы, не забываем их расставить хотя бы в правильной последовательности, чтобы сначала была функция, а только потом её вызывали.


Конечная строчка кода — это:
addButton();
собственно, сам вызов начальной функции.

Теперь к самой сути ассистента

Этот этап я начинал с создания сначала векторной базы данных, а потом уже писал своего ассистента.

Итак, нам нужна будет база, по которой ассистент ищет ответы на вопросы. Мне с этим повезло: у меня были методички и техническая документация в цифровом варианте. Главное, чтобы всё было в текстовом формате!

Берём свою базу. Заранее я её не подготавливал, поскольку в ней ассистент и так хорошо ориентируется, и кладём в отдельный текстовый файл у себя на ПК.

Далее переходим в лк openai и выбираем Хранилище, там выбираем именно Vector stores (Векторные магазины), надеюсь, правильно перевёл)
Нажимаем на создание нового и загружаем ему наш файл с базой.
После чего уже в созданной базе нажимаем на создание ассистента. Напоминаю: мы не переходим в ассистенты, а создаём его прямо из базы. Так к ассистенту при создании привязывается база.

Да, возможно, её получится подключить отдельно, но я так и не нашёл, где именно это можно сделать.

И уже после этого можно брать наш идентификатор ассистента и писать под него код.

Хочу заметить, что версию GPT в плагине я выбирал gpt4o‑mini. И работает плагин из России у меня без VPN.

На данный код и разбор я потратил, в общем и целом, пару-тройку часов и, надеюсь, кому-то она сэкономит немного времени:‑)

Комментарии (1)


  1. Dolios
    27.07.2024 17:32

    Далее переходим в лк openai и выбираем Хранилище, там выбираем именно Vector stores (Векторные магазины), надеюсь, правильно перевёл)