image


В этой статье рассказано, как написать и развернуть клиент для ChatGPT при помощи Wing и Next.js.

Рассмотренное здесь приложение может работать локально (в локальном симуляторе облака), либо его можно развернуть в облаке у вашего провайдера.

Введение


Если требуется обеспечить дополнительный контроль над вашими данными, то для этого удобно написать клиент ChatGPT и развернуть его в вашей облачной инфраструктуре.

Если развернуть в облаке большую языковую модель (LLM), это упрочит как конфиденциальность, так и безопасность вашего проекта.

Иногда есть повод беспокоиться о том, как именно хранятся или обрабатываются ваши данные на удалённых серверах, особенно, если вы пользуетесь проприетарными LLM-платформами, например, ChatGPT от OpenAI. Дело может быть либо в чувствительности самих данных, заливаемых на платформу, либо в других факторах, связанных с конфиденциальностью.

В таком случае вы выиграете, если самостоятельно разместите LLM в вашей облачной инфраструктуре или станете использовать её прямо на локальной машине. Обратите внимание на Wing.
Wing — это облачно-ориентированный язык программирования, на котором удобно писать и разворачивать облачные приложения, совершенно не беспокоясь, на какой инфраструктуре это делается. Этот язык одновременно упрощает сборку в облаке, поскольку предоставляет вам возможность самостоятельно определять облачную инфраструктуру и управлять ею, а также программировать приложения – всё на одном и том же языке.
Wing не привязан к конкретному облаку, то есть, написанные на нём приложения можно компилировать и развёртывать на разных облачных платформах.

Знакомьтесь: Wing

image



Приступим!


Для изучения этой статьи вам потребуется:

  • Базовое представление о Next.js
  • Установить Wing на вашей машине. Если не знаете, как это делается — не волнуйтесь. Мы подробно разберём всю процедуру в этом проекте.
  • Получить ключ к API OpenAI.

Создание проектов


Для начала нужно установить Wing на вашей машине. Выполните следующую команду:

npm install -g winglang

Подтвердите установку, отметив версию:

wing -V

Создавайте приложения на Next.js и Wing.


mkdir assistant
cd assistant
npx create-next-app@latest frontend
mkdir backend && cd backend
wing new empty

Мы успешно создали проекты на Wing и Next.js в каталоге assistant directory. Наш клиент для ChatGPT называется Assistant. Круто звучит, правда?

В каталогах для фронтенда и бэкенда содержатся, соответственно, наши приложения на Next и Wing. Команда wing new empty создаёт три следующих файла: package.json, package-lock.json и main.w. Последний файл послужит нам входной точкой в приложение.

Запускаем наше приложение на локальной машине в симуляторе Wing


В симуляторе Wing вы можете выполнять ваш код, писать модульные тесты, а также отлаживать ваш код прямо на локальной машине. Вам при этом не потребуется развёртывать код в конкретном облачном провайдере. Так ускоряются итерации при разработке.

При помощи этой команды можно запустить ваше приложение Wing на локальной машине:

wing it

Ваше приложение Wing запустится по адресу localhost:3000.

image

Настраиваем бекэнд


  • Давайте установим библиотеку OpenAI для Wing и библиотеки React. Библиотека OpenAI служит стандартным интерфейсом для взаимодействия с LLM. Библиотека React позволяет подключить бэкенд на Wing к приложению на Next.

    npm i @winglibs/openai @winglibs/react

  • Импортируйте следующие пакеты в ваш файл main.w. Также давайте импортируем все прочие библиотеки, которые нам понадобятся.

    bring openai
    bring react
    bring cloud
    bring ex
    bring http

bring — это оператор импорта в Wing. Можете считать, что bring обеспечивает в Wing ровно тот же функционал, что и import в JavaScript.

cloud — это облачная библиотека Wing. Она предоставляет стандартный интерфейс для Cloud API, Bucket, Counter, Domain, Endpoint, Function и многих других облачных ресурсов. ex — это стандартная библиотека для взаимодействия с таблицами и облачной базой данных Redis, а http предназначается для вызова различных HTTP-методов, то есть, для отправки информации на удалённые ресурсы и для извлечения её оттуда.

Как вам обзавестись ключом к API OpenAI


В нашем приложении мы воспользуемся gpt-4-turbo, но вам для этого подойдёт и любая другая модель от OpenAI.

  • Создайте учётную запись в OpenAI, если ещё не сделали этого. Чтобы создать новый ключ к API, перейдите по ссылке platform.openai.com/api-keys и выберите Create new secret key (Создать новый секретный ключ).
image

  • Задайте Name (Имя), Project (Проект) и Permissions (Права доступа), затем щёлкните Create secret key (Создать секретный ключ).

image


Инициализация OpenAI


Для инициализации нашего API к OpenAI создадим специальный класс. Мы хотим, чтобы этот класс можно было переиспользовать.

Мы добавим в наш класс Assistant личностное измерение (personality), чтобы ИИ-ассистенту можно было задавать эту настройку, передавая ему промпт.

let apiKeySecret = new cloud.Secret(name: "OAIAPIKey") as "OpenAI Secret";

class Assistant {
    personality: str;
    openai: openai.OpenAI;

    new(personality: str) {
        this.openai = new openai.OpenAI(apiKeySecret: apiKeySecret);
        this.personality = personality;
    }

    pub inflight ask(question: str): str {
        let prompt = `you are an assistant with the following personality: ${this.personality}. ${question}`;
        let response = this.openai.createCompletion(prompt, model: "gpt-4-turbo");
        return response.trim();
    }
}

В Wing определение инфраструктуры сочетается с определением логики приложения благодаря использованию концепций «предполётного» и «полётного» кода соответственно.

Предполётный (Preflight) код (как правило, это инфраструктурные определения) выполняется всего один раз во время компиляции, тогда как полётный (inflight) код будет работать во время всего выполнения программы, реализуя при этом поведение вашего приложения.

Некоторые примеры предполётного кода — корзины для облачного хранения данных, очереди и конечные точки API. При определении предполётного кода добавлять ключевое слово preflight не требуется, Wing применяет его по умолчанию. Однако перед блоком полётного кода требуется добавлять ключевое слово inflight.

В вышеприведённом коде есть блок inflight. Блоки inflight нужны в тех случаях, когда вы пишете асинхронный код, который во время выполнения может непосредственно взаимодействовать с ресурсами через их inflight-API.

Тестирование и сохранение облачного секрета


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

Создадим в корневом каталоге бекэнда файл .env и передадим туда ключ к нашему API:

OAIAPIKey = Your_OpenAI_API_key

Можно тестировать ключи к API OpenAI прямо на локальной машине, ставя ссылку на наш файл .env. Затем, в случае, если мы планируем развернуть приложение на AWS, нам понадобится выполнить всю работу по настройке менеджера секретов AWS.

image

Отправляемся на сайт AWS и заходим в наш аккаунт через консоль. Если у вас такого аккаунта нет, то его можно создать бесплатно.

image

Далее переходим в менеджер секретов (Secrets Manager) и пробуем хранить в нём значения наших ключей к API.

image

image

Мы сохранили ключ к нашему API в облачном секрете под названием OAIAPIKey. Скопируем ключ, перейдём к окну терминала и подключимся к секрету, который теперь хранится на платформе AWS.

wing secrets

Далее вставьте в окно терминала ваш ключ к API как обычное значение. Теперь ваши ключи как следует сохранены и можно приступать к взаимодействию с приложением.



Сохранение ответов ИИ в облаке


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

Давайте создадим другой класс, использующий класс Assistant. В него мы будем передавать настройки «personality» вашего ИИ, а также промпт. Кроме того, будем сохранять ответы каждой модели как текстовые файлы в формате txt и класть их в облачную корзину.

let counter = new cloud.Counter();

class RespondToQuestions {
    id: cloud.Counter;
    gpt: Assistant;
    store: cloud.Bucket;

    new(store: cloud.Bucket) {
        this.gpt = new Assistant("Respondent");
        this.id = new cloud.Counter() as "NextID";
        this.store = store;
    }

    pub inflight sendPrompt(question: str): str {
        let reply = this.gpt.ask("{question}");
        let n = this.id.inc();
        this.store.put("message-{n}.original.txt", reply);
        return reply;
    }
}



Мы настроили «личность» нашего ассистента как «Respondent» (Отвечающий). То есть, мы хотим, чтобы он отвечал на вопросы. Кроме того, можно позволить пользователю, работающему с клиентской частью приложения, задавать эту настройку personality всякий раз при отправке промптов.

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

Давайте определим нашу базу данных

Определение базы данных


В Wing встроен компонент ex.Table — это нереляционная (NoSQL) база, в которой можно хранить данные и запрашивать их оттуда.

let db = new ex.Table({
    name: "assistant",
    primaryKey: "id",
    columns: {
        question: ex.ColumnType.STRING,
        answer: ex.ColumnType.STRING
    }
});



В наше определение базы данных мы добавили два столбца. В первом мы храним пользовательские промпты, а во втором — ответы модели.

Создание маршрутов API и программирование логики


Нам требуется возможность отправлять данные на бекэнд и забирать их оттуда. Давайте создадим маршруты POST и GET.

let api = new cloud.Api({ cors: true });

api.post("/assistant", inflight((request) => {
    // Здесь записана логика запроса POST
}));

api.get("/assistant", inflight(() => {
    // Здесь записана логика запроса GET
}));



let myAssistant = new RespondToQuestions(store) as "Helpful Assistant";

api.post("/assistant", inflight((request) => {
    let prompt = request.body;
    let response = myAssistant.sendPrompt(JSON.stringify(prompt)); 
    let id = counter.inc(); 

    // Вставляем в базу данных промпт и ответ
    db.insert(id, { question: prompt, answer: response });

    return cloud.ApiResponse({
        status: 200
    });
}));

По маршруту POST мы собираемся передавать в модель пользовательский промпт, получаемый с клиентской части, а также получать ответ. Как промпт, так и ответ будут храниться в базе данных. При помощи cloud.ApiResponse можно отправлять ответ, получив запрос от пользователя…

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

api.get("/assistant", inflight(() => {
    let questionsAndAnswers = db.list();

    return cloud.ApiResponse({
        body: JSON.stringify(questionsAndAnswers),
        status: 200
    });
}));

Бэкенд готов. Давайте протестируем его в локальном симуляторе облака.

Запустим его при помощи Wing.

Перейдём по адресу localhost:3000 и зададим вопрос нашему ассистенту.

image

Как наш вопрос, так и ответ ассистента будут сохранены в базе данных. Взгляните.

image

Предоставление URL нашего API на клиенте


Необходимо предоставить URL к нашему API, расположенному на бэкенде, передав этот URL на фронтенд, написанный на Next. Именно в этом нам поможет библиотека React, которую мы установили ранее.

let website = new react.App({
    projectPath: "../frontend",
    localPort: 4000
});

website.addEnvironment("API_URL", api.url);

Добавим следующий код в наш файл layout.js из приложения на Next.

import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
};

export default function RootLayout({ children }) {
    return (
        <html lang="en">
            <head>
            <script src="./wing.js" defer></script>
           </head>
            <body className={inter.className}>{children}</body>
        </html>
    );
}

Теперь у нас есть доступ к API_URL прямо в приложении Next.

Реализация логики фронтенда


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

import { useEffect, useState, useCallback } from 'react';
import axios from 'axios';

function App() {

    const [isThinking, setIsThinking] = useState(false);
    const [input, setInput] = useState("");
    const [allInteractions, setAllInteractions] = useState([]);

    const retrieveAllInteractions = useCallback(async (api_url) => {
            await axios ({
              method: "GET",
              url: `${api_url}/assistant`,
            }).then(res => {
              setAllInteractions(res.data)
            })
  }, [])

    const handleSubmit = useCallback(async (e)=> {
        e.preventDefault()

        setIsThinking(!isThinking)


        if(input.trim() === ""){
          alert("Chat cannot be empty")
          setIsThinking(true)

        }

          await axios({
            method: "POST",
            url: `${window.wingEnv.API_URL}/assistant`,
            headers: {
              "Content-Type": "application/json"
            },
            data: input
          })
          setInput("");
          setIsThinking(false);
          await retrieveAllInteractions(window.wingEnv.API_URL);     

  })

    useEffect(() => {
        if (typeof window !== "undefined") {
            retrieveAllInteractions(window.wingEnv.API_URL);
        }
    }, []);

    // Here you would return your component's JSX
    return (
        // Здесь следует содержимое JSX
    );
}

export default App;

Функция retrieveAllInteractions выбирает все вопросы и ответы, содержащиеся в базе данных бэкенда. Функция handSubmit отправляет пользовательский промпт на бэкенд.

Добавим реализацию JSX.

import { useEffect, useState } from 'react';
import axios from 'axios';
import './App.css';

function App() {
    // ...

    return (
        <div className="container">
            <div className="header">
                <h1>My Assistant</h1>
                <p>Ask anything...</p>
            </div>

            <div className="chat-area">
                <div className="chat-area-content">
                    {allInteractions.map((chat) => (
                        <div key={chat.id} className="user-bot-chat">
                            <p className='user-question'>{chat.question}</p>
                            <p className='response'>{chat.answer}</p>
                        </div>
                    ))}
                    <p className={isThinking ? "thinking" : "notThinking"}>Generating response...</p>
                </div>

                <div className="type-area">
                    <input 
                        type="text" 
                        placeholder="Ask me any question" 
                        value={input} 
                        onChange={(e) => setInput(e.target.value)} 
                    />
                    <button onClick={handleSubmit}>Send</button>
                </div>
            </div>
        </div>
    );
}

export default App;

Запускаем приложение на локальной машине


Перейдите в каталог с вашим бэкендом и запустите приложение Wing на локальной машине следующей командой

cd ~assistant/backend
wing it

Также запустите клиентский код Next.js:

cd ~assistant/frontend
npm run dev

Давайте посмотрим, как выглядит ваше приложение.

image

Давайте попросим ИИ-ассистент задать разработчику пару вопросов через приложение, написанное на Next.

image

Развёртывание приложений на AWS


Мы рассмотрели, как наше приложение работает на локальной машине. На языке Wing его также можно развернуть на любом облачном провайдере, в том числе, на AWS. Чтобы развернуть приложение на AWS, вам понадобятся Terraform и AWS CLI, сконфигурированные с вашими учётными данными.
  • Скомпилируйте приложение под Terraform/AWS при помощи tf-aws. Эта команда приказывает компилятору воспользоваться Terraform в качестве движка для предоставления ресурсов, привязав их все к набору ресурсов AWS, задаваемому по умолчанию.

    cd ~/assistant/backend
    wing compile --platform tf-aws main.w


  • Выполняем команды Init и Apply в Terraform

    cd ./target/main.tfaws
    terraform init
    terraform apply



Обратите внимание: на выполнение terraform требуется некоторое время.

Полный код к этому посту выложен здесь.

Заключение


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

В этой статье было продемонстрировано, как на языке Wing можно с лёгкостью создавать масштабируемые облачные приложения, совершенно не волнуясь о том, на какой именно инфраструктуре их придётся развёртывать.

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