ChatGPTと会話可能なTeams ChatBotを作る Functions編

# 概要

この記事には以下の要素が含まれます。
- Azure OpenAI、openaiライブラリの解説
- Azure Functionsの構築
- Azure FunctionsとAzure KeyVaultの統合

この記事では、Azure OpenAI内のGPTモデルを使用したTeams ChatBotの作成方法について解説します。  
Teamsアプリケーションとして追加することで、Teams上で対話形式でAIを使用できます。

完成図
![2023-09-20_10h34_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/0f7fc609-3212-5a64-3ece-8d8e660687a5.png)

## アプリケーション構成

本アプリケーションは、Microsoft Teams上で動作するチャットボットです。  
Azure Functions, Azure OpenAI, CosmosDB, KeyVaultなどのサービスを組み合わせて実現します。以下が主な構成です。

- Microsoft Teams: チャットインターフェイス
- Azure Functions: バックエンド処理
- Azure OpenAI: モデルデプロイ
- CosmosDB: 会話履歴管理
- KeyVault: シークレット管理
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/e65fe451-c02a-f334-0eec-eaa0c28d0d62.png)

# チャット関数の構築

今回はこの範囲に絞って解説します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/f79431d8-c225-5c2b-eaec-8c642fa026fe.png)

## Azure OpenAI

Azure OpenAIは、Microsoft Azure上で動作するOpenAIのプラットフォームです。
Portalでリソース作成後、[OpenAI Studio](https://oai.azure.com/portal)からモデルをデプロイしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/b02f764a-5b1e-9caf-3447-051baaa1196e.png)

### openaiライブラリについて

[openaiライブラリ](https://platform.openai.com/docs/api-reference/introduction?lang=python)はPython向けに提供されているOpenAI APIクライアントです。  
本家OpenAIへの呼び出しも可能ですが、今回はAzure OpenAIの呼び出しに使用します。  
以下にAPIの一つである`Chat Completion`について軽く使用例と解説を示します。

```python: 使用例
import openai

# 変数設定
openai.api_type = "azure"
openai.api_base = "your_api_endpoint"
openai.api_key = "your_api_key"
openai.api_version = "api_version"

# API通信部分
response = openai.ChatCompletion.create(
    engine="gpt35-sample",
    messages = [
        {"role":"system","content":"You are an AI assistant that helps people find information."}, 
        {"role":"user","content":"コミュニケーションとは何か、50文字以内で"}, 
        {"role":"assistant","content":"コミュニケーションとは、情報や意見を相手に伝えることであり、双方向のやり取りが含まれる。"},
        {"role":"user","content":"英語に訳して"}
    ],
    max_tokens=500,
    temperature=0.7
)

print(response.choices[0].message)
# {"role": "assistant", "content": "Communication is the act of conveying information or opinions to someone, and it involves a two-way exchange."}
```

#### messagesについて

Chat Completionでは以下の要素を持つメッセージオブジェクトのリストを使用します。  
会話履歴をリストとして送信することで、履歴をもとにした対話が可能となります。

| キー | 値 | 説明 |
|:-:|:-:|:-:|
| role | `system` `user` `assistant` | メッセージの発信者。`system`はシステムメッセージ、`user`はユーザー、`assistant`はモデル。 |
| content | 会話内容 | ユーザーの入力、モデルからの応答の内容。 |


#### その他のパラメータ

また、Chat Completionの実行時は以下の様なパラメータを使用することで出力結果を調整することが可能です。

| パラメータ | 入力値 | 説明 |
|:-:|:-:|:-:|
| `max_tokens` | 1~ | モデルの応答の長さ。文字長ではなく[トークン数](https://platform.openai.com/tokenizer)。 |
| `temperature` | 0.0 ~ 0.1 | ランダム性の増加。数値が高い程回答に一貫性が無くなる。 |
| `top_p` | 0.0 ~ 0.1 | ランダム性の増加その2。数値が高い程回答に一貫性が無くなる。temperatureとは二者択一。 |
| `frequency_penalty` | -2.0 ~ 2.0 | 同一行繰り返しの抑制。数値が高い程一度出た行が出にくくなる。 |
| `presence_penalty` | -2.0 ~ 2.0 | 同一トピック繰り返しの抑制。数値が高い程新しいトピックについて話しやすくなる。 |
| `stop` | 停止シーケンスのリスト | 応答を停止させるトークンの設定。最大4つまで指定でき、指定のトークンが出力されるとモデルが応答を停止する。 |

## Azure Functions

Azure OpenAIと直接対話を行う部分です。  
今回はFunctionsにHttpトリガーの関数を実装しAPI的に使用できるようにしました。  
また、クラスは以下の様に`チャット関数`と`チャットクライアント`の2つで構成しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/1b979886-e71e-5a2c-5b87-f88e221a9a66.png)

### チャットクライアントの作成

始めにチャット関数から呼び出す`ChatClient`クラスを作成します。  
[openaiライブラリについて](#openaiライブラリについて)で解説した通り、変数設定後に`openai.ChatCompletion.create`でモデルと対話を行っています。

```Python: ChatClient.py
import os

import openai


class ChatClient:
    openai.api_type: str = 'azure'
    openai.api_base: str = os.environ['OPENAI_ENDPOINT']
    openai.api_key: str = os.environ['OPENAI_API_KEY']
    openai.api_version: str = os.environ['OPENAI_VERSION']

    model: str = os.environ['OPENAI_MODEL']
    temperature: str = os.environ['OPENAI_TEMPERATURE']
    max_tokens: = os.environ['OPENAI_MAX_TOKENS']

    def chat(self, message: dict) -> dict:
        messages: list = []
        messages.append(message)

        response = openai.ChatCompletion.create(
            engine=self.model,
            messages = messages,
            temperature=self.temperature,
            max_tokens=self.max_tokens
            )

        if response.choices[0].finish_reason == 'stop':
            return response.choices[0].message
        else :
            raise Exception(response.choice[0])
```

### チャット関数の作成

チャット関数側はシンプルにHTTPトリガーで作成し、チャットクライアントを呼び出すだけとしました。

```Python: Chat/__init__.py
import azure.functions as func

from ChatClient import ChatClient


def main(req: func.HttpRequest) -> func.HttpResponse:
    response: func.HttpResponse = None

    if req.get_body():
        message = json.loads(req.get_json())

        client = ChatClient()
        chat_response = json.dumps(client.chat(message))

        response = func.HttpResponse(
            chat_response,
            status_code=200
        )
    else:
        response = func.HttpResponse(
            "request is not exist",
            status_code=400
        )

    return response
```

### アプリケーション設定

[こちら](#チャットクライアントの作成)のコードで使用している環境変数(`os.environ[~]`)については、Azure Functionsの「アプリケーション設定」に設定することで使用可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/69cf1952-9edc-7816-18f7-f1aae02c5f06.png)

## Azure KeyVault

Azure Key Vaultはセキュリティ情報（APIキー等）管理用サービスです。  
先程はアプリケーション設定にAPIキーを直接保存していましたが、KeyVaultにシークレットとしてAPIキーを保存することでより安全にAPIを使用できるようになります。

### シークレットの作成

リソース作成後、`シークレット`>`生成/インポート`からシークレットを新規作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/b3a1f3b5-834e-2866-03c5-27510e347680.png)

作成後、シークレット識別子が発行されるのでコピーしておきます。

### FunctionsとKeyVaultの統合

作成したシークレットをFunctions側から読み込めるようにします。

#### アクセスポリシー設定

Functionsの`ID`からオブジェクトIDを発行します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/9651eb5c-a967-b7a3-596c-bce9275f353e.png)   

その後、KeyVaultの`アクセスポリシー`にFunctionsを登録します。  
今回は権限をシークレットの取得のみに絞っています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/b07f7c87-4c38-fb02-8df7-de88c894d074.png)

#### アプリケーション設定変更

最後に、先程まで平文で入力していたアプリケーション設定を変更します。  
基本は以下の`SecretUri`に参照先のシークレット識別子をコピーするだけです。
```
@Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret/)
```

無事参照ができるようになれば、アプリケーション設定の「ソース」にチェックマークが付きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/3a1d0507-e733-266c-7848-16092ac3c084.png)

## テスト

Functionsの`概要`>`関数`から今回作成したチャット関数を開き、メニューの`コードとテスト`を開きます。  
その後、`テストと実行`からjsonを送信し、モデルからの応答が確認できれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3072221/19d9ab61-2da6-40be-db54-f61fc718292a.png)

# 終わりに

今回はFunctions ~ OpenAIの部分について解説しました。  
次回はFunctionsからCosmosDBにアクセスし、会話履歴を保持・使用できるように改造していきます。

## あとがき

Azure初心者の方でも作成できるように、ガイド的な意味合いも込めて記事を作成しました。  


また、今回は記事作成の依頼があり作成してみましたが、慣れていないこともありかなり苦戦しました。  
もう何本か書いて慣れる必要がありますね…。

## 参考
- [OpenAI - API Reference](https://platform.openai.com/docs/api-reference)
- [Microsoft Learn - クイックスタート: Azure OpenAI Service で GPT-35-Turbo と GPT-4 を使い始める](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=programming-language-python)
- [Microsoft Learn - Azure App Service と Azure Functions でアプリ設定として Key Vault 参照を使用する](https://learn.microsoft.com/ja-jp/azure/app-service/app-service-key-vault-references?tabs=azure-cli)