Создайте веб-приложение в Pure Python, похожее на ChatGPT, используя Reflex
Создайте веб-приложение на Pure Python, аналогичное ChatGPT, с использованием Reflex
Используйте API OpenAI для создания веб-приложения чата на чистом Python с развертыванием в одну строку
![Chat app GIF Автор](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/1*k6mWPhN50QEVEZA16IZwlg.gif)
Последние несколько месяцев я играл с невероятными новыми чат-ботами LLM, включая Llama 2, GPT-4, Falcon 40B и Claude 2. Один вопрос, который меня постоянно беспокоит, – как я могу создать свой собственный пользовательский интерфейс для чат-бота, который вызывает все эти великолепные LLM в качестве API?
Существует бесчисленное множество вариантов для создания красивых пользовательских интерфейсов, но в качестве инженера по машинному обучению у меня нет опыта работы с JavaScript или любым другим языком фронт-энда. Я искал способ создать свое веб-приложение, используя только тот язык, который я знаю в настоящее время – Python!
Я решил использовать сравнительно новый фреймворк с открытым исходным кодом под названием Reflex, который позволяет мне создавать как бэк-энд, так и фронт-энд исключительно на Python.
Отказ от ответственности: Я работаю в качестве главного инженера в Reflex, где вношу свой вклад в открытый исходный код фреймворка.
- Большие модели языка, ALBERT — Lite BERT для самообучения.
- Реализация нейронного кодировщика Transformer с нуля с использованием JAX и Haiku 🤖
- История открытых LLM-программ начальные годы (Часть первая)
В этом уроке мы рассмотрим, как создать полноценное AI приложение для чата с нуля, используя чистый Python — вы также можете найти весь код в этом репозитории GitHub.
Вы узнаете, как:
- Установить
reflex
и настроить вашу среду разработки. - Создать компоненты для определения и стилизации вашего пользовательского интерфейса.
- Использовать состояние для добавления интерактивности в ваше приложение.
- Развернуть ваше приложение с помощью одной команды, чтобы поделиться им с другими.
Настройка вашего проекта
Мы начнем с создания нового проекта и настройки нашей среды разработки. Сначала создайте новый каталог для вашего проекта и перейдите в него.
~ $ 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
, чтобы вернуть компонент, отображающий один вопрос и ответ.
![Изображение автора (код ниже)](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*r9qwOTRVWwNjXC-kpXe8vw.png)
# 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
.
![Изображение автора (код ниже)](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*5zsmqHQstQczCwqX4sCl7Q.png)
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 для отправки вопроса.
![Изображение автора (код ниже)](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*54_QdmCmsBSOs0vWgKE8ZA.png)
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
и используем их в компонентах. В данной точке приложение должно выглядеть так:
![Изображение от автора](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*y2zlPR6_1OQ4z9Ghk4dOaw.png)
# 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
, чтобы использовать состояние вместо текущих фиксированных вопросов и ответов.
![Изображение от автора](https://ai.miximages.com/miro.medium.com/v2/resize:fit:640/format:webp/1*w58kZZ3q-juomLxPR3BBQg.png)
# 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. Поделитесь своими созданиями приложений в социальных сетях, отметьте меня и я буду рад оставить отзыв или помочь с ретвитом!