Teams × Azure OpenAIでチャットボットを作る① ~雛形アプリ作成~

# 完成品

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/84cd44ab-61f6-44fb-c768-b207a32bbfc5.png)


# Prerequisites

- [VSCode](https://code.visualstudio.com/download)
- [M365開発者用アカウント](https://developer.microsoft.com/en-us/microsoft-365/dev-program)
    - または開発の許可があるTeamsテナント



https://learn.microsoft.com/ja-jp/microsoftteams/platform/toolkit/tools-prerequisites




--------------------------------------------------------------------
# 1. 開発者用アカウントのセットアップ

https://developer.microsoft.com/en-us/microsoft-365/dev-program

登録すると、**ユーザ名@xxx.onmicrosoft.com**が払い出されます。
90日間無料です。


以下のポータルにアクセスすると、アプリの作成・デプロイなどの管理をGUIで行えます。
**VSCodeでアプリ作成 → ポータルからデプロイ**といった形になりそうです。

https://dev.teams.microsoft.com/

以下はアプリのデプロイイメージです。
アプリを作成後、ポータルに作成したアプリが反映されているので、アプリをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/9f2af207-233f-59a6-b5ce-42c9bf1df728.png)


右上の**Publish**をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/21f33643-8c92-1a35-d1e6-8849a0c17d76.png)


デプロイ方法を選択します。

- **Zipファイルに固めるか**
    - 作成したアプリケーションがZipファイルとしてダウンロードされます
- **組織に送信するか**
    - 管理者にアプリケーションの検証依頼が飛ぶ？（ここの動きは要調査です。）
- **Teams公式アプリにするか**
    - Microsoftに承認依頼が飛びます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/ee0cc2d5-5672-c3ad-47d6-47afbc18f4c1.png)



# 2. 雛形プロジェクト作成

VSCodeの拡張機能「**Teams Toolkit**」をインストールし、
**ユーザ名@xxx.onmicrosoft.com**でサインインします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8039d526-5b91-fb35-2736-332e0d8f8219.png)


**Create a New App**を選択します。
初期構築の場合以下のような表示になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/e1e1f776-9800-6bdd-2478-9354a942c6f7.png)


初期構築ではない場合、DEVELOPMENTメニューにあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/2879c8ba-9027-d663-095a-e026b248389c.png)



作成するアプリケーションの種別を選択します。

- **Bot**：会話形式のアプリケーションを作成する場合
    - 今回の環境のメインとなるユーザと言語モデルの対話部分です。
- **Tab**：Teams, OutlookなどにWebコンテンツを埋め込む場合 (Teamsの上部にあるやつ)
    - プロンプトテンプレートとQAを置いたり？（次回以降調査）
- **Message Extention**：
    - プロンプト共有とかに使えそう？（次回以降調査）

まずはチャットボットを作成するので、**Bot**を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b17c9c08-b539-d034-2fd8-c8cefe813cad.png)


一から作って理解したかったため、**Basic Bot**を選択しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/b416dbae-225d-165d-fc43-ae63cc31d3e0.png)

開発言語を選択します。
Javascriptの上位互換という言葉に惹かれて**TypeScript**を選択しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/09396447-fdb8-b25d-3747-94d9fc5f986c.png)






--------------------------------------------------------------------------------

# 3. エミュレータの起動 → ローカルでのデバッグ環境を整える
VSCodeのデバッグ機能でデバッグを実行します。
左側タブの虫マークをクリックし、実行ボタン(緑の▶)をクリックします。
Chromeを選択した場合、ChromeでTeamsのエミュレータが立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/cd997216-c39e-1117-ddf9-9889dd264acb.png)


Chrome上で立ち上がったエミュレータでは、
先ほど作成した雛形プロジェクトがデプロイされた世界線のTeamsが模擬されています。
**追加**を選択すると、Teamsに雛形プロジェクトが追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/a1744992-d458-500a-6e30-87e91a6aa17f.png)

雛形のBotがどんな感じかChrome上で確認できます。
また、エミュレータを立ち上げたままローカルのソースコードを編集して保存すると、自動でエミュレータ側にも反映されます。
これでローカルでのデバッグ環境が整いました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/cf9c5492-896c-ccf2-2481-0cc51ad6d801.png)

















-------------------------------------------------------

# 4. ユーザが送信したメッセージをバックエンドに流す

今回は以下のような構成とします。

- a.ユーザがTeams上でメッセージ入力
- b. メッセージハンドラーでメッセージを受け取り、FlaskにメッセージをPOSTする
- c. FlaskがメッセージをAzure OpenAIにPOSTし、モデルからの出力を受け取る
- d. ユーザのチャット画面にモデルの出力を表示


>**ハンドラー（Handler）は、特定のアクションやイベントに対して処理を行うためのコンポーネントや関数です。ボットやアプリケーションの中で、ユーザーの入力やシステムの状態変化など、さまざまなイベントが発生する場合、それに対応する処理を行うためにハンドラが使用されます。**


## ①TeamsSDKでPOSTリクエストの処理を記述

先ほど作成した雛形プロジェクトの```teamsBot.ts```を編集します。
(TypeScript初心者なのでコメント間違ってたらすみません、、)

```ts:
// TeamsActivityHandlerを継承したTeamsBotクラス
export class TeamsBot extends TeamsActivityHandler {
  // コンストラクタ
  constructor() { 
    super(); // 親クラスのコンストラクタを呼び出し

    // メッセージハンドラーを作成。contextにユーザからの送信データが含まれている。
    this.onMessage(async (context, next) => {
      console.log("Running with Message Activity.");

      let txt = context.activity.text; // 入力テキストを取り出す
      const removedMentionText = TurnContext.removeRecipientMention(context.activity); //メンションを除去
      if (removedMentionText) {
        // 値を小文字に変換、改行を削除、空白削除
        txt = removedMentionText.toLowerCase().replace(/\n|\r/g, "").trim();
      }
      
      // FlaskにPOST
      const post_body:{msg: string} = {
        msg: txt
      }

      // Flaskからのレスポンスに含まれるデータを取り出す
      const response = await axios.post("http://localhost:5000/api/test", post_body)
      const message:string = response.data.message;

      // ユーザのチャット画面にmsgを表示
      await context.sendActivity(MessageFactory.text(message))

      // 次のハンドラーが処理可能な状態にする
      await next();
    })
  }
}
```


上記のコードでメッセージを**http://localhost:5000/api/test**
にPOSTすることができます。
まだWebサーバ側でエンドポイントを作っていないので以下のようなエラーが返ってきます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/8fa917b1-0988-ca50-55bd-da7a5cd1ad3f.png)



## ②POSTリクエストを受け取るWebサーバを作る(Flask)

FlaskでWebサーバを構築し、エンドポイントを作ります。
(※必要に応じてPython仮想環境を作成します。)

まずは必要なパッケージをインストールします。

```sh:
pip install flask
```

VSCodeで別のウィンドウを開き、**api**フォルダを作成します。
apiフォルダの下に**app.py**を作成し、以下のようなコードを配置します。

```py:api/app.py
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/api/test", methods=["POST"])
def post_test():
    user_input = request.json['msg']

    response = {
        "message": user_input + " :)"
    }
    
    return jsonify(response)


if __name__ == "__main__":
    app.run(debug=True) # ローカル&デバッグモード有効化で起動
```

また、後ほど使うと思われるので**requirements.txt**を作成し、
必要なパッケージを記載しておきます。


```py:api/requirements.txt
flask
```

ここまでで以下のようなフォルダ構成になっているかと思います。
（Python仮想環境を作成 & githubで管理しているため、それらが含まれていますが無くても大丈夫です。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/38ec9db1-455b-e53c-7bfc-d645d434a669.png)


実行確認を行います。

```sh: Webサーバ起動
cd api
python app.py
```

```sh:POSTリクエスト送信
curl -X POST -H "Content-Type: application/json" -d '{"msg": "Hello"}' http://localhost:5000/api/test

# JSON形式でレスポンスが返ってこればOKです。
{
  "message": "Hello :)"
}
```


## ③Teams→Flaskの繋ぎこみ

繋ぎこみといってもWebサーバを起動させたまま、エミュレータ上からメッセージ送信をしてみるだけです。
既にHTTPリクエストによって繋ぎこまれてます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/1c783920-3efb-3509-2d82-8d9645d75284.png)

ユーザの入力をちょっと明るくしてオウム返しするチャットボットができました。

Flask側でAzure OpenAIのエンドポイントに流す処理を追加し、ユーザの入力に応じてモデルの出力を得る機能を実装します。

## ④Azure OpenAIを使用したチャット機能を作成

今回は、今後の機能拡張を考慮してLangchainを用いてPythonから言語モデルを使用する構成とします。

まず、Azureポータルからリソースを作成し、APIキー、エンドポイントなど必要な情報を控えます。

https://qiita.com/sakue_103/items/582198236c59d4d8147c


ユーザの入力内容をAzure OpenAIに投げる処理を書きます。
**api/modules**フォルダを作成し、**chat_agent.py**を作成します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/176c6820-46fe-76ea-1359-17652e2ba184.png)


```py:chat_agent.py
from langchain.chat_models import AzureChatOpenAI
from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationChain
from langchain.prompts import (
    ChatPromptTemplate, 
    MessagesPlaceholder, 
    SystemMessagePromptTemplate, 
    HumanMessagePromptTemplate
)
import os

TEMPLATE = """あなたはアシスタントです。
小学生でもわかる例えを使用して説明すべきです。
"""

# メモリを作成。会話履歴を保持
memory = ConversationBufferMemory(return_messages=True)

class Chat_Agent:
    def __init__(self):
        self.llm = AzureChatOpenAI(
            openai_api_type = "azure",
            openai_api_version = os.getenv('API_VERSION'),
            openai_api_key = os.getenv('OPENAI_API_KEY'),
            openai_api_base = os.getenv('ENDPOINT'),
            deployment_name = os.getenv('MODEL_NAME'),
            temperature=0.1
        )

    def chat(self, user_input):
        # プロンプトを作成
        prompt = ChatPromptTemplate.from_messages([
            SystemMessagePromptTemplate.from_template(TEMPLATE),
            MessagesPlaceholder(variable_name="history"),
            HumanMessagePromptTemplate.from_template("{input}")
        ])

        # 会話用チェーンを作成
        conversation = ConversationChain(
            memory=memory,
            prompt=prompt,
            llm=self.llm
        )

        # 会話する
        response = conversation.predict(input=user_input)

        return response

if __name__ == "__main__":
    test = Chat_Agent()
    
    res1 = test.chat("AWSってなに？")
    print(res1)

    res2 = test.chat("拡張性について知りたい。")
    print(res2)
```

実行に必要なパッケージをインストールします。requirements.txtにも追記しておきます。

```sh:
pip install langchain openai
```


また、環境変数を設定します。
langchain側で指定の変数名があるため、一致させておきます。

```sh:
$Env:OPENAI_API_KEY="APIキー" 
$Env:ENDPOINT="モデルのエンドポイント"
$Env:MODEL_NAME="モデルのデプロイ名"
$Env:API_VERSION="2023-03-15-preview"
```

起動確認を行います。

```sh:
python chat_agent.py


# res1
AWSは、Amazon Web Servicesの略で...
例えるなら、AWSはインターネット上の大きな倉庫のようなもので、...

# res2
拡張性とは、システムやサービスが必要に応じて柔軟に拡大することができる性質のことです...
小学生に例えると、拡張性があるサービスは、レゴブロックのようなものです。...
```


## ⑤チャット機能を追加・動作確認

**api/app.py**を編集し、先ほどのチャット機能を統合します。


```py:api/app.py
from flask import Flask, request, jsonify
from modules.chat_agent import Chat_Agent
chat_client = Chat_Agent()

app = Flask(__name__)

@app.route("/api/test", methods=["POST"])
def post_test():
    # ユーザの入力を取り出す
    user_input = request.json['msg']

    # Azure OpenAIに投げる
    res = chat_client.chat(user_input)
    
    response = {
        "message": res
    }
    
    return jsonify(response)


if __name__ == "__main__":
    app.run(debug=True) # ローカル&デバッグモード有効化で起動
```


Webサーバを起動した後、エミュレータからメッセージを入力してみます。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393221/84cd44ab-61f6-44fb-c768-b207a32bbfc5.png)


会話できていますね
ただ、システムプロンプトの**小学生でもわかる例えを使って**という指示が無視されているような気がします、、（要調査）


# 次回
本番環境を想定して、次回は以下を実施します。

- 自宅のPCに入っているTeamsにアプリケーションをデプロイ
- バックエンドをAzure Containers Appsに移行