Google Apps Script AI組み込みプログラミング
この記事は、私が所属している学習コミュニティ「ノンプロ研」のアドベントカレンダー1日目への寄稿です。
さて、今年はChatGPTのようなAIの活用についての話題が多くありました。しかし、これらの話題の多くは「チャットでAIを活用する方法」が中心となっています。
もちろんそれも画期的で素晴らしいのですが、個人的にはAIをチャットだけで活用するのは少しもったいないと思っています。
これまでこのnoteでも発信してきた「AIをプログラミングに組み込んで利用する」というアプローチがAI活用のマイブームで、本記事では、このアプローチについて整理してみたいと思っています。
※なお、この記事で扱うAIは、特に記載がない場合、ChatGPTなどの大規模言語モデル(LLM)を指します。
AIをチャットだけで使うのは勿体ない
「AIをプログラミングで活用する」と聞くと、多くの場合、「AIにプログラムコードを書かせる」ことがイメージされますよね。
ですが、今回で紹介する「AI組み込みプログラミング」は、それとはちょっと趣旨が違います。
AI組み込みプログラミングは、AIを単なるチャット相手として使うのではなく、AIをプログラムの処理プロセスに組み込んでその入出力を利用するという方法です。
たとえば、チャットインターフェイスを通じてAIを活用する場合、ユーザーである人間が毎回インプットを行う必要があります。しかし、AIをプログラム内に組み込んでしまえば、スケジュールを設定してAIに定期的な処理を任せるなど、人が入力をする手間を省くことができます。これ、かなり便利なんです。
プログラミングにAIを組み込む際のポイント
では、具体的にどのようにAIをプログラミングに組み込めば良いのでしょうか?
これを考えるうえで、まず押さえておきたいのが、活用するLLM(大規模言語モデル)の特徴です。
LLMは、ユーザーが入力したプロンプトに対して、論理的な理由付けを行って答えを返している訳ではありません。むしろ、与えられた文脈(コンテクスト)において確率的に最も適した単語やフレーズを予測し、それをテキストとして生成しています。
このような特徴を持つAI(LLM)が得意とする処理を整理すると次の3つにまとめられます。
大から小へ
コンテクストからデータへ
データからコンテクストへ
大から小へ
「大から小へ」とは、複雑な情報の中から必要な要素を抽出したり、広い文脈の中から特定のデータを引き出したりする作業です。
例えば、web上のニュース記事から要点を抜き出し、短い要約文を生成するなどですね。
そしてこれは、人間が理解できる自然言語に限りません。
私は、Googleアラートを使って登録したキーワードのニュースを毎日RSSで取得しています。しかし、すべてのニュース記事を読む時間がないので、記事のページのHTMLをGoogle Apps Script(以降GAS)で取得し、その内容をAIに解析させて要約を出力させています。
HTMLはタグなどを含む非常に大きなテキストです。それを人間が読み解くのは大変ですが、AIにまるごとテキストを送信し、小さく要約してもらっています。
コンテクストからデータへ
続いては、文脈(コンテクスト)からデータへの変換についてです。
プログラミングの弱みというか特徴は、厳密なロジックに基づいて処理が行われることです。そのため、入力されるデータの形式が想定されたものと違う場合、エラーになってしまいます。
ところが、AIは抽象的なデータから文脈を理解して、そこから出力をすることができます。この特徴を利用して、抽象的なコンテクストからプログラミングが扱うことができる具体的なデータへの変換をさせるという活用法があります。
抽象的なコンテクストの代表は、やはり画像でしょうか。
人間は、画像から容易に必要な情報を読み取ることができますが、通常プログラミングでは、画像からテキストを抽出することは簡単ではありません。
しかし、最近のAIはマルチモーダルと言って異なる種類のデータソース、例えば画像、音声、テキストなどを統合して理解し、処理する能力を持ちます。
このマルチモーダルAIを活用して、画像データから必要なテキストを抽出し、それをプログラミングで処理しやすい構造化データに変換することができます。
以下は、ある日の睡眠記録画像からデータを抽出してスプレッドシートに転記するGASの実装例です。
事前にOpenAIのAPIキーの取得が必要です。
// OpenAI APIキーを設定
const OPENAI_API_KEY = 'YOUR_OPENAI_API_KEY';
// 画像ファイルのIDを設定(適宜置き換えてください)
const IMG_FILE_ID = 'YOUR_IMAGE_FILE_ID';
/**
* 画像から睡眠データを解析する関数
*
* @return {void}
*/
function analyzeSleepDataImage() {
const imgFileId = IMG_FILE_ID;
// 画像ファイルかどうかチェック
if (!isImageFile(imgFileId)) {
console.log('これは画像ファイルじゃないよ');
return;
}
// 画像ファイルのMIMEタイプを取得
const mimeType = getFileMimeType(imgFileId);
// ファイルをBase64エンコードしたBlobを取得
const base64EncodedStr = getBase64EncodedStr(imgFileId);
// 画像ファイルのデータURLを作成
const imgUrl = `data:${mimeType};base64,${base64EncodedStr}`;
// AIモデルを用いて画像からデータを抽出
const model = 'gpt-4o-mini';
const prompt = `
画像は睡眠の記録データです。この画像から以下の形式のJSON文字列を出力してください。
{
"記録日": "yyyy/MM/dd",
"睡眠時間": "hh:mm:ss",
"覚醒": "hh:mm:ss",
"レム": "hh:mm:ss",
"コア": "hh:mm:ss",
"深い": "hh:mm:ss"
}
`;
const answer = imageInput(OPENAI_API_KEY, imgUrl, prompt, model);
const objAnswer = JSON.parse(answer);
// スプレッドシートに記録を追加する
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName('睡眠記録');
const aryValues = Object.values(objAnswer);
sheet.appendRow(aryValues);
}
/**
* ファイルIDを受け取り、ファイルが画像かどうかを判定する関数
*
* @param {string} fileId - ファイルのID
* @return {boolean} ファイルが画像であればtrue、そうでなければfalse
*/
function isImageFile(fileId) {
const file = DriveApp.getFileById(fileId);
const blob = file.getBlob();
const mimeType = blob.getContentType();
const imageMimeTypes = ['image/png', 'image/jpeg', 'image/gif'];
return imageMimeTypes.includes(mimeType);
}
/**
* ファイルIDを受け取り、ファイルのMIMEタイプを返す関数
*
* @param {string} fileId - ファイルのID
* @return {string} ファイルのMIMEタイプ
*/
function getFileMimeType(fileId) {
const file = DriveApp.getFileById(fileId);
const blob = file.getBlob();
return blob.getContentType();
}
/**
* ファイルIDを受け取り、Base64エンコードした文字列を返す関数
*
* @param {string} fileId - ファイルのID
* @return {string} Base64エンコードされた文字列
*/
function getBase64EncodedStr(fileId) {
const file = DriveApp.getFileById(fileId);
const blob = file.getBlob();
return Utilities.base64Encode(blob.getBytes());
}
/**
* OpenAIのChat APIを使用して画像を解析する関数
*
* @param {string} apiKey - OpenAIのAPIキー
* @param {string} imgUrl - 画像のデータURL
* @param {string} prompt - ユーザーからのプロンプト
* @param {string} [model='gpt-4o-mini'] - 使用するモデル(デフォルトは 'gpt-4o-mini')
* @return {string} Chat APIからの応答
*/
function imageInput(apiKey, imgUrl, prompt, model = 'gpt-4o-mini') {
const urlChat = 'https://api.openai.com/v1/chat/completions';
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{ type: 'image_url', image_url: { url: imgUrl } }
]
}
];
const payload = {
model: model,
messages: messages
};
const params = {
contentType: 'application/json',
headers: { Authorization: `Bearer ${apiKey}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(urlChat, params).getContentText();
const objChat = JSON.parse(response);
if (!objChat.choices || objChat.choices.length === 0) {
return 'ノーコメントです。';
}
return objChat.choices[0].message.content;
}
データからコンテクストへ
最後に「大から小へ」とも関連しますが、既に手元にある構造化されたデータから、抽象的なコンテクストを出力させるのもAIの得意分野です。
例えば、スプレッドシートに以下のような成績表があり、この成績表を分析したい場合などです。
GASの実装例です。今回はGeminiを使ってみました。こちらも事前にAPIキーの取得が必要です。
/**
* スプレッドシートの成績表シートにある全てのレコードを取得して、AIに分析させる関数
* @return {void}
*/
function analyzeScores() {
// ユーザーにプロンプトを入力してもらう
const userInput = Browser.inputBox('プロンプト入力', 'AIに分析させたい内容を入力してください:', Browser.Buttons.OK_CANCEL);
// キャンセルが押された場合は処理を終了
if (userInput === 'cancel') { return };
// スプレッドシートを取得
const ss = SpreadsheetApp.getActiveSpreadsheet();
// 成績表シートを取得
const sheetScores = ss.getSheetByName('成績表');
if (!sheetScores) {
SpreadsheetApp.getUi().alert('「成績表」という名前のシートが見つからないよ!');
return;
}
// シートの全データを取得
const rangeData = sheetScores.getDataRange();
const aryValues = rangeData.getValues();
// 配列をオブジェクト形式に変換
const [keys, ...values] = aryValues;
const aryObjRecord = values.map(row => {
const obj = {};
keys.map((key, index) => obj[key] = row[index]);
return obj;
});
const scores = JSON.stringify(aryObjRecord);
// GeminiAPIにJSON文字列を解析させる
const apiKey = 'GEMINI_API_KEY'; // ここを自分のAPIキーに置き換えてね
const model = 'gemini-1.5-flash';
const urlChat = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
const systemRole = 'あなたは「だよ・だね」と親しみやすい言葉で話すAIアシスタントです。';
const prompt = `${userInput}
以下の成績表を参考に詳しく分析してください。
#成績表:
${scores};
`;
// APIリクエストのボディを作成
const objPayload = {
contents: [
{
parts: [
{ text: prompt }
],
role: 'user'
}
],
systemInstruction: {
parts: [
{ text: systemRole }
],
role: 'model'
}
};
// JSON形式でレスポンスが必要な場合、以下の処理を追加
if (systemRole.includes('JSON') && prompt.includes('JSON')) {
objPayload.generationConfig = { response_mime_type: 'application/json' };
}
// リクエストヘッダーとパラメータを設定
const objHeaders = {
'Content-Type': 'application/json'
};
const objParams = {
method: 'post',
headers: objHeaders,
payload: JSON.stringify(objPayload)
};
try {
// APIにリクエストを送信
const response = UrlFetchApp.fetch(urlChat, objParams);
const objChat = JSON.parse(response.getContentText());
// 回答を取得
const answer = objChat.candidates.length > 0
? objChat.candidates[0].content.parts[0].text
: '分析結果が見つからないみたいだよ。';
// 結果をUIに表示
showResultDialog_(answer);
} catch (error) {
// エラーハンドリング
SpreadsheetApp.getUi().alert(`エラーが発生したよ: ${error.message}`);
}
}
/**
* ダイアログで結果を表示する関数
*
* @param {string} result - 表示する結果
* @return {void}
*/
function showResultDialog_(result) {
const htmlOutput = HtmlService.createHtmlOutput(`<p>${result}</p>`)
.setWidth(800)
.setHeight(400);
SpreadsheetApp.getUi().showModalDialog(htmlOutput, '分析結果');
}
AI組み込みプログラミングは面白い
いかがだったでしょうか。
AIをチャットだけで使っているのはもったいないと、少しでも思っていただけるとうれしいです。
AI組み込みプログラミングの可能性はまだまだ多くあると思っており、さまざまな人がどのようなアイデアで組み込んでいくのか、とても興味があります。ぜひ皆さんも試していただき、さまざまなアイデアや知見をシェアしてもらえるとうれしいです。
2025年も、しばらくプログラミングにどうAIを組み込んでいくかということを研究し続けていきたいと思っています。
このテーマで技術同人誌書けたらいいなーと思いつつ、目の前に積み上がった日々のタスクに忙殺されています笑
プログラミングを学ぶならノンプロ研
最後に、全くプログラミングができなかった私が、ここまでできるようになったのは、学習コミュニティ「ノンプロ研」に参加したおかげです。
AIがプログラミングのコードを出力してくれる現在であっても、プログラミングの基礎を学習するメリットはたくさんあります。
この点に関しては、以下の記事でもう少し詳しく書きました。
プログラミングを学んでみたいなぁと思った方は、ぜひノンプロ研に参加してみてください。
参加される際は、freeeloverのnoteを見て!とアピールしてもらえると、私が紹介者としてアマゾンギフト券がもらえるので、私もハッピーです(笑)。
ぜひ一緒に楽しく学びましょう!