【Python GUI】fletで作成するChatGPTアプリ

※本サイトにはプロモーション・広告が含まれています。

(最終更新月:2023年4月)

当記事では、Pythonをベースとしたfletで作成したChatGPTアプリの概要やその作り方を解説しています。

コードまですべて解説しているので、ぜひ最後までご覧ください。

筆者プロフィール

筆者プロフィールアイコン

【現職】プロダクトマネージャー

【副業】ブログ(月間17万PV)/YouTube/Web・アプリ制作

「プログラミング × ライティング × 営業」の経験を活かし、30後半からのIT系職へシフト。当サイトでは、実際に手を動かせるWebアプリの開発を通じて、プログラミングはもちろん、IT職に必要な情報を提供していきます。

【当ブログで紹介しているサイト】

当サイトチュートリアルで作成したデモ版日報アプリ

Django × Reactで開発したツール系Webアプリ

✔人に見せても恥ずかしくないコードを書こう

「リーダブルコード」は、わかりやすく良いコードの定義を教えてくれる本です。

  • 見るからにきれいなコードの書き方
  • コードの分割方法
  • 変数や関数の命名規則

エンジニアのスタンダートとすべき基準を一から解説しています。

何回も読むのに値する本なので、ぜひ手にとって読んでみてください。

fletで作るChatGPTボットデスクトップアプリの概要

当記事でご紹介するアプリの仕組みはこちらのとおり。

fletアプリケーションを作り、ユーザー向けのフォームや表示テキストはもちろん、ChatGPTAPIとの連携をおこないます。

当記事の構成

当記事では、ChatGPTアプリの書き方を以下の2とおりで説明します。

  • Pythonパッケージ「openai」を使用する方法
  • JSONデータでAPIエンドポイントへリクエストする方法

どちらもご説明いたしますが、以下のアプリケーションは同じ仕様です。

次章よりそれぞれの方法を解説します。

フォーム入力画面

Pythonのfletでは、簡単に画面を作れます。

ChatGPTに投げるリクエストに含めたい値をフォームに入力して送信します。

回答を画面に表示する

レスポンスから必要な情報を抜き出して、画面上に表示させます。

Pythonパッケージ「openai」を使用する方法

openaiを使用する方法では、モデル「gpt-4 Trubo」を使用します。

以下の順で解説していきます。

  • 必要なライブラリのインストール
  • GPTとのやり取りを担うコード「gpts.py」
  • fletでUI担当のコード「main.py」

必要なライブラリのインストール

仮想環境を立ち上げたら、以下のrequirements.txtを使って、パッケージをインストールしよう。

altgraph==0.17.4
annotated-types==0.6.0
anyio==3.7.1
arrow==1.3.0
binaryornot==0.4.4
certifi==2023.11.17
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
cookiecutter==2.5.0
distro==1.8.0
exceptiongroup==1.2.0
flet==0.12.2
flet-core==0.12.2
flet-runtime==0.12.2
h11==0.14.0
httpcore==0.17.3
httpx==0.24.1
idna==3.4
Jinja2==3.1.2
markdown-it-py==3.0.0
MarkupSafe==2.1.3
mdurl==0.1.2
oauthlib==3.2.2
openai==1.3.4
packaging==23.2
pydantic==2.5.1
pydantic_core==2.14.3
Pygments==2.17.2
pyinstaller==6.2.0
pyinstaller-hooks-contrib==2023.10
pyperclip==1.8.2
pypng==0.20220715.0
python-dateutil==2.8.2
python-dotenv==1.0.0
python-slugify==8.0.1
PyYAML==6.0.1
qrcode==7.4.2
repath==0.9.0
requests==2.31.0
rich==13.7.0
six==1.16.0
sniffio==1.3.0
text-unidecode==1.3
tqdm==4.66.1
types-python-dateutil==2.8.19.14
typing_extensions==4.8.0
urllib3==2.1.0
watchdog==3.0.0
websocket-client==1.6.4
websockets==11.0.3

インストールのコマンドはこちらです。

pip install -r requirements.txt

バージョンなどが異なると、思うように動作しない場合もあります。

こちらの記事に沿って進める場合はバージョンも揃えてください。

GPTとのやり取りを担うコード「gpts.py」

openaiを使用する方法は、APIエンドポイントのものより短くすみます。

今回は、以下の情報をnamedtupleで取り出す方法です。

  • GPTからの返信テキスト「reply_text」
  • 質問と返答にかかったトークン数「total_tokens」
  • 返答のJSONデータ「json」
import os
from openai import OpenAI
from collections import namedtuple


def ask_gpt(role: str, msg: str):
    client = OpenAI(
        api_key=<ここにAPIキーを入れる>,
    )
    completion = client.chat.completions.create(
        model="gpt-4-1106-preview",
        messages=[
            {"role": "system", "content": role},
            {"role": "user", "content": msg}
        ]
    )

    REPLY = namedtuple('REPLY', [
        'reply_text',
        'total_tokens',
        'json'
    ])
    reply = REPLY(
        completion.choices[0].message.content,
        completion.usage.total_tokens,
        completion.json()
    )
    return reply

APIキーについては適切な方法で、指定してください。

fletでUI担当のコード「main.py」

長いですが、以下のUIを担当するコードをすべて公開します。

import flet as ft
from gpts import ask_gpt
import pyperclip

GPT_MODEL = "gpt-4-1106-preview"


def main(page: ft.Page):
    page.title = "GPTee"
    page.scroll = ft.ScrollMode.AUTO
    page.update()
    role_input = ft.TextField(label="ロールを入力", prefix_text="あなたは、",
                              multiline=True, min_lines=2)
    your_role = ft.Text("")
    question_input = ft.TextField(
        label="お願いすることを入力",
        multiline=True,
        min_lines=3,
        max_lines=10)
    your_question = ft.Text("")

    output = ft.Text("", no_wrap=False, max_lines=10, selectable=True)
    all_json = ft.Text("")
    outputContainer = ft.Container(
        content=output,
    )

    def output_copy_btn_clicked(e):
        pyperclip.copy(output.value)

    output_copy_btn = ft.ElevatedButton(
        "回答をコピー",
        icon=ft.icons.CONTENT_COPY,
        disabled=True,
        on_click=output_copy_btn_clicked)

    def json_copy_btn_clicked(e):
        pyperclip.copy(all_json.value)

    json_copy_btn = ft.ElevatedButton(
        "JSONデータをコピー",
        icon=ft.icons.FILE_COPY,
        disabled=True,
        on_click=json_copy_btn_clicked)

    def all_copy_btn_clicked(e):
        all = role_input.value + "\n" + question_input.value + \
            "\n" + output.value + "\n" + all_json.value
        pyperclip.copy(all)

    all_copy_btn = ft.ElevatedButton(
        "全てコピー",
        icon=ft.icons.FOLDER_COPY_SHARP,
        disabled=True,
        on_click=all_copy_btn_clicked)

    def request_btn(e):
        output.value = ""
        output.update()
        your_role.value = "あなたは" + role_input.value
        your_role.update()

        your_question.value = question_input.value
        your_question.update()

        reply = ask_gpt(
            role=your_role.value,
            msg=your_question.value
        )
        output.value = reply.reply_text
        output.update()
        all_json.value = reply.json
        all_json.update()
        output_copy_btn.disabled = False
        output_copy_btn.update()
        json_copy_btn.disabled = False
        json_copy_btn.update()
        all_copy_btn.disabled = False
        all_copy_btn.update()

    def switched(e):
        if switch.value:
            role_input.value = "優秀なライターで、与えられた見出しに対して、わかりやすい文章を100から200字程度で執筆します。"
            your_role.value = role_input.value
            role_input.read_only = True
            role_input.update()
            your_role.update()
        else:
            role_input.value = ""
            your_role.value = ""
            role_input.read_only = False
            role_input.update()
            your_role.update()

    switch = ft.Switch(label="ライーターモード", value=False, on_change=switched)

    page.add(
        ft.Row(
            [
                role_input,
                switch
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                question_input
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                ft.FilledButton(
                    "リクエスト",
                    on_click=request_btn
                ),
                output_copy_btn,
                json_copy_btn,
                all_copy_btn
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                ft.Text("【ロール】"),
                your_role
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                ft.Text("【依頼内容】"),
                your_question
            ],
            width=page.window_width,
        ),
        outputContainer,
        ft.Row(
            [
                ft.Text("【レスポンスデータ】"),
            ],
            width=page.window_width,
        ),
        all_json,
    )


ft.app(target=main)

以下のコマンドでアプリが立ち上がります。

python main.py

アプリ化するには、requirements.txtにもある「pyinstaller」を使って、以下のコマンドを打ちましょう。

flet pack main.py

「main.py」は、当記事でご紹介しているファイル名。

皆さんの環境に合わせて、ファイル名を調整してください。

JSONデータでAPIエンドポイントへリクエストする方法

JSONデータを作成し、APIエンドポイントへリクエストを飛ばす方法を見ていきます。

モデルは「gpt-3.5-turbo」と古いものですが、参考にはなるはずです。

  • リクエスト・レスポンスデータの概要
  • 当方法の動画解説
  • GPTとのやり取りを担うコード「gpts.py」
  • fletでUI担当のコード「main.py」

リクエスト・レスポンスデータの概要

リクエストとレスポンスデータを見ておきましょう。

取り出したいデータがどこにあるかを把握できます。

  • リクエストデータ
  • レスポンスデータ

リクエストデータ

リクエストとして、以下の形式でJSONデータを飛ばします。

req_data = {
             "model": "gpt-3.5-turbo",
            "messages":[
                {
                    "role": "system","content": <ロールはここにいれる>
                 },
                {"role": "user", "content": <テキストはここに入れる>},
            ] ,
            "max_tokens": 4000, 
            }

max_tokensは、最大値だけ気にしながらお好きな値を入れましょう。

指定しなくてもリクエストとしては問題ありません。

レスポンスデータ

レスポンスは、ネストがかかったJSONデータです。

以下のようになるので、必要なものを抜き出す必要があります。

{
    "id": "chatcmpl-70RAZc0sTE6rrLZ2clciXJmWCdyiI",
    "object": "chat.completion",
    "created": 1680338795,
    "model": "gpt-3.5-turbo-0301",
    "usage": {
        "prompt_tokens": 51,
        "completion_tokens": 52,
        "total_tokens": 103
    },
    "choices": [
        {
            "message": {
                "role": "assistant",
                "content": "import hmac\nimport hashlib\n\nmessage = \"\u3053\u308c\u306f\u30c6\u30b9\u30c8\u6587\u5b57\u5217\"\nkey = \"mysecretkey\".encode('utf-8')\ndigest = hmac.new(key, message.encode('utf-8'), hashlib.sha256).hexdigest()\n\nprint(digest)"
            },
            "finish_reason": "stop",
            "index": 0
        }
    ]
}

当方法の動画解説

YouTube動画でも作り方を一から解説。

トータル20分で一から作成可能です。

ChatGPTAPIの登録方法などが知りたい方はこちらをご覧ください。

GPTとのやり取りを担うコード「gpts.py」

動画上でもコードを公開していますが、アプリ内のコードはこちらです。

import requests, json,os
from dotenv import load_dotenv

#ここでAPIキーを保存している
env_path = "assets/.env"
load_dotenv(env_path)
GPT_TOKEN = os.environ.get("GPT_AUTH")

class GetCompletion():
    def __init__(self, role, text):
        self.EP = "https://api.openai.com/v1/chat/completions"
        self.headers = {
            "Content-Type": "application/json",
            'Authorization': 'Bearer ' + GPT_TOKEN
            }
        self.role = role
        self.text = text
        
    def make_request_data(self):
        req_data = {
                    "model": "gpt-3.5-turbo",
                    "messages":[
                        {
                            "role": "system","content": self.role
                         },
                        {"role": "user", "content": self.text},
                    ] ,
                    "max_tokens": 4000, 
                    }
        json_data = json.dumps(req_data)
        res = requests.post(self.EP, data=json_data, headers=self.headers)
        status_code = res.status_code
        data = json.loads(res.text.encode('utf-8'))
        return data, status_code
    
    def get_reply(self):
        data, status_code = self.make_request_data()
        indented_data = json.dumps(data, indent=4)
        choices = data.get("choices")[0]
        message = choices.get("message") 
        content = message.get("content")
        msg = content.strip()

        return indented_data, msg, status_code

fletでUI担当のコード「main.py」

import flet as ft
from gpts import GetCompletion
import pyperclip
from scraper import get_scraped_text

#pyperclip.copy(text)

def main(page: ft.Page):
    page.title = "GPTee"
    page.scroll = ft.ScrollMode.AUTO
    page.update()
    input_role = ft.TextField(label="ロールを入力", prefix_text="あなたは、", multiline=True, min_lines=2)
    your_role = ft.Text("")
    input_question = ft.TextField(label="お願いすることを入力", multiline=True, min_lines=3, max_lines=10)
    your_question = ft.Text("")
    output = ft.Text("", no_wrap=False, max_lines=10, selectable=True)
    all_json = ft.Text("")
    outputContainer=ft.Container(
        content=output,
        
    )

    def output_copy_btn_clicked(e):
        pyperclip.copy(output.value)
        
    output_copy_btn = ft.ElevatedButton(
        "回答をコピー", 
        icon=ft.icons.CONTENT_COPY,
        disabled=True,
        on_click=output_copy_btn_clicked)
    
    def json_copy_btn_clicked(e):
        pyperclip.copy(all_json.value)
        
    json_copy_btn = ft.ElevatedButton(
        "JSONデータをコピー", 
        icon=ft.icons.FILE_COPY,
        disabled=True,
        on_click=json_copy_btn_clicked)
    
    def all_copy_btn_clicked(e):
        all = input.value + "\n" + favor.value + "\n" + output.value + "\n" + all_json.value
        pyperclip.copy(all)
        
    all_copy_btn = ft.ElevatedButton(
        "全てコピー", 
        icon=ft.icons.FOLDER_COPY_SHARP,
        disabled=True,
        on_click=all_copy_btn_clicked)
    
    def request_btn(e):
        output.value = ""
        output.update()
        your_role.value = "あなたは" + role_input.value
        your_role.update()
        your_question.value = question_input.value
        your_question.update()

        instance = GetCompletion(
                        role=your_role.value 
                        text=your_question.value)

        data, msg, status_code = instance.get_reply()
        output.value = msg
        output.update()
        all_json.value = data
        all_json.update()
        output_copy_btn.disabled=False
        output_copy_btn.update()
        json_copy_btn.disabled=False
        json_copy_btn.update()
        all_copy_btn.disabled=False
        all_copy_btn.update()
    
    def switched(e):
        if switch.value:
            input.value = "優秀なライターで、与えられた見出しに対して、わかりやすい文章を100から200字程度で執筆します。"
            inputed.value = input.value
            input.read_only = True
            input.update()
            inputed.update()
        else:
            input.value = ""
            inputed.value = ""
            input.read_only = False
            input.update()
            inputed.update()
        
    switch = ft.Switch(label="ライーターモード", value=False,on_change=switched)
            
    page.add(
        ft.Row(
            [
                input,
                switch
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                favor
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                ft.FilledButton(
                    "リクエスト",
                    on_click=request_btn
                ),
                output_copy_btn,
                json_copy_btn,
                all_copy_btn
            ],
            width=page.window_width,
        ),
        ft.Row(
            [   
                ft.Text("【ロール】"),
                inputed
            ],
            width=page.window_width,
        ),
        ft.Row(
            [
                ft.Text("【依頼内容】"),
                favored
            ],
            width=page.window_width,
        ),
        outputContainer,
        ft.Row(
            [
                ft.Text("【レスポンスデータ】"),
            ],
            width=page.window_width,
        ),
        all_json,
    )
    
ft.app(target=main)

まとめ

fletはPythonをベースとしたデスクトップアプリなどが簡単に作れるフレームワークです。

Pythonに慣れている方ならかなり簡単に作れるはず。

ChatGPTだけでなく、いろいろと作れるのでぜひ試してみてください。

タイトルとURLをコピーしました