Создайте веб-приложение в Pure Python, похожее на ChatGPT, используя Reflex

Создайте веб-приложение на Pure Python, аналогичное ChatGPT, с использованием Reflex

Используйте API OpenAI для создания веб-приложения чата на чистом Python с развертыванием в одну строку

Chat app GIF Автор

Последние несколько месяцев я играл с невероятными новыми чат-ботами LLM, включая Llama 2, GPT-4, Falcon 40B и Claude 2. Один вопрос, который меня постоянно беспокоит, – как я могу создать свой собственный пользовательский интерфейс для чат-бота, который вызывает все эти великолепные LLM в качестве API?

Существует бесчисленное множество вариантов для создания красивых пользовательских интерфейсов, но в качестве инженера по машинному обучению у меня нет опыта работы с JavaScript или любым другим языком фронт-энда. Я искал способ создать свое веб-приложение, используя только тот язык, который я знаю в настоящее время – Python!

Я решил использовать сравнительно новый фреймворк с открытым исходным кодом под названием Reflex, который позволяет мне создавать как бэк-энд, так и фронт-энд исключительно на Python.

Отказ от ответственности: Я работаю в качестве главного инженера в Reflex, где вношу свой вклад в открытый исходный код фреймворка.

В этом уроке мы рассмотрим, как создать полноценное AI приложение для чата с нуля, используя чистый Python — вы также можете найти весь код в этом репозитории GitHub.

Вы узнаете, как:

  1. Установить reflex и настроить вашу среду разработки.
  2. Создать компоненты для определения и стилизации вашего пользовательского интерфейса.
  3. Использовать состояние для добавления интерактивности в ваше приложение.
  4. Развернуть ваше приложение с помощью одной команды, чтобы поделиться им с другими.

Настройка вашего проекта

Мы начнем с создания нового проекта и настройки нашей среды разработки. Сначала создайте новый каталог для вашего проекта и перейдите в него.

~ $ mkdir chatapp~ $ cd chatapp

Далее мы создадим виртуальное окружение для нашего проекта. В этом примере мы будем использовать venv для создания виртуального окружения.

chatapp $ python3 -m venv .venv$ source .venv/bin/activate

Теперь мы установим Reflex и создадим новый проект. Это создаст новую структуру каталогов в нашем проектном каталоге.

chatapp $ pip install reflexchatapp $ reflex init────────────────────────────────── Инициализация chatapp ──────────────────────────────────Успех: chatapp инициализированchatapp $ lsassets          chatapp         rxconfig.py     .venv

Вы можете запустить шаблонное приложение, чтобы убедиться, что все работает.

chatapp $ reflex run────────────────────────────────── Запуск Reflex App ──────────────────────────────────Компиляция:  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00─────────────────────────────────────── Приложение работает ───────────────────────────────Приложение запущено по адресу: http://localhost:3000

Вы должны увидеть, что ваше приложение работает по адресу http://localhost:3000.

Reflex также запускает сервер бэк-энда, который обрабатывает всю управляющую информацию и общение с фронт-эндом. Вы можете проверить, что сервер бэк-энда работает, перейдя по ссылке http://localhost:8000/ping.

Теперь, когда у нас настроен наш проект, давайте создадим наше приложение!

Базовый фронт-энд

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

Отображение вопроса и ответа

Мы изменим функцию index в файле chatapp/chatapp.py, чтобы вернуть компонент, отображающий один вопрос и ответ.

Изображение автора (код ниже)
# chatapp.pyimport reflex as rxdef index() -> rx.Component:    return rx.container(        rx.box(            "Что такое Reflex?",            # Вопрос пользователя справа.            text_align="right",        ),        rx.box(            "Способ создания веб-приложений на чистом Python!",            # Ответ слева.            text_align="left",        ),    )# Добавление состояния и страницы в приложение.app = rx.App()app.add_page(index)app.compile()

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

Мы также добавляем некоторое базовое оформление для компонентов. Компоненты принимают на вход именованные аргументы, называемые props, которые изменяют внешний вид и функциональность компонента. Мы используем свойство text_align для выравнивания текста по левому и правому краю.

Повторное использование компонентов

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

Изображение автора (код ниже)
def qa(question: str, answer: str) -> rx.Component:    return rx.box(        rx.box(question, text_align="right"),        rx.box(answer, text_align="left"),        margin_y="1em",    )def chat() -> rx.Component:    qa_pairs = [        (            "Что такое Reflex?",            "Способ создания веб-приложений на чистом Python!",        ),        (            "Что я могу создать с его помощью?",            "Все, начиная от простого сайта и заканчивая сложным веб-приложением!",        ),    ]    return rx.box(        *[            qa(question, answer)            for question, answer in qa_pairs        ]    )def index() -> rx.Component:    return rx.container(chat())

Ввод в чат

Теперь мы хотим предоставить пользователю возможность задать вопрос. Для этого мы используем компонент input для ввода текста и компонент button для отправки вопроса.

Изображение автора (код ниже)
def action_bar() -> rx.Component:    return rx.hstack(        rx.input(placeholder="Задайте вопрос"),        rx.button("Спросить"),    )def index() -> rx.Component:    return rx.container(        chat(),        action_bar(),    )

Оформление

Добавим стилизации к приложению. Более подробную информацию о стилизации можно найти в документации по стилизации. Чтобы сохранить наш код чистым, мы переместим стилизацию в отдельный файл chatapp/style.py.

# style.py# Общие стили для вопросов и ответов.shadow = "rgba(0, 0, 0, 0.15) 0px 2px 8px"chat_margin = "20%"message_style = dict(    padding="1em",    border_radius="5px",    margin_y="0.5em",    box_shadow=shadow,    max_width="30em",    display="inline-block",)# Установка конкретных стилей для вопросов и ответов.question_style = message_style | dict(    bg="#F5EFFE", margin_left=chat_margin)answer_style = message_style | dict(    bg="#DEEAFD", margin_right=chat_margin)# Стили для строки ввода.input_style = dict(    border_width="1px", padding="1em", box_shadow=shadow)button_style = dict(bg="#CEFFEE", box_shadow=shadow)

Мы импортируем стили в chatapp.py и используем их в компонентах. В данной точке приложение должно выглядеть так:

Изображение от автора
# chatapp.pyimport reflex as rxfrom chatapp import styledef qa(вопрос: str, ответ: str) -> rx.Component:    return rx.box(        rx.box(            rx.text(вопрос, style=style.question_style),            text_align="right",        ),        rx.box(            rx.text(ответ, style=style.answer_style),            text_align="left",        ),        margin_y="1em",    )def chat() -> rx.Component:    qa_pairs = [        (            "Что такое Reflex?",            "Способ создания веб-приложений на чистом Python!",        ),        (            "Что я могу сделать с ним?",            "Все, от простого сайта до сложного веб-приложения!",        ),    ]    return rx.box(        *[            qa(question, answer)            for question, answer in qa_pairs        ]    )def action_bar() -> rx.Component:    return rx.hstack(        rx.input(            placeholder="Задайте вопрос",            style=style.input_style,        ),        rx.button("Задать", style=style.button_style),    )def index() -> rx.Component:    return rx.container(        chat(),        action_bar(),    )app = rx.App()app.add_page(index)app.compile()

Приложение выглядит хорошо, но пока не очень функционально! Теперь давайте добавим немного функционала.

Состояние

Теперь давайте сделаем чат-приложение интерактивным, добавив состояние. Состояние – это место, где мы определяем все переменные, которые могут изменяться в приложении, и все функции, которые могут их изменять. Больше информации о состоянии можно найти в документации по состоянию.

Определение состояния

Мы создадим новый файл с именем state.py в директории chatapp. В нашем состоянии будут храниться текущий заданный вопрос и история чата. Мы также определим обработчик событий answer, который будет обрабатывать текущий вопрос и добавлять ответ в историю чата.

# state.pyimport reflex as rxclass State(rx.State):    # Текущий заданный вопрос.    question: str    # Отслеживание истории чата в виде списка кортежей (вопрос, ответ).    chat_history: list[tuple[str, str]]    def answer(self):        # Наш чат-бот пока не очень умный...        answer = "Я не знаю!"        self.chat_history.append((self.question, answer))

Привязка состояния к компонентам

Теперь мы можем импортировать состояние в chatapp.py и ссылаться на него в наших фронтенд-компонентах. Мы изменим компонент chat, чтобы использовать состояние вместо текущих фиксированных вопросов и ответов.

Изображение от автора
# chatapp.pyfrom chatapp.state import State...def chat() -> rx.Component:    return rx.box(        rx.foreach(            State.chat_history,            lambda messages: qa(messages[0], messages[1]),        )    )...def action_bar() -> rx.Component:    return rx.hstack(        rx.input(            placeholder="Задайте вопрос",            on_change=State.set_question,            style=style.input_style,        ),        rx.button(            "Задать",            on_click=State.answer,            style=style.button_style,        ),    )

Обычные циклы на Python for не работают для итерации по переменным состояния, потому что эти значения могут изменяться и неизвестны на этапе компиляции. Вместо этого мы используем компонент foreach для итерации по истории чата.

Мы также связываем событие on_change ввода с обработчиком событий set_question, который будет обновлять переменную состояния question, когда пользователь вводит текст в поле ввода. Мы также связываем событие on_click кнопки с обработчиком событий answer, который будет обрабатывать вопрос и добавлять ответ в историю чата. Обработчик событий set_question – это неявно определенный встроенный обработчик событий. Каждая базовая переменная имеет свой обработчик. Узнайте больше в разделе документации по событиям в разделе Setters.

Очистка поля ввода

В настоящее время поле ввода не очищается после нажатия пользователя на кнопку. Мы можем исправить это, связав значение поля ввода с переменной состояния question с помощью value=State.question и очистив его при выполнении обработчика событий answer с помощью self.question = ''.

# chatapp.pydef action_bar() -> rx.Component:    return rx.hstack(        rx.input(            value=State.question,            placeholder="Задайте вопрос",            on_change=State.set_question,            style=style.input_style,        ),        rx.button(            "Спросить",            on_click=State.answer,            style=style.button_style,        ),    )

# state.pydef answer(self):    # Наш чат-бот пока не очень умный...    answer = "Я не знаю!"    self.chat_history.append((self.question, answer))    self.question = ""

Потоковый текст

Обычно обновления состояния отправляются на клиентскую часть, когда обработчик событий возвращает результат. Однако нам нужно потоково получать текст от чат-бота по мере его создания. Мы можем сделать это, используя оператор yield в обработчике событий. См. документацию по оператору yield для получения дополнительной информации.

# state.pyimport asyncio...async def answer(self):    # Наш чат-бот пока не очень умный...    answer = "Я не знаю!"    self.chat_history.append((self.question, ""))    # Очищаем поле вопроса.    self.question = ""    # Возвращаем значение, чтобы очистить входной интерфейс перед продолжением.    yield    for i in range(len(answer)):        # Приостановимся, чтобы показать эффект потоковой передачи.        await asyncio.sleep(0.1)        # Добавляем по одной букве к выводу.        self.chat_history[-1] = (            self.chat_history[-1][0],            answer[: i + 1],        )        yield

Использование API

Мы будем использовать API OpenAI, чтобы придать нашему чат-боту некоторый интеллект. Нам нужно изменить обработчик событий, чтобы отправить запрос в API.

# state.pyimport osimport openaiopenai.api_key = os.environ["OPENAI_API_KEY"]...def answer(self):    # Наш чат-бот теперь умеет думать!    session = openai.ChatCompletion.create(        model="gpt-3.5-turbo",        messages=[            {"role": "user", "content": self.question}        ],        stop=None,        temperature=0.7,        stream=True,    )    # Добавляем ответы по мере того, как чат-бот отвечает.    answer = ""    self.chat_history.append((self.question, answer))    # Очищаем поле вопроса.    self.question = ""    # Возвращаем значение, чтобы очистить входной интерфейс перед продолжением.    yield    for item in session:        if hasattr(item.choices[0].delta, "content"):            answer += item.choices[0].delta.content            self.chat_history[-1] = (                self.chat_history[-1][0],                answer,            )            yield

Наконец, у нас есть наш чат-бот на искусственном интеллекте!

Заключение

Следуя этому учебнику, мы успешно создали наше приложение чата с использованием ключа API OpenAI, исключительно на языке Python.

Чтобы запустить это приложение сейчас, выполните простую команду:

$ reflex run

Чтобы развернуть его для обмена с другими пользователями, выполните команду:

$ reflex deploy

Надеюсь, этот учебник вдохновит вас создать свои собственные приложения, основанные на LLM. Я с нетерпением жду, чтобы узнать о ваших достижениях в социальных сетях или в комментариях.

Если у вас есть вопросы, пожалуйста, оставьте их в комментариях ниже или напишите мне в Твиттере по адресу @tgotsman12 или в LinkedIn. Поделитесь своими созданиями приложений в социальных сетях, отметьте меня и я буду рад оставить отзыв или помочь с ретвитом!