Перевод статьи «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. Мне не терпится увидеть, что у вас получится в итоге, поэтому, пожалуйста, пишите в социальных сетях или в комментариях.