VOOZH about

URL: https://note.com/eurekachan/n/nb54e7122a351

⇱ アイドルグループのX投稿をAIに渡して、活動予定を自動的にGoogleカレンダーに入れる #LLMで推し活 【LLM・LLM活用 Advent Calendar 2024】|松note


👁 見出し画像

アイドルグループのX投稿をAIに渡して、活動予定を自動的にGoogleカレンダーに入れる #LLMで推し活 【LLM・LLM活用 Advent Calendar 2024】

👁 松note

この記事は LLM・LLM活用 Advent Calendar 2024 の8日目です。

友人がアイドルを熱心に推しています。二次元専門オタクである僕も、何度かライブに連れて行ってもらいました。直接拝見するととても魅力的なので、近所でイベントがあるならば行ってみようと思いました。

ところが、いわゆる地下とか駆け出しとか呼ばれるタイプのアイドルグループの場合、公式Webサイトはあまり機能してなかったりして、Xのアカウントが公式情報発信の役割を果たしていることが多いようです。また、駆け出しアイドルの方が出演するライブイベントも、やっぱり、開催元によるサイトはなく、Xアカウントでタイムテーブルの画像がドンと貼られているだけ、というのが多かったです。熱心なファンならこれでいいのでしょうが、僕みたいにちょっと興味を持ち始めたくらいの人間には正直ハードルが高い印象がありました。

これはLLMで効率化できる匂いがする。

というわけで、やってみたのが今回の記事です。

ちなみに、最後に、Xポストを取り込むときの罠に気づいて撃沈するのですが、ここはLLM活用とは若干異なる課題なので一旦はスルーしてくださるとありがたいです。

TL;DR

図にすると以下の様な感じです。

👁 Image

中継サーバーはVercelで立てました。理由は単にVercelを使ってみたかったからです。

XをIFTTTで受け取る

Xの投稿をスクリプトで取得する場合、Xの公式APIを使うならば、最低でも月200ドルかかります。日本円で約3万円! それが毎月! 高い!

👁 Image
https://developer.x.com/en/portal/products/basic より 
Freeプランでできるのは投稿のみで、取得はできません

そこで、IFTTTを使うことにしました。IFTTTでは、月に3.49ドル(約530円)のProプランで、限定的ながら特定のアカウントによるXポストのテキストを取得できます。(実はここに罠があったので後述)

👁 Image
https://ifttt.com/plans

IFTTTで、トリガーとなるIfの部分で「New tweet by a specific user(指定したユーザーの新規投稿)」を選択し、ウォッチしたいXアカウントを指定します。

処理をするThenの部分ではWebhookを指定し、自作の中継サーバーへ、XのPost内容を渡します。

👁 Image

Webhook(Make a web request)の内容はこんな感じです。

👁 Image

これで、推しアイドルがXを更新する度に、その内容を取得することが出来ました。

自作の中継サーバーで、Xポストの内容をClaudeでGoogleカレンダーに渡せる形式に変換する

Xへのポストは、あくまでもアイドル本人か運営の方が書いた自然言語のテキストです。ここから、活動予定を抽出します。

シンプルには、以下の様なプロンプトでやってみます。

以下の文章は、アイドルグループ○○○○の公式Xに投稿された文章です。
====
本文は「${text}」で、${createdAt}に投稿されました。
====
ちなみに現在時刻は${today}です。この文章に、明日以降の活動内容が含まれいていたら、その日時と場所と概要を、以下の形式のJSONで出力してください。タイムゾーンは特別な指定が無い限りは日本標準時です。
====
{
'summary': 'イベントのタイトル',
'location': '場所',
'description': 'イベントの概要',
'start': {
'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻',
},
'end': {
'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻',
},
}
====
日本語で回答してください。また、JSON以外の要素は不要です。

プロンプトの例

${text}、${createdAt}、${today}は、それぞれ、取得する部分の変数です。お使いのプログラム言語で読み替えてください(本記事の最後にTypeScriptのサンプルを載せます)

ただ、LLMはいつも同じようにJSONを返してくれるとは限らないので、Structured OutputのようにJSONの形式を指定して返答させるようにしつつ、ちゃんとバリデーションもするといいと思います。この辺は書くと長くなるので一旦省略。

Googleカレンダーに登録

とりあえずは、以下の様なJSONが無事に得られたと仮定します。最近のLLMは賢いので、実験レベルならこれくらい雑にやってもだいたいいい感じになります。すごい。

{
'summary': 'XXアイドルフェス出演',
'location': '東京ドーム',
'description': 'XXアイドルフェスのYYステージに出演。元投稿は https://x.com/**************/status/************',
'start': {
'dateTime': '2025-01-28T17:00:00+09:00',
},
'end': {
'dateTime': '2025-01-28T18:00:00+09:00',
}

このJSONを、GoogleカレンダーAPIに従って、投稿します。

ここに行くまでがちょっと大変なのですが、公式のドキュメントを読むのが一番わかりやすいので、それでやってみてください(丸投げ)

大雑把には

  • Google Cloudのアカウントを作る

  • Google Cloudのプロジェクトを作成する

  • 作成したプロジェクトで、APIを有効にする

  • そのプロジェクトで、OAuth 同意画面を構成する

  • 認証情報を作成する

  • 認証情報をダウンロードして、credentials.json という名前にして、プロジェクトのフォルダ内に置く

すでにGoogle Cloudを使っている人はほとんどの部分をスキップできると思うのですが、初めての場合、ここが結構面倒です。また、Google Cloud側も不定期に仕様変更があるので、公式ドキュメントを一つ一つ追っていくのが面倒なようで実は一番楽だと思います。

上記のGoogle公式のドキュメントでは、読み書きするカレンダーIDが

calendarId: 'primary',

となっています。これは、そのアカウントで通常使われているカレンダーを読み書きすることになります。

ただ、こういう風に自動でGoogleカレンダーに予定を登録する際には、登録する先のカレンダーを分けておくと便利です。

今回は、oshikatsu という名前のカレンダーをGoogleカレンダー上で作成しました。歯車マークの設定画面から、この作成したカレンダーの設定に飛び、カレンダーIDを探します。

以下のスクリーンショットのように、長めの文字列がカレンダーIDです。

👁 Image

このIDを指定すれば、予定の追加は分離されるので使いやすいと思います。当該部分のサンプルコードはこんな感じです(TypeScript)

/**
 * イベントを追加する関数
 */
async function addEvents(eventJson: ApiResponse) {
 try {
 const auth = await authorize();
 const calendar = google.calendar({ version: 'v3', auth });
 await calendar.events.insert({
 calendarId: '■■■ここにカレンダーIDを入れる■■■',
 requestBody: {
 summary: eventJson.summary,
 location: eventJson.location,
 description: eventJson.description,
 start: {
 dateTime: eventJson.start.dateTime,
 },
 end: {
 dateTime: eventJson.end.dateTime,
 },
 },
 });
 console.log('Event added successfully');
 } catch (error) {
 console.error('Error adding event:', error);
 throw new Error('Failed to add event');
 }
}
// イベント追加を受け付けるエンドポイント
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: VercelRequest, res: VercelResponse) => {
 if (req.method !== 'POST') {
 res.status(405).json({error: 'POSTリクエストのみ受付'});
 return;
 }

 const json = typeof req.body === 'string'
 ? JSON.parse(req.body.split('\n\n')[1])
 : req.body;

 if (!isApiResponse(json)) {
 console.log("Invalid JSON:", json); // デバッグ用
 res.status(400).json({error: 'Invalid JSON format'});
 return;
 }

 try {
 await addEvents(json);
 res.status(200).json({message: 'Event added successfully'});
 } catch (error) {
 res.status(500).json({error: 'Failed to add event'});
 }
}

実際にやってみる

とりあえず、実際にやってみましょう。実験は有名なアイドルでということで、乃木坂46のライバルグループ「僕が見たかった青空」の公式Xから、以下のポストを使わせていただきました。僕が見たかった青空は公式サイトもしっかり整備されていてわざわざこんなことをしなくても活動を追いやすいのですが、「○○というアイドルグループは公式サイトが貧弱だから」と名指しするのもなんですので、有名どころを使わせていただきます。

\僕青YouTube更新📢💙/

「初めて好きになった人」LIVE映像を公開🎥https://t.co/SOK48xUOmv

12月1日(日)に東京で開催した
「好きすぎてUp and down」発売記念
全国フリーライブから映像を公開!

雲組メンバー☁️12人の
パフォーマンスをお楽しみください🕺

明日は名古屋でフリーライブです😉🎤 pic.twitter.com/2VZbNlvdDG

— 僕が見たかった青空 (@BOKUAOofficial) December 7, 2024

このテキストを、IFTTTで抽出すると、以下の様になります。

{
 text: '\僕青YouTube更新📢💙/\n\n「初めて好きになった人」LIVE映像を公開🎥\nhttps://t.co/SOK48xUOmv\n\n12月1日(日)に東京で開催した\n「好きすぎてUp and down」発売記念\n全国フリーライブから映像を公開!\n\n雲組メンバー☁️12人の\nパフォーマンスをお楽しみください🕺\n\n明日は名古屋でフリーライブです😉🎤 https://t.co/2VZbNlvdDG',
 createdAt: 'December 07, 2024 at 08:01PM',
 linkUrl: ' https://twitter.com/BOKUAOofficial/status/1865350977068675167'
}

このテキスト情報から、以下の様にプロンプトにしました。ここは改良の余地がありそう。

"以下はアイドルグループ「僕が見たかった青空」公式Xに投稿された文章です。本文は「 \僕青YouTube更新📢💙/\n\n「初めて好きになった人」LIVE映像を公開🎥\nhttps://t.co/SOK48xUOmv\n\n12月1日(日)に東京で開催した\n「好きすぎてUp and down」発売記念\n全国フリーライブから映像を公開!\n\n雲組メンバー☁️12人の\nパフォーマンスをお楽しみください🕺\n\n明日は名古屋でフリーライブです😉🎤 https://t.co/2VZbNlvdDG」で、December 07, 2024 at 08:01PMに投稿されました。ちなみに現在時刻は2024/12/7 20:02:48です。リンクは https://twitter.com/BOKUAOofficial/status/1865350977068675167です。\n ここから、「僕が見たかった青空」のイベント・番組・記事などへの出演情報を抽出してください。誰が、いつ、どこで、などをJSONで出力してください。\n 形式は以下の通りです。timezoneは明示されない限り Asia/Tokyo です。\n {'summary': 'イベントのタイトル','location': '場所','description': 'イベントの概要と参照元のXのURL','start': { 'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻',},'end': {'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻', },}\n 日本語で回答してください。また、JSON以外の要素は不要です。"

Claude APIに渡したプロンプト

このプロンプトをClaude APIに渡し、その返事をGoogleカレンダーAPIに渡します。

 // 作成したプロンプトをClaude APIに送信する
 const msg = await anthropic.messages.create({
 model: "claude-3-5-sonnet-20241022",
 max_tokens: 2048,
 messages: [{ role: "user", content: prompt }],
 });

 // Claudeからのレスポンスが正常だったら内容を取得する
 if (msg.type === 'message') {
 // レスポンスの内容を取得
 let responseByClaude: string;
 // @ts-ignore
 responseByClaude = msg.content[0].text;

 // エスケープ文字の処理を行う
 const cleanedResponse = responseByClaude.replace(/\\"/g, '"').replace(/\\n/g, '');

 // 2. JSONオブジェクトに変換
 try {
 const jsonResponse = JSON.parse(cleanedResponse);

 // 同じサーバーにある別のAPIエンドポイント addCalendar にPOSTリクエストを送信
 const responseByCalendar = await axios.post('http://■■■デプロイしたVercelのアドレス■■■/api/addCalendar', jsonResponse);
 console.log(responseByCalendar.data);

 // DiscordのWebhookにカレンダー書き込みの結果を通知
 const content = '```json\n' + JSON.stringify(responseByCalendar.data, null, 2) + '\n```';
 const [responseByDiscord] = await Promise.all([fetch(DISCORD_WEBHOOK_URL, {
 method: 'POST',
 headers: {'Content-Type': 'application/json'},
 body: JSON.stringify({content})
 })]);
 } catch (error) {
 console.error("JSONのパースに失敗しました:", error);
 // Discordにパースエラーだったことを送信
 const content = '```json\n' + JSON.stringify({ error: 'JSONのパースに失敗しました。' + text }, null, 2) + '\n```';
 }

Claudeからは以下の様に返ってきていました。

"{\n \"summary\": \"好きすぎてUp and down発売記念全国フリーライブ\",\n \"location\": \"名古屋\",\n \"description\": \"フリーライブの開催\\nhttps://twitter.com/BOKUAOofficial/status/1865350977068675167\",\n \"start\": {\n \"dateTime\": \"2024-12-08T00:00:00+09:00\"\n },\n \"end\": {\n \"dateTime\": \"2024-12-08T23:59:59+09:00\"\n }\n}"

これの改行コードなどを取って整形すると以下の様になります。

{
 "summary": "好きすぎてUp and down発売記念全国フリーライブ",
 "location": "名古屋",
 "description": "フリーライブの開催 https://twitter.com/BOKUAOofficial/status/1865350977068675167",
 "start": {
 "dateTime": "2024-12-08T00:00:00+09:00"
 },
 "end": {
 "dateTime": "2024-12-08T23:59:59+09:00"
 }
}

この返事を /api/addCalendar のエンドポイントに渡しています。その処理は、「Googleカレンダーに登録する」のブロックで書いたような感じです。

これらの処理がうまくいけば、以下の様に登録されているはずです。

👁 Image

元のX投稿には「明日」としか書かれていなかったので、丸一日スケジュールに入ってしまいましたが、12月7日のポストに書かれた「明日」をちゃんと12月8日だと解釈してくれているのでありがたいです。推し活している相手ならば、ここまでの情報があれば後は自分で調べるでしょうから、まずまずの成果と言えそうです。

というわけで、X投稿の内容を自動的にGoogleカレンダーに登録できました! 嬉しい!

IFTTTではX投稿を完全に取得できない

よっしゃ、上手くいった、と思ったのですが。画像が取得できません。アイドルのイベントでは、タイムテーブルが画像で貼られるだけということもあるので、ここは大きな弱点です。

多分、月200ドル以上のXのDeveloper APIが使えるプランに入ったりすれば取得できるのかもしれません(さすがに高額すぎるので試していません)。あるいは、スクレイピングでゴニョゴニョする手もあるとは思うのですが、それはXの利用規約に反するので、アカウントをBANされたりしてしまうかも。

とはいえ、推しのXの投稿からカレンダーへの自動登録ができるようになるかもしれないというのは楽しいです。XのDeveloper APIが使える環境にある人 or 組織にいる場合は、試してみても楽しそうです(結果知りたい!)。

というか、こういうサービスあったら便利だなと思うので、どこかの会社が実際に作ってくれないかしら……(他力本願寺)。

(おまけ)タイムテーブル画像を読み取る

XのDeveloper APIが使えるなら画像の読み取りも自動化できそうですが、一旦、普通にWebインターフェースから聞いてみました。参考として、Tokyo Idol Festival のタイムテーブルを使わせてもらいます。

◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢
#TIF2024 タイムテーブル公開‼️
◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢
アイドルたちが
各日のステージを盛り上げます✨

詳細はコチラ🔽https://t.co/0rf3J7BlRE

各プレイガイドにてチケット一般発売中🎫https://t.co/aiygDQfPfR pic.twitter.com/5exlA3bArO

— TIP&TIF 公式 (@TIP_TIF_staff) July 17, 2024

8月4日のHOT STAGEで、11:50-12:20に出演するAKB48の出演情報をclaude 3.5 sonnetに聞いてみました。

👁 Image
間違ってる!

間違ってました。8月3日ではないですし、ステージもHEAT GARAGEではありません。頑張れClaude。

ただ、画像の読み取りはまさにいま各社がしのぎを削って猛烈にバージョンアップしている最中ですので、多分、来年にはこの辺は実用に耐えうる水準になっていると思います。

ちなみに、画像読み込みで評判がよさげな、最新のGeminiでも試してみたのですが、やっぱり間違っていました。

👁 Image
Gemini Experimental 1206を利用

ちなみにChatGPT o1も間違えていました。

👁 Image
ChatGPT o1を12月7日に利用


AKB48の場合、有名なので学習データとなる情報が世にありすぎて、逆に、それが迷いになっているのかも……?

例にしたタイムテーブルの画像が細かすぎるというのはあるのですが、実際のアイドルイベントはかなり複雑で細かいタイムテーブルが出るので、これくらいは読み込める未来を期待しているところです。

サンプルスクリプト(VercelにデプロイしたTypeScript)

フォルダ構成はこんな感じです。

.
├── pages
│ └── api
│ ├── addCalendar.ts
│ └── toClaude.ts
├── .env
├── credentials.json
└── token.json

credentials.json が、Google Cloudの管理画面からダウンロードした認証情報ファイルです。プロジェクトのルートに置きます。初回の認証時に token.json が生成されます。.envにはClaudeのAPI KEYを入れています。

あとは、package.jsonやtsconfig.jsonなども、自動的に生成されますが、そこは成り行き任せでやりました。

以下がソースです。TypeScriptを使うのもVercelを使うのも初めてなので、めちゃくちゃなのはご容赦ください。

// toClaude.ts
// IFTTTから来たXの投稿内容をClaudeに渡すエンドポイント

import { VercelRequest, VercelResponse } from '@vercel/node';
import axios from 'axios';
import * as dotenv from 'dotenv';
import { Anthropic } from '@anthropic-ai/sdk';

dotenv.config();

let DISCORD_WEBHOOK_URL: string;
// @ts-ignore
DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL;


const anthropic = new Anthropic({
 apiKey: process.env.ANTHROPIC_API_KEY, // .envファイルからAPIキーを取得
});

// JSONを受け取るエンドポイントを作成
export default async (req: VercelRequest, res: VercelResponse) => {
 // POSTメソッド以外は許可しない
 if (req.method !== 'POST') {
 res.status(405).json({ error: 'このエンドポイントはPOSTリクエストのみを受け付けます。' });
 return;
 }

 const data = req.body;
 // 開始時にデータをログ出力
 console.log("受信したbody:", req.body);
 console.log("bodyの型:", typeof req.body);

 // データが存在しない場合
 if (!data) {
 res.status(400).json({ error: 'JSONデータが提供されていません。' });
 return;
 }

 // 受け取ったdataを変換する
 const json = typeof req.body === 'string'
 ? JSON.parse(req.body.split('\n\n')[1])
 : req.body;
 //デバッグのために一旦console.logで受け取ったデータを表示する
 console.log("jsonの内容:", json);
 console.log("json.text:", json.text);

 // JSONを解析し、プロンプトに変換する。

 // 本文はstring型であるため、textとして定義する。
 const text: string = json.text;

 // 作成日時はstring型であるため、createdAtとして定義する。
 const createdAt: string = json.createdAt;

 // リンクURLはstring型であるため、linkUrlとして定義する。
 const linkUrl: string = json.linkUrl;

 // 現在の日時を日本時間でtodayに入れる
 const today = new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" });

 // プロンプトを作成する
 const prompt = `以下はアイドルグループ「僕が見たかった青空」公式Xに投稿された文章です。本文は「${text}」で、${createdAt}に投稿されました。ちなみに現在時刻は${today}です。リンクは${linkUrl}です。
 ここから、「僕が見たかった青空」のイベント・番組・記事などへの出演情報を抽出してください。誰が、いつ、どこで、などをJSONで出力してください。
 形式は以下の通りです。timezoneは明示されない限り Asia/Tokyo です。
 {'summary': 'イベントのタイトル','location': '場所','description': 'イベントの概要','start': { 'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻',},'end': {'dateTime': 'タイムゾーン付きISO 8601形式による開始時刻', },}
 日本語で回答してください。また、JSON以外の要素は不要です。`;

 // 作成したプロンプトをClaude APIに送信する
 const msg = await anthropic.messages.create({
 model: "claude-3-5-sonnet-20241022",
 max_tokens: 2048,
 messages: [{ role: "user", content: prompt }],
 });

 // Claudeからのレスポンスが正常だったら内容を取得する
 if (msg.type === 'message') {
 // レスポンスの内容を取得
 let responseByClaude: string;
 // @ts-ignore
 responseByClaude = msg.content[0].text;

 // Discordに送信するメッセージを作成
 const content = '```json\n' + JSON.stringify(responseByClaude, null, 2) + '\n```';

 // 1. エスケープ文字の処理を行う
 const cleanedResponse = responseByClaude.replace(/\\"/g, '"').replace(/\\n/g, '');

 // 2. JSONオブジェクトに変換
 try {
 const jsonResponse = JSON.parse(cleanedResponse);

 console.log(jsonResponse);
 console.log(jsonResponse.summary); // イベントのタイトル

 // 同じサーバーにある別のAPIエンドポイント addCalendar にPOSTリクエストを送信
 const responseByCalendar = await axios.post('https://■■■デプロイした場所■■■.vercel.app/api/addCalendar', jsonResponse);
 console.log(responseByCalendar.data);

 res.status(200).json({ message: 'Xの読み込みが成功しました。' });
 }
};

// addCalendar.ts
// カレンダーに書き込むエンドポイント
import { VercelRequest, VercelResponse } from '@vercel/node';
import { readFile, writeFile } from 'fs/promises';
import * as dotenv from 'dotenv';
import { google, calendar_v3 } from 'googleapis';
import { authenticate } from "@google-cloud/local-auth";
import path from 'path';
import { OAuth2Client } from 'google-auth-library';


// dotenvを読み込む
dotenv.config();

// 型定義
interface ApiResponse {
 summary: string;
 location: string;
 description: string;
 start: {
 dateTime: string;
 };
 end: {
 dateTime: string;
 };
}

// 受け取ったJSONがApiResponse型に適合しているかチェックする関数
function isApiResponse(json: any): json is ApiResponse {
 console.log("Received JSON:", json);
 if (typeof json !== 'object') {
 console.log("Failed: not object");
 return false;
 }
 if (typeof json.summary !== 'string') {
 console.log("Failed: summary", typeof json.summary);
 return false;
 }
 if (typeof json.location !== 'string') {
 console.log("Failed: location", typeof json.location);
 return false;
 }
 if (typeof json.description !== 'string') {
 console.log("Failed: description", typeof json.description);
 return false;
 }
 if (typeof json.start !== 'object') {
 console.log("Failed: start not object");
 return false;
 }
 if (typeof json.start.dateTime !== 'string') {
 console.log("Failed: start.dateTime not string");
 return false;
 }
 if (typeof json.end !== 'object') {
 console.log("Failed: end not object");
 return false;
 }
 if (typeof json.end.dateTime !== 'string') {
 console.log("Failed: end.dateTime not string");
 return false;
 }
 return true;
}

// If modifying these scopes, delete token.json.
const SCOPES = ['https://www.googleapis.com/auth/calendar']; //カレンダーへの読み込み、書き込み両方をOKするスコープ
const CREDENTIALS_PATH = path.resolve('./credentials.json');
const TOKEN_PATH = path.resolve('./token.json');


/**
 * 保存済みの認証情報を読み込む
 *
 * @return {Promise<OAuth2Client | null>}
 */
async function loadSavedCredentialsIfExist(): Promise<OAuth2Client | null> {
 try {
 const content = await readFile(TOKEN_PATH, { encoding: 'utf8' });
 const credentials = JSON.parse(content);
 return google.auth.fromJSON(credentials) as OAuth2Client;
 } catch (err) {
 return null;
 }
}

/**
 * 認証情報をファイルに保存する
 *
 * @param {OAuth2Client} client - 認証クライアント
 * @return {Promise<void>}
 */
async function saveCredentials(client: OAuth2Client): Promise<void> {
 const content = await readFile(CREDENTIALS_PATH, { encoding: 'utf8' });
 const keys = JSON.parse(content);
 const key = keys.installed || keys.web;
 const payload = JSON.stringify({
 type: 'authorized_user',
 client_id: key.client_id,
 client_secret: key.client_secret,
 refresh_token: client.credentials.refresh_token,
 });
 await writeFile(TOKEN_PATH, payload, { encoding: 'utf8' });
}

/**
 * Google APIの認証クライアントを取得する
 *
 * @return {Promise<OAuth2Client>}
 */
async function authorize(): Promise<OAuth2Client> {
 let client = await loadSavedCredentialsIfExist();
 if (client) {
 return client;
 }
 client = await authenticate({
 scopes: SCOPES,
 keyfilePath: CREDENTIALS_PATH,
 }) as OAuth2Client;
 if (client.credentials) {
 await saveCredentials(client);
 }
 return client;
}


/**
 * イベントを追加する関数
 * @param eventJson
 */
async function addEvents(eventJson: ApiResponse) {
 try {
 const auth = await authorize();
 const calendar = google.calendar({ version: 'v3', auth });
 await calendar.events.insert({
 calendarId: '■■■ここにカレンダーIDを入れる■■■',
 requestBody: {
 summary: eventJson.summary,
 location: eventJson.location,
 description: eventJson.description,
 start: {
 dateTime: eventJson.start.dateTime,
 },
 end: {
 dateTime: eventJson.end.dateTime,
 },
 },
 });
 console.log('Event added successfully');
 } catch (error) {
 console.error('Error adding event:', error);
 throw new Error('Failed to add event');
 }
}

// イベント追加を受け付けるエンドポイント
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: VercelRequest, res: VercelResponse) => {
 if (req.method !== 'POST') {
 res.status(405).json({error: 'POSTリクエストのみ受付'});
 return;
 }

 const json = typeof req.body === 'string'
 ? JSON.parse(req.body.split('\n\n')[1])
 : req.body;

 if (!isApiResponse(json)) {
 console.log("Invalid JSON:", json); // デバッグ用
 res.status(400).json({error: 'Invalid JSON format'});
 return;
 }

 try {
 await addEvents(json);
 res.status(200).json({message: 'Event added successfully'});
 } catch (error) {
 res.status(500).json({error: 'Failed to add event'});
 }
}

いずれはStructured Outputを使って、よりちゃんとしたJSONが返ってくるようにしてみたいです。いつかやる!

LLM・LLM活用 Advent Calendar 2024 8日目でした。


いいなと思ったら応援しよう!

この記事が参加している募集