Azure OpenAI Service の GPT-3.5 を使って、 Dynamics 365 データに関して回答してくれるアプリを作ってみた (概要編)

# はじめに

こんにちは。仕事で Dynamics 365 を担当している keijiinoue と申します。  
Azure OpenAI Service の GPT-3.5 を使って、 Dynamics 365 データに関して回答してくれるアプリを作ってみました。この記事では、私がトライしたかったことと、そのために採用した技術や実装について紹介します。  

なお、この記事は概要編です。詳細版は以下の記事です。

https://qiita.com/keijiinoue/items/bd5da5685770707b56d3


**完成したものを実行したイメージ動画：**
![AOAIGPTD365SidePaneHeroLoop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455051/c53731b9-9bf6-4379-8292-2ad55b7f5dd4.gif)

:::note info
使用したライブラリはすべて MIT ライセンスのものです。
:::

:::note warn
この記事で紹介しているものは、私が個人的に開発したものであり、以下の「トライしたかったこと」 のためだけに開発したものです。 Dynamics 365 において Azure OpenAI Service を利用するアプリを開発するところのベスト プラクティスを示すものではありません。
:::

:::note warn
Dynamics 365 において 生成 AI 関連機能の恩恵を受けたい場合には、通常は Dynamics 365 の Copilot 機能群を利用いただくことになると思います。この記事は、私がコードを伴う開発をしたかったために、 Copilot 機能群を利用せずに開発したものです。
:::

## トライしたかったこと

- Dynamics 365 上で普通にユーザーが操作したら何回か画面遷移しなければいけないようなデータについても、 GPT に 1 回質問したらサクッと回答してもらいたい
- チャットのやり取りと、 Dynamics 365 フォームとが、必要に応じてインタラクティブに挙動して欲しい
- なるべく Dynamics 365 におけるライトな開発手法を利用して、クラウド上の他のリソースを利用せずに、ブラウザ上で動作するようにしたい

## 実装を終えた感想

採用した技術や実装方法は後述しますが、先に実装を終えた感想を述べます。  

- GPT の Function calling (関数呼び出し) が素晴らしい！GPT の呼び出し回数が減り、コード量も減った！
- チャットと連動する Dynamics 365 フォームとのインタラクティブな挙動は、見ていて楽しい！実装してみて良かった！
- もし、今回のアプリを、実際のお客様プロジェクトにて開発したとしたら、 ROI はとっても低いんじゃないだろうか。もっとお客様のベネフィットに直結するようなユースケースを考えて、それを実現するためのアプリが開発されるべきだろう。

## 採用した技術

- Dynamics 365 Sales アプリ上の サイド ペイン で動作させる Web リソース、およびそれを操作する Dataverse Client API
- Dataverse Web API
- Dataverse ソリューションの環境変数
- チャンネルメッセージング API
- Azure OpenAI Service の GPT-3.5 (モデル "gpt-35-turbo-16k")
- GPT の Function calling (関数呼び出し) 

## 採用した技術を実装した方法について (概要編)

### Azure OpenAI Service の GPT-3.5 (モデル "gpt-35-turbo-16k")

私がこのアプリを開発した時には (そしてひょっとしたら今もなお) ブラウザ上の JavaScript 用の Azure OpenAI Service の SDK は存在しませんでした。  
そこで、今回以下の REST API をコールする実装としました。  

- [Azure OpenAI Service の REST API リファレンス](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/reference)

そして、私は GPT からの返答を stream 形式で受け取りたかったので、ネットを検索したところ、以下の npm パッケージ `@microsoft/fetch-event-source` が利用できそうだと見出し、利用しています。

- [@microsoft/fetch-event-source](https://www.npmjs.com/package/@microsoft/fetch-event-source)

なお、今回は私は Azure OpenAI Service のモデル "gpt-35-turbo-16k" を採用しています。  
以前は "gpt-35-turbo" を使っていたのですが、 Function calling (関数呼び出し) を利用するとプロンプトのトークンがオーバーしてしまうことがあったため、 "gpt-35-turbo-16k" にしました。  

さて、「トライしたかったこと」に以下がありました。

> Dynamics 365 上で普通にユーザーが操作したら何回か画面遷移しなければいけないようなデータについても、 GPT に 1 回質問したらサクッと回答してもらいたい

これを実装するために、前述の Dataverse Web API を呼び出して取得した JSON を利用しています。JSON から余計な要素を取り除いてすっきりさせた文字列を GPT に渡すプロンプトに含めています。  

例えば "このお客様企業で AR025 や TX083 に関してどんなデータがありますか？" というユーザーの質問に対して、以下のようなプロンプトを渡しています。system ロールのメッセージ内に JSON テキストを含めています。  
```json
{
  "messages": [
    {
      "role": "system",
      "content": "You are an AI assistant that helps people find information.\nAs a result of searching the database, you are getting several types of data related to a company, such as:\n[{\"@search.entityname\":\"incident\",\"@search.objectid\":\"12cf1749-8672-4942-99ea-8a06958f9a1c\",\"owneridname\":\"Hirayama Rei\",\"title\":\"AR025 異音がする\",\"statecode\":[\"アクティブ\"],\"createdon\":\"2023/05/08 15:17\",\"modifiedon\":\"2023/11/12 14:25\",\"prioritycode\":[\"高\"],\"caseorigincode\":[\"電話\"],\"ticketnumber\":\"CAS-01046-X7N5D8\",\"customeridname\":\"港コンピュータ株式会社\"},{\"@search.entityname\":\"opportunity\",\"@search.objectid\":\"38ffcfab-bae8-ed11-a7c6-000d3a341b5a\",\"owneridname\":\"Hirayama Rei\",\"name\":\"港コンピュータ様 AR025を10台\",\"statecode\":[\"オープン\"],\"createdon\":\"2023/05/02 16:27\",\"modifiedon\":\"2023/05/10 15:31\",\"estimatedvalue\":35026500,\"customeridname\":\"港コンピュータ株式会社\",\"estimatedclosedate\":\"2023/06/28\"},{\"@search.entityname\":\"opportunity\",\"@search.objectid\":\"3db23897-582b-45d7-93e9-1ae95175abd5\",\"owneridname\":\"Inoue Keiji\",\"name\":\"海外部門 TX083 港コンピュータ様\",\"statecode\":[\"オープン\"],\"createdon\":\"2023/05/08 14:06\",\"modifiedon\":\"2023/05/10 15:32\",\"estimatedvalue\":10000000,\"customeridname\":\"港コンピュータ株式会社\",\"estimatedclosedate\":\"2023/08/31\"},{\"@search.entityname\":\"quote\",\"@search.objectid\":\"c512103b-00ee-ed11-8849-000d3a341e8f\",\"owneridname\":\"Inoue Keiji\",\"name\":\"AR025を10台 港コンピュータ様\",\"statecode\":[\"下書き\"],\"createdon\":\"2023/05/09 9:27\",\"modifiedon\":\"2023/05/09 9:28\",\"customeridname\":\"港コンピュータ株式会社\",\"totalamount\":5399900}]\n\nWhen displaying data, use bulleted lists with detailed information, not sentences.\nAlso, when answering, please add a URL link for each data. The format of the URL link is as follows:\n[{{name or title}}](https://japan1.crm.dynamics.com/main.aspx?appid=42d4ddb7-079b-ec11-b400-000d3a35cdc7&pagetype=entityrecord&etn={@search.entityname}&id={@search.objectid})"
    },
    {
      "role": "user",
      "content": "このお客様企業で AR025 や TX083 に関してどんなデータがありますか？"
    }
  ]
}
```

ここで、通常のユーザー操作であれば、複数のテーブル (エンティティ) に跨る検索ですので、 [Dataverse 検索](https://learn.microsoft.com/ja-jp/power-apps/user/relevance-search) 機能を利用して検索し、特定の取引先企業でフィルターして、という操作が必要です。が、今回は GPT に任せてサクッと回答してもらっています。  
これってきっと世間で言われている ~~RAG (Retrieval Augmented Generation)~~ グラウンディング (Grounding) なのだと思います。  

### GPT の Function calling (関数呼び出し)

最初にこのアプリを開発し始めたときには Function calling 機能は無く、 Few-shot の手法で、ユーザーの質問の意図を判別していました。  
その後 Function calling (関数呼び出し) が利用できるようになってから、実装しました。  
例えば、ユーザーがある質問をした際に GPT に渡しているプロンプトには、以下のように `functions` の記述があります。今回私は 5 個の function を定義しました。  

```json
{
  "messages": [
    {
      "role": "user",
      "content": "このお客様にはどんなサポート案件がありますか？Surface に関するものだけを抽出してください。"
    }
  ],
  "functions": [
    {
      "name": "provide_info_about_account",
      "description": "Provide information about data or product relating to a particular customer company. The customer company is usually an enterprise corporation, referred to as an 'account' or '企業' or '取引先企業' or 'お客様企業' in this system.",
      "parameters": {
        "type": "object",
        "properties": {
          "account_name": {
            "type": "string"
          },
          "product_name": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "account_name",
          "product_name"
        ]
      }
    },
    {
      "name": "provide_info_about_opportunities_for_account",
      "description": "Provide information about opportunities relating to a particular customer company. Do not provide information about the product. The customer company is usually an enterprise corporation, referred to as an 'account' or '企業' or '取引先企業' in this system. In this system, an 'opportunity' is also referred to as an '案件' or '営業案件'.",
      "parameters": {
        "type": "object",
        "properties": {
          "account_name": {
            "type": "string"
          }
        },
        "required": [
          "account_name"
        ]
      }
    },
    {
      "name": "provide_info_about_opportunities_and_products_for_account",
      "description": "Provide information about opportunities and those products relating to a particular customer company. The customer company is usually an enterprise corporation, referred to as an 'account' or '企業' or '取引先企業' in this system. In this system, an 'opportunity' is also referred to as an '案件' or '営業案件'.",
      "parameters": {
        "type": "object",
        "properties": {
          "account_name": {
            "type": "string"
          },
          "product_name": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "account_name",
          "product_name"
        ]
      }
    },
    {
      "name": "provide_info_about_incidents_for_account",
      "description": "Provide information about every incidents relating to a particular customer company. The customer company is usually an enterprise corporation, referred to as an 'account' or '企業' or '取引先企業' in this system. In this system, an 'incidents' is also referred to as an 'サポート案件' or 'チケット' or 'cases' or 'SR' or 'service requests'.",
      "parameters": {
        "type": "object",
        "properties": {
          "account_name": {
            "type": "string"
          }
        },
        "required": [
          "account_name"
        ]
      }
    },
    {
      "name": "provide_info_about_my_active_incidents_for_account",
      "description": "Provides information limited to my active incidents relating to a particular customer company. The customer company is usually an enterprise corporation, referred to as an 'account' or '企業' or '取引先企業' in this system. In this system, an 'incidents' is also referred to as an 'サポート案件' or 'チケット' or 'cases' or 'SR' or 'service requests'.",
      "parameters": {
        "type": "object",
        "properties": {
          "account_name": {
            "type": "string"
          }
        },
        "required": [
          "account_name"
        ]
      }
    }
  ]
}
```

その実装により、どのような質問に対してどの function が返されるか、の実例が以下です。  

| 質問 | 返される function |
| --- | --- |
| 「このお客様企業で AR025 や TX083 に関してどんなデータがありますか？」 | provide_info_about_account |
| 「この企業に関する営業案件の中で、提案製品として TX083 を含むものだけを表示してください。」 | provide_info_about_opportunities_and_products_for_account |
| 「このお客様にはどんなサポート案件がありますか？」 | provide_info_about_incidents_for_account |

返される function 毎に、どのように Dataverse Web API を呼び出すべきかが分岐されるようなコードを実装しました。  
Function calling を使わずに、 Few-shot を利用していた時には、どのような  Dataverse Web API を呼び出すべきかを判定した後に、検索対象となるレコードを抽出するような 2 段階の GPT 呼び出しが必要でした。 Function calling を使うことで、それを 1 段階に減らすことができました。  

なお、私が開発したアプリでは、内部的な GPT 呼び出しについても、ビジュアルにわかりやすくするために、チャットのやり取りとして (以下のイメージ動画の赤色吹き出し部分) 表示できるようにしました。これは完全に開発される方に説明するためのものです。  

**内部的な GPT 呼び出しイメージ動画：**
![AOAIGPTD365SidePaneInternalRequestLoop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455051/0faa8ede-f693-8818-0b6e-92e653c5fcbf.gif)

## 最後に

概要編は以上です。以下の詳細版に、より多くの実装した方法などの記述がありますので、よろしければご覧ください。  

https://qiita.com/keijiinoue/items/bd5da5685770707b56d3

最後までご覧いただきましてありがとうございました。  

### アドベントカレンダー

本記事は、以下のアドベントカレンダー "Microsoft Top Partner Engineer's Advent Calendar 2023 - ZDNET Japan" 12月9日パート に投稿したものです。

https://japan.zdnet.com/extra/ms_202311/35211313/