В последнее время все большей популярностью пользуются различные чаты на основе ChatGPT. Они доступны не только в формате веб-версий или telegram-ботов, но и в виде отдельных приложений для разных платформ.
Я, как пользователь систем на базе ядра Linux и человек, постоянно мониторящий магазин Flathub в поиске чего-нибудь интересного, в один прекрасный день увидел новое приложение под названием Bavarder. Приложение написано на языке Python с использованием фреймворка GTK4 и библиотеки libadwaita. Программа позволяет общаться с ChatGPT, но делается это в двух текстовых полях. В одном пишется запрос, а в другом пользователь получает ответ. Мне это показалось не очень удобным и наглядным:
Я решил на основе первой версии этого приложения(с тегом 0.1.1) создать свое. Интерфейс будет состоять только из одной текстовой области, которая будет использоваться как для ввода, так и для вывода информации. В моей версии будет только одна сеть, и это будет BAI Chat, как и в первой версии Bavarder.
BAI Chat – это нейросеть созданная на основе ChatGPT и предназначенная для использования в самых разных областях. Она не требует авторизации и ключей API.
В Bavarder пользователь видит только один текущий вопрос и ответ на него. В моей версии программы вся текущая переписка с ботом будет доступна для просмотра пользователем. Благодаря тому, что весь процесс общения происходит в одном окне, будет сохраняться контекст, то есть бот не будет терять нить разговора, по крайней мере, первые пару десятков сообщений. В более поздних выпусках автор решил эту проблему с контекстом, но формат вывода запросов и сообщений остался прежним. Можно сказать, что моя версия как раз и задумывалась для того, чтобы улучшить формат вывода переписки. Репозиторий моей версии приложения можно найти здесь.
Начало разработки
Я еще ни разу не работал с приложениями на Python, тем более написанными на GTK. В основном в своих разработках я использовал язык Vala. Но на этом языке пока что нет необходимых библиотек, позволяющих работать с BAI Chat. Из-за недостатка опыта работы с Python и было решено не писать приложение с нуля, а взять за основу уже существующее.
В программе используется библиотека baichat-py. В Bavarder создан файл манифеста и дополнительный к нему файл с модулями, где указываются архивы необходимых библиотек для нейросети. В более поздних версиях приложения их там довольно много. Так как в моей версии используется только BAI Chat, то нет смысла расписывать манифест на два файла.
Таким образом, в манифесте, помимо модуля приложения, прописаны еще два модуля: python3-baichat-py и blueprint-compiler. Последний нужен для получения классического файла пользовательского интерфейса в формате ui из файла в формате blp. Подробнее об этом формате можно прочитать в официальной документации.
Ниже приводится фрагмент манифеста с началом модуля python3-baichat-py:
"modules" : [
{
"name": "python3-baichat-py",
"buildsystem": "simple",
"build-commands": [
"pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"baichat-py\" --no-build-isolation"
],
"sources": [
{
"type": "file",
"url": "https://files.pythonhosted.org/packages/c2/fd/1ff4da09ca29d8933fda3f3514980357e25419ce5e0f689041edb8f17dab/aiohttp-3.8.4.tar.gz",
"sha256": "bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c"
}
Весь модуль приводить не буду, так как он довольно объемный. Узнать, как подключается blueprint-compiler, можно по вышеуказанной ссылке на документацию. Есть еще вот эта статья с краткой инструкцией и примером использования.
Изменение интерфейса
Как уже говорилось, я решил поменять интерфейс приложения, и если оригинал выглядит вот так:
То мой вариант смотрится гораздо проще:
Заголовок с текстом «Message» я удалил за ненадобностью, так же как и «Response». Иконку меню перенес вправо. Для этого в файле window.blp потребовалось добавить метку [end] для компонента MenuButton:
Adw.HeaderBar {
[end]
MenuButton {
primary: true;
menu-model: main-menu;
icon-name: "open-menu-symbolic";
tooltip-text: _("Main Menu");
}
styles ["flat"]
}
Почему потребовалось перенести меню? Да, просто для GTK-приложений положение меню слева не является естественным. У большинства программ, написанных на этом фреймворке, меню расположено именно в правой части окна.
Для удаления нижней текстовой области, которая в оригинальной версии предназначена для вывода ответа, нужно в window.blp найти и удалить компонент Adw.PreferencesGroup с идентификатором bot_group. При удалении компонента надо быть аккуратнее и постараться не удалить лишние фигурные скобки. В коде оставшейся текстовой области ничего менять не нужно.
Иконку для значка приложения было решено взять с сайта Iconfinder, потому как дизайнер из меня так себе. В разделе свободных иконок я подобрал наиболее подходящую по тематике в формате svg. С помощью расширения SVG для VS Code я изменил цвет иконки — то, что у меня получилось, можно увидеть в репозитории, в папке icons.
Значок для кнопки отправки запроса остался тот же, что и в оригинальном приложении. Не вижу смысла в его изменении. Единственное, что хочу отметить, — для этого значка используется отдельное изображение, находящееся в той же папке icons, что и остальные. Видимо, не нашлось подходящего значка среди набора стандартных иконок.
Удаление настроек
Теперь, когда интерфейс программы приведен к нужному виду, настало время заняться дальнейшими преобразованиями в коде приложения. Прежде всего необходимо убрать те участки кода, которые ссылаются на удаленные компоненты. Далее можно удалить настройки, так как они больше не нужны.
В первой версии приложения Bavarder была только одна настройка, позволяющая включить автоматическую очистку поля для ввода сообщения после получения ответа. Это уже не требуется, так как в приложении теперь есть только одно текстовое поле, работающее за двоих. Для начала потребуется удалить файлы preferences.py и preferences.blp. Также нужно удалить соответствующий item из main-menu в window.blp:
menu main-menu {
section {
item {
label: _("Preferences");
action: "app.preferences";
}
item {
label: _("Keyboard Shortcuts");
action: "win.show-help-overlay";
}
item {
label: _("About Bavarder");
action: "app.about";
}
}
}
У последнего item следует отредактировать текстовую метку и заменить «Bavarder» на «BAI Chat». Не стоит забывать и о сборочных сценариях. Приложение использует сборочную систему meson. Нужно найти сценарии meson.build в каталогах src и ui и удалить в них ссылки на указанные выше файлы настроек.
Ввод и вывод информации
Вся логика приложения описана в файле main.py. Функция для получения ответа от BAI Chat выглядит следующим образом:
def ask(self, prompt):
chat = BAIChat(sync=True)
try:
response = chat.sync_ask(self.prompt)
except KeyError:
self.win.banner.set_revealed(False)
return ""
except socket.gaierror:
self.win.banner.set_revealed(True)
return ""
else:
self.win.banner.set_revealed(False)
return response.text
В этой части менять ничего не потребовалось. Изменения коснулись функции cleanup, которая принимает на вход ответ нейросети и отображает его в виджете bot_text_view, который был удален. Вот так выглядит эта функция в первоначальном виде:
def cleanup(response):
self.win.spinner.stop()
self.win.ask_button.set_visible(True)
self.win.wait_button.set_visible(False)
t.join()
self.win.bot_text_view.get_buffer().set_text(response)
if self.clear_after_send:
self.win.prompt_text_view.get_buffer().set_text("")
t = threading.Thread(target=thread_run)
t.start()
В этой функции также происходит автоматическая очистка виджета prompt_text_view, который теперь выполняет и роль удаленного bot_text_view. Этот участок кода больше не нужен. Вот так выглядит функция в моем варианте:
def cleanup(response):
self.win.spinner.stop()
self.win.ask_button.set_visible(True)
self.win.wait_button.set_visible(False)
t.join()
self.resp = self.prompt + '\n\n******\n\n' + response + '\n\n******\n\n'
self.win.prompt_text_view.get_buffer().set_text(self.resp)
GLib.timeout_add(1000, self.scroll_to_last_position)
t = threading.Thread(target=thread_run)
t.start()
Переменная prompt определяется в функции on_ask_action, которая вызывается нажатием на кнопку отправки запроса. Переменной присваивается весь текст, содержащийся в виджете, и тут же происходит ее проверка на пустоту:
self.prompt = self.win.prompt_text_view.get_buffer().props.text
if self.prompt == "" or self.prompt is None:
return
Для большей наглядности запросы пользователя и ответы бота отделяются друг от друга. В функции do_activate прописана установка фокуса на текстовую область, чтобы пользователь мог сразу начать работать с программой после ее запуска:
self.win.prompt_text_view.grab_focus()
Еще нужно подумать об автоматической прокрутке текста на позицию последнего вопроса. Так как область вывода текста обновляется после каждого получения ответа на вопрос, то пользователь будет постоянно возвращаться к началу переписки. Чтобы этого не происходило, была добавлена вот эта функция:
def scroll_to_last_position(self):
adj = self.win.scrolled_window.get_vadjustment()
adj.set_value(self.pos)
self.win.scrolled_window.set_vadjustment(adj)
return False
Переменная pos получает новое значение каждый раз, когда отправляется очередной запрос:
self.pos = self.win.scrolled_window.get_vadjustment().get_value()
Вызов функции scroll_to_last_position пришлось делать через таймаут, так как буферу виджета prompt_text_view необходимо какое-то время на заполнение, после чего можно работать с контейнером scrolled_window, в котором располагается виджет. Значение первого параметра для функции timeout_add приведено для примера. Можно выставить и гораздо меньшее значение.
Больше никаких изменений в части ввода и вывода информации я не вносил.
Дальнейшая работа
Вся основная работа уже проделана. Остается только удалить неиспользуемые функции, переименовать файлы в репозитории и в соответствии с этим внести необходимые правки в сборочные сценарии, файлы ресурсов, манифест и прочее.
Что касается неиспользуемых функций, то таковой является функция копирования в буфер обмена текста из удаленной текстовой области. Виджет, при нажатии на который вызывалась эта функция, был удален. В новом варианте программы осталась только одна такая функция и вызывается она по нажатию на соответствующую кнопку, расположенную рядом с кнопкой отправки запроса:
Также лишней в коде является функция вызова окна настроек, так как это окно больше не существует.
Комбинации клавиш остались без изменений. Их тут всего две:
self.create_action("quit", lambda *_: self.quit(), ["<primary>q"])
...
self.create_action("ask", self.on_ask_action, ["<primary>Return"])
Еще хотелось бы упомянуть окно с информацией о приложении. Так как программа использует библиотеку libadwaita, то естественно, что для вывода такой информации используется окно Adw.AboutWindow. В этом окне разработчик может указать, например, название приложения, его версию, лицензию и прочее. Вот как это может быть реализовано:
def on_about_action(self, widget, _):
about = Adw.AboutWindow(
transient_for=self.props.active_window,
application_name="BAI Chat",
application_icon="io.github.alexkdeveloper.baichat",
developer_name="Alex K",
developers=["Alex K https://github.com/alexkdeveloper/baichat"],
license_type=Gtk.License.GPL_3_0,
version="1.0.0",
copyright="(c) 2023 Alex K")
about.present()
Для сборки приложения отлично подойдет интегрированная среда разработки Builder. Она является официальной средой разработки для GNOME. Среда автоматически скачает и установит все, что указано в манифесте программы. Запуск приложения должен пройти без проблем. Там же можно создать пакет flatpak.
Приложение получилось довольно минималистичным, что, в принципе, и требовалось. Целью этой работы было создание своей версии программы Bavarder, в которой пользователю доступен весь текст переписки с сетью BAI Chat. Я считаю, что цель была достигнута.
Автор статьи @KAlexAl
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
- 15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS