コンテンツにスキップ

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)で保存します。

[openai]
api_key = sk-????????????????????????????????????????

環境変数に設定するのも 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();
	}
}

6.9 サンプルアプリ