6. ChatGPT API の利用¶
6.1 準備 1 | OpenAI API キーの発行¶
OpenAI アカウントにサインインし、支払い手段の登録を済ませた状態で https://platform.openai.com/account/api-keys の「Create new secret key」から sk-
で始まる OpenAI API キーを取得します(一度しか表示されません)。
API の費用が高額になることが心配な場合は、Usage limits を設定できます。デフォルトでは毎月 120 ドルです。
6.2 準備 2 | OpenAI API キーの管理¶
API キーは他者に知られてはいけません。コード中に API キーを直接埋め込むと、そのコードをシェアできなくなるため、 API キーをコードの外部に保存することにします。最も簡単な方法として、テキストファイルに保存する方法を説明します。
現在のプロジェクトのアプリフォルダ(engine や example があるフォルダ)に apikey.txt
というファイルを作成し、取得した API キーを次のような形式(INI)で保存します。
環境変数に設定するのも OK
上記以外の方法として、環境変数に API キーを保存し、String key = EnvironmentVariable::Get(U"MY_OPENAI_API_KEY");
で取得するのもよいでしょう。
6.3 OpenAI API キーの読み込み¶
apikey.txt
に保存した API キーを読み込むプログラムを作成します
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
void Main()
{
// 読み込めているかを確認する
Print << LoadAPIKey();
while (System::Update())
{
}
}
6.4 ChatGPT と会話する¶
- 次のような関数を作成することで、ChatGPT にメッセージを送り、その回答を取得できます
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
/// @brief ChatGPT にメッセージを送り、その返答を取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
String Chat(const String input, const String SECRET_API_KEY)
{
// SECRET_API_KEY が空の文字列である場合は失敗
if (not SECRET_API_KEY)
{
Print << U"API key is empty.";
return{};
}
// ChatGPT に送るリクエストの構築
JSON chat;
chat[U"model"] = U"gpt-3.5-turbo";
chat[U"messages"].push_back({ { U"role", U"user" }, { U"content", input } });
const std::string data = chat.formatUTF8();
const HashTable<String, String> headers =
{
{ U"Content-Type", U"application/json" },
{ U"Authorization", (U"Bearer " + SECRET_API_KEY) },
};
// ChatGPT からの返答を保存するファイル
const FilePath SavePath = U"result.json";
// ChatGPT にリクエストを送信
if (const auto response = SimpleHTTP::Post(U"https://api.openai.com/v1/chat/completions", headers, data.data(), data.size(), SavePath))
{
// レスポンスのステータスコードが [200 OK] でない場合
if (not response.isOK())
{
Print << U"status code: {}"_fmt(FromEnum(response.getStatusCode()));
// 401 は無効な API キーが原因
if (response.getStatusCode() == HTTPStatusCode::Unauthorized)
{
Print << U"無効な API キーです。";
}
return{};
}
// レスポンスの JSON から返答部分を抜き出して返す
const JSON result = JSON::Load(SavePath);
return result[U"choices"][0][U"message"][U"content"].getString().trimmed();
}
else
{
Print << U"FAILED";
return{};
}
}
void Main()
{
// API キー
const String SECRET_API_KEY = LoadAPIKey();
// 質問文
const String input = U"日本で一番高い山は何ですか?";
// 回答文
const String output = Chat(input, SECRET_API_KEY);
if (output)
{
Print << U"ChatGPT の回答: " << output;
}
while (System::Update())
{
}
}
6.5 テキストボックスから質問する¶
- 質問文をテキストボックスから編集する場合は次のようにします
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
/// @brief ChatGPT にメッセージを送り、その返答を取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
String Chat(const String input, const String SECRET_API_KEY)
{
// SECRET_API_KEY が空の文字列である場合は失敗
if (not SECRET_API_KEY)
{
Print << U"API key is empty.";
return{};
}
// ChatGPT に送るリクエストの構築
JSON chat;
chat[U"model"] = U"gpt-3.5-turbo";
chat[U"messages"].push_back({ { U"role", U"user" }, { U"content", input } });
const std::string data = chat.formatUTF8();
const HashTable<String, String> headers =
{
{ U"Content-Type", U"application/json" },
{ U"Authorization", (U"Bearer " + SECRET_API_KEY) },
};
// ChatGPT からの返答を保存するファイル
const FilePath SavePath = U"result.json";
// ChatGPT にリクエストを送信
if (const auto response = SimpleHTTP::Post(U"https://api.openai.com/v1/chat/completions", headers, data.data(), data.size(), SavePath))
{
// レスポンスのステータスコードが [200 OK] でない場合
if (not response.isOK())
{
Print << U"status code: {}"_fmt(FromEnum(response.getStatusCode()));
// 401 は無効な API キーが原因
if (response.getStatusCode() == HTTPStatusCode::Unauthorized)
{
Print << U"無効な API キーです。";
}
return{};
}
// レスポンスの JSON から返答部分を抜き出して返す
const JSON result = JSON::Load(SavePath);
return result[U"choices"][0][U"message"][U"content"].getString().trimmed();
}
else
{
Print << U"FAILED";
return{};
}
}
void Main()
{
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// API キー
const String SECRET_API_KEY = LoadAPIKey();
const Font font{ FontMethod::MSDF, 48, Typeface::Medium };
// テキストボックスの中身
TextEditState textEditState;
// 回答文
String output;
while (System::Update())
{
SimpleGUI::TextBox(textEditState, Vec2{ 40, 40 }, 600);
if (SimpleGUI::Button(U"送信", Vec2{ 660, 40 }, 80, (not textEditState.text.isEmpty())))
{
// 質問文
const String input = textEditState.text;
// 回答文
output = Chat(input, SECRET_API_KEY);
}
if (output)
{
font(output).draw(20, Rect{ 40, 100, 720, 600 }, ColorF{ 0.25 });
}
}
}
6.6 非同期処理を行う¶
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
/// @brief ChatGPT にメッセージを送り、その返答を取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
String Chat(const String input, const String SECRET_API_KEY)
{
// SECRET_API_KEY が空の文字列である場合は失敗
if (not SECRET_API_KEY)
{
Print << U"API key is empty.";
return{};
}
// ChatGPT に送るリクエストの構築
JSON chat;
chat[U"model"] = U"gpt-3.5-turbo";
chat[U"messages"].push_back({ { U"role", U"user" }, { U"content", input } });
const std::string data = chat.formatUTF8();
const HashTable<String, String> headers =
{
{ U"Content-Type", U"application/json" },
{ U"Authorization", (U"Bearer " + SECRET_API_KEY) },
};
// ChatGPT からの返答を保存するファイル
const FilePath SavePath = U"result.json";
// ChatGPT にリクエストを送信
if (const auto response = SimpleHTTP::Post(U"https://api.openai.com/v1/chat/completions", headers, data.data(), data.size(), SavePath))
{
// レスポンスのステータスコードが [200 OK] でない場合
if (not response.isOK())
{
Print << U"status code: {}"_fmt(FromEnum(response.getStatusCode()));
// 401 は無効な API キーが原因
if (response.getStatusCode() == HTTPStatusCode::Unauthorized)
{
Print << U"無効な API キーです。";
}
return{};
}
// レスポンスの JSON から返答部分を抜き出して返す
const JSON result = JSON::Load(SavePath);
return result[U"choices"][0][U"message"][U"content"].getString().trimmed();
}
else
{
Print << U"FAILED";
return{};
}
}
/// @brief ChatGPT にメッセージを送り、その返答を非同期で取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
AsyncTask<String> ChatAsync(const String input, const String SECRET_API_KEY)
{
return Async(Chat, input, SECRET_API_KEY);
}
void Main()
{
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// API キー
const String SECRET_API_KEY = LoadAPIKey();
const Font font{ FontMethod::MSDF, 48, Typeface::Medium };
// テキストボックスの中身
TextEditState textEditState;
// 非同期タスク
AsyncTask<String> task;
// 回答文
String output;
while (System::Update())
{
SimpleGUI::TextBox(textEditState, Vec2{ 40, 40 }, 600);
if (SimpleGUI::Button(U"送信", Vec2{ 660, 40 }, 80,
((not textEditState.text.isEmpty()) && (not task.isValid()))))
{
output.clear();
// 質問文
const String input = textEditState.text;
task = ChatAsync(input, SECRET_API_KEY);
}
// ChatGPT の応答を待つ間はローディング画面を表示する
if (task.isValid())
{
Circle{ Scene::Center(), 50 }.drawArc(Scene::Time() * 120_deg, 300_deg, 4, 4);
}
// 非同期処理が完了した場合
if (task.isReady())
{
// 非同期処理の結果を取得する
output = task.get();
}
if (output)
{
font(output).draw(20, Rect{ 40, 100, 720, 600 }, ColorF{ 0.25 });
}
}
// 非同期処理を終了してから終了する
if (task.isValid())
{
task.wait();
}
}
6.7 質問や返答を扱いやすくする¶
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
/// @brief ChatGPT にメッセージを送り、その返答を取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
String Chat(const String input, const String SECRET_API_KEY)
{
// SECRET_API_KEY が空の文字列である場合は失敗
if (not SECRET_API_KEY)
{
Print << U"API key is empty.";
return{};
}
// ChatGPT に送るリクエストの構築
JSON chat;
chat[U"model"] = U"gpt-3.5-turbo";
chat[U"messages"].push_back({ { U"role", U"user" }, { U"content", input } });
const std::string data = chat.formatUTF8();
const HashTable<String, String> headers =
{
{ U"Content-Type", U"application/json" },
{ U"Authorization", (U"Bearer " + SECRET_API_KEY) },
};
// ChatGPT からの返答を保存するファイル
const FilePath SavePath = U"result.json";
// ChatGPT にリクエストを送信
if (const auto response = SimpleHTTP::Post(U"https://api.openai.com/v1/chat/completions", headers, data.data(), data.size(), SavePath))
{
// レスポンスのステータスコードが [200 OK] でない場合
if (not response.isOK())
{
Print << U"status code: {}"_fmt(FromEnum(response.getStatusCode()));
// 401 は無効な API キーが原因
if (response.getStatusCode() == HTTPStatusCode::Unauthorized)
{
Print << U"無効な API キーです。";
}
return{};
}
// レスポンスの JSON から返答部分を抜き出して返す
const JSON result = JSON::Load(SavePath);
return result[U"choices"][0][U"message"][U"content"].getString().trimmed();
}
else
{
Print << U"FAILED";
return{};
}
}
/// @brief ChatGPT にメッセージを送り、その返答を非同期で取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
AsyncTask<String> ChatAsync(const String input, const String SECRET_API_KEY)
{
return Async(Chat, input, SECRET_API_KEY);
}
void Main()
{
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// API キー
const String SECRET_API_KEY = LoadAPIKey();
const Font font{ FontMethod::MSDF, 48, Typeface::Medium };
// テキストボックスの中身
TextEditState textEditState;
// 非同期タスク
AsyncTask<String> task;
// 回答文
String output;
while (System::Update())
{
SimpleGUI::TextBox(textEditState, Vec2{ 40, 40 }, 240);
if (SimpleGUI::Button(U"に登場する敵モンスターを生成", Vec2{ 300, 40 }, 360,
((not textEditState.text.isEmpty()) && (not task.isValid()))))
{
output.clear();
// 質問文
String input = U"RPG ゲームで" + textEditState.text + U"に登場する敵モンスターを 1 種類考えてください。\n";
input += U"出力は次のような JSON 形式で、日本語で出力してください。回答に JSON データ以外を含まないで下さい。\n";
input += UR"({ "name": "敵の名前", "desc" : "説明" })";
task = ChatAsync(input, SECRET_API_KEY);
}
// ChatGPT の応答を待つ間はローディング画面を表示する
if (task.isValid())
{
Circle{ Scene::Center(), 50 }.drawArc(Scene::Time() * 120_deg, 300_deg, 4, 4);
}
// 非同期処理が完了した場合
if (task.isReady())
{
// 非同期処理の結果を取得する
output = task.get();
}
if (output)
{
font(output).draw(20, Rect{ 40, 100, 720, 600 }, ColorF{ 0.25 });
}
}
// 非同期処理を終了してから終了する
if (task.isValid())
{
task.wait();
}
}
6.8 回答の JSON をパースする¶
# include <Siv3D.hpp>
/// @brief API キーを INI 形式のファイルから読み込みます。
/// @return 読み込んだ API キー。失敗した場合は空の文字列
String LoadAPIKey()
{
INI ini{ U"apikey.txt" };
if (not ini)
{
Print << U"apikey.txt が存在しません。";
return{};
}
Optional<String> value = ini.getOpt<String>(U"openai", U"api_key");
if (not value)
{
Print << U"[openai] api_key が存在しません。";
return{};
}
return *value;
}
/// @brief ChatGPT にメッセージを送り、その返答を取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
String Chat(const String input, const String SECRET_API_KEY)
{
// SECRET_API_KEY が空の文字列である場合は失敗
if (not SECRET_API_KEY)
{
Print << U"API key is empty.";
return{};
}
// ChatGPT に送るリクエストの構築
JSON chat;
chat[U"model"] = U"gpt-3.5-turbo";
chat[U"messages"].push_back({ { U"role", U"user" }, { U"content", input } });
const std::string data = chat.formatUTF8();
const HashTable<String, String> headers =
{
{ U"Content-Type", U"application/json" },
{ U"Authorization", (U"Bearer " + SECRET_API_KEY) },
};
// ChatGPT からの返答を保存するファイル
const FilePath SavePath = U"result.json";
// ChatGPT にリクエストを送信
if (const auto response = SimpleHTTP::Post(U"https://api.openai.com/v1/chat/completions", headers, data.data(), data.size(), SavePath))
{
// レスポンスのステータスコードが [200 OK] でない場合
if (not response.isOK())
{
Print << U"status code: {}"_fmt(FromEnum(response.getStatusCode()));
// 401 は無効な API キーが原因
if (response.getStatusCode() == HTTPStatusCode::Unauthorized)
{
Print << U"無効な API キーです。";
}
return{};
}
// レスポンスの JSON から返答部分を抜き出して返す
const JSON result = JSON::Load(SavePath);
return result[U"choices"][0][U"message"][U"content"].getString().trimmed();
}
else
{
Print << U"FAILED";
return{};
}
}
/// @brief ChatGPT にメッセージを送り、その返答を非同期で取得します。
/// @param input ChatGPT へのメッセージ
/// @param SECRET_API_KEY OpenAI API キー
/// @return メッセージへの返答。失敗した場合は空の文字列
AsyncTask<String> ChatAsync(const String input, const String SECRET_API_KEY)
{
return Async(Chat, input, SECRET_API_KEY);
}
/// @brief モンスターの情報
struct Monster
{
/// @brief 名前
String name;
/// @brief 説明
String desc;
};
void Main()
{
Scene::SetBackground(ColorF{ 0.8, 0.9, 1.0 });
// API キー
const String SECRET_API_KEY = LoadAPIKey();
const Font font{ FontMethod::MSDF, 48, Typeface::Medium };
// テキストボックスの中身
TextEditState textEditState;
// 非同期タスク
AsyncTask<String> task;
// モンスターの情報
Optional<Monster> monster;
while (System::Update())
{
SimpleGUI::TextBox(textEditState, Vec2{ 40, 40 }, 240);
if (SimpleGUI::Button(U"に登場する敵モンスターを生成", Vec2{ 300, 40 }, 360,
((not textEditState.text.isEmpty()) && (not task.isValid()))))
{
monster.reset();
// 質問文
String input = U"RPG ゲームで" + textEditState.text + U"に登場する敵モンスターを 1 種類考えてください。\n";
input += U"出力は次のような JSON 形式で、日本語で出力してください。回答に JSON データ以外を含まないで下さい。\n";
input += UR"({ "name": "敵の名前", "desc" : "説明" })";
task = ChatAsync(input, SECRET_API_KEY);
}
// ChatGPT の応答を待つ間はローディング画面を表示する
if (task.isValid())
{
Circle{ Scene::Center(), 50 }.drawArc(Scene::Time() * 120_deg, 300_deg, 4, 4);
}
// 非同期処理が完了した場合
if (task.isReady())
{
// 非同期処理の結果を取得する
if (const String output = task.get())
{
// ChatGPT の返答メッセージに含まれる JSON をパースする
if (const JSON json = JSON::Parse(output))
{
// 念のため、指定したフォーマットになっているかを確認する
if ((json.hasElement(U"name") && json[U"name"].isString())
&& (json.hasElement(U"desc") && json[U"desc"].isString()))
{
// モンスターの情報を JSON から取得する
monster = Monster{
.name = json[U"name"].getString(),
.desc = json[U"desc"].getString(),
};
}
}
}
}
// モンスターの情報がある場合
if (monster)
{
const String text = U"名前: {}\n説明: {}"_fmt(monster->name, monster->desc);
font(text).draw(20, Rect{ 40, 100, 720, 600 }, ColorF{ 0.25 });
}
}
// 非同期処理を終了してから終了する
if (task.isValid())
{
task.wait();
}
}