Pinecone Assistantを使用してみた

## 1. はじめに
Pinecone Assistantを利用してchatBotを作成してみました。Pinecone Assistantは、テキストファイルやPDFからRAG（Retrieval-Augmented Generation、検索拡張生成）を構築できるサービスです。今回は、楽天APIを使用してホテル情報を取得し、そのデータを元にRAGを構築してみました。FlaskサーバーからPinecone Assistantをストリーミングで呼び出し、ユーザーからの質問に対してchatBotが応答できるようにしました。

## 2. データファイルの作成
楽天のAPIを使用して、8,890件のホテルデータを抽出しました。各ホテルの情報を1件1ファイルとして取得するプログラムを作成しましたが、Pinecone Assistantの無料版では最大10ファイルまでしかアップロードできないため、最終的にはすべてのデータを1つのファイルに結合しました。

以下は、ホテル情報を取得するためのPythonコードです。
```python
#
#  省略
#
    # URLはAPIのエンドポイント
    response = requests.get(URL)
    if response.status_code == 200:
        data = response.json()
        hotelinfomations = data["hotels"]
        for hotel in hotelinfomations:
            info = hotel["hotel"][0]["hotelBasicInfo"]
            text = createTextFile(info)

            with open(f"{info['hotelNo']}.txt", "w", encoding="utf-8") as f:
                f.write(text)
    else:
        print(response.status_code)
#
#  データの編集
#
def createTextFile(info):
    text = f"hotelName：{info["hotelName"]}\n"
    text += f"hotlInfo：{info["hotelSpecial"]}\n" 
    text += f"useReview：{info["userReview"]}\n"
    text += f"access：{info['access']}\n"
    text += f"address：{info['address1']}{info['address2']}\n" 
    
    return text  
```
#### 取得した情報
各ホテルごとにAPIの戻り値から次の情報を取得しました。ユーザレビューをもっと取得したかったのですが、力不足であきらめました。
1. ホテル名
2. ホテル情報
3. 最新のユーザレビュー１件
4. アクセス方法
5. 住所

#### 結合したファイルのイメージ
各ホテルごとのファイルを結合したイメージは次のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1284235/6f12699a-900a-bcc3-6fb1-c322b65ba037.png)

## 3. Pinecone Assistantへのアップロード

### 3.1　アシスタントの作成 
Pineconeのコンソールから"Create an Assistant"ボタンを押下して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1284235/c7ccfaea-aff6-cdbf-5281-3ebdf8f665d2.png)

### 3.2 ファイルアップロード

作成したホテル情報のデータファイルをPinecone Assistantにアップロードします。プログラムからでもアップロードできますが、ブラウザからファイルアップロードしました。画面に右端の"Drag and Drop"の領域にファイルをドラッグすることでアップロードできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1284235/a859b714-08ce-a235-6699-6c9715c91d3d.png)

### 3.3 確認

データ登録完了後、Pinecone Assistantのコンソールから質問と回答を確認することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1284235/446184d3-fb51-b4f2-2be9-1cc9eaab7341.png)


## 4. chatBotの実装について
前提として、PineconeのAPIキーが必要です。Pinecone Assistantを使用して、ユーザの任意の質問をもとにホテル情報を検索するchatBotを実装しました。

### 4.1 プログラムについて
Flaskを使用してサーバ側の処理を作成しました。

#### Pinecone Assistant Clientの初期化
PineconeのAPIキーと、作成したPinecone Assistantの名前（hotelsearchassistant)を使用してPinecone Assistantのclientを初期化します。

```python
    pc = Pinecone(api_key=pinecone_key)
    # Pinecone Assistant名の設定
    pinecone_assistant = "hotelsearchassistant"
#
#  省略
#
def get_assistant():
    assistant = pc.assistant.Assistant(
        assistant_name= pinecone_assistant, 
    )   
    return assistant
```
#### Pinecone Assistantとの応答（サーバ側処理）
クライアントからの質問を受信し、Pinecone Assistantと接続し、回答をストリーミング形式で受信・処理します。回答はJSON形式でも受信可能です。

```python
from flask import Flask, request, jsonify, render_template
from pinecone_plugins.assistant.models.chat import Message
from flask_socketio import SocketIO

app = Flask(__name__)
socketio = SocketIO(app)
#
# 途中のコードを省略
#
@app.route('/chat', methods=['POST'])
def chat():
    try:
        message = request.json['message']
        assistant = get_assistant()
        msg = Message(content=message)
 
        chunks = assistant.chat(messages=[msg],stream=True)
        
        for chunk in chunks:
            if chunk.type == "content_chunk" :
                socketio.emit('message', chunk.delta.content)
            elif chunk.type == "message_start" :
                socketio.emit('message', "$$start$$")
            elif chunk.type == "message_end" :
                socketio.emit('message', "$$stop$$")
        
        # 
        answer = "end" 
                
        return jsonify({
            'answer': answer,
        })

    except Exception as e:
        print(e)
        return jsonify({'error': str(e)}), 500
```
* ストリーミング処理: Pinecone Assistantから受け取ったデータを、その種類に応じて適切に処理し、Socket.IOを通じてクライアントにリアルタイムで送信しています。ストリーミングの開始と終了時には、それぞれ`$$start$$`と`$$stop$$`をクライアントに返しています

* レスポンス: ストリーミングが完了した後、単純な完了メッセージ（"end"）を返していますが、これはリクエストの終わりを示すものです

#### クライアント側の処理(サーバーからの回答を受ける部分)
Socket.IOを使用してサーバーからストリーミングされるメッセージを処理し、画面にリアルタイムで表示させました。
```javascript
        socket.on('message', (() => {
            let streamDiv, streamString;
        
            return data => {
                const chatContainer = document.getElementById('chat-container');
        
                if (data === "$$start$$") {
                    chatContainer.querySelector('.bot-message:last-child .loading')?.parentElement.remove();
                    streamDiv = document.createElement('div');
                    streamDiv.className = "message bot-message";
                    chatContainer.appendChild(streamDiv);
                    streamString = "";
                } else if (data === "$$stop$$") {
                    streamDiv = streamString = null;
                } else if (streamDiv) {
                    streamString += data;
                    streamDiv.innerHTML = DOMPurify.sanitize(marked.parse(streamString));
                }
        
                chatContainer.scrollTop = chatContainer.scrollHeight;
            };
        })());
```
1. メッセージ受信の準備
   * Socket.IOの'message'イベントをリッスンします
   * 受信したデータに応じて適切な処理を行います

2. メッセージの種類別処理
   * ストリーミング開始 (\\$\\$start\\$\\$):
      * 既存のローディング表示を削除します
      * 新しいメッセージ表示用のdiv要素を作成し、チャットコンテナに追加します
      * メッセージ累積用の文字列を初期化します
   * ストリーミング中のメッセージ内容
      * 受信したデータをメッセージ文字列に追加
      * Markdown形式に変換し、安全にHTMLとして表示
   * ストリーミング終了 (\\$\\$stop\\$\\$)
     * 使用していた変数をクリアします 
　
3. 表示の更新:
   * 新しいメッセージが追加されるたびに、チャットコンテナを自動スクロールします

## 5. おわりに
Pinecone Assistantを活用すると、複雑な設定なしで、ファイルをアップロードするだけで簡単にRAGを構築できることを実感しました。また、応答でストリームを選択すると、リアルタイムの表示が可能です。楽しく遊べたので無料版のトークン数の上限をもうちょっと上げてくれたらと思いました。