Power Platform から Azure OpenAI の Function Calling を利用してみよう


# はじめに


Azure OpenAI でも Function Calling が利用できるようになりました。
Function Calling 自体については、以下の記事がとても分かりやすいです。

https://zenn.dev/microsoft/articles/azure-openai-add-function-calling    

私なりの解釈を述べると、以下の通りです。


* Function Calling という名前だが、ChatGPT 自体が Function を コールするわけではない
* Function の情報も渡して上げると、リクエスト内容を踏まえ、よしなにどの Function を利用するか判断してくれる。Function をコールする必要がない場合はいつも通り ChatGPT の学習データを元に回答する
* Function をコールする必要がある際、渡した Function の定義を踏まえ、パラメーターを生成してくれる。そして、それ以外の不要な情報を返さない
* Function をコールしない場合においても、何らかのサービスとデータ連携をする際、決まったフォーマットのデータを取得したい場合にも便利

厳密には、今までも同じようなことはできない訳ではなかった認識ですが、必要に応じて API 連携したりデータ連携したりしたい際、例えば、JSON のデータとして回答してほしいとします。

そのような際、例えば、以下のように、**明示的に指示をしても回答に余計な文字が入ってしまい、プロンプトを色々変更しても上手く行かないときがあり悪戦苦闘することもありました。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/8dffe5ad-d626-aac3-dee9-f52267f4b3af.png)

しかし、Function Calling の場合、以下のように Function の情報を渡すことで、Function をコールした方がいいと判断した場合、そのように判断した旨 ("finish_reason": "function_call") および arguments でパラメーターを返してくれます。

このパラメーターを API の呼び出しやデータ連携に活用します。これにより、今までやろうとしていたことがよりやりやすくなったと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/d50fe3bf-1364-7488-02d3-7d00b9962691.png)

■リクエスト

```
{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
      "content": "日本で一番高い山は？",
      "role": "user"
    },
    {
      "content": "日本で一番高い山は富士山です。",
      "role": "assistant"
    },
    {
      "content": "ユーザーを作成して。名前は test4 です",
      "role": "user"
    }
  ],
  "functions": [
    {
      "name": "create_user",
      "description": "新しいユーザーを作成します",
      "parameters": {
        "type": "object",
        "properties": {
          "accountEnabled": {
            "type": "boolean",
            "description": "アカウントが有効かどうか"
          },
          "displayName": {
            "type": "string",
            "description": "表示名"
          },
          "mailNickname": {
            "type": "string",
            "description": "メールニックネーム、エイリアス、mailnickname、名前、Name"
          },
          "userPrincipalName": {
            "type": "string",
            "description": "UPN、ユーザープリンシパルネーム、UserPrincipanName"
          },
          "passwordProfile": {
            "type": "object",
            "description": "パスワードプロフィール",
            "properties": {
              "forceChangePasswordNextSignIn": {
                "type": "boolean",
                "description": "次回サインイン時にパスワードを変更するかどうか"
              },
              "password": {
                "type": "string",
                "description": "パスワード"
              }
            }
          }
        },
        "required": [
          "mailNickname"
        ]
      }
    }
  ]
}
```

■レスポンス
```
{
  "id": "chatcmpl-7hqNGR8w1TadpSfiC4XztkVka4lui",
  "object": "chat.completion",
  "created": 1690683786,
  "model": "gpt-35-turbo-16k",
  "prompt_annotations": [
    {
      "prompt_index": 0,
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "choices": [
    {
      "index": 0,
      "finish_reason": "function_call",
      "message": {
        "role": "assistant",
        "function_call": {
          "name": "create_user",
          "arguments": "{\n  \"displayName\": \"test4\",\n  \"mailNickname\": \"test4\"\n}"
        }
      },
      "content_filter_results": {}
    }
  ],
  "usage": {
    "completion_tokens": 25,
    "prompt_tokens": 222,
    "total_tokens": 247
  }
}

```

今回は、そのような便利な機能である Function Calling を Power Platform から利用する方法を紹介します。

これまで紹介した「Add your data」と合わせ、全体構成としては以下のようなイメージになります。

https://qiita.com/Takashi_Masumori/items/410e7e92e6f93ec823c2

利用者からすると、Power Apps を入り口にして、ChatGPT から回答を得たり、必要に応じて独自データを検索した上で回答を得たり、必要に応じて関数を呼んで処理をしてもらったり回答を得たりすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/fa2566f5-ada1-96ed-dd25-4f0f6bc6e49e.png)

# Power Automate 側実装

まず、フローの全体像は以下のようなイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/370a8216-dcbb-ad49-30a9-911485389725.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/995c928d-c8a8-d8b8-a404-1c6d4725121a.png)

コアな部分の説明をします。まず、以下の部分です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/b0865da0-95a7-f9d4-b291-6feb844ac347.png)

今回は、Microsoft Graph の以下の API を利用する想定で、
"function"を付与しています。

"description" の部分が非常に重要で、受け取った文章から、この Function を呼んだ方がいい、その際、入力した情報がそれぞれどのパラメーターとして渡されるかに関わってきます。

そのため、Function Calling の場合、この辺を工夫する必要があると思います。

https://learn.microsoft.com/en-us/graph/api/user-post-users?view=graph-rest-1.0&tabs=http

```
{
  "model": "gpt-3.5-turbo",
  "messages": @{outputs('Request')},
  "functions": [
    {
      "name": "create_user",
      "description": "新しいユーザーを作成します",
      "parameters": {
        "type": "object",
        "properties": {
          "accountEnabled": {
            "type": "boolean",
            "description": "アカウントが有効かどうか"
          },
          "displayName": {
            "type": "string",
            "description": "表示名"
          },
          "mailNickname": {
            "type": "string",
            "description": "メールニックネーム、エイリアス、mailnickname、名前、Name"
          },
          "userPrincipalName": {
            "type": "string",
            "description": "UPN、ユーザープリンシパルネーム、UserPrincipanName"
          },
          "passwordProfile": {
            "type": "object",
            "description": "パスワードプロフィール",
            "properties": {
              "forceChangePasswordNextSignIn": {
                "type": "boolean",
                "description": "次回サインイン時にパスワードを変更するかどうか"
              },
              "password": {
                "type": "string",
                "description": "パスワード"
              }
            }
          }
        },
        "required": [
          "mailNickname"
        ]
      }
    }
  ]
}
```

finish_reason を元に、関数の呼び出しが必要かどうか判断します。

```
body('JSON_の解析')?['choices'][0]?['finish_reason']
```

finish_reason が function_call の場合、API をコールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/c5324aed-ae8c-6d42-f35a-0d66e4af498e.png)

そちらの回答を元に再度 ChatGPT にリクエストをします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/a6931716-938e-db4e-1b9d-e8da0e1b8717.png)

以下の箇所の処理にやや苦労しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/979e4c85-48e4-a9d5-ad84-bedcb0b9a34d.png)


```
json(concat(
  substring(triggerBody()['text'], 0, sub(length(triggerBody()['text']), 1)),
  ',',
  json(concat(
    '{"role":"function","name":"create_user","content":"',
    body('JSON_の解析_Create_User')?['displayName'],
    '"}'
  )),
  ']'
))

```

実際のリクエスト内容は以下のような感じになります。

```
{
  "model": "gpt-3.5-turbo",
  "messages": [
    {
      "content": "日本で一番高い山は？",
      "role": "user"
    },
    {
      "content": "日本で一番高い山は富士山です。",
      "role": "assistant"
    },
    {
      "content": "ユーザーを作成して。名前は test4 です",
      "role": "user"
    },
    {
      "role": "function",
      "name": "create_user",
      "content": "test4"
    }
  ],
  "functions": [
    {
      "name": "create_user",
      "description": "新しいユーザーを作成します",
      "parameters": {
        "type": "object",
        "properties": {
          "accountEnabled": {
            "type": "boolean",
            "description": "アカウントが有効かどうか"
          },
          "displayName": {
            "type": "string",
            "description": "表示名"
          },
          "mailNickname": {
            "type": "string",
            "description": "メールニックネーム、エイリアス、mailnickname、名前、Name"
          },
          "userPrincipalName": {
            "type": "string",
            "description": "UPN、ユーザープリンシパルネーム、UserPrincipanName"
          },
          "passwordProfile": {
            "type": "object",
            "description": "パスワードプロフィール",
            "properties": {
              "forceChangePasswordNextSignIn": {
                "type": "boolean",
                "description": "次回サインイン時にパスワードを変更するかどうか"
              },
              "password": {
                "type": "string",
                "description": "パスワード"
              }
            }
          }
        },
        "required": [
          "mailNickname"
        ]
      }
    }
  ]
}
```

以下はレスポンスです。次は、"finish_reason": "stop"となっています。

```
{
  "id": "chatcmpl-7hqNJkKOmWm72XENdJlbKKmCRKLaw",
  "object": "chat.completion",
  "created": 1690683789,
  "model": "gpt-35-turbo-16k",
  "prompt_annotations": [
    {
      "prompt_index": 0,
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "新しいユーザー \"test4\" を作成しました。"
      },
      "content_filter_results": {
        "hate": {
          "filtered": false,
          "severity": "safe"
        },
        "self_harm": {
          "filtered": false,
          "severity": "safe"
        },
        "sexual": {
          "filtered": false,
          "severity": "safe"
        },
        "violence": {
          "filtered": false,
          "severity": "safe"
        }
      }
    }
  ],
  "usage": {
    "completion_tokens": 19,
    "prompt_tokens": 229,
    "total_tokens": 248
  }
}
```

こちらを元に Power Apps に返答します。

# Power Apps

Power Apps 側の処理はこれまで紹介した記事の内容からほとんど変えていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/f1eb7ff3-a800-17bf-567a-c06fc096df24.png)

一応、Function Calling が利用された場合は、チャットを継続した際会話をクリアするようにしています。

```
UpdateContext({locLoading: true});
If(
    gblIsFunctionCall,
    Clear(colChat);
    Set(
        gblIsFunctionCall,
        false
    );
    
);
Collect(
    colChat,
    {
        role: "user",
        content: txtChatMessage.Text
    }
);
ClearCollect(
    colManufacturedChat,
    colChat
);
If(
    gblTotalTokens > 3500,
    ClearCollect(
        colManufacturedChat,
        LastN(
            colChat,
            3
        )
    )
);
If(
    chkIsAskBasedOnInternalData,
    Set(
        gblChatGPTResponse,
        'ChatGTPAzureOpenAI(Addyourdata)'.Run(JSON(colChat));
    );
    ,
    Set(
        gblChatGPTResponse,
        FunctionCalling.Run(JSON(colManufacturedChat))
        //ChatGTPAzureOpenAI.Run(JSON(colManufacturedChat));
    );
    
);
If(
    gblChatGPTResponse.result = "0" And gblChatGPTResponse.finishreason = "stop",
    Collect(
        colChat,
        {
            role: gblChatGPTResponse.role,
            content: gblChatGPTResponse.content
        }
    );
    Set(
        gblTotalTokens,
        Value(gblChatGPTResponse.totaltokens)
    );
    Set(
        gblShowlblChat,
        true
    );
    Select(
        galChat,
        CountRows(galChat.AllItems)
    );
    ,
    gblChatGPTResponse.finishreason = "function_call",
    Collect(
        colChat,
        {
            role: "function_call",
            content: gblChatGPTResponse.content
        }
    );
    Set(
        gblIsFunctionCall,
        true
    );
    Select(
        galChat,
        CountRows(galChat.AllItems)
    );
    ,
    Set(
        gblShowErrorMessage,
        true
    );
    
);
Reset(txtChatMessage);
UpdateContext({locLoading: false});
```

動かしてみます。まず、これまで作成している通り、チェックボックスの有無で、必要に応じて独自データの検索を行っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/ca339ebf-3b05-07dc-3225-3a9c52f459df.png)

以下は Function Calling により、最終的に Microsoft Graph を利用してユーザーが作成されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/706e4501-29fc-37d6-8da7-5d80c7c22b3e.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3074511/e9ebf339-b1c2-421a-8183-48e01c20afcc.png)

# まとめ

今回は、Power Platform から Azure OpenAI の Function Calling を利用する方法について紹介しました。お話を聞くだけでは少しイメージ湧かなかったですが、実際に作成してみることで理解が深まった気がします。

これまで作成していた、「Add your data」含め、Power Apps を入り口にして、必要に応じて独自データを検索したり API をコールしたり他のサービスとデータ連携したりすることができるようになりました。

あとは、どんな独自データを利用したいか、どんな API と連携したいか、どんなサービスとデータ連携したいか、アイディア次第で色んな応用ができると思います。

これまで紹介した記事含め、作成の仕方も記載しているので、少しでもその観点で参考になれば幸いです。