📆

Azure OpenAI ServiceのFunction callingを使って自然言語でGoogleカレンダー操作

2023/08/25に公開

こんにちは、Happy Elements 株式会社でエンジニアをしておりますryoooです。

先日、以下の記事で弊社における社内ルール等に対するチャットシステムをご紹介いたしました。
https://zenn.dev/happy_elements/articles/1484b177b6a7f7

Function calling機能を使って、こちらのチャットツール上で自然言語でGoogleカレンダーを操作する機能を追加しましたので、本記事では実装時の工夫を紹介させていただきます。

この機能により、以下のようなことが可能となっています。

自然言語でカレンダー予定を作成

自然言語でカレンダー予定を取得

Function callingとは

簡単にいうとGPT-4に対して「あなたはこういう関数を利用できるから、利用したい場合は引数をレスポンスしてね」と伝えて、GPT-4とバックエンドサーバーで何度かやり取りをしながら、ユーザーの要求を解決していく仕組みです。

実装したFunction

実装したFunctionは以下のとおりです。

  • 従業員マスター取得
  • 従業員カレンダー取得
  • 従業員カレンダー予定作成

従業員マスター取得のFunction仕様

Function仕様は以下のようなjsonでGPT-4に送信します。

{
  name: "fetch_staff_list", # ← 関数名
  description: "Fetch staff list.",
  parameters: {
    type: :object,
    properties: { # ← 関数のパラメーター仕様
      prefix_and_suffix_name_match: {
        type: :string,
        description: "Filter by staff name. Please specify the part of the name" + \
                    "you want to search for with prefix and suffix.",
      },
      team: {
        type: :string,
        description: "Filter by team name. Please specify if you want to narrow down the team name.",
        enum: [ # 部署名は仮のものとなっております
	  "general_affairs - 総務部",
	  "human_resources - 人事部",
	  "development - 開発部",
	  ・
	  ・
	  ・
        ],
      },
    },
  },
}

工夫

ポイントは、enumの箇所です。
パラメーター仕様としてenumを渡すことでGPT-4から戻ってくる関数のパラメーターを限定することができるのですが、あえて日本語名を含めています。
こうすることによって、ユーザーからのプロンプト内に日本語で部署名が含まれていた際に、LLM側で正確にパラメーターを指定できるようにしています。

※ 実際はRubyでteams.map(&:code_and_name)などとすることでマスターテーブルのチーム定義と連動するようにしています。

従業員マスター取得のFuctionレスポンス

{
  role: "function",
  name: "fetch_staff_list",
  content: JSON.dump(staffs.map {|staff|
    {
      name: staff.name,
      email_without_domain: staff.email_without_domain,
      team: {
        code: staff.team.code,
        name: staff.team.name,
      },
    }
  }),
}

工夫

Functionの結果は、contentの中にjson文字列で渡すことでGPT-4に送信します。
この結果を用いて、GPT-4は次のアクションを決めることになります。
GPT-4の次のアクションは以下の2つです。

  • 追加でFunctionを呼ぶ
  • ユーザーに結果のメッセージを返す

GPT-4がこれらいずれかのアクションを達成しやすいように、レスポンスを考えるのが良さそうです。
具体的には、以下の工夫をしています。

  1. 追加でFunctionを呼ぶ際にパラメーターを指定しやすいように、カレンダー操作Functionのパラメーター名と一致させる(email_without_domain)
  2. ユーザーに結果のメッセージを返しやすいように、staff.team.nameといった和名をしっかり含めておく。(メッセージに何を採用するかはGPTに判断させる)

本Functionの副次的効果として、GPT-4を使って部署や職種を指定した上でメンバー一覧を出力できるようになりました。

従業員カレンダー取得のFunction仕様

{
  name: "fetch_staff_calendar_events",
  description: "Fetch staff calendar.",
  parameters: {
    type: :object,
    properties: {
      email_without_domain: {
        type: :string,
        description: "Please specify the email addresses(without domain) of the staff" + \
                     "whose appointments you want to search.",
        enum: staffs.map(&:email_without_domain).compact,
      },
      time_min: {
        type: :string,
        description: "Specify the start date and time of the range you want to search for events." + \
                     "The format is below.\n2023-08-23T00:00:00+09:00",
      },
      time_max: {
        type: :string,
        description: "Specify the end date and time of the range you want to search for events." + \
                     "The format is below.\n2023-08-23T00:00:00+09:00",
      },
    },
    required: [ :email, :time_min, :time_max ],
  },
}

工夫

従業員数は多いんですが、email_without_domainのenum定義に全従業員のメールアドレスを指定することで、操作対象のカレンダーをホワイトリスト指定としています。

  • メールアドレスからドメインを除去した理由
    Function callingは最大64個ものFunctionを指定できるのですが、トークンを食えば食うほど精度が悪くなるという情報を目にしていたため、メールアドレスのドメインは冗長と判断して除去しました。
    その場合でも、emailというパラメーター名だとGPTが混乱する可能性を鑑みて、email_without_domainと変数仕様を明確に名前に含めるようにしています。

従業員カレンダー予定作成のFunction仕様

{
  name: self.name,
  description: "Add a new event to your calendar." + \
               "Before calling this function," + \
               "surely tell the user what you want to create and make sure they are willing to create it." + \
               "And assign the resulting message to confirm_answer." + \
               "After creating, please tell the user the result in the following format." + \
               "タイトル:健康診断\n" + \
               "日時:8/25(金) 12:00〜13:00\n" + \
               "参加者:◯◯、☓☓\n" + \
               "Googleカレンダーで確認する",
  parameters: {
    type: :object,
    properties: {
      email_without_domain: {
        type: :string,
        description: "Please specify the email addresses(without domain) of the event owner.",
        enum: staffs.map(&:email_without_domain).compact,
      },
      summary: {
        type: :string,
        description: "title of event",
      },
      start_at: {
        type: :string,
        description: "Specify the start time of the range you want to search for appointments." + \
                     "The format is below.\n2023-08-23T00:00:00+09:00",
      },
      end_at: {
        type: :string,
        description: "Specify the end time of the range you want to search for appointments." + \
                     "The format is below.\n2023-08-23T00:00:00+09:00",
      },
      attendees: {
        type: :string,
        description: "For attendees other than the event owner," + \
                     "specify the email_without_domain separated by commas.",
      },
    },
    required: [ :email_without_domain, :summary, :start_at, :end_at ],
  },
}

工夫

ここでやりたかったこととしては、「予定を作成する前に、ユーザーに確認メッセージをはさむ」 ことでした。
結論からいうと、現状完璧にはできていません。

試したこととしては以下のとおりです。

descriptionで指示

まずはFunction定義のdescriptionで「必ず事前にユーザーに対して作成予定の詳細を伝えた上で、承認いただけた場合のみ作成すること」などと記載しました。

これにより、予定に対して曖昧さがある場合は確認するようにはなりましたが、確認フォーマットを指定してもフォーマット通りには確認してくれませんでした。

「明日の午後で◯◯さんと1時間MTG」などと曖昧な感じで言うと、「作成しますがよろしいですか?」と確認してくれますが、「明日の午後で◯◯さんと1時間MTGしたい」などと言うと、確認はできたものとして、確認なしに作ってしまいます。

Functionを使うにあたって、パラメーターとして必須な情報が不明ならユーザーメッセージで聞いてくれるんじゃないか説

Function callingの挙動として、必須パラメーターの情報が足りない場合はユーザーに聞いてくれるので、以下のようなパラメーターを作ってrequiredに指定してみましたが、意図したとおりには動きませんでした。

      confirm_key: {
        type: :string,
        description: "A response message obtained by asking the user for their intention to create.",
      },

この引数のdescriptionには、以下のような英文も設定してみたのですが、最終的に目的は達成できずでした。
この引数の値を得るために、ユーザーに対して作成しようとしている予定をメッセージとして表示してください。そのうえで、ユーザーから回答として得られた文字列を指定してください。

おそらくこうあるべき?

今回の要件では不要と判断しましたが、確実にユーザー確認を行いたければ、Function callingから予定作成のレスポンスが帰ってきた際に、アプリ側でユーザーへの確認メッセージを作成・出力した上で、ユーザーからokをもらってから実際に作成する処理を挟み込めば、必ず確認する仕様にもできると思います。

別の方法もあるかもしれませんが、現状の私の認識では確実に確認を行いたいのであれば上記のようにするのが良さそうです。

曜日を正しく出力するための工夫

カレンダー操作の結果のメッセージで、正しい曜日をちゃんと出力させたかったのですが、GPTは曜日の扱いに弱いです。
ここについては、以下のように今日の情報をシステムプロンプトとして入れることで曜日の扱いに強くしています。

{
  role: "system",
  content: "User name is ◯◯, email is ◯◯@・・・" + \
           "Today is 2023年8月25日(金)",
}

合わせて、操作者の名前とメールアドレスを入れておくことで、Function callingの往復回数を減らしています。

すべてをログに残す

LLMとバックエンドサーバーの間でどういったやりとりがあり、どのように処理が行われたのか?についてはきっちりログに残すべきです。
ときにはユーザーに「LLMとバックエンドサーバーの間でどのように処理されたか」を表示する必要があるかもしれません。

ループ回数制限を入れておく

LLMとバックエンドサーバーの間のやりとりには回数制限を設け、あまりに長く制御を奪うようなプロンプトはチェックすべきです。
トークン数制限とどちらが先に引っ掛かるかどうかってところですが転ばぬ先の杖程度で。

セキュリティの観点からの参考情報

おわりに

最後までみていただきありがとうございます。
Function callingはめちゃくちゃ応用力のある仕組みで、組み合わせ方次第で様々な処理を自然言語でさばけそうでした。
今後も様々なFunctionを定義してゲーム開発に集中できる環境を作っていきたいと考えています。

よろしければ、ハート・フォロー・シェアをいただけますと喜びます :)

自分の手がけたゲームを、何万人もの人にあそんでもらえる体験

Happy Elements株式会社は「エリオスライジングヒーローズ」、「あんさんぶるスターズ!!」、「メルクストーリア」などGoogle PlayやApp Store上でいくつもの人気スマートフォンゲームを運営しています。
現在も新規タイトルを鋭意制作中です。

「面白いゲームを作りたい」とお考えの方、「スキルの高い仲間と一緒に働きたい」と感じている方からのご応募をお待ちしています。「自分の手がけたゲームを、何万人もの人に遊んでもらえる」そんな体験を、ぜひ私たちと一緒にしてみませんか?
https://recruitment.happyelements.co.jp/

Happy Elements

Discussion