09_API_ChatGPT
ChatGPT APIを利用する
ChatGPT APIへ質問を送り、返ってきた回答を表示します。
準備する
CherryIoTをPCに接続する
CherryIoTをPCのUSB-A挿入口に接続します。
Wi-Fiの情報を確認する
CherryIoTを接続する2.4GHz Wi-FiのSSID(ネットワーク名)とパスワードを確認します。Wi-Fiルーター本体のラベルや設定画面に記載されています。分からない場合は、ネットワークの管理者に確認してください。
OpenAI APIを準備する
OpenAI Platformへログインする
ログイン画面が表示されたら、任意の方法でOpenAI Platformへログインします。

API Keysを開く
OpenAI Platformのホーム画面で、左側メニューから「API Keys」をクリックします。

新しいAPIキーを作成する
API Keys画面で「Create new secret key」をクリックします。

APIキーの名前を設定する
Nameに任意の名前を入力し、「Create secret key」をクリックします。

APIキーを保存する
作成されたシークレットAPIキーをコピーし、安全な場所に保存してから「Done」をクリックします。APIキーは重要な認証情報なので、第三者へ公開しないでください。

Billingを開く
OpenAI APIの利用料金はChatGPTの有料プランとは別です。APIを利用するため、Billingを開いて支払い設定を行います。画面に「Go to Billing」が表示されている場合は、そこからBillingへ移動できます。

支払い設定を始める
Billing画面で「Add payment details」をクリックします。

支払い方法を設定する
支払い方法を選択し、必要な情報を入力して「Continue」をクリックします。

購入するクレジット額を設定する
初回に購入するAPI利用クレジットの金額を入力します。この例では5ドルに設定しています。自動でクレジットを追加しない場合は「Use auto-reload」をオフにして、「Continue」をクリックします。

支払い内容を確認する
購入するクレジット額と支払い方法を確認し、「Confirm payment」をクリックします。

サンプルコード
GitHubで公開しているサンプルコードです。コードをコピーして Arduino IDEで使用します。
GitHubからコードを読み込んでいます...プログラムを書き込む
上のサンプルコードをコピーし、Arduino IDEに貼りつけます。
設定情報を書き換える
コード内の以下の箇所を、「1 準備する」で確認・取得した情報に 書き換えます。
| コード内の項目 | 入力する情報 |
|---|---|
ssid | 接続するWi-FiのSSID |
password | Wi-Fiのパスワード |
openaiKey | OpenAI APIのシークレットAPIキー |
const char* ssid = "ここにSSIDを入力";const char* password = "ここにパスワードを入力";String openaiKey = "ここにAPIキーを入力";コードの左側にある項目名や記号は残し、例を参考に値の部分だけを 書き換えます。値を囲む"、行末の;、 証明書の開始・終了行などは削除しません。書き換えたあと、xxxxxなどの仮の文字が残っていないことを確認してください。
Wi-Fiのパスワード、APIキー、トークン、証明書などを入力したコードは、 Webサイト、SNS、GitHubなどへ公開しないでください。
ボードとポートの設定を確認し、左上の 「→(書き込みボタン)」を押してCherryIoTに書き込みます。
初めて書き込む方は「はじめてのCherryIoT」で詳しい手順を確認してください。
動作確認
シリアルモニターで確認する
ChatGPTの回答を確認する
Arduino IDEの画面右上にあるシリアルモニターのアイコンをクリックします。CherryIoTがWi-Fiへ接続したあと、コードで指定した2つの質問に対するChatGPTの回答が順番に表示されることを確認します。回答の内容は実行するたびに異なる場合があります。

シリアルモニターに、コードで指定した2つの質問に対するChatGPTの回答が表示されるなら成功です。
コードの解説
Wi-Fi経由でOpenAI APIへリクエストを送り、応答データから回答を取り出します。
GitHubからコードを読み込んでいます...String role = "You are a helpful assistant.";ChatGPTへ与える役割を、roleへ保存します。ここでは「役に立つアシスタント」として回答するよう指定しています。
String model = "gpt-3.5-turbo";OpenAI APIで使用するモデル名を、modelへ保存します。
String system_content =roleの内容を、APIへ送れるsystemメッセージのJSON形式へ組み立て、system_contentへ保存します。systemメッセージは、会話全体で守る役割や方針をモデルへ伝えるデータです。
String historical_messages = system_content;会話履歴を保存するhistorical_messagesを用意し、最初はsystemメッセージだけを入れます。後から質問と回答を追加することで、前の会話を踏まえた回答を受け取れます。
void setup() {setup関数は最初に1回だけ実行されます。ここでシリアル通信とWi-Fi接続を開始し、2つの質問をOpenAI APIへ送ります。
Serial.begin(115200);Wi-Fiの接続状況とChatGPTの回答をPCへ表示するため、115200bpsでシリアル通信を開始します。
connectWiFi();connectWiFi関数を呼び出し、OpenAI APIへ通信する前にWi-Fiへ接続します。
Serial.println(openAI_chat("私は山梨県に住んでいます。"));openAI_chat関数へ最初の質問を渡し、OpenAI APIから返された回答をシリアルモニターへ表示します。この質問と回答は会話履歴へ追加されます。
Serial.println(openAI_chat("私の地域で有名なものを教えてください。"));同じopenAI_chat関数へ2つ目の質問を渡し、回答を表示します。1つ目の質問で山梨県に住んでいることが履歴へ保存されているため、「私の地域」が山梨県を指す会話として送信されます。
void loop() {loop関数の中は空です。質問はsetup関数で1回だけ送信するため、繰り返し行う処理はありません。
void connectWiFi(){Wi-Fiへの接続処理を、connectWiFi関数にまとめます。関数にまとめることで、setup関数から名前を指定して接続処理を呼び出せます。
Serial.print("ssid:");接続先のSSIDを表示する前に、「ssid:」を改行せず表示します。
Serial.print(ssid);ssidに保存したWi-Fi名を、改行せず表示します。
Serial.println(" に接続します。");続けて「 に接続します。」と表示し、改行します。
WiFi.begin(ssid, password);ssidとpasswordに保存した情報を使って、Wi-Fiへの接続を開始します。
Serial.print("WiFiに接続中");Wi-Fiへ接続中であることを表示します。
while(WiFi.status() != WL_CONNECTED) {Wi-Fiへ接続済みになるまで、接続状態を繰り返し確認します。
Serial.print(".");接続待ちであることが分かるように、「.」を改行せず表示します。
Serial.println("接続しました。");Wi-Fiへの接続が完了したことを表示します。
Serial.print("IPアドレス:");続けてIPアドレスを表示することが分かるように、「IPアドレス:」を改行せず表示します。
Serial.println(WiFi.localIP());CherryIoTに割り当てられたIPアドレスを表示します。
String openAI_chat(String message) {引数messageで受け取った質問をOpenAI APIへ送り、取り出した回答文を文字列として返す処理を、openAI_chat関数にまとめます。
WiFiClientSecure client_tcp;OpenAI APIとHTTPS通信するため、WiFiClientSecure型の接続client_tcpを用意します。
client_tcp.setInsecure();このサンプルでは、OpenAIサーバーの証明書が正しいかを確認する処理を省略します。通信自体はHTTPSですが、証明書を検証しないため、学習用の簡易設定です。
message.replace("\"","'");質問文に含まれる二重引用符を一重引用符へ置き換えます。二重引用符はJSON内で文字列の区切りに使うため、そのまま含めるとJSONの形が崩れる可能性があります。
String user_content =質問文を、roleがuserのメッセージとしてJSON形式へ組み立て、user_contentへ保存します。
historical_messages += ", "+user_content;新しい質問を、それまでの会話履歴historical_messagesの末尾へ追加します。
String request =使用するモデル名と会話履歴を、OpenAI APIへ送るJSON形式にまとめ、requestへ保存します。
if (client_tcp.connect("api.openai.com", 443)) {OpenAI APIのサーバーapi.openai.comへ、HTTPSで使う443番ポートを指定して接続します。接続できた場合だけ送信処理を行います。
client_tcp.println("POST /v1/chat/completions HTTP/1.1");Chat Completions APIの接続先へ、POST方式でデータを送ることを示すHTTPリクエストの先頭行を送信します。
client_tcp.println("Connection: close");応答を受け取った後に接続を閉じることを、HTTPヘッダーで指定します。
client_tcp.println("Host: api.openai.com");通信先のホスト名がapi.openai.comであることを、HTTPヘッダーへ追加します。
client_tcp.println("Authorization: Bearer " + openaiKey);APIキーをBearer認証情報としてHTTPヘッダーへ追加します。OpenAIはこの情報を使って利用者を確認します。
client_tcp.println("Content-Type: application/json; charset=utf-8");送信する本文がUTF-8のJSON形式であることを、HTTPヘッダーへ追加します。
client_tcp.println("Content-Length: " + String(request.length()));送信するJSON本文の文字数をContent-Lengthとして伝えます。サーバーはこの値を使って本文の終わりを判断します。
client_tcp.println();HTTPヘッダーとJSON本文を区切るため、空行を送信します。
for (int i = 0; i < request.length(); i += 1024) {整数型の変数iを0から始め、requestの長さ未満の間、処理後にiを1024増加させて繰り返します。長いJSONを1024文字ずつに分けて送るための処理です。
client_tcp.print(request.substring(i, i+1024));requestのi文字目から最大1024文字を取り出し、OpenAI APIへ送信します。substringは、指定した範囲の文字列を取り出す命令です。
String getResponse="",Feedback="";回答本文を保存するgetResponseと、受信中の1行を確認するFeedbackを、空の文字列で用意します。
boolean state = false;現在、回答本文を読み取っている途中かを記録するboolean型の変数stateを用意し、最初はfalseにします。
int waitTime = 20000;OpenAI APIからの応答を待つ上限時間として、20000ミリ秒をwaitTimeへ保存します。
long startTime = millis();待ち時間を測る開始時刻として、起動後の経過時間をmillisで取得し、startTimeへ保存します。
while ((startTime + waitTime) > millis()) {現在時刻が開始時刻と待ち時間の合計を超えるまで、最大20秒間、応答を待ち続けます。
Serial.print(".");回答を待っていることが分かるように、「.」を改行せず表示します。
delay(100);0.1秒待ちます。
while (client_tcp.available()) {OpenAI APIから読み取れるデータが届いている間、1文字ずつ受信します。
char c = client_tcp.read();受信したデータを1文字読み取り、char型の変数cへ保存します。
if (state==true)stateがtrue、つまり回答本文の読み取り中である場合だけ、次の処理を行います。
getResponse += String(c);受信した1文字をgetResponseの末尾へ追加し、回答文を組み立てます。
if (c == '\n')受信した文字が改行文字であるかを確認します。
Feedback = "";改行を受信した場合は、次の行を確認できるようにFeedbackを空へ戻します。
else if (c != '\r')受信した文字が改行ではなく、復帰文字でもない場合はこちらへ進みます。
Feedback += String(c);受信した1文字をFeedbackへ追加し、応答の1行を組み立てます。
if (Feedback.indexOf("\",\"content\":\"")!=-1||Feedback.indexOf("\"content\": \"")!=-1)Feedbackの中に回答本文の項目を示すcontentが見つかったかを確認します。APIの応答形式にある空白の違いにも対応するため、2種類の文字列を調べています。||は、どちらか一方の条件を満たせばtrueになる記号です。
state=true;contentの開始位置を見つけたため、回答本文の読み取り中であることを示すstateをtrueにします。
if (getResponse.indexOf("\"},")!=-1&&state==true) {getResponseの中に回答の終端を示す文字列があり、stateがtrueかを確認します。
state=false;回答の終端を見つけたため、回答本文の読み取り中ではない状態へ戻します。
getResponse = getResponse.substring(0,getResponse.length()-3);回答文の末尾に含まれたJSONの区切り文字3文字を取り除き、回答本文だけにします。
} else if (getResponse.indexOf("\"")!=-1&&c == '\n'&&state==true) {別の応答形式で、引用符を含む回答の末尾と改行を検出した場合はこちらへ進みます。
state=false;回答の終端を見つけたため、回答本文の読み取り中ではない状態へ戻します。
getResponse = getResponse.substring(0,getResponse.length()-2);回答文の末尾に含まれた不要な2文字を取り除きます。
startTime = millis();データを1文字受信するたびにstartTimeを現在時刻へ更新します。受信が続いている間に待ち時間切れにならないようにするためです。
if (getResponse.length()>0) {getResponseに1文字以上の回答が入ったかを確認します。
client_tcp.stop();回答を受信できたため、OpenAI APIとの接続を終了します。
if (getResponse.endsWith("\"")) {回答文の末尾に不要な二重引用符が残っているかを確認します。
getResponse.remove(getResponse.length() - 1);末尾に残った不要な二重引用符を1文字削除します。
String assistant_content =受け取った回答を、roleがassistantのメッセージとしてJSON形式へ組み立て、assistant_contentへ保存します。
historical_messages += ", "+assistant_content;回答を会話履歴historical_messagesへ追加します。次の質問を送る際に、この回答も一緒に送られます。
Serial.println("");回答を表示する前に改行します。
return getResponse;取り出した回答文を、openAI_chat関数を呼び出した処理へ返します。
client_tcp.stop();待ち時間内に回答を取り出せなかったため、OpenAI APIとの接続を終了します。
Serial.println(Feedback);20秒以内に回答文を取り出せなかった場合は、最後に受信していた内容を表示し、通信状態を確認できるようにします。
return "error";回答を取得できなかったことを示す「error」を、呼び出し元へ返します。
return "Connection failed";OpenAI APIのサーバーへ接続できなかった場合は、「Connection failed」を呼び出し元へ返します。
void openAI_chat_reset() {会話履歴を最初の状態へ戻す処理を、openAI_chat_reset関数にまとめます。このサンプルでは呼び出していませんが、別の会話を最初から始めるときに使用できます。
historical_messages = system_content;会話履歴をsystemメッセージだけの状態へ戻し、それまでの質問と回答を削除します。
