TeamsとChatGPTを利用したチャットBOTを作る

皆さん、Teams使ってますか？

結構会社とかで採用されているところは多いのではないでしょうか。TeamsでGPTとチャットができたらな～なんて思ったりすることもありますよね。。

今回はそんなTeamsのチャットボット（GPT）を作ってみたいと思います。

私自身Teamsを会社で使ってるのですが、セキュリティが厳しくBOTを気軽に入れられないので一旦は、他社の方々向けになりますかね。

個人で作っているというところもあり、Teams自体ではテストできていませんが、BotFramework EmulatorというTeamsのBOTを作成する際にデバッグする環境がMicrosoftから用意されているので今回はそれを使います。

自社でも使えたらいいなとか思ったり・・

▼完成形▼
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/78247fa7-67d1-b3a0-6317-1ca235237573.png)



ここで、会話セッションをスタートすると、会話が始まります。

※このセッションは何をさしているかというと、会話履歴をGPTに投げているためになります。このセッションはいわば会話履歴ということですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/561df166-f1bc-70a7-9b84-b03b1e6cd2bc.png)


こんな感じで、ドラえもんのことを聞いたら答えてくれます。

# 今回使うSDKなど

- BotFameworkSDK v4
    - https://github.com/microsoft/botframework-sdk
- BotFrameworkEmulator
    - https://github.com/microsoft/botframework-emulator

# では、早速APIから作っていきます。

https://github.com/umaxiaotian/TeamsExtension

### 今回やることとして以下の機能を実装していきます。

- 会話履歴を保存
- 会話履歴から回答

まずいきなりですが、全APIコードになります。

```python
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from langchain.llms import OpenAI
import openai
import os
from dotenv import load_dotenv

# .envファイルから環境変数を読み込む
load_dotenv()

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,   # 追記により追加
    allow_methods=["*"],      # 追記により追加
    allow_headers=["*"]       # 追記により追加
)

# Pydanticモデルを定義

class PageData(BaseModel):
    pageid: str
    qa: str
    history: object

os.environ["OPENAI_API_KEY"] = os.getenv("API_KEY")
openai.api_key = os.environ["OPENAI_API_KEY"]

@app.get("/")
async def root():
    return {"message": "Hello Obachat Engine!"}

@app.post("/question/")
def process_data(data: PageData):
    message = [{"role": "system", "content": "あなたは賢いAIです。あなたは下記の内容に答えてください。"}]

    for col in data.history:
        USER = col[0]
        TALK = col[1].replace("\n", "")
        message.append({"role": f"{USER}", "content": f"{TALK}"})
    message.append({"role": f"user", "content": f"{data.qa}"})
    res = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=message,
     
        temperature=1
    )

    print(res["choices"][0]["message"]["content"])
    return {"ans": res["choices"][0]["message"]["content"]}
```

今回は、FastAPIで実装しています。

### では、コードの要所を説明して行きたいと思います。

まず、PydanticでCLIENTから受け取るJSONについて定義したいと思います。

```python
class PageData(BaseModel):
    qa: str
    history: object
```

ここの部分で定義しました。

qaに質問内容が、historyにはobject形式で会話履歴が入るようにしました。

下記で、POSTメソッドで/questionがコールされたときのイベント処理を記載します。

```python
@app.post("/question/")
def process_data(data: PageData):
```

取得した、会話履歴をmessageのobjectにappendします。

```python
message = [{"role": "system", "content": "あなたは賢いAIです。あなたは下記の内容に答えてください。"}]
for col in data.history:
        USER = col[0]
        TALK = col[1].replace("\n", "")
        message.append({"role": f"{USER}", "content": f"{TALK}"})
```

ちなみに会話履歴を残すためには、下記のようにする必要があります。

```python
{"role": "system", "content": "あなたは賢いAIです。あなたは下記の内容に答えてください。"}, 
{"role": "user", "content": "ドラえもんは何歳？"},
{"role": "assistant", "content": "45歳"},  
{"role": "user", "content": "さっきなんて言った？"}       
```

上記のように役割と、会話内容を入れたObject形式を

```python
res = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=message,
        temperature=1
    )
```

ここのmessagesで読み取ってやります。これで、会話履歴は補完されました。

あとはこれを返してやるだけです。

```python
return {"ans": res["choices"][0]["message"]["content"]}
```

# 次は本題のBOT(Client)を作っていきます。

今回はコードが長すぎなので、私のGithubのリポジトリを案内させてください。

https://github.com/umaxiaotian/TeamsExtension

こちらのAPI.pyはAPIのコードです。

下記のような感じのコマンドを入力すると実行できます。※Uvicornは入れておいてくださいね。

```python
uvicorn API:app --host 0.0.0.0 --port 8888 --reload
```

## では本題のコード解説です。

### 構造

まず、このコードがどのような構造になっているかというと、セッション開始というボタンが用意されており、このセッションを開始すると、会話内容の記録が始まります。

APIをコールする際は、会話履歴＋質問内容を合わせて送信し、コールバックが帰ってきたら、その内容をTeams上に表示します。

下記の部分で、カード＆ボタンを定義しています。

```jsx
const menu = CardFactory.heroCard(
    'メニュー',
    undefined,
    CardFactory.actions([
        {
            type: 'postBack',
            title: '新しいAIチャットセッションを始める',
            value: '#TALKSTART'
        },
        {
            type: 'postBack',
            title: '会話セッションを終了する',
            value: '#TALKEXIT'
        },
        {
            type: 'postBack',
            title: 'ヘルプ',
            value: '#HELP'
        }
    ])
);
```

↓この部分。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/b41464ec-92a7-90a1-0bcf-b3a9c2a7374b.png)

下記の部分は、APIにコールするときの関数になります。

urlの部分はそれぞれの環境によって違うかもなので、合わせて変更してください。

下記でユーザーの質問内容と、会話履歴を送信します。

```jsx
async postToQuestionAPI(userMessage, history) {
        const url = 'http://127.0.0.1:8888/question';

        // JSONデータを作成
        const data = {
            qa: userMessage, //ユーザーの質問内容
            history: history  //会話履歴
        };

        try {
            // POSTリクエストを送信して応答を取得
            const response = await axios.post(url, data);
            // console.log(response.data)
            return response.data['ans'];
        } catch (error) {
            console.error('Error sending POST request:', error.message);
            return 'エラーが発生しました。';
        }
    }
```

下記が、流れを書いている部分になります。

このonMessageメソッドはBotFramework v4についている関数で、ユーザーからメッセージが来た時に必ず発火する関数になります。

その中で、#?から始まり?#で終わるもの、#で始まるもの、@で始まるものは、システムコマンドとして受け取りたいため、この文言が来たら、APIへはコールせず内部処理に流すようにします。

会話セッションの開始や、ヘルプの処理、セッションの終了の処理が記述してあります。

```jsx
this.onMessage(async (context, next) => {
            // 正規表現パターンを定義
            const pattern = /^(#.*\?#|@.*|#.*)$/;
            const userMessage = context.activity.text;
            // 文字列が正規表現パターンにマッチするか判定
            if (pattern.test(userMessage)) {
                // 文字列が正規表現パターンにマッチするか判定
                switch (userMessage) {
                    case '#TALKSTART':
                        conversationIsStart = true
                        await context.sendActivity(MessageFactory.text(NewSessionText, NewSessionText));
                        break;
                    case '#HELP':
                        await context.sendActivity(MessageFactory.text(helpText, helpText));
                        break;
                    case '#MENU':
                    case '@obabot':
                        const message = MessageFactory.attachment(menu, welcomeText);
                        await context.sendActivity(message);
                        break;
                    case '#TALKEXIT':
                        if (conversationIsStart == true) {
                            conversationData = {}
                            conversationIsStart = false;
                            const conversationNone = `会話セッションを終了いたしました。またのご利用をお待ちしております。`
                            await context.sendActivity(MessageFactory.text(conversationNone, conversationNone));
                        } else {
                            const conversationNone = `会話セッションが開始されていませんので、セッションを終了する事ができませんでした。`
                            await context.sendActivity(MessageFactory.text(conversationNone, conversationNone));
                        }
                }
```

こちらの正規表現にマッチしなかったものは、APIコールするようにしています。

```jsx
const pattern = /^(#.*\?#|@.*|#.*)$/;
```

下記は、メンバーが追加されたときに発火するメソッドになります。

メンバーが追加された際にBOTは使い方を簡単に提示する必要があるかなと思い実装しています。

```jsx
this.onMembersAdded(async (context, next) => {
            const membersAdded = context.activity.membersAdded;
            for (let cnt = 0; cnt < membersAdded.length; ++cnt) {
                if (membersAdded[cnt].id !== context.activity.recipient.id) {
                    // カードと応答メッセージを送信
                    const message = MessageFactory.attachment(menu, welcomeText);
                    await context.sendActivity(message);
                }
            }
            // next() を呼び出すことで、次の BotHandler が実行されることを保証します。
            await next();
        });
```

下記が出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/e16a2d56-432d-54f0-42f4-ee3f3e367b95.png)




ここまでくれば、あとは使うだけなので、BOTをビルドした後にインストールして使ってみてくださいね。

私は残念ながら、すぐには使えないかもなので、ビルドして使った人いたら感想教えて欲しいです🙏

本当は自社で使いたいんだけどなぁ（　＾ω＾）・・・

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/b6daed65-918e-a1ca-382f-443f76719e80.png)
ドラえもん久しく見てないな・・・・