- ホーム
- ブログ
- Google Chat Bot
- Google Chat Botの開発
この記事は、【 可茂IT塾 Advent Calendar 2025 】の21日目の記事です。
今回開発したのは、Google Chat上で動作する「会議室予約ボット」です。
5つの会議室(201-205)を対象に、チャットの対話を通じて予約・確認・キャンセルができるシステムです。裏側ではGoogleカレンダーと連携して予定を登録し、Googleスプレッドシートで予約データやユーザーの予約ポイントなどを管理します。
具体的には、以下のような流れで予約を行います。
ユーザー:
!予約追加
Bot: 「予約する会議室を選んでください」 → [201][202][203](ボタン表示)
ユーザー:201(ボタンをクリック)
Bot: 「日付を教えてください(例: 12月25日)」
ユーザー:12/25
Bot: 「開始時刻を教えてください(例: 13:00)」
ユーザー:13:00
...(中略)...
Bot: 「以下の内容で予約しますか? [はい][いいえ]」
ユーザー:はい
Bot: 「✅ 201号室の予約が完了しました!」
一見シンプルに見えますが、Google Chatボットのようなサーバーレス/HTTPベースのアプリを開発する際、最も頭を悩ませるのが、この「対話のコンテキスト(文脈)」の管理です。
ボットのエンドポイントは1回のリクエストごとに処理を完結させる必要があり、基本的にはステートレス(無状態)です。しかし、今回の会議室予約システムでは、「どの部屋を?」「いつ?」「何時から?」といった情報を順番にユーザーから聞き出す必要があります。
本記事では、このステートレスな環境で、どのようにして堅牢な対話型ステートマシンを構築したかを説明します。
まずは、システムがどのような状態(ステート)を辿るのかは以下のとおりです。
stateDiagram-v2
[*] --> IDLE
IDLE --> SELECTING_ROOM: /reserve コマンド
SELECTING_ROOM --> SELECTING_DATE: 部屋選択
SELECTING_DATE --> SELECTING_START_TIME: 日付入力
SELECTING_START_TIME --> SELECTING_END_TIME: 開始時刻入力
SELECTING_END_TIME --> CONFIRMING: 終了時刻入力
CONFIRMING --> PROCESSING: 「はい」
CONFIRMING --> IDLE: 「いいえ(キャンセル)」
PROCESSING --> COMPLETED: カレンダー登録完了
COMPLETED --> IDLE
今回のステートマシンによる対話管理を支えるディレクトリ構造は以下のようになっています。関心の分離を徹底し、状態遷移ロジック(state/)と具体的なメッセージ処理(handlers/)を分けています。
src/
├── handlers/ # イベントハンドラー
│ ├── MessageHandler.ts # テキスト入力を受信
│ └── CardActionHandler.ts # ボタン操作を受信
├── state/ # ステートマシン(今回の主役)
│ ├── ReservationStateMachine.ts # 状態管理の核
│ ├── ReservationState.ts # 状態の型定義
│ └── StateHandlers.ts # 各状態の具体的な振る舞い
├── validation/ # バリデーションロジック
│ ├── DateValidator.ts
│ └── RoomValidator.ts
└── utils/ # 共通ユーティリティ
└── DateUtils.ts # 日付パースなど
複雑なフローを管理するための第一歩は、状態を明示的に定義することです。TypeScriptの enum を使い、それぞれのステージを分離しました。
export enum ReservationState {
IDLE = 'idle',
SELECTING_ROOM = 'selecting_room', // 部屋選択中
SELECTING_DATE = 'selecting_date', // 日付選択中
SELECTING_START_TIME = 'selecting_start_time', // 開始時刻選択中
SELECTING_END_TIME = 'selecting_end_time', // 終了時刻選択中
CONFIRMING = 'confirming', // 最終確認中
PROCESSING = 'processing', // 処理中
}
対話型ボットで最も困難なのは、ユーザーから不適切な入力が来たときのハンドリングです。例えば、日付を聞いているのに「こんにちは」と返されたり、深夜2時のような予約できない時間を指定されたりする場合です。
これを解決するため、各状態(State)ごとに「入力の検証(Validate)」と「遷移(Handle)」をカプセル化した StateHandler を実装しました。
以下は、ユーザーが入力した日付を解析し、未来の日付であることをバリデーションするハンドラーの一部です。
export class DateSelectionHandler implements StateHandler {
handle(input: string, context: ReservationContext): StateTransitionResult {
const validation = this.validate(input, context);
if (!validation.valid) {
// バリデーション失敗時は、メッセージを添えて同じ状態に留める
return {
success: false,
nextState: ReservationState.SELECTING_DATE,
message: validation.errors?.join('\n') || 'エラーが発生しました',
};
}
// 入力(例:「12月25日」)を解析してコンテキストに保存
const parsed = DateUtils.parseUserDateInput(input);
context.selectedDate = new Date(year, parsed.month - 1, parsed.day);
return {
success: true,
nextState: ReservationState.SELECTING_START_TIME,
message: `${DateUtils.formatForDisplay(context.selectedDate)}ですね!次に開始時刻を教えてください。`
};
}
}
このように、各状態の処理をクラスとして独立させることで、「今どの状態にいて、次に何をすべきか」というロジックがスパゲッティコードにならず、見通しの良い設計になりました。
サーバー側が状態を持てないため、現在どの状態(State)にいるか、および入力済みのコンテキスト(Context)は、Google Apps Scriptの CacheService を活用して一時保存しています。
リクエストが来るたびに:
State と Context を復元StateHandler で入力を処理State と Context を再度キャッシュへ保存この「ロード → 実行 → 保存」のサイクルを徹底することで、ユーザーからはまるでボットが会話を覚えているかのように見せることができました。
当初は if 文の羅列で書いていたこのシステムですが、ステートマシンパターンを採用したことで以下のメリットがありました。
enum を1つ増やすだけで済みます。もし対話型ボットの開発で、コードが複雑化して悩んでいる方がいれば、ぜひ「状態を明示的に管理する」というアプローチが有効だと思います。
可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。
Read More