【Part2】NotionとLangChainを統合させてみた

Part1で、NotionとLangchainを、統合しQAが行える事がわかりましたね。

ではPart２では何をするのかというと、ChromeやEdgeから開いているNotionページの質問を投げかけて答えさせるようなチャットボット拡張機能を作ってみたいと思います。

※JS周りなのであんまりAI関連ではないかもしれませんが・・・

APIはいつものFastAPIをつかって、Part1をラッピングします。

今回の拡張機能の仕様

- NotionのページのpageIDと質問内容をAPIに送信してその戻り値をチャットに表示する。
- Notion以外の時は、対応していない旨のモーダルを出す。
    - 今回はとりあえずNotionだけ

▼完成形▼ 【Notionページの場合】

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/3709d1fa-f0d6-df89-f834-bcb95319005b.png)


会話していないときは、Enptyコンポーネントを呼んで、「質問履歴がありません…」を表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/6a1464de-470d-70fc-26c1-78a6f8b9af41.png)


▼完成形▼ 【Notion以外のページの場合】
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/f907fb7b-4af2-f82e-2631-3a8e675e4b33.png)


# まずはAPI

```jsx
import os
import openai
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware

from llama_index import ListIndex,NotionPageReader

app = FastAPI()

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

integration_token ="" #Notionのキー
os.environ["OPENAI_API_KEY"]="" #OPENAI API KEY
openai.api_key = os.environ["OPENAI_API_KEY"]
# Pydanticモデルを定義
class PageData(BaseModel):
    pageid: str
    qa: str
    

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

@app.post("/question/")
def process_data(data: PageData):
    #読み込み
    documents = NotionPageReader(integration_token=integration_token).load_data(
        page_ids=[f"{data.pageid}"]
    )
    index = ListIndex.from_documents(documents)
    query_engine = index.as_query_engine()
    response = query_engine.query(data.qa)

    return {"ans": response.response}
```

とりあえず、こんな超シンプルコードを書きます。

コードの解説をちょっとすると、POSTメソッドで、/questionにリクエストが有り、JSONにpageidとqaが入っていれば、その内容を問い合わせて、リターンするといったようなものになります。

# 拡張機能の開発を始める

始めるに当たって、まずはchrome/Edgeの拡張機能の構造を知っておく必要があります。

私の場合、拡張機能を作ったことがあるので、ある程度頭に入っていたのですんなり作れていますが、初めての人はまずは構造を理解してください。

[Chrome拡張の作り方 (超概要) - Qiita](https://qiita.com/RyBB/items/32b2a7b879f21b3edefc)

↑で大体分かりますかね。

今回はここの説明にある、popupを作っていきたいと思います。

素のJSで作るのはあんまりおもしろくありませんし、React使いたいなあと探していたところ、こんなフレームワークを見つけました。

https://github.com/PlasmoHQ/plasmo

このフレームワークで作れば、下記のブラウザでも動作する拡張機能が作れるらしいです。

- chrome-mv3 (default)
- firefox-mv2
- firefox-mv3 (experimental)
- Any chromium-based browser (e.g. Edge, Brave, Opera, etc.) should work, e.g:
    - edge-mv3
    - brave-mv3
    - opera-mv3
    - safari-mv3

ほぇ。。。すごい・・

じゃあまずは、ライブラリのインストール

```python
pnpm create plasmo example-dir
cd example-dir
pnpm dev
```

これで、大まかな物は入ります。

ここにいろんなものを付け足していきます。

私はpnpmでaxios(APIコール用）と、antd(UIコンポーネント）を入れました。

ちなみに今回使ったAntDesign UIコンポーネントですが、Reactだと結構有名なUIコンポーネントになります。

[Ant Design - The world's second most popular React UI framework](https://ant.design/)

デザインも綺麗ですし、ダークモードもあったりで最近お気に入りで使っています。

UIコンポーネントは簡単に画面構成が作れていいですね～～

つぎに、今回編集していくpopup.tsxを開きます。

下記（私のリポジトリ）の中にある、popup.tsxを参考にしてください。

https://github.com/umaxiaotian/NotionExtention

全部説明すると結構長いので、必要なところを抜粋して解説します。

```jsx
// メッセージ追加時に常に一番下にスクロール
  useEffect(() => {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
      // 現在のタブのURLを取得
      const currentURL = tabs[0].url;
      // console.log(currentURL); // 現在のページのURLがコンソールに表示されます
      const splittedArray = currentURL.split(/[-/]/);
      const page_id = splittedArray[splittedArray.length - 1];

      // console.log(page_id); // 出力結果: ID
      setPageid(page_id)
      if (currentURL.includes("notion.so")) {
        setIsModalOpen(false)
      } else {
        setIsModalOpen(true)
      }

    });
    messageContainerRef.current.scrollTop = messageContainerRef.current.scrollHeight;
  }, [messages]);
```

ここのuseEffectで初期時にchrome.tabs.queryから現在のタブのURLを取得して来ます。

その値をsetPageidに格納します。

※ここで何をしているかというと、今いるタブのアドレスが、Notionかそうでないかの判定と、Notion出会った場合はPageIDの取得を行っています。

ちなみに、NotionでなければsetIsModalOpen関数を発火させ、モーダルを表示するかしないかをBoolean値で入力します。

```jsx
//APIへ送信
      const data = {
        pageid: pageid,
        qa: userMessage.text,
      };
      const response = await axios.post('http://127.0.0.1:8888/question', data);
```

ここで、APIに値を送って、そのリターンをチャットに表示させます。

ちなみにpageidがNotionのページID qaが質問内容です。


# まとめ
結構作るの楽しいので、みなさんも作ってみてはどうでしょうか。
質問があれば、Twitter(X?)かここの質問コーナーで受け付けます。