Как стать автором
Обновить
244.06
FirstVDS
Виртуальные серверы в ДЦ в Москве

Приложение BAI Chat на GTK4 и Python

Уровень сложностиПростой
Время на прочтение8 мин
Количество просмотров15K

В последнее время все большей популярностью пользуются различные чаты на основе 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

Теги:
Хабы:
Всего голосов 1: ↑1 и ↓0+1
Комментарии1

Публикации

Информация

Сайт
firstvds.ru
Дата регистрации
Дата основания
Численность
51–100 человек
Местоположение
Россия
Представитель
FirstJohn