見出し画像

ガチでプログラミング知識ゼロ!生成AIの力だけでLLMアプリケーションを作ってみた-HuggingChat Python API🤗連携アプリ-

2022年から生成AIを活用して、プログラミング知識ゼロでアプリケーションを作成しています。これまで作成したものも含めてNoteに投稿していきます。
プログラミング知識ゼロ、経験ゼロ!『ド素人の、ド素人による、ド素人のため』の実践体験です。
プログラミングの勉強も一切したことはありませんし、全て生成AIに任せて作成しているので有識者の方ような難しいことは一切分かりかねます。
プログラミング知識ゼロ、経験ゼロでもこんなアプリケーションを自作できたという自己満足の内容になっていますので、悪しからずご容赦ください。


ChatGPTなどのサービスをAPI経由で使うとなんだかんだコストがかかる・・・

当然のことですが、ChatGPTもGeminiもClaudeも、主要な生成AIのサービスとアプリケーションを連携させるためのAPIにはコストがかかります。
かといって、ローカル環境でLLMを実行するためには高性能なPCの準備から必要ですし、仮にそんな環境が手に入っても、私にはLLMをローカルで動かすようなスキルもありません。
手元にある型落ちのフッツーのスペックのWindowsPC(Windows10,Corei-5,メモリ16GB)で、ただ自分が面白いと思えるアプリケーションが作れたらいいんです。
「クレジットカードの請求に怯えることなく使えるLLM環境ないかなー」と考えていたところ…
見つけましたっ!

HuggingChat

HuggingChatは、Hugging Faceが開発したオープンソースのAIチャットボットで、誰でも無料で使えるらしい!!!

HuggingChatの画面
利用可能なモデル(2024年8月投稿時点)

Noteの投稿時点(2024年8月投稿時点)で以下のモデルが利用可能。

  • meta-llama/Meta-Llama-3.1-70B-Instruct

  • CohereForAI/c4ai-command-r-plus

  • mistralai/Mixtral-8x7B-Instruct-v0.1

  • NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO

  • 01-ai/Yi-1.5-34B-Chat

  • mistralai/Mistral-7B-Instruct-v0.3

  • microsoft/Phi-3-mini-4k-instruct

特に嬉しいのは、"Meta/Llama3.1"、"Cohere/Command R+"、"Microsoft/Phi-3"が使えること。用途によってはGPT-4に匹敵すると言われているLLMが無償で使えるのは本当にありがたい!
少し前まではGoogle Gemmaもありましたが投稿時点では消えていました。
オープンモデルがリリースされるたびに利用可能なモデルが最新版になったり、モデル自体が増えたり減ったりしているのだと思います。

ちなみに、HuggingChatにはAssistantsという、ChatGPTでいうGPTsと同じような機能があり、ユーザーが作成したカスタムアプリケーションが公開されている。(間違いなくChatGPTを意識している…)

HuggingChatを自作アプリケーションから呼び出したい

話を戻すと、これ自体は無償で利用できるチャットサービスですが、私がやりたいのはLLMアプリケーションを自作することです。
つまり、HuggingChatを自作のアプリケーションから呼び出さないといけないので、HuggingChatのAPIを提供して欲しいのですが、残念ながら公式からはAPIが提供されていない…(投稿時点)

しかし、、、
世界にはそんなことを当然考えて提供している方はおられるもので、HuggingChat Python API(非公式)を提供されている神な方がおられます!!!
https://github.com/Soulter/hugging-chat-api

以下のライブラリをインストールすればOK!

pip3 install hugchat

使い方のサンプルコードも記載されています!

from hugchat import hugchat
from hugchat.login import Login

# Log in to huggingface and grant authorization to huggingchat
EMAIL = "your email"
PASSWD = "your password"
cookie_path_dir = "./cookies/" # NOTE: trailing slash (/) is required to avoid errors
sign = Login(EMAIL, PASSWD)
cookies = sign.login(cookie_dir_path=cookie_path_dir, save_cookies=True)

# Create your ChatBot
chatbot = hugchat.ChatBot(cookies=cookies.get_dict())  # or cookie_path="usercookies/<email>.json"

message_result = chatbot.chat("Hi!") # note: message_result is a generator, the method will return immediately.

# Non stream
message_str: str = message_result.wait_until_done() # you can also print(message_result) directly. 
# get files(such as images)
file_list = message_result.get_files_created() # must call wait_until_done() first!

# tips: model "CohereForAI/c4ai-command-r-plus" can generate images :)

# Stream response
for resp in chatbot.query(
    "Hello",
    stream=True
):
    print(resp)

# Web search
query_result = chatbot.query("Hi!", web_search=True)
print(query_result)
for source in query_result.web_search_sources:
    print(source.link)
    print(source.title)
    print(source.hostname)

# Create a new conversation
chatbot.new_conversation(switch_to = True) # switch to the new conversation

# Get conversations on the server that are not from the current session (all your conversations in huggingchat)
conversation_list = chatbot.get_remote_conversations(replace_conversation_list=True)
# Get conversation list(local)
conversation_list = chatbot.get_conversation_list()

# Get the available models (not hardcore)
models = chatbot.get_available_llm_models()

# Switch model with given index
chatbot.switch_llm(0) # Switch to the first model
chatbot.switch_llm(1) # Switch to the second model

# Get information about the current conversation
info = chatbot.get_conversation_info()
print(info.id, info.title, info.model, info.system_prompt, info.history)

# Assistant
assistant = chatbot.search_assistant(assistant_name="ChatGpt") # assistant name list in https://huggingface.co/chat/assistants
assistant_list = chatbot.get_assistant_list_by_page(page=0)
chatbot.new_conversation(assistant=assistant, switch_to=True) # create a new conversation with assistant

# [DANGER] Delete all the conversations for the logged in user
chatbot.delete_all_conversations()

プログラミング知識ゼロの私には暗号にしか見えませんが、別にコードの内容なんか分からなくてもいいんです!!!
これさえ手に入れば・・・
あとは、作りたいアプリケーションの要件と、HuggingChat APIの使い方をChatGPTにインプットして、プログラムコード生成するだけ!!!

アプリケーションの要件

今回はHuggingChat APIをテストするためのアプリケーションなので、自作チャットアプリに接続してみることにしました。
主な要件は以下の通り。

  • HuggingChat APIを使用したチャットボット

  • GUIインターフェース

  • ユーザー認証機能(HuggingFaceのログイン機能)

  • HuggingChatで利用可能なモデルを自由に切り替えできる機能

  • チャット履歴のクリアと外部ファイルへのエクスポート

実際に作ったアプリケーション

ChatGPTに要件とAPI使用方法を読み込ませてから、ChatGPTと対話すること約3時間、無事に私が想定した通りに動作するHuggingChatをLLMとして動作する自作アプリケーションが完成しました!
(何度も言いますが、私はプログラミング知識ゼロですので、生成されたコードは動くかどうかでしか評価できません。やりたいことが動作するかどうかだけが唯一の確認方法です。)

アプリケーション画面

デモンストレーション

プログラミングができる方からしたら、この程度のアプリケーションは瞬殺なのでしょうが、私にとってはこれが約3時間で作れたことに非常に感動!

ソースコード

こちらが、実際のソースコードです。
※このソースコードにより生じた如何なる損害についても、一切の責任は負いかねます。あらかじめご了承ください。

import sys
import configparser
import os
import re
import time
import logging
from PyQt5 import QtGui
from PyQt5.QtCore import Qt, QTimer, QEvent
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QLineEdit, QPushButton, QTextEdit, QVBoxLayout, QWidget, QFileDialog, QMessageBox, QDialog, QComboBox, QHBoxLayout
from hugchat import hugchat
from hugchat.login import Login

def save_login_details(email, password):
    config = configparser.ConfigParser()
    config['LOGIN'] = {
        'Email': email,
        'Password': password
    }
    with open('config.ini', 'w') as configfile:
        config.write(configfile)

def load_login_details():
    config = configparser.ConfigParser()
    config.read('config.ini')
    email = config.get('LOGIN', 'Email', fallback='')
    password = config.get('LOGIN', 'Password', fallback='')
    return email, password

class LoginDialog(QDialog):
    def __init__(self, parent=None):
        super(LoginDialog, self).__init__(parent)
        font = QFont("Yu Mincho", 11) 
        self.setFont(font)

        self.setWindowTitle("User Login")
        self.setFixedSize(350, 200)

        layout = QVBoxLayout()
        layout.setSpacing(10)
        layout.setContentsMargins(10, 10, 10, 10)

        self.email_entry = QLineEdit(self)
        self.email_entry.setPlaceholderText("Enter your email")
        layout.addWidget(self.email_entry)

        self.password_entry = QLineEdit(self)
        self.password_entry.setPlaceholderText("Enter your password")
        self.password_entry.setEchoMode(QLineEdit.Password)
        layout.addWidget(self.password_entry)

        self.login_button = QPushButton("Login", self)
        self.login_button.setStyleSheet("QPushButton { color: white; background-color: blue; font-weight: bold; padding: 6px; }")
        self.login_button.clicked.connect(self.save_login)
        layout.addWidget(self.login_button)

        self.setLayout(layout)

    def save_login(self):
        save_login_details(self.email_entry.text(), self.password_entry.text())
        self.accept()

class ChatApplication(QMainWindow):
    def __init__(self):
        super().__init__()
        font = QFont("Yu Mincho", 11) 
        self.setFont(font) 

        self.setWindowTitle("Hugging Chat Application")
        self.setGeometry(100, 100, 600, 800)

        self.model_names = [
            "meta-llama/Meta-Llama-3.1-70B-Instruct",
            "CohereForAI/c4ai-command-r-plus",
            "mistralai/Mixtral-8x7B-Instruct-v0.1",
            "NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO",
            "01-ai/Yi-1.5-34B-Chat",
            "mistralai/Mistral-7B-Instruct-v0.3",
            "microsoft/Phi-3-mini-4k-instruct"
        ]
        self.current_model_name = self.model_names[0]

        self.initUI()
        self.login_and_initialize_chat()

        self.model_switched = False 

    def initUI(self):
        widget = QWidget(self)
        self.setCentralWidget(widget)
        layout = QVBoxLayout(widget)

        self.model_label = QLabel(f"Model: {self.current_model_name}", self)
        layout.addWidget(self.model_label)

        chat_widget_height = 4

        self.chat_widget = QTextEdit(self)
        self.chat_widget.setReadOnly(True)
        layout.addWidget(self.chat_widget, chat_widget_height)  
    
        self.message_entry = QTextEdit(self)
        self.message_entry.setPlaceholderText("Ask Anything...")
        self.message_entry.setFixedHeight(150)
        layout.addWidget(self.message_entry, 1)

        self.send_button = QPushButton("🤗Send🤗", self)
        self.send_button.clicked.connect(self.send_message)
        layout.addWidget(self.send_button)

        button_layout = QHBoxLayout() 

        self.model_button = QPushButton("Switch Model", self)
        self.model_button.clicked.connect(self.ask_model_to_switch)
        button_layout.addWidget(self.model_button)
        button_layout.setStretch(0, 1) 

        self.clear_button = QPushButton("Clear", self)
        self.clear_button.clicked.connect(self.clear_chat)
        button_layout.addWidget(self.clear_button)
        button_layout.setStretch(1, 1)  

        self.export_button = QPushButton("Export", self)
        self.export_button.clicked.connect(self.export_chat)
        button_layout.addWidget(self.export_button)
        button_layout.setStretch(2, 1)  

        layout.addLayout(button_layout) 

        self.message_entry.installEventFilter(self) 

    def login_and_initialize_chat(self):
        email, password = load_login_details()
        if not email or not password:
            login_dialog = LoginDialog(self)
            if login_dialog.exec_():
                email, password = load_login_details()

        if email and password:
            retries = 5
            while retries > 0:
                try:
                    sign = Login(email, password)
                    cookies = sign.login(cookie_dir_path='./cookies/', save_cookies=True)
                    self.chatbot = hugchat.ChatBot(cookies=cookies.get_dict())
                    break
                except Exception as e:
                    retries -= 1
                    if retries == 0:
                        QMessageBox.critical(self, "Login Error", f"Failed to initialize chat after multiple attempts: {str(e)}")
                        self.close()
                    else:
                        time.sleep(5)
        else:
            QMessageBox.critical(self, "Login Error", "Login details are missing. Please restart the application.")
            self.close()

    def send_message(self):
        message = self.message_entry.toPlainText().strip()
        if message:
            self.update_chat(f"You: {message}", "left")
            self.message_entry.clear()
            self.message_entry.setPlaceholderText("Ask Anything...")
            QTimer.singleShot(100, lambda: self.get_response(message)) 

    def get_response(self, message):
        logging.info(f"Sent message: {message}")
        print(f"Sending message: {message}")  
        response = self.chatbot.chat(message)
        if isinstance(response, dict) and 'message' in response and 'errorId' in response:
            logging.error(f"Server returns an error: {response['message']}")
            print(f"Received response: {response.text}")  
            self.update_chat(f"Error - {response['message']}", "left")
        else:
            logging.info(f"Received response: {getattr(response, 'text', 'No text found in response')}")
            print(f"Received response: {response.text}") 
            self.chat_widget.setTextColor(QtGui.QColor("#00796B"))
            self.update_chat(f"AI: {response.text}", "left")
            self.chat_widget.setTextColor(QtGui.QColor("#000000"))

    def update_chat(self, message, side):
        self.chat_widget.append(message)

    def ask_model_to_switch(self):
        dialog = QDialog(self)
        dialog.setFont(self.font())
        dialog.setWindowTitle("Select Model")
        dialog.setFixedSize(400, 200)

        layout = QVBoxLayout(dialog)

        combobox = QComboBox(dialog)
        combobox.addItems(self.model_names)
        layout.addWidget(combobox)

        confirm_button = QPushButton("Confirm", dialog)
        confirm_button.clicked.connect(lambda: self.switch_model(combobox.currentIndex(), dialog))
        layout.addWidget(confirm_button)

        dialog.exec_()

    def switch_model(self, model_index, dialog=None):
        if model_index is not None and 0 <= model_index < len(self.model_names):
            try:
                self.current_model_name = self.model_names[model_index]
                self.model_label.setText(f"Model: {self.current_model_name}")
                self.chatbot.new_conversation(model_index, switch_to=True)
                self.chat_widget.setTextColor(QtGui.QColor("#FF0000")) 
                self.update_chat(f"System: Model switched to {self.current_model_name}", "left")
                self.chat_widget.setTextColor(QtGui.QColor("#000000")) 
                print(f"Model switched to: {self.current_model_name}") 

                if dialog:
                    dialog.close() 
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to switch model: {str(e)}")
                if dialog:
                    dialog.close()

    def clear_chat(self):
        self.chat_widget.clear()

    def export_chat(self):
        chat_content = self.chat_widget.toPlainText().strip()
        if chat_content:
            file_path = QFileDialog.getSaveFileName(self, "Save chat as...", filter="Text files (*.txt);;All files (*.*)")[0]
            if file_path:
                with open(file_path, "w", encoding="utf-8") as file:
                    file.write(chat_content)
                QMessageBox.information(self, "Export Successful", f"Chat has been exported successfully to {file_path}")
            else:
                QMessageBox.information(self, "Export Cancelled", "Export operation was cancelled.")

    def eventFilter(self, obj, event):
        if obj == self.message_entry:
            if event.type() == QEvent.KeyPress:
                if event.key() == Qt.Key_Return and not (event.modifiers() & Qt.ShiftModifier):
                    self.send_message()
                    return True
                elif event.key() == Qt.Key_Return and (event.modifiers() & Qt.ShiftModifier):
                    self.message_entry.insertPlainText("\n")
                    return True
            elif event.type() == QEvent.FocusIn:
                self.message_entry.setPlaceholderText("")
            elif event.type() == QEvent.FocusOut:
                if not self.message_entry.toPlainText().strip():
                    self.message_entry.setPlaceholderText("Ask Anything...")
        return super(ChatApplication, self).eventFilter(obj, event)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    chat_app = ChatApplication()
    chat_app.show()
    sys.exit(app.exec_())

※ソースコードに実装されているモデルは投稿時点のHuggingChatのモデルです。

この記事が気に入ったらサポートをしてみませんか?