Google Chat Botの開発

image

この記事は、【 可茂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 を活用して一時保存しています。

リクエストが来るたびに:

  1. ユーザーIDをキーにキャッシュから現在の StateContext を復元
  2. 該当する StateHandler で入力を処理
  3. 更新された StateContext を再度キャッシュへ保存

この「ロード → 実行 → 保存」のサイクルを徹底することで、ユーザーからはまるでボットが会話を覚えているかのように見せることができました。


まとめ:設計を変えることで得られたもの

当初は if 文の羅列で書いていたこのシステムですが、ステートマシンパターンを採用したことで以下のメリットがありました。

  1. テストが容易になった: 各ハンドラーが独立しているため、「不適切な日付が入力されたらエラーメッセージを返す」といったユニットテストが簡単に書けるようになりました。
  2. 拡張性が向上した: 例えば「予約人数の入力」というステップを追加したくなっても、新しいハンドラーを作って enum を1つ増やすだけで済みます。

もし対話型ボットの開発で、コードが複雑化して悩んでいる方がいれば、ぜひ「状態を明示的に管理する」というアプローチが有効だと思います。

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More

関連の記事

タグ

Flutter (127)初心者向け (32)イベント (19)Google Apps Script (17)Nextjs (13)可茂IT塾 (12)AI (9)React (8)riverpod (7)デザイン (7)Firebase (7)Figma (6)VSCode (6)JavaScript (6)ChatGPT (5)Slack (5)TypeScript (5)新卒 (4)就活 (4)Prisma (4)Dart (4)アプリ開発 (4)お知らせ (4)FlutterWeb (3)経験談 (3)NestJS (3)tailwindcss (3)ワーケーション (3)インターン (3)Web (2)Obsidian (2)Supabase (2)設計 (2)線型計画法 (2)事例 (2)Git (2)CSS (2)Freezed (2)Image (2)File (2)GitHub Actions (2)Material Design (2) (2)会社員 (2)画像 (2)Mac (2)iOS (2)React Hooks (2)社会人 (2)大学生 (2)RSS (1)Google (1)CodeRunner (1)vibe-kanban (1)NotebookLM (1)個人開発 (1)SVG (1)Android (1)Unity (1)WebView (1)Twitter (1)フルリモート (1)TextScaler (1)textScaleFactor (1)学生向け (1)Java (1)Spring Boot (1)shell script (1)正規表現 (1)table (1)テーブル (1)hooks (1)パワーポイント (1)ブックマーク (1)Pocket (1)ブクマク (1)MCPサーバー (1)OpenAI (1)ベクトル検索 (1)趣味 (1)モンスターボール (1)SCSS (1)Swift (1)MapBox (1)Cupertino (1)gpt-oss (1)生成AI (1)llama.cpp (1)LLM (1)ListView (1)postgresql (1)cloudrun (1)gcp (1)就活浪人 (1)既卒 (1)保守性 (1)iPad (1)シェアハウス (1)スクレイピング (1)PageView (1)画面遷移 (1)dotenvx (1)dotenv (1)Python (1)flutter_hooks (1)Gmail (1)GoogleWorkspace (1)ShaderMask (1)google map (1)Google Places API (1)GCPコンソール (1)Google_ML_Kit (1)Vercel (1)Google Domains (1)DeepLeaning (1)深層学習 (1)Google Colab (1)Google Chat Bot (1)Firebase Analytics (1)Gemini AI (1)コード生成 (1)GitHub Copilot (1)gemini (1)オンラインオフィス (1)html (1)オブジェクト指向 (1)クラスの継承 (1)ポリモーフィズム (1)LINE Messaging API (1)LINE Notify (1)LINE (1)Bitcoin (1)bitFlyer (1)コミュニティー (1)文系エンジニア (1)build_runner (1)ヒーター (1)作業効率 (1) (1)Flutter実践開発 (1)permission_handler (1)flutter_local_notifications (1)markdown (1)GlobalKey (1)ValueKey (1)Key (1)アイコン (1)go_router (1)FireStorage (1)debug (1)datetime_picker (1)Apple Store Connect (1)FlutterGen (1)デバッグ (1)Widget Inspector (1)VRChat (1)API (1)検索機能 (1)ローディング (1)Skeletonizer (1)Simmer (1)Shader (1)FFI (1)Rust (1)SharedPreferences (1)オフラインサポート (1)Navigator (1)メール送信 (1)FlutterFlow (1)Firebase App Distribution (1)Fastlane (1)Dio (1)CustomClipper (1)ClipPath (1)video_player (1)IMA (1)カスタム認証 (1)英語 (1)学習 (1)ポッドキャスト (1)アニメーション (1)Arduino (1)ESP32 (1)フリーランス (1)会社設立 (1)csv (1)docker (1)GithubActions (1)Dialog (1)BI (1)トラブルシューティング (1)エディタ (1)Cursor (1)sora2 (1)iPhone (1)Gemini CLI (1)Claude Code (1)LifeHack (1)ショートカット (1)Chrome (1)高校生 (1)キャリア教育 (1)非同期処理 (1)生体認証 (1)BackdropFilter (1)レビュー (1)Antigravity (1)getAuth (1)クローズドテスト (1)PlayConsole (1)Algolia (1)コンサルティング (1)Symbol (1)

お知らせ

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!(募集停止中)

可茂IT塾ではFlutter/Reactのインターンを募集しています!可茂IT塾のエンジニアの判断で、一定以上のスキルをを習得した方には有給でのインターンも受け入れています。

Read More
U30可茂ITインターンハッカソン

U30可茂ITインターンハッカソン

12月28,29日開催。2日間でアプリ開発の企画から完成までを目指す!U30可茂ITインターンハッカソンを開催します。

Read More