

Перевод статьи «Build a ChatGPT-esque Web App in Pure Python using Reflex».
Последние несколько месяцев я играю со всеми новыми невероятными чат-ботами LLM, включая Llama 2, GPT-4, Falcon 40B и Claude 2. Меня постоянно мучает один вопрос: как создать собственный пользовательский интерфейс чат-бота, который будет обращаться ко всем этим замечательным LLM как к API?
Вариантов создания красивых пользовательских интерфейсов существует бесчисленное множество. Но у меня как у ML-инженера нет опыта работы с JavaScript или каким-либо другим языком фронтенда. Я искал способ создать свое веб-приложение, используя только тот язык, который я знаю сейчас, — Python!
Я решил использовать Reflex — достаточно новый фреймворк с открытым исходным кодом. Он позволил мне построить как бэкенд, так и фронтенд исключительно на Python.
Дисклеймер: я работаю инженером-основателем в компании Reflex, где вношу свой вклад в разработку фреймворка с открытым исходным кодом.
В этом руководстве я расскажу, как создать с нуля полноценное приложение для чата с искусственным интеллектом на чистом Python. Весь код можно также найти в GitHub-репозитории.
Вы узнаете, как:
- Установить
reflexи настроить среду разработки. - Создать компоненты для определения и стилизации пользовательского интерфейса.
- Использовать состояние для добавления интерактивности в ваше приложение.
- Развернуть приложение с помощью одной команды, которой можно поделиться с другими людьми.
Настройка проекта
Начнем с создания нового проекта и настройки среды разработки. Сначала создайте новый каталог для проекта и перейдите в него.
~ $ mkdir chatapp ~ $ cd chatapp
Далее мы создадим виртуальную среду для нашего проекта. В данном примере для создания виртуальной среды мы будем использовать venv.
chatapp $ python3 -m venv .venv $ source .venv/bin/activate
Теперь установим Reflex и создадим новый проект. Это создаст новую структуру каталогов в каталоге нашего проекта.
chatapp $ pip install reflex chatapp $ reflex init ────────────────────────────────── Initializing chatapp ─────────────────────────────────── Success: Initialized chatapp chatapp $ ls assets chatapp rxconfig.py .venv
Вы можете запустить приложение-шаблон, чтобы убедиться, что все работает.
chatapp $ reflex run ─────────────────────────────────── Starting Reflex App ─────────────────────────────────── Compiling: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 1/1 0:00:00 ─────────────────────────────────────── App Running ─────────────────────────────────────── App running at: http://localhost:3000
Перейдя по адресу http://localhost:3000, вы должны увидеть, что ваше приложение запущено.
Reflex также запускает сервер бэкенда, который управляет всеми состояниями и взаимодействует с фронтендом. Чтобы проверить, что сервер бэкенда запущен, перейдите по адресу http://localhost:8000/ping.
Теперь, когда проект настроен, давайте создадим наше приложение!
Базовый фронтенд
Начнем с определения фронтенда для нашего чат-приложения. В Reflex фронтенд можно разбить на независимые компоненты многократного использования. Дополнительную информацию можно найти в документации по компонентам.
Отображение вопроса и ответа
Мы модифицируем функцию index в файле chatapp/chatapp.py, чтобы вернуть компонент, отображающий один вопрос и ответ.


# chatapp.py
import reflex as rx
def index() -> rx.Component:
return rx.container(
rx.box(
"What is Reflex?",
# The user's question is on the right.
text_align="right",
),
rx.box(
"A way to build web apps in pure Python!",
# The answer is on the left.
text_align="left",
),
)
# Add state and page to the app.
app = rx.App()
app.add_page(index)
app.compile()
Компоненты могут быть вложены друг в друга для создания сложных макетов. Здесь мы создаем родительский контейнер, содержащий два поля для вопроса и ответа.
Мы также добавляем некоторые базовые стили для компонентов. Компоненты принимают аргументы в виде ключевых слов — пропы, которые изменяют внешний вид и функциональность компонента. Мы используем проп 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 = [
(
"What is Reflex?",
"A way to build web apps in pure Python!",
),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
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="Ask a question"),
rx.button("Ask"),
)
def index() -> rx.Component:
return rx.container(
chat(),
action_bar(),
)
Стилизация
Давайте добавим в приложение некоторые стили. Более подробную информацию о стилях можно найти в документации по стилизации. Чтобы сохранить чистоту кода, мы перенесем стилизацию в отдельный файл chatapp/style.py.
# style.py
# Common styles for questions and answers.
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",
)
# Set specific styles for questions and answers.
question_style = message_style | dict(
bg="#F5EFFE", margin_left=chat_margin
)
answer_style = message_style | dict(
bg="#DEEAFD", margin_right=chat_margin
)
# Styles for the action bar.
input_style = dict(
border_width="1px", padding="1em", box_shadow=shadow
)
button_style = dict(bg="#CEFFEE", box_shadow=shadow)
Мы импортируем стили в chatapp.py и используем их в компонентах. На этом этапе приложение должно выглядеть следующим образом:


# chatapp.py
import reflex as rx
from chatapp import style
def qa(question: str, answer: str) -> rx.Component:
return rx.box(
rx.box(
rx.text(question, style=style.question_style),
text_align="right",
),
rx.box(
rx.text(answer, style=style.answer_style),
text_align="left",
),
margin_y="1em",
)
def chat() -> rx.Component:
qa_pairs = [
(
"What is Reflex?",
"A way to build web apps in pure Python!",
),
(
"What can I make with it?",
"Anything from a simple website to a complex web app!",
),
]
return rx.box(
*[
qa(question, answer)
for question, answer in qa_pairs
]
)
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
placeholder="Ask a question",
style=style.input_style,
),
rx.button("Ask", style=style.button_style),
)
def index() -> rx.Component:
return rx.container(
chat(),
action_bar(),
)
app = rx.App()
app.add_page(index)
app.compile()
Приложение выглядит хорошо, но оно пока не очень полезно! Давайте добавим немного функциональности.
Состояние
Теперь давайте сделаем приложение чата интерактивным, добавив состояние (англ. state). Состояние — это место, где мы определяем все переменные, которые могут изменяться в приложении, и все функции, которые могут их изменять. Подробнее о состоянии можно узнать в документации по состоянию.
Определение состояния
Мы создадим новый файл state.py в каталоге chatapp. Наше состояние будет отслеживать текущий заданный вопрос и историю чата. Мы также определим обработчик события answer, который будет обрабатывать текущий вопрос и добавлять ответ в историю чата.
# state.py
import reflex as rx
class State(rx.State):
# The current question being asked.
question: str
# Keep track of the chat history as a list of (question, answer) tuples.
chat_history: list[tuple[str, str]]
def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, answer))
Привязка состояния к компонентам
Теперь мы можем импортировать состояние в chatapp.py и ссылаться на него в наших компонентах фронтенда. Мы модифицируем компонент chat для использования состояния вместо текущих фиксированных вопросов и ответов.


# chatapp.py
from 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="Ask a question",
on_change=State.set_question,
style=style.input_style,
),
rx.button(
"Ask",
on_click=State.answer,
style=style.button_style,
),
)
Обычные циклы for не подходят для итерации по переменным состояния, поскольку эти значения могут меняться и не известны на момент компиляции. Вместо этого для перебора истории чата мы используем компонент foreach.
Мы также привяжем событие входа on_change к обработчику события set_question, который будет обновлять переменную состояния question при вводе пользователем текста.
Мы привязываем событие on_click кнопки к обработчику событий answer, который будет обрабатывать вопрос и добавлять ответ в историю чата. Обработчик события set_question является встроенным неявно определенным обработчиком события. Каждый базовый var имеет такой обработчик. Подробнее об этом можно узнать в документации по событиям в разделе «Setters».
Очистка ввода
В настоящее время значение input не очищается после нажатия пользователем кнопки. Мы можем исправить это, привязав значение ввода к question при помощи value=State.question, и очищать его при запуске обработчика события для answer при помощи self.question = ''.
# chatapp.py
def action_bar() -> rx.Component:
return rx.hstack(
rx.input(
value=State.question,
placeholder="Ask a question",
on_change=State.set_question,
style=style.input_style,
),
rx.button(
"Ask",
on_click=State.answer,
style=style.button_style,
),
)
# state.py
def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, answer))
self.question = ""
Потоковый текст
Обычно обновления состояния отправляются на фронтенд при возврате обработчика события. Однако мы хотим передавать текст из чат-бота в момент его генерации. Это можно сделать, выйдя из обработчика события. Дополнительную информацию можно найти в документации по выходу из события.
# state.py
import asyncio
...
async def answer(self):
# Our chatbot is not very smart right now...
answer = "I don't know!"
self.chat_history.append((self.question, ""))
# Clear the question input.
self.question = ""
# Yield here to clear the frontend input before continuing.
yield
for i in range(len(answer)):
# Pause to show the streaming effect.
await asyncio.sleep(0.1)
# Add one letter at a time to the output.
self.chat_history[-1] = (
self.chat_history[-1][0],
answer[: i + 1],
)
yield
Использование API
Мы будем использовать API OpenAI, чтобы придать нашему чат-боту интеллектуальность. Для этого нам необходимо изменить обработчик событий, чтобы отправлять запрос к API.
# state.py
import os
import openai
openai.api_key = os.environ["OPENAI_API_KEY"]
...
def answer(self):
# Our chatbot has some brains now!
session = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{"role": "user", "content": self.question}
],
stop=None,
temperature=0.7,
stream=True,
)
# Add to the answer as the chatbot responds.
answer = ""
self.chat_history.append((self.question, answer))
# Clear the question input.
self.question = ""
# Yield here to clear the frontend input before continuing.
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
Наконец, у нас есть наш чат-бот с искусственным интеллектом!
Заключение
Следуя этому руководству, вы успешно создали приложение Chat App, используя API-ключ OpenAI, исключительно на языке Python.
Для запуска этого приложения теперь можно выполнить простую команду:
$ reflex run
Чтобы развернуть приложение, т.е. предоставить его другим пользователям, мы можем выполнить следующую команду:
$ reflex deploy
Я надеюсь, что это руководство вдохновит вас на создание собственных приложений, основанных на LLM. Мне не терпится увидеть, что у вас получится в итоге, поэтому, пожалуйста, пишите в социальных сетях или в комментариях.
