ラズパイPicoWでChatGPTのようなチャットボットの作り方

PicoWでChatGPTの作り方

「RaspberryPi PicoW」を使って「OpenAI」の「API」で「gpt-3.5-turbo」を使った「ChatGPT」のようなチャットボットの作り方を、コピペ用サンプルプログラムで詳しく紹介します。

ブラウザから入力した質問を、サーバーに設定した「PicoW」から「CircuitPython」で「OpenAI」に送信し、自然言語処理AI(gpt-3.5-turbo)で処理した結果を回答として取得して、ブラウザにチャット形式で表示していきます。

実用性は?ですが、質問が送信されて、回答が返ってくるまでの過程をシリアルモニタ上で確認できるため、OpenAIのAPIの使い方の学習に最適と思います。
回答の仕方に人格(キャラ設定)もできるので、いろんなキャラでどんな回答が返ってくるか試してみると結構面白い回答が返ってきたりするので是非お試しを♪

「PicoW」をサーバーにしてデータを送受信する方法は以下のリンクで詳しく紹介しています。
今回は「CircuitPython」で作成しています。

ラズパイPicoW Wi-Fi通信、遠隔操作&表示 CircuitPython編
Raspberry Pi PicoWのWi-Fi通信機能を使用してサーバー機能を利用した遠隔操作、データ監視する方法をCircuitPythonで詳しく紹介します。
チャットボットの動作確認には別途「OpenAI」で「APIキー」を発行する必要があります。無償期間があるため、しばらくは無償で使用できますが、その後は登録して有償での使用となります。
「APIキー」は流出すると悪用される危険性もあるため安全に管理して下さい。
今回の構成ではソースファイルに直接「APIキー」を書いていないため、簡単に読み取られることはないと思いますが「PicoW」内に保存しているため、USBで接続して内部ファイルを開くと確認できます。
あくまで個人利用として「APIキー」の流出の可能性がある場合は、削除して再発行して下さい。
スポンサーリンク

1.全体構成

・動作紹介

全体の構成は下写真のようになります。
OLED表示器は「PicoW」に接続してパソコン等のブラウザでチャットボットの画面を表示させるためのURL(IPアドレス)を確認するためだけに使用します。
無くても開発環境の「Thonny」等のシリアルモニタ(シェル)でも確認できるため「PicoW」単体でも動作確認できます。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

「PicoW」の電源を入れると上画像のようにOLED表示器にWi-Fi接続先のSSIDとチャットボットに接続するURL(IPアドレス)が表示されます。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

チャットボットが回答を取得中には「PicoW」本体のLEDが点灯します。
点灯中は通信中なので回答が返ってくるまでしばらく待ちましょう。

「PicoW」のURL(IPアドレス)が確認できたら、下画像のようにパソコンやスマホでブラウザを立ち上げて、アドレスバーにURL(IPアドレス)を入力します。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

上画像はGoogle Chromeです。
アドレスバーにURL(IPアドレス:環境によって異なります。ここでは「192.168.0.10」)を入力してエンターを押すと、上画像のチャットボットの画面が表示されます。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

チャットボットに質問をするには、上画像のようにテキストエリアに質問を入力して[送信]ボタンを押します。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

質問を送信すると、上画像のように「Loading…」が点滅表示するのでしばらく待ちましょう。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

しばらく待つと回答が返ってきます。
回答が返ってくると、テキストエリアの下に今回の回答と質問のトークン数と料金が表示されます。
(トークン数と料金はこの後こちらで紹介しています)


スマホのブラウザのアドレスバーにURL(IPアドレス)を入力しても下画像のように使用できます。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo
複数台からアクセスできますが、回答を得るための処理は「PicoW」1台が行うため、順番に処理することになります。
また、今回のチャットボットでは前回の質問内容を参照して回答をするように設定しているため、全く違う質問を交互にすると変な回答が返ってくる可能性があります。

・トークン数と使用料金の目安も確認できる

さらに質問をすると、チャット形式で履歴が表示されていきます。

テキストエリア下の「単価」には最新の質問と回答にかかったトークン数と金額が表示されます。
「合計」には一連のチャットにかかったトークン数と金額の合計が加算されて表示されるため、使用料の目安として下さい。

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo
金額については1000トークンの単価を0.002ドル(gpt-3.5-turboの単価)として、1ドル=130円換算で計算しているだけで、為替レートによって変動するので目安としてご確認ください。

・準備するもの

「PicoW」本体のみでも動作しますが、OLED表示器(SSD1306)があると接続先URL(IPアドレス)の確認が簡単にできるのでおすすめです。

OLED表示器は下画像のようにブレッドボードとジャンパー線を使用すると簡単に接続することができるので下画像を参考に接続して下さい。
(SSD1306接続方法:Vcc→3.3V、GND→GND[0V]、SCL→GP17、SDA→GP16)

ラズパイPicoWチャットボットの作り方gpt-3.5-turbo
ラズパイPicoWチャットボットの作り方gpt-3.5-turbo

「RaspberryPi PicoW」本体は日本国内の技術基準に適合したものを使用する必要があります。
「Amazon」でも買えるかもしれませんが、以下のスイッチサイエンスさんなら確実に適合品が買えるのでこちらでの購入をお勧めします。

SwitchScience
スイッチサイエンス

今回使用したブレッドボードはサンハヤト製の6連結ピンのものです。
安価なものはありますが 5連結ピンのものが多く、ピンが少ないため物足りないです。
少し割高ですが6連結ピンの方が自由度があっておすすめです。

OLED表示器SSD1306は今回は画面サイズ「128×32」のものを使用しました。
今回使用したのは表示色が青色しか余っていなかったので青色ですが、個人的には表示色は白色が一番見やすくて綺麗と思います。

ジャンパー線はケースもついたサンハヤト製がおすすめです。安いのもありますが・・・お好みで

「PicoW」は基本的には基板単体のみで端子がついていないため、端子がない場合は以下のようなピンヘッダーを半田付けして使用する必要があります。
ブレッドボードに挿してから半田付けすると意外と簡単です。

スポンサーリンク

2.OpenAIについて

今回作成したチャットボットには「OpenAI」社の「自然言語処理AI:gpt-3.5-turbo」を使用しています。

「OpenAI」とは「人類の利益のために安全で有益な汎用人工知能(AGI)を構築すること」を目的として、2015 年にイーロン・マスクや、現在のOpenAIのCEOサム・アルトマンらによって設立された非営利の人工知能研究組織です。

テキストの生成、言語の翻訳、さまざまな種類のクリエイティブコンテンツの作成、質問への回答を行うことができる大規模な言語モデルの開発など、いくつかの注目すべき成果を上げており有名なものに「ChatGPT」があります。

・ChatGPTとは

「ChatGPT(チャットジーピーティー)」は、「OpenAI」が開発したAIチャットサービスです。
2022年11月30日に公開され、人間のような自然な会話ができることで話題となリました。

「ChatGPT」には、膨大なテキストとコードのデータセットでトレーニングされた「GPT-3.5」という言語モデルが搭載されており、質問をするだけで、ニュース、エンターテインメント、スポーツ等のさまざまな種類の情報だけでなく、プログラミングのコードや歌詞、物語など、さまざまなクリエイティブなテキストコンテンツをチャット形式で得ることができます。

以下のサイトからアカウントを作成してサインインすると無料で使用することができます。

Introducing ChatGPT
We’ve trained a model called ChatGPT which interacts in a conversational way. The dialogue format makes it possible for ChatGPT to answer followup questions, ad...
「ChatGPT」は基本無料ですが、アクセス数が多い場合には接続できなかったり、回答が得られなかったりします。また、短時間で多くの情報をやり取りすると、制限がかかってしばらく使えなくなったりします。
これを避けたい場合は優先的に接続することができる、有料(月額20ドル)の「ChatGPT Plus」の使用を検討しましょう。
今回作ったチャットボットのように「API」を使えば、使った分だけ支払えば良いのでおすすめです。

・自然言語処理AI「gpt-3.5-turbo」とは

「OpenAI」社は様々な「自然言語処理AI」を提供しており、これらは「API(Application Programming Interface)」を使用することで利用することができます。

「API」を使用することで、自分で作成したプログラムからテキストを「OpenAI」へ送信すると、指定した「自然言語処理AI」で処理を実行して、その結果を得ることができます。

「API」の利用は有料で使用料(文字数に由来するトークン数)によって料金が決まるため、クレジットカードの登録が必要となります。

現時点で最新のものは「gpt-4」ですが、まだベータ版で、使用するには待機リストに登録して空きを待つ必要があります。

「gpt-3.5-turbo」は登録さえすれば、すぐに使うことができます。
使用料は1000Token(トークン)あたり0.002ドル(詳細は以下のこちらを参照)のため非常に安価です。

今回はこの「gpt-3.5-turbo」の「API」を使用して「PicoW」から「OpenAI」に質問を送信して回答を得ることで、チャットボットを作成しています。

・APIとは、APIキーの取得方法

「API」とは「Application Programming Interface」の略で、「ソフトウェアアプリケーション」と「プログラム」間で情報をやり取りする仕組みです。

「API」を使用すると、ソフトウェア開発者は他の企業が開発したコードを自分のアプリケーションの中で使用できます。これにより、開発者の時間と労力を節約し、新しいアプリケーションの開発を容易にすることができます。

「OpenAI」の「自然言語処理AI:gpt-3.5-turbo」も「API」で使用することができます。

「API」の利用には「APIキー」の発行が必要で、使用するためには文字数に由来するトークン数によって使用料金が発生するため、アカウントの作成とクレジットカード情報の登録が必要です。

APIキーの取得方法

「gpt-3.5-turbo」の「API」を使用するための登録方法から「APIキー」の取得方法、注意事項について以下から紹介します。

まずは以下の「OpenAI」のサイトへアクセスします。

OpenAI
Introducing the ChatGPT app for iOS
OpenAIのAPIキー取得方法

上画像のようなサイトが表示されたら右上の[menu]をクリックします。

OpenAIのAPIキー取得方法

「OpenAI」のアカウントをお持ちでない方はドロップダウンリストから[Sign up]をクリックします。「ChatGPT」を使っていてアカウントのある方は[Log in]をクリックして以下は飛ばしてこちらへどうぞ

OpenAIのAPIキー取得方法

新規でアカウントの作成をする場合は上画像で[Email address]を入力して[Continue]ボタンをクリックします。

OpenAIのAPIキー取得方法

次に[パスワード]を決めて入力(8文字必要)したら[Continue]ボタンをクリックします。

OpenAIのAPIキー取得方法

上画像のように表示されたらメールが届いているのでメールを確認しましょう。

OpenAIのAPIキー取得方法

メールを確認すると、上画像のようなメールが届いているので、その中の[Verify email address]をクリックします。

OpenAIのAPIキー取得方法

メールアドレスの確認が完了すると、上画像のような画面が表示されるので[login]をクリックします

OpenAIのAPIキー取得方法

ここからはログイン画面になります。
上画像のように表示されたら、メールアドレスを入力して[Continue]ボタンをクリックします。

OpenAIのAPIキー取得方法

次に自分で決めた[パスワード]を入力して[Continue]ボタンをクリックします。

OpenAIのAPIキー取得方法

次に名前と生年月日を入力して[Continue]ボタンをクリックします。

OpenAIのAPIキー取得方法

電話番号の登録が必要なため、SNSメールが受信可能な携帯電話の番号を入力します。
上画像のように日本を選択すると国コード「+81」が表示されているので、これに続けて入力して[Send code]ボタンをクリックします。

OpenAIのAPIキー取得方法

しばらくすると携帯に「OpenAI 認証コード」が届くので6桁のコードを入力します。

これでアカウントの作成は完了です。
私はすでに登録済みの電話番号を入力したので画像はないですが、最後には用途の確認画面が出ていると思います。
個人の利用なら「I’m exploring personal use」を選択して下さい。
OpenAIのAPIキー取得方法

すでに登録済みの電話番号を入力すると上画像のような画面で、無料のクレジットはつかないと案内され有料プランへのアップグレードを促されます。


アカウントの作成が完了したら、APIキーの発行を行います。
次回からログインすると以下のような画面が表示されます。

OpenAIのAPIキー取得方法

上画像右上の自分のアカウント名をクリックします。

OpenAIのAPIキー取得方法

ドロップダウンメニューから[View API keys]をクリックします。

OpenAIのAPIキー取得方法

上画像のような画面が表示されたら[Create new secret key]ボタンをクリックします。

OpenAIのAPIキー取得方法

上画像の画面では作成する「APIキー」の用途がわかるように任意の名前をつけられます。
適当に名前を入力して[Create secret key]ボタンをクリックします。

OpenAIのAPIキー取得方法

上画像の画面のように「APIキー」が発行されます。
右にあるコピーボタンでコピーできるので安全な場所に保管しておきましょう。

「APIキー」が確認できるのはこの画面だけです。後から確認することはできないのでここで確認して保存しておきましょう。
もし確認し忘れても何回でも発行できるので削除して再発行すれば大丈夫です。
OpenAIのAPIキー取得方法

上画像のように作成した「APIキー」の名前や作成日、最後に使用した日時が確認できますが「APIキー」自体は確認できません。
「APIキー」がわからない場合は右端の[ゴミ箱]アイコンをクリックして削除して再発行しましょう。

「APIキー」は流出した場合、不正に使用される可能性があるため、誰にも知られないように大切に管理しておきましょう。
定期的に「APIキー」の使用状況を確認し、身に覚えのない使用や「APIキー」の流出の可能性がある場合は削除し再発行するようにしましょう。
有料プランに登録したときには月額の使用上限を適切に設定しておきましょう。

・API使用料金、トークンについて

「API」利用のアカウントを新規作成した時点で3ヶ月間限定の無料のAPI使用料(18ドル)が割り当てられます。
使いきれなかった場合は無くなってしまうので思いっきり使いましょう。


使用料の確認は「OpenAI」のホームページからログインし、下画像のように画面右上のアカウント名をクリックして[Manage account]をクリックします。

OpenAIのAPIキー使用料の確認

下画像のようなメニューが画面左に表示されるので[Usage]をクリックします。

OpenAIのAPIキー使用料の確認

下画像のような画面が表示され、今月の「API」の使用料が確認できます。
[Free trial usage]で無料枠の金額の使用状況が確認できます。

OpenAIのAPIキー使用料の確認
私はすでに使い切っているので「$0.00」になっています。
ちなみに私は3ヶ月で結構使ったつもりですが2ドルも使えませんでした・・・

トークン(Token)と単価について

「トークン」とは「自然言語処理AI(モデル)」が処理するテキストの単位で「API」の使用料もこの「トークン」単位で決まります。

「gpt-3.5-turbo」等のモデルは、テキストをトークンに分解して理解して処理します。
トークンは単語または単なる文字の塊です。たとえば、「hamburger」という単語は「hum」、「bur」、「ger」というトークンに分割されますが、「pear」のような短く一般的な単語は単一のトークンになります。

1byteで表現できるアルファベットに対して日本語は3byteのような複数byteで表現されるため、同じ内容の質問でも、英語よりも日本語の方がトークン数は多くなる傾向にあります。

トークン数のカウントは、質問と回答の内容だけではなく、プロンプトや参照情報を組み合わせた合計になります。

モデルごとに1度に処理できる最大のトークン数は決まっており、それを超えた処理はできません。
テキストがどのようにトークンに変換されるかについて詳しくは「 tokenizer tool 」を参照ください。


モデルごとの処理可能な最大トークン数と単価は以下表のようになります。

モデル最大処理トークン数1000トークンあたりの単価
(送信:prompt/受信:completion)
gpt-4 8k8192$0.03(prompt)/ $0.06(completion)
gpt-4 32k8192$0.06(prompt)/ $0.12(completion)
gpt-3.5-turbo4096$0.002
「gpt-3.5-turbo」は「gpt-4」よりかなり安いです。
私自身まだ「gpt-4」は使えていませんが「gpt-3.5-turbo」でも十分な回答が得られているため「gpt-4」が使えるようになってもメインは「gpt-3.5-turbo」を使うと思います。
スポンサーリンク

3.シンプルなサンプルプログラム(コピペ)

今回作成するチャットボットで「API」を使用するプログラムは「CircuitPython」で作成しています。
「Circuit Python」は「PicoW」本体をUSBメモリーのように扱い、ライブラリやhtmlファイル、「APIキー」を保存しておくファイルを保存して管理することができます。


「CircuitPython」の基本的な使用方法と「PicoW」をサーバーとして使用する方法は以下のリンクで詳しく紹介しています。

ラズパイPicoの使い方 CircuitPython&開発環境Thonny
Raspberry Pi PicoでCircuitPythonを使ったプログラミング方法を開発環境Thonnyを使用してインストールからライブラリの追加、サンプルプログラム(コピペ)による動作確認(液晶表示SSD1306を例に)まで詳しく紹介します。
ラズパイPicoW Wi-Fi通信、遠隔操作&表示 CircuitPython編
Raspberry Pi PicoWのWi-Fi通信機能を使用してサーバー機能を利用した遠隔操作、データ監視する方法をCircuitPythonで詳しく紹介します。

「API」を使用して「OpenAI」から質問の回答を取得するだけのシンプルなサンプルプログラムは以下になります。コピペで貼り付けて実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

import wifi
import socketpool
import ssl
from adafruit_requests import Session

# Wi-FiアクセスポイントのSSID、パスワードとOpenAIから取得したAPIキーの設定
SSID = "自宅のWi-Fi接続先SSIDを設定"
PASSWORD = "自宅のWi-Fi接続先パスワードを設定"
API_KEY = "APIキーを設定"

# API使用設定
ENDPOINT = 'https://api.openai.com/v1/chat/completions' # OpenAI接続先(エンドポイント)
MODEL = "gpt-3.5-turbo"                                 # AIモデル
SYSTEM = ""     # 回答の人格(キャラ)設定(「〜のように回答してください」等を記入)
assistant = ""  # 回答するにあたっての参照情報を設定
prompt = "こんにちは"  # Chatbotへの質問内容

# Wi-Fi接続
print("Connecting to Wi-Fi...")
wifi.radio.connect(SSID, PASSWORD)

# socketpoolとrequestsライブラリを使用するための設定
pool = socketpool.SocketPool(wifi.radio)
requests = Session(pool, ssl.create_default_context())

# OpenAIからの応答を取得する関数
def get_chat_response(prompt):
    # APIリクエストヘッダーを設定
    headers = {
        'Content-Type': 'application/json; charset=utf-8',
        'Authorization': 'Bearer ' + API_KEY
    }
    # APIリクエストデータを設定
    data = {
        'model': MODEL,             # OpenAIのAIモデルを指定
        'messages': [
            {'role': 'system', 'content': SYSTEM },       # 回答の仕方、傾向(人格、キャラ)設定
            {'role': 'assistant', 'content': assistant }, # 回答するための参照情報設定
            {'role': 'user', 'content': prompt }          # 質問内容
        ], # 質問内容を「prompt」に指定
        'max_tokens' : 500,         # 生成するテキストの最大長(トークン数)
        'temperature' : 1,          # 値が大きいほど生成される文章が多様化する(0〜2)
        # 'top_p' : 1,              # 値が大きいほど多様な単語が選択される(0〜1)※temperatureとの併用は非推奨
        'frequency_penalty' : 0.0,  # 値が大きいほど同じ単語が繰り返し出現することを防止する(-2.0~2.0)
        'presence_penalty' : 0.0    # 値が大きいほど同じ意味を持つフレーズや文の出現を防止する(-2.0~2.0)
    }
    # APIリクエストを送信
    response = requests.post(ENDPOINT, headers=headers, json=data, timeout=60)
    # API応答を解析
    response_json = response.json()
    print(response_json)          # 受信データ
    print(response_json['usage']) # トークン使用料(質問、回答、合計)
    message = response_json['choices'][0]['message']['content'].strip() # 回答を抽出
    return message

# Chatbotの回答を取得
response = get_chat_response(prompt)
print('\nUser: ' + prompt)
print('\nAI: ' + response)

サンプルプログラムを開発環境「Thonny」に貼り付けて実行すると、以下のように「シェル」内で回答を確認することができます。

OpenAIのAPIキー使用方法、サンプルプログラム

サンプルプログラムの中の「prompt」の「こんにちは」を他の質問内容に書き換えて実行してどんな回答が得られるか確認してみましょう。

・質問の送信から回答の取得方法

まずは以下のサンプルプログラムの「11〜16行目」のように「OpenAI」への接続先エンドポイント、AIモデル、質問内容を設定します。
(「SYSTEM」と「assistant」 についてはこの後こちらで詳しく紹介します。)

# API使用設定
ENDPOINT = 'https://api.openai.com/v1/chat/completions' # OpenAI接続先(エンドポイント)
MODEL = "gpt-3.5-turbo"                                 # AIモデル
SYSTEM = ""     # 回答の人格(キャラ)設定(「〜のように回答してください」等を記入)
assistant = ""  # 回答するにあたっての参照情報を設定
prompt = "こんにちは"  # Chatbotへの質問内容

「API」を使用して「OpenAI」へ質問を送信し回答を得るには以下のサンプルプログラムの「28〜48行目」のように「ヘッダー情報」と「送信データ」を「JSON」形式で指定して「OpenAI」のサーバーでhttp通信でPOST送信(リクエスト)します。

# APIリクエストヘッダーを設定
headers = {
    'Content-Type': 'application/json; charset=utf-8',
    'Authorization': 'Bearer ' + API_KEY
}
# APIリクエストデータを設定
data = {
    'model': MODEL,             # OpenAIのAIモデルを指定
    'messages': [
        {'role': 'system', 'content': SYSTEM },       # 回答の仕方、傾向(人格、キャラ)設定
        {'role': 'assistant', 'content': assistant }, # 回答するための参照情報設定
        {'role': 'user', 'content': prompt }          # 質問内容
    ], # 質問内容を「prompt」に指定
    'max_tokens' : 500,         # 生成するテキストの最大長(トークン数)
    'temperature' : 1,          # 値が大きいほど生成される文章が多様化する(0〜2)
    # 'top_p' : 1,              # 値が大きいほど多様な単語が選択される(0〜1)※temperatureとの併用は非推奨
    'frequency_penalty' : 0.0,  # 値が大きいほど同じ単語が繰り返し出現することを防止する(-2.0~2.0)
    'presence_penalty' : 0.0    # 値が大きいほど同じ意味を持つフレーズや文の出現を防止する(-2.0~2.0)
}
# APIリクエストを送信
response = requests.post(ENDPOINT, headers=headers, json=data, timeout=60)

データ送信後「OpenAI」のサーバーで処理された回答のデータが「JSON」形式で返ってくる(レスポンス)ため、これを受信して以下のサンプルプログラムの「49〜54行目」のように回答を抽出して確認します。

# API応答を解析
response_json = response.json()
print(response_json)          # 受信データ
print(response_json['usage']) # トークン使用料(質問、回答、合計)
message = response_json['choices'][0]['message']['content'].strip() # 回答を抽出

質問として「こんにちは」を送信した時の受信(レスポンス)データは以下のような「JSON」形式のデータになります。

{
    'object': 'chat.completion',
    'created': 1684767533,
    'model': 'gpt-3.5-turbo-0301',
    'usage': {
        'total_tokens': 36,
        'prompt_tokens': 19,
        'completion_tokens': 17
    },
    'id': 'chatcmpl-7J1HpUhhTxvvwvgC594vBDwYPWrrG',
    'choices': [
        {
            'finish_reason': 'stop',
            'message': {
                'content': 'こんにちは!どのようなご用件でしょうか?',
                'role': 'assistant'
            },
            'index': 0
        }
    ]
}

回答は「choices」→「message」→「content」に格納されています。

その他にもトークン数が送信(prompt_tokens)、受信(completion_tokens)、合計(total_tokens)で確認できるので、回答取得にかかった費用の目安にすることができます。

・環境変数設定ファイル(settings.toml)の使い方

上のサンプルプログラムでは「APIキー」を直接コード内に記入していますが、これはセキュリティ的には「APIキー」の流出の危険性があり、好ましくありません。

実際は「APIキー」は別の場所に保存して参照して使用するようにします。

「PicoW」ではUSBを接続すればすべてのファイルを見られるため、結局は同じなのですが・・・実際のサーバーではサーバー内に「環境変数」として設定しておく等して、そこから「APIキー」を取得します。

「CircuitPython」には「環境変数」設定用のファイルとして「setting.toml」ファイルというものがあるので、チャットボットのプログラムではここから参照しています。

セキュリティ的には安全とは言えませんが「SSID」や「パスワード」「APIキー」を毎回コードに書き込む必要がなくなるため便利です。

「setting.toml」ファイルの内容は以下のように記入します。

CIRCUITPY_WIFI_SSID="自宅のWi-Fi接続先SSIDを設定"
CIRCUITPY_WIFI_PASSWORD="自宅のWi-Fi接続パスワードを設定"
OPENAI_API_KEY="OpenAIから取得したAPIキーを設定"

これを使用するためには、上のサンプルプログラムの「1〜10行目」を以下と置き換えます。
外部ファイル参照のために「os」ライブラリもインポートします。

import os # 外部ファイル参照用ライブラリ
import wifi
import socketpool
import ssl
from adafruit_requests import Session

# Wi-FiアクセスポイントのSSID、パスワードとAPIキーをsetting.tomlファイルから取得
SSID = os.getenv("CIRCUITPY_WIFI_SSID")         # Wi-Fi接続先SSID
PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD") # Wi-Fi接続先パスワード
API_KEY = os.getenv("OPENAI_API_KEY")           # APIキー
「PicoW」本体をUSBに接続するとUSBメモリーのように認識されるため、「setting.toml」ファイルを開けば、比較的簡単に内容を確認することができるため、情報の流出には十分注意しましょう。
「APIキー」の流出の可能性が考えられる場合は「OpenAI」のサイトで「APIキー」を削除し、再発行するようにしましょう。

・回答の仕方に人格(キャラ)設定する方法

回答の仕方に人格(キャラ)を設定することができます。

キャラ設定には「SYSTEM」の中に「〜のように回答してください」と設定します。
細かいキャラ設定は「assistant」の中に設定します。

試しに以下のように設定して、実行してみました。

SYSTEM = "あなたは猫です。猫のように回答して下さい"  # 回答の人格(キャラ)設定(「〜のように回答してください」等を記入)
assistant = "自分のことは「我輩」と言う"          # 回答するにあたっての参照情報を設定
prompt = "あなたは誰ですか?"                    # Chatbotへの質問内容

回答は「我輩は自然言語処理の人工知能エージェントである。」でした・・・

ここは「我輩は猫である。」と言って欲しかったですが、流石に無理でしたねw。
でも、ちゃんと「〜である。」で回答してきてくれているところは感心しました。

「assistant」には前回の回答内容を入れるようにしておくと、次の質問で前回の回答も踏まえた回答を得られるようにできたりするので、いろいろ試してみましょう。

4.チャットボットのサンプルプログラム(コピペ)

ブラウザから質問して回答をチャット形式で得られる「チャットボット」のサンプルプログラムは以下になります。

開発環境に「Thonny」を使用した方法で以下から詳しく紹介します。

・メインプログラム(code.py:CircuitPython)

まずは「Thonny」を起動して、下のサンプルプログラムをコピペで貼り付けます。

PicoWでChatGPTの作り方

「API」を使用して「OpenAI」から質問の回答を取得するサンプルプログラムは以下になります。コピペで貼り付けて実行してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

import os               # ファイル操作など、OSレベルの機能を提供するために使用
import socketpool       # TCP/IP接続用のソケット管理用
import wifi             # WiFi接続を管理用
import digitalio        # デジタルピンの入出力を制御用
from board import *     # マイクロコントローラーのボード定義から全てのピンをインポート
import ssl              # SSL通信用
from adafruit_requests import Session # HTTPリクエスト送信用
import json             # JSONデータ処理用
from adafruit_httpserver import Server, Request, FileResponse, JSONResponse # サーバー用ライブラリ

# 本体LED表示設定
led = digitalio.DigitalInOut(LED)           # 本体LEDをGPIO端子に割り当て
led.direction = digitalio.Direction.OUTPUT  # LED端子を出力に設定

# Wi-FiアクセスポイントのSSIDとパスワードを取得(PicoW内に保存されている「settings.toml」ファイルから読み込む)
SSID = os.getenv("CIRCUITPY_WIFI_SSID")         # Wi-Fi接続先SSID
PASSWORD = os.getenv("CIRCUITPY_WIFI_PASSWORD") # Wi-Fi接続先パスワード

# OpenAIへの接続設定
API_KEY = os.getenv("OPENAI_API_KEY") # APIキー(PicoW内に保存されている「settings.toml」ファイルから読み込む)
ENDPOINT = 'https://api.openai.com/v1/chat/completions' # OpenAIのアドレス指定
MODEL = "gpt-3.5-turbo"                                 # OpenAIのAIモデル指定
SYSTEM = ""     # 回答の人格(キャラ)設定(「〜のように回答してください」と記入)
assistant = ""  # 回答するにあたっての参照情報を設定(今回は質問ごとに前回の回答を設定)

# Wi-Fi接続を実行
wifi.radio.connect(SSID, PASSWORD)  # 指定したSSIDとパスワードで接続
print("Connected to", SSID)         # 接続先SSID表示

# ソケットプールを作成してHTTPサーバーを起動するための準備
pool = socketpool.SocketPool(wifi.radio)  # Wi-Fi接続上でソケットを管理するためのオブジェクトを作成
requests = Session(pool, ssl.create_default_context())  # SSL通信設定
server = Server(pool, "/static", debug=True)      # サーバー起動時にPicoWのルートディレクトリからデータを提供できるようにする

# OpenAIからの応答を取得する関数
def get_chat_response(prompt, assistant):
    # APIリクエストヘッダーを設定
    headers = {
        'Content-Type': 'application/json; charset=utf-8',
        'Authorization': 'Bearer ' + API_KEY  # APIキーを指定
    }
    # APIリクエストデータを設定
    data = {
        'model': MODEL,             # OpenAIのAIモデルを指定
        'messages': [
            {'role': 'system', 'content': SYSTEM },       # 回答の仕方、傾向(人格、キャラ)設定
            {'role': 'assistant', 'content': assistant }, # 回答するための参照情報設定
            {'role': 'user', 'content': prompt }          # 質問内容
        ], # 質問内容を「prompt」に指定
        'max_tokens' : 500,         # 生成するテキストの最大長(トークン数)
        'temperature' : 1,          # 値が大きいほど生成される文章が多様化する(0〜2)
        # 'top_p' : 1,                # 値が大きいほど多様な単語が選択される(0〜1)※temperatureとの併用は非推奨
        'frequency_penalty' : 0.0,  # 値が大きいほど同じ単語が繰り返し出現することを防止する(-2.0~2.0)
        'presence_penalty' : 0.0    # 値が大きいほど同じ意味を持つフレーズや文の出現を防止する(-2.0~2.0)
    }
    # APIリクエストを送信
    print("リクエストJSON:")
    print(data)
    response_json = requests.post(ENDPOINT, headers=headers, json=data, timeout=60)  # OpenAIのアドレスを「ENDPOINT」に指定
    # API応答を解析
    response_data = response_json.json()  # HTTPレスポンスのJSONをPythonオブジェクトに変換
    return response_data                  # Pythonオブジェクトを返す

# OLED使用の場合(SSID,IPアドレス確認用)--------------------------------------------------------------
# 別途ドライバ、フォントファイルの準備が必要。詳細はこちらを参照ください→ https://logikara.blog/raspi_pico_oled_circuitpy
def oled_disp():
    import busio            # I2C通信制御用
    import adafruit_ssd1306 # OLED SSD1306制御用
    # I2C 通信設定
    i2c = busio.I2C(GP17, GP16)  # (SCL, SDA)17,16
    display = adafruit_ssd1306.SSD1306_I2C(128, 32, i2c, addr=0x3C)
    # OLED表示設定
    display.fill(0) # 表示内容消去
    display.hline(0, 10, 128, True) # (x, y, 長さ, 色) 指定座標から横線
    # 文字表示("表示内容", x, y, 色, フォント, サイズ)
    display.text("ChatBOT:" + MODEL, 0, 0, True, font_name="font5x8.bin", size=1)
    display.text("SSID:" + SSID, 0, 13, True, font_name="font5x8.bin", size=1)
    display.text("URL:" + str(wifi.radio.ipv4_address), 0, 24, True, font_name="font5x8.bin", size=1)
    # 設定した内容を表示
    display.show()
try:
  oled_disp() # OLED表示処理(未接続なら次の処理へ)
except RuntimeError:  # No pull up found on SDA or SCL; check your wiringエラーを無視する
    print("OLED not connected")
    pass
# ------------------------------------------------------------------------------------------------

# ルート(IPアドレス)アクセス時に実行される関数(トップページ表示、ポート80は省略可)
@server.route("/")
def base(request: Request):           # HTTPリクエストを処理する関数を定義
    return FileResponse(request, "index.html", "/") # レスポンスとしてPicoWルートフォルダの「index.html」ファイルを読み込みHTMLデータとして返す

# OpenAIからの回答取得実行(IPアドレス/questionアクセス時に実行)される関数
@server.route("/question", "POST")    # アクセスURLとPOST受信設定(POST指定なしの場合はGETのみ受信)
def question(request: Request):   # HTTPリクエストを処理する関数を定義
    global assistant                  # 前回の回答格納用
    led.value = True                  # 本体LEDを点灯
    data = json.loads(request.body)   # リクエストボディからJSONデータを取得
    print("\nリクエストボディJSON:")
    print(data)
    
    # OpenAIへの質問内容を表示
    prompt = data["question"] # 取得したJSONデータの「question(key)」から「質問(value)」を取得
    print('\nUser: ' + prompt)  # 質問を表示

    # OpenAIからの回答(レスポンス)を取得して表示
    response_data = get_chat_response(prompt, assistant) # OpenAIからの回答取得関数を実行し回答をPythonオブジェクトで格納
    print("\n OpenAIからのレスポンスデータ:")
    print(response_data)
    assistant = response_data['choices'][0]['message']['content'].strip() # Pythonオブジェクトから回答のみを抽出(次回の参照内容として格納)
    print('\nAI: ' + assistant)               # 回答を表示
    led.value = False                         # 本体LEDを消灯

    return JSONResponse(request, response_data) # JSONデータではなくPythonオブジェクト(dict型)で返す

# HTTPサーバーを開始して待ち受けるIPアドレスを表示する
print(f"Listening on http://{wifi.radio.ipv4_address}") # f文字列を使って「IPアドレス」と「ポート番号」を埋め込んで表示
server.serve_forever(str(wifi.radio.ipv4_address))      # HTTPサーバーを指定された「IPアドレス」と「ポート」で開始

「コピペ」したら保存ボタンをクリックします。

PicoWでChatGPTの作り方

保存場所を聞いてくるので「CircuitPythonデバイス」をクリックします。

PicoWでChatGPTの作り方

以下のようなウインドウが表示されたら「code.py」をクリックして[OK]ボタンをクリックします。

PicoWでChatGPTの作り方

「上書く?」と聞いてくるので[はい]をクリックします。

PicoWでChatGPTの作り方

これでメインの「CircuitPython」のプログラムは準備完了です。

・ブラウザ表示用ChatBOT(html, CSS, JavaScript)

次にブラウザで表示する「チャットボット」のプログラムを準備します。
このファイルは「PicoW」本体内に保存して「code.py」を実行したときに呼び出されます。

「Thonny」で下画像のように[新規作成]ボタンを押します。

PicoWでChatGPTの作り方

「untitled」タブが表示されるので下のサンプルプログラムをコピペで貼り付けます。


ブラウザで表示するチャットボットのサンプルプログラムは以下になります。コピペして、ファイル名を「index.html」として「PicoW」のメインフォルダに保存して下さい。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ChotGPT(gpt-3.5-turbo)</title>
    <style>
      body {
        background-color: #333;
        color: #ccc;
        font-family: Arial, sans-serif;
        margin: auto;
        max-width: 800px;
      }
      h1 {
        margin: 15px 0 5px;
        text-align: center;
      }
      form {
        align-items: center;
        display: flex;
        flex-direction: column;
        margin-top: 30px;
      }
      label {
        display: block;
        font-size: 16px;
        margin-bottom: 5px;
      }
      textarea {
        border: none;
        border-radius: 10px;
        height: 100px;
        margin-bottom: 3px;
        padding: 10px;
        width: 80%;
      }
      #token-price {
        font-size: 12px;
        margin-bottom: 3px;
      }
      button {
        background-color: #bb2944;
        border: none;
        border-radius: 5px;
        color: #FFF;
        cursor: pointer;
        font-size: 16px;
        margin-bottom: 30px;
        padding: 5px 20px;
        width: 120px;
      }
      #chat-area {
        color: #333;
      }
      .question-balloon, .answer-balloon {
        border-radius: 10px;
        margin: 20px;
        max-width: 80%;
        padding: 10px;
        position: relative;
        width: 80%;
      }
      .question-balloon {
        background-color: #f9e7d9;
        margin-left: 10%;
        text-align: right;
      }
      .question-balloon::before {
        border: 15px solid transparent;
        border-right: 15px solid #f9e7d9;
        content: '';
        margin-top: -15px;
        position: absolute;
        right: -15px;
        top: 50%;
      }
      .answer-balloon {
        background-color: #dafbfb;
        margin-right: 10%;
        text-align: left;
      }
      .answer-balloon::before {
        border: 15px solid transparent;
        border-left: 15px solid #dafbfb;
        content: '';
        left: -15px;
        margin-top: -15px;
        position: absolute;
        top: 50%;
      }
      .loading {
        color: #aaa;
        margin: 20px;
        margin-left: 20px;
        padding: 10px;
      }
      /* .loadingクラスに対するアニメーション定義 */
      @keyframes blinking {
        0% { opacity: 0.3; }
        50% { opacity: 1; }
        100% { opacity: 0.3; }
      }
      /* .loadingクラスのスタイル設定とアニメーション適用 */
      .loading {
        animation-name: blinking; /* アニメーション名 */
        animation-duration: 2s; /* アニメーション時間 */
        animation-delay: 0s; /* アニメーション開始までの遅延時間 */
        animation-iteration-count: infinite; /* 無限に繰り返し */
      }
      @media only screen and (max-width: 480px) {
        body {
          margin: 0;
          padding: 0;
          width: 100%;
        }
        h1 { font-size: 24px; }
      }
    </style>
</head>
<body>
    <h1>ChotGPT(gpt-3.5-turbo)</h1>
    <div id="chat-area"></div>
    <form>
        <label for="question-input">質問を入力して送信ボタンを押してください</label>
        <textarea id="question-input" required></textarea>
        <div id="token-price"></div>
        <button type="submit">送信</button>
    </form>

    <script>
        let allTokens = 0;  // トークン合計
        let allPrices = 0;  // API使用料合計
        const tokenPriceDisp = document.getElementById("token-price");
        // トークン数から金額換算、ブラウザ表示処理関数
        function tokensPrice(total_tokens) {
            const rate = 130;                                       // ドル-円 レート
            let totalPrice = total_tokens * (0.002 * rate) / 1000;  // 質問ごとのAPI使用料(単価)
            allTokens += total_tokens                               // トークン合計を加算
            allPrices += totalPrice;                                // API使用料合計を加算
            tokenPriceDisp.innerText ="単価:¥" + totalPrice.toFixed(3) + "(Token:" + total_tokens + ")/合計:¥" + allPrices.toFixed(2) + "(Token:" + allTokens + " ※$1=¥" + rate + ")";
        }
        tokensPrice(0);

        // トークン数取得関数
        function getToken(responseJson) {
            // コンソールログに表示
            console.log(responseJson);
            const prompt_tokens = responseJson.usage.prompt_tokens;
            const completion_tokens = responseJson.usage.completion_tokens;
            const total_tokens = responseJson.usage.total_tokens;
            console.log("送信トークン数:" + prompt_tokens);
            console.log("受信トークン数:" + completion_tokens);
            console.log("合計トークン数:" + total_tokens);
            tokensPrice(responseJson.usage.total_tokens);  // トークン数から金額換算ブラウザ表示
        }

        const chatArea = document.getElementById('chat-area');
        const form = document.querySelector('form');
        // フォーム送信時の処理
        form.addEventListener('submit', async (e) => {
            e.preventDefault();
            const questionInput = document.getElementById('question-input');
            const question = questionInput.value.trim();
            if (!question) {
                return;
            }
            // 質問の吹き出しを作成
            const questionBalloon = document.createElement('div');
            questionBalloon.classList.add('question-balloon');
            questionBalloon.textContent = question;
            chatArea.appendChild(questionBalloon);
            questionInput.value = '';
 
            // 回答を取得する前に Loading... を表示する
            const loadingDiv = document.createElement('div');
            loadingDiv.innerHTML = 'Loading...';
            loadingDiv.classList.add('loading');
            chatArea.appendChild(loadingDiv);

            // fetchAPIでPicoWに質問を送信してレスポンスをJSON形式で取得
            const url = '/question';  // リクエスト先URL
            try {
                const response = await fetch(url, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Accept': 'application/json'
                    },
                    body: JSON.stringify({ question })
                });
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }

                const responseJson = await response.json();
                getToken(responseJson);   // トークン数取得関数実行
                // 回答を取得
                let answer = responseJson.choices[0].message.content;
                answer = answer.replace(/\n/g, '<br>');
                chatArea.removeChild(loadingDiv); // 回答が取得できたら Loading... を削除する
                // 回答の吹き出しを作成
                const answerBalloon = document.createElement('div');
                answerBalloon.classList.add('answer-balloon');
                answerBalloon.innerHTML = answer;
                chatArea.appendChild(answerBalloon);
            } catch (error) {
                console.error('There was a problem with the fetch operation:', error);
                return { error: true, message: error.message };
            }
        });
    </script>
</body>
</html>

「コピペ」で貼り付けたら下画像のように[保存]ボタンを押します。

PicoWでChatGPTの作り方

保存場所を聞いてくるので「CircuitPython」をクリックします。

PicoWでChatGPTの作り方

下画像のようなウインドウが表示されたら[ファイル名:]のところに「index.html」と入力して[OK]ボタンをクリックします。

PicoWでChatGPTの作り方

これでチャットボットをブラウザで表示する「html」のプラグラムは準備完了です。

・環境変数設定ファイル(settings.toml)

次にWi-Fi接続先の「SSID」「パスワード」と「APIキー」を保存しておく「settings.toml」ファイルを設定します。

「settings.toml」は「PicoW」内に既にあるので、下画像のように[開く]ボタンを押します。

PicoWでChatGPTの作り方

ファイルを開く場所は「CircuitPython」を選択します。

PicoWでChatGPTの作り方

下画像のようなウインドウが表示されたら「settings.toml」を選択して、[OK]ボタンをクリックします。

PicoWでChatGPTの作り方

初期の「settings.toml」には何も記入されていないため、下のファイルを「コピペ」して貼り付けます。


Wi-Fi接続のための「SSID」「パスワード」と「APIキー」を保存しておく環境設定ファイル「Setting .toml」のサンプルファイルは以下になります。自分の環境に合わせて設定してください。
※下コード(黒枠)内の右上角にある小さなアイコンのクリックでもコピーできます。

CIRCUITPY_WIFI_SSID="自宅のWi-Fi接続先SSIDを設定"
CIRCUITPY_WIFI_PASSWORD="自宅のWi-Fi接続パスワードを設定"
OPENAI_API_KEY="OpenAIから取得したAPIキーを設定"

「コピペ」で貼り付けて各情報を入力したら[保存]ボタンをクリックして保存します。

PicoWでChatGPTの作り方

これで環境設定ファイル「settings.toml」ファイルは準備完了です。

・必要なライブラリの準備

最後に必要な「ライブラリ」とOLED表示器の「フォント」を準備します。

ライブラリの追加方法は以下のリンクを参照して下さい

ラズパイPicoの使い方 CircuitPython&開発環境Thonny
Raspberry Pi PicoでCircuitPythonを使ったプログラミング方法を開発環境Thonnyを使用してインストールからライブラリの追加、サンプルプログラム(コピペ)による動作確認(液晶表示SSD1306を例に)まで詳しく紹介します。

ライブラリは以下の「Adafruit」社のホームページからダウンロードして下さい。

CircuitPython - Libraries
The easiest way to program microcontrollers

ダウンロードするライブラリは「PicoW」にインストールした「CircuitPython」のバージョンに合わせて下さい。バージョンは「Thonny」の「シェル」部に表示されています。
今日時点の最新のものはバージョン8.1.0の「circuitpython-community-bundle-8.x-mpy-20230523.zip」になります。

ライブラリをダウンロードしたらパソコンのダウンロードフォルダに以下のようなファイルがあるので展開してフォルダを開きます。

PicoWでChatGPTの作り方

ダウンロードしたライブラリフォルダを開くと下画像のように「examples」と「lib」フォルダがあります。

「examples」フォルダにはOLED表示器の「フォント」ファイルが入っています。
「lib」フォルダには「ライブラリ」ファイルが入っています。

PicoWでChatGPTの作り方

上画像の2つのフォルダの中から必要なファイルを以下のように「PicoW」内部の各フォルダにコピーして保存していきましょう。

まずは、パソコンに認識されている「PicoW」フォルダを開きます。フォルダの内容は下画像のようになっています。

PicoWでChatGPTの作り方

下画像の「PicoW」本体のルートに、ダウンロードした「example」フォルダの中から「フォント」ファイル「font5x8.bin」を以下のようにコピーします。

PicoWでChatGPTの作り方

次に下画像のように「PicoW」の方の「lib」フォルダを開いて、ダウンロードした「lib」フォルダの中から下画像のように4つの「ライブラリ」ファイルをコピーします。

PicoWでChatGPTの作り方

以上で「チャットボット」を使用する準備が整いました。

動作確認

準備が整ったら動作確認をしてみましょう。
下画像のように[code.py]タブをクリックして[実行]ボタンをクリックします。

PicoWでChatGPTの作り方

Wi-Fi接続が開始されてサーバーが起動するとIPアドレス(値は環境によって異なります。)が表示されるので、これをブラウザのアドレス(URL)バーに入力します。

PicoWでChatGPTの作り方

サーバーへの接続に成功すると「シェル」部に下画像のように表示されます。
環境によって異なりますが、以下の「192.168.0.9」はアクセスしてきたクライアントのIPアドレスです。「”200 OK”」が表示されていたら通信は成功です。

PicoWでChatGPTの作り方

ブラウザには下画像のような画面が表示されるので、白いテキストエリアに「こんにちは」と入力して[送信]ボタンを押すと、質問が送信されます。

回答が返ってくるまでは少し時間がかかるのでしばらく待ちます。しばらくすると回答が返ってきます。

PicoWでChatGPTの作り方

「Tonny」の「シェル」部には下画像のように、質問と回答意外にも送受信した詳細情報が表示されるので、データ構造を確認できます。

PicoWでChatGPTの作り方

続けて質問するとチャットのように回答が得られるので、いろいろ質問してみましょう♪

5.まとめ

「Raspberry Pi PicoW」を使用した「チャットボット」の作り方を詳しく紹介しました。
小さなマイコンボードでもWi-Fi通信機能があれば、他の機種でも同じように作成することができます。

「ChatGPT」は基本無料で使用できますが、無料版では機密情報の質問は学習モデルとして使用される可能性があるため避けた方が良いのです。この場合は有料版も検討されますが、20ドルとそれなりに費用がかかります。

「API」を使用した方法であれば安くて、使った分だけ支払えば良いため、上手に使えば低コストで多くの情報を得ることができます。

今回は「CircuitPython」で実現しているため「Python」で「openai」ライブラリを使用するよりも少々手間ではありますが、今回の使い方を理解すると、言語が変わってもデータ構造は同じため、他の言語で「OpenAI」の「API」を使うことも容易になります。

今回の「チャットボット」の「html、CSS、JavaScript」について、私には作成するスキルはないため、ベースは「ChatGPT」に書いてもらいましたw、それなりに動いたので引き続き「ChatGPT」とのやり取りで完成させています。ほんと便利になりました。

「ChatGPT」は自分自身ではコードを書いてもらう以外は使い道が思いつきませんでしたが、細かい設定ができると色々使い道は広がりそうです。

便利な使い方ができたらまた紹介していきたいと思います。

コメント

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