watsonx.data Milvus + watsonx.ai RAGハンズオン Part4: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!

この内容は2024年11月27日(水)にホテル雅叙園東京で開催された「[IBM TechXchange Japan 2024](https://www.linkedin.com/posts/ibm_ibmtechxchange-activity-7270719294588542976-rA6u?utm_source=share&utm_medium=member_desktop)」で実施したwatsonxハンズオン「さわってみよう ベクトル・データベース watsonx.dataでRAG体験」の内容です。QiitaではPart1, Part2, Part3, Part4の四部構成で、　この投稿は**Part4**となります。

- [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)
-  [Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!](https://qiita.com/nishikyon/items/9531d3cbbc893822e494/)
- [Parr3: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!](https://qiita.com/nishikyon/items/a275d7a557e11d1b2d6b/)
- **Part4: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!(当投稿)**

# 0.前提
- [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)を実施済み

 - [Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)と同じ実施環境

尚、[Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!](https://qiita.com/nishikyon/items/9531d3cbbc893822e494/) および　[Parr3: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!](https://qiita.com/nishikyon/items/a275d7a557e11d1b2d6b/)は実施していなくいても、実行は可能ですが、Part2およびPart3の内容を使用していますので、ぜひ実施してからこちらに進んでください。

# 1. ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!

この内容はjupyter notebook https://github.com/IBM/japan-technology/blob/main/techxchange/2024-watsonx-handson-1/notebooks/techxchange_handson_04_chatbot.ipynb とほぼ同じです。Link先notebookはwatsonx.ai Studioでの実行を前提としていますが、当内容はLocalも含め一般的なpython実行環境が前提です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/562bcdfe-5881-d46e-5008-92d2a3eac671.png)

### 1. 必要なライブラリーのインストール
必要に応じてpipにて以下のライブラリーを導入します:
(最後にgradioのインストールがあるのが Part1,2,3と異なる部分です)
```py
pip install -Uq 'ibm-watsonx-ai>=1.1.15'
pip install -Uq 'langchain>=0.3.3'
pip install -Uq 'langchain-ibm>=0.3.1'
pip install -Uq 'langchain-milvus>=0.1.6'
pip install -Uq 'langchain-community>=0.3.2'
pip install -Uq 'pymilvus>=2.4.8'
pip install -Uq gradio
```

以下2〜5はPart1: Excelをベクトル化してベクトルDB Milvusに入れようの2〜5と全く同じです。
また7〜8はPart2: Part2: ベクトルDB Milvusに入ったデータで類似検索してみよう!の7〜8と全く同じです。

### 2. apikeyの設定
apikey をセットしてください。
IBM Cloud環境でのAPIKEYの取得方法は[こちら](https://qiita.com/nishikyon/items/fa8b951267efbe911fe7)を参考にしてください。

またwatsonx.dataとwatsonx.ai Studioは同じAPIKEYである前提で同じ変数を使用していますので、もし違う場合は2つ変数を準備するようにしてください。
```py
apikey = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
```

### 3. Milvus接続情報の設定
Milvus接続情報の値をセットしてください。
- Milvus Host名
- Milvus Port番号

Milvus接続情報の詳細取得手順は[こちら](https://github.com/IBM/japan-technology/tree/main/techxchange/2024-watsonx-handson-1/watsonx_data_get_milvus_info.md) を参照してください。

またmy_connection_argsに設定するkeyの詳細は[pymilvusのMilvusClientのパラメータ](https://milvus.io/api-reference/pymilvus/v2.5.x/MilvusClient/Client/MilvusClient.md)を参照してください。

```py
milvus_host="xxx.xxx.xxx.com"
milvus_port="YYYY"

# Milvus接続情報パラメータののセット
my_connection_args ={
 'uri': f'https://{milvus_host}:{milvus_port}', 
 'token': f'ibmlhapikey:{apikey}'
}
```

### 4. watsonx.ai Studio Project idの設定
watsonx.ai Studioで実行する場合は、このノートブックが実行されるプロジェクトからProject idを取得します。 watsonx.ai Studio以外で実行する場合は、Project idを入力してください。

**Hint**: project_id はプロジェクトを表示し、管理タブから project_id を取得可能です.

```py
import os
try:
    project_id = os.environ["PROJECT_ID"]
except KeyError:
    project_id = "<ProjrctID>"
```

### 5.watsonx.aiのAuthentication用のエンドポイントのURLの設定
Waston Machine Learningのインスタンスを作成したリージョンで決まります。 https://ibm.github.io/watson-machine-learning-sdk/setup_cloud.html#authentication より

Dallas: https://us-south.ml.cloud.ibm.com
London: https://eu-gb.ml.cloud.ibm.com
Frankfurt: https://eu-de.ml.cloud.ibm.com
Tokyo: https://jp-tok.ml.cloud.ibm.com

```py
watsonx_url = "https://us-south.ml.cloud.ibm.com" #ダラスの場合
```

### 6. 必要ライブラリーのImport
```py
import pandas as pd
from langchain.schema.document import Document
import json
from langchain_milvus import Milvus
import os
from ibm_watsonx_ai.metanames import EmbedTextParamsMetaNames
from langchain_ibm import WatsonxEmbeddings
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames as GenParams
from langchain_ibm import WatsonxLLM
from langchain.prompts import PromptTemplate
from langchain.schema.runnable import RunnablePassthrough
```
### 7. Embeddingモデルの取得
[Part1: Excelをベクトル化してベクトルDB Milvusに入れよう](https://qiita.com/nishikyon/items/81a6de44aaf52f49eb0f/)の11と全く同じです。
ここでは`intfloat/multilingual-e5-large`を使います

- https://huggingface.co/intfloat/multilingual-e5-large
- https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx&locale=ja#multilingual-e5-large

LangChainで使用できるwatsonx.aiのEmbeddingモデルintfloat/multilingual-e5-largeがあるので、今回はこちらを使用します:
- https://python.langchain.com/docs/integrations/text_embedding/ibm_watsonx/


> 尚、`intfloat/multilingual-e5-large`はオープンソースで公開されているので、watsonx.aiのEmbeddingモデルを使用しなくとも、ローカルにダウンロードすることで使用可能です。 その場合のコードはこちらです(今回は使用しません)
> ```py
> from langchain_huggingface import HuggingFaceEmbeddings
>from tqdm.autonotebook import tqdm
>
>embeddings = HuggingFaceEmbeddings(model_name="intfloat/multilingual-e5-large")
>```

<br>
下記は`apikey`はMilvusと同じものを使用しています。異なる場合は変数`apikey`を変更してください。

```py
# watsonx.aiのEmbeddingモデル取得
embed_params = {
    EmbedTextParamsMetaNames.TRUNCATE_INPUT_TOKENS: 512,
    EmbedTextParamsMetaNames.RETURN_OPTIONS: {"input_text": True},
}

embeddings = WatsonxEmbeddings(
    model_id="intfloat/multilingual-e5-large",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id
    )
```

### 8. ベクトルDB Milvusに接続
```py
from langchain_milvus import Milvus

vector_store = Milvus(
    embeddings,
    connection_args =my_connection_args,
    collection_name = my_collection
)
```

**ここからが、Part1とPart2とは異なる内容です。**

### 9. watsonx.ai LLMの取得　および　プロンプトテンプレートの作成

今回はLLMを3つ切り替えて試せるようにします。以下の3つのLLMを取得し、それに適したプロンプトテンプレートを作成します。またRAGなし(ベクトルDBの検索結果を使わずLLM自力回答)も切り替えて試せるようにRAGなし用のプロンプトテンプレートを作成しています。

- mixtral-8x7b-instruct-v01
- llama-3-2-3b-instruct
- granite-3-8b-instruct


Embeddingモデルと同様に下記apikeyはMilvusと同じものを使用しています。異なる場合は変数apikeyを変更してください。

#### 9-1. mixtral-8x7b-instruct-v01
```py
custom_llm = {}
rag_prompt = {}
no_rag_prompt = {}

# 使用するLLMのパラメータ: mixtral-8x7b-instruct-v0
key = 'mixtral-8x7b-instruct-v01'
generate_params = {
    GenParams.MAX_NEW_TOKENS: 16384,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1
}

# LangChainで使うllm: mixtral-8x7b-instruct-v0
custom_llm[key] = WatsonxLLM(
    model_id="mistralai/mixtral-8x7b-instruct-v01",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id,
    params=generate_params,
)

# LangChainで使うPrompt(RAGあり): mixtral-8x7b-instruct-v0
template = """
 
 
あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。
questionに答えるために、以下のcontextを使用し必ず日本語でanswerを作成してください。
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
回答を書くときは、context内の単語をできるだけ使⽤してください。context中に質問に対する回答がない場合は、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。「文書中に質問に対する回答が明記されていません。」と回答した場合、そこで回答を終わりにしてください。
contextの内容がブランクの場合、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。
セッションについて回答する場合は、タイトル、開始時刻、終了時刻、会場、概要、レベルを回答してください。回答が200文字以上の場合、回答はなるべく箇条書きを含めてわかりやすく回答してください。





 
 質問が質問の文章ではなく意味がわからない場合
もう少し詳しく説明していただけますか？


context: {context}
question: {question}
answer: 
"""

# Prompt Templateを作成します
rag_prompt[key] = PromptTemplate.from_template(template)


# LangChainで使うPrompt(RAGなし): mixtral-8x7b-instruct-v0
template = """
 
 
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。。
あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。





 
 質問が質問の文章ではなく意味がわからない場合
もう少し詳しく説明していただけますか？

question: {question}
answer: 
"""

# Prompt Templateを作成します
no_rag_prompt[key] = PromptTemplate.from_template(template)
```

#### 9.2 llama-3-2-3b-instruct
```py
# 使用するLLMのパラメータ:llama-3-2-3b-instruct
key = 'llama-3-2-3b-instruct'
generate_params = {
    GenParams.MAX_NEW_TOKENS: 8192,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1
}

# LangChainで使うllm: llama-3-2-3b-instruct
custom_llm[key] = WatsonxLLM(
    model_id="meta-llama/llama-3-2-3b-instruct",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id,
    params=generate_params,
)

# LangChainで使うPrompt(RAGあり): llama-3-2-3b-instruct
template = """

user

あなたは2024年にIBMが開発したAI言語モデル「Granite」です。あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。
questionに答えるために、以下のcontextを使用し必ず日本語でanswerを作成してください
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
回答を書くときは、context内の単語をできるだけ使⽤してください。context中に質問に対する回答がない場合は、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。「文書中に質問に対する回答が明記されていません。」と回答した場合、そこで回答を終わりにしてください。
contextの内容がブランクの場合、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。
セッションについて回答する場合は、タイトル、開始時刻、終了時刻、会場、概要、レベルを回答してください。回答が200文字以上の場合、回答はなるべく箇条書きを含めてわかりやすく回答してください。

user

question:{question}
context: {context}

assistant

"""

# Prompt Templateを作成します
rag_prompt[key] = PromptTemplate.from_template(template)

# LangChainで使うPrompt(RAGなし): llama-3-2-3b-instruct
template = """

user

あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。

user

question:{question}

assistant

"""

# Prompt Templateを作成します
no_rag_prompt[key] = PromptTemplate.from_template(template)
```

#### 9.3 granite-3-8b-instruct
```py
# 使用するLLMのパラメータ: granite-3-8b-instruct
key = 'granite-3-8b-instruct'
generate_params = {
    GenParams.MAX_NEW_TOKENS: 4096,
    GenParams.MIN_NEW_TOKENS: 0,
    GenParams.DECODING_METHOD: "greedy",
    GenParams.REPETITION_PENALTY: 1
}

# LangChainで使うllm: granite-3-8b-instruct
custom_llm[key] = WatsonxLLM(
    model_id="ibm/granite-3-8b-instruct",
    url=watsonx_url,
    apikey=apikey,
    project_id=project_id,
    params=generate_params,
)

# LangChainで使うPromptt(RAGあり): granite-3-8b-instruct
template = """
system
なたは2024年にIBMが開発したAI言語モデル「Granite」です。あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。
questionに答えるために、以下のcontextを使用し必ず日本語でanswerを作成してください
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
回答を書くときは、context内の単語をできるだけ使⽤してください。context中に質問に対する回答がない場合は、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。「文書中に質問に対する回答が明記されていません。」と回答した場合、そこで回答を終わりにしてください。
contextの内容がブランクの場合、「文書中に質問に対する回答が明記されていません。」とだけ回答してください。
セッションについて回答する場合は、タイトル、開始時刻、終了時刻、会場、概要、レベルを回答してください。回答が200文字以上の場合、回答はなるべく箇条書きを含めてわかりやすく回答してください。
context: {context}



user
question:{question}


assistant

"""


# Prompt Templateを作成します
rag_prompt[key] = PromptTemplate.from_template(template)

# LangChainで使うPromptt(RAGなし): granite-3-8b-instruct
template = """
system
あなたは2024年にIBMが開発したAI言語モデル「Granite」です。
あなたは親切で、礼儀正しく、誠実なアシスタントです。常に安全を保ちながら、できるだけユーザーの役に立つように詳しく回答してください。
回答には、有害、非倫理的、⼈種差別的、性差別的、有毒、危険、または違法なコンテンツを含めてはいけません。回答は社会的に偏⾒がなく、本質的に前向きなものであることを確認してください。
必ず⽇本語の文章で回答してください。知ったかぶりをしないでください。
質問が意味をなさない場合、または事実に⼀貫性がない場合は、正しくないことに答えるのではなく、その理由を説明してください。質問の答えがわからない場合は、誤った情報を共有しないでください。
回答が200文字以上の場合、回答はなるべく箇条書きを含めてわかりやすく回答してください。



user
question:{question}


assistant

"""

# Prompt Templateを作成します
no_rag_prompt[key] = PromptTemplate.from_template(template)
```

### 10.  Retrieverの作成
ここでは「[ Part3: ベクトル・データベース Milvusとwatsonx.ai LLMでRAGを構成して、質問をしてみよう!](https://qiita.com/nishikyon/items/a275d7a557e11d1b2d6b/)」の 「閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを使います.

類似スコアの閾値と検索件数をGUIで指定できるようにします。

#### 10-1. 閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを作成
```py
# 閾値以上の類似スコアを持つ文書のみを返す」CustomRetrieverを作成
# asyncの方は省略
from langchain.schema.vectorstore import VectorStoreRetriever
from langchain.schema.document import Document
from langchain.callbacks.manager import (
    CallbackManagerForRetrieverRun,
)
from typing import List

class CustomRetriever(VectorStoreRetriever):
    def _get_relevant_documents(
        self, query: str, *, run_manager: CallbackManagerForRetrieverRun
    ) -> List[Document]:
        top_k = self.search_kwargs.get("k", 4)
        docs_and_similarities = self.vectorstore.similarity_search_with_score(query, k=top_k)
                
        threshold = self.search_kwargs.get("score_threshold", 0)
       
        return [doc for doc, score in docs_and_similarities if score >= threshold and score <= 1]
```

#### 10-2. 類似スコアの閾値と検索件数を指定して、Retrieverを作成する関数を作成
「12. gradioでGUI作成」で使います
```py
# 類似スコアの閾値と検索件数を指定して、Retrieverを作成する関数
def get_Retriever(vectorstore, search_score_flag, score, k):
    if search_score_flag == "On": # 類似スコアの閾値を指定する場合
        return CustomRetriever(vectorstore = vectorstore, search_kwargs={"score_threshold": score, "k":k})
    else: # 類似スコアの閾値を指定しない場合
        return vectorstore.as_retriever(search_kwargs={"k": k})   
```

### 11. 検証用「類似検索のみ実施」で使用する類似度スコアと類似検索を返す関数の作成
- 「12. gradioでGUI作成」で使います。
- Retrieverでは類似度スコアは返さないので、GUIで検証用として「類似検索のみ実施」で使用する類似度スコアと類似検索を返す関数を別途作成します。

```py
def get_similarity_search_with_score(vectorstore, search_score_flag, query, **kwargs):
    top_k = kwargs.get("k", 4)
    docs_and_similarities = vectorstore.similarity_search_with_score(query, k=top_k)
    if search_score_flag == "On": # 類似スコアの閾値を指定する場合
        threshold = kwargs.get("score_threshold", 0)
        return [(score, doc) for doc, score in docs_and_similarities if score >= threshold and score <= 1]
    else: # 類似スコアの閾値を指定しない場合
        return [(score, doc) for doc, score in docs_and_similarities]        
```

### 12. テストしてみます (オプション・省略可能)
- 作成したRetrieverの類似検索テストしてみます
- LLMを指定してChainを作成し、質問してみます
- 「類似検索のみ実施」してみます

#### 12-1. 作成したRetrieverの類似検索テスト¶
]
以下に設定したretrieverをget_Retrieverを使用して取得:
- RAG=使用する(ON)
- 類似スコアの閾値=0.83
- 最大取得件数=10

```py
# 作成したRetrieverの類似検索テスト
query="RAGに関するセッションの詳細を教えてください"

retriever = get_Retriever(vector_store, "On", 0.83, 10)
docs = retriever.invoke(query)
# print(docs)
for doc in docs:
    print({"content": doc.page_content, "metadata": doc.metadata} )
    print("---------")       
```

出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/1015df14-b0fa-a7cb-0981-51fc4862a355.png)

#### 12-2. Chainの作成とLLMを使用した回答作成


以下に設定したretrieverをget_Retrieverを使用して取得:
- RAG=使用する(ON)
- 類似スコアの閾値=0.83
- 最大取得件数=10

その後LLM`mixtral-8x7b-instruct-v01`を指定してChainを作成しています。

```py
# Chainの作成
# 1つKeyを指定してください

# key = 'llama-3-2-3b-instruct'
key = 'mixtral-8x7b-instruct-v01'
# key = 'granite-3-8b-instruct'
retriever = get_Retriever(vector_store, "On", 0.83, 10)
rag_chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | rag_prompt[key]
    | custom_llm[key]
)
```
上記Chainのテストです

```py
# 質問してみます

query="RAGに関するセッションの詳細を教えてください"
# query="watsonx.dataに関するセッションの詳細を教えてください"
# query="TechXchangeについて概要を教えてください"
# query="量子コンピューター関連のセッションを教えてください"
print(rag_chain.invoke(query))
```

出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/44f9e757-24d9-40f8-2d69-ae405ce74251.png)


#### 12-3. 類似検索のみ実施

類似検索のみ実施する`get_similarity_search_with_score`のテストです。
類似スコアの閾値を指定する場合としない場合を、On/Offで切り替えできます。
Onの場合は類似スコアの閾値をscore_thresholdで設定します。
kは最大取得件数です。

```py
query="RAGに関するセッションの詳細を教えてください"

# 類似スコアの閾値を指定する場合
docs = get_similarity_search_with_score(vector_store, "On", query, score_threshold=0.85, k=3)
for score, doc in docs:
    print({"score": score, "content": doc.page_content, "metadata": doc.metadata} )
    print("---------")

print("===============================================================")
# 類似スコアの閾値を指定しない場合
docs = get_similarity_search_with_score(vector_store, "Off", query, k=3)
for score, doc in docs:
    print({"score": score, "content": doc.page_content, "metadata": doc.metadata} )
    print("---------")
```
閾値指定する場合としない場合の2パターンテストしています。
出力:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/5b3157a6-745f-334d-0438-a53ff0d45204.png)


### 13. gradioでGUI作成
[gradio](https://www.gradio.app/)を使ってGUIを作り、いろいろな条件でRAG構成を試してみましょう！

尚、会話履歴を読むようにしていないので、**毎回フルセンテンスで質問するようにお願いします**。

プロンプトを変更して試してもOKです。

public URLがインターネット上のURLになります。このLinkは72時間保持されます.

share_flag :
- Watson Studio使用の場合はTrue
- **Trueにするとインターネットに公開されるので、機密情報がある場合はTrueにしないようにしてください。**
- PC上のnotebookで実行する場合は、FalseにしてLocalPCでの実行が可能です。

下のセルを実行するとnotebookの場合はGUIが表示されます。ただし、表示されるGUIは使いにくので、localで実行している場合は`local URL`の値、watsonx.ai Studio使用の場合は`public URL`の値をクリックして、Webブラウザーで開いて操作してみましょう。<br>
エラーメッセージ等はnotebookの場合はnotebookに出ますので、エラーが発生した場合はnotebookのセル出力のGUIの下の方を参照してください。

```py
import gradio as gr

# Watson Studio仕様の場合はTrue
# インターネットに公開されるので、機密情報がある場合はTrueにしないようにしてください。
# PC上のnotebookで実行する場合は、FalseにしてLocalPCでの実行が可能です。

share_flag = True
# share_flag = False

def chat_streaming(message, history, RAG_flag, model_name, search_k, search_score_flag, search_score ):
    gr_retriever = get_Retriever(vector_store, search_score_flag, search_score, search_k)
    gr_rag_chain = None
        
    if RAG_flag == "Off":
        gr_rag_chain = (
            {"question": RunnablePassthrough()}
            | no_rag_prompt[model_name]
            | custom_llm[model_name] 
        )
    else:
        gr_rag_chain = (
            {"context": gr_retriever, "question": RunnablePassthrough()}
            | rag_prompt[model_name]
            | custom_llm[model_name] 
        )
    
    if message is not None:
        if len(message.strip()) < 1:
            yield "質問を入力してください"
            return
               
        partial_message = f"**{model_name}:**\n"
        for response in gr_rag_chain.stream(message):
            partial_message += response    
            yield partial_message 
            

def change_search_score(flag):
    if flag == "On":
        interactive_flag = True
    else:
        interactive_flag = False
    return gr.update(interactive = interactive_flag, visible= interactive_flag)    
    

def get_search_result(msg, search_k, search_score_flag, search_score):
    docs = get_similarity_search_with_score(vector_store, search_score_flag, msg, score_threshold=search_score, k=search_k)
    result_str =""
    if len(docs) < 1:
        return gr.update(value="検索結果がありません")    
    for i, (score, doc) in enumerate(docs):
        # result_str += str(i+1)+': {"content": ' + json.dumps(doc.page_content, ensure_ascii=False)+ ', "metadata": ' + json.dumps(doc.metadata, ensure_ascii=False)+'} \n'
        result_str += f"{i+1}: {{" \
                  f"  \"score\": {score},\n" \
                  f"  \"content\": {json.dumps(doc.page_content, ensure_ascii=False)},\n" \
                  f"  \"metadata\": {json.dumps(doc.metadata, ensure_ascii=False)}\n" \
                  f"}}\n---------\n"
    return gr.update(value=result_str)    
    
    
CSS = """
#row1 {flex-grow: 1; align-items: unset;}
    .form {height: fit-content;}
""" 


with gr.Blocks(fill_height=True,  css=CSS) as demo:
    gr.Markdown("## TechXchange Japan Q&A")

    with gr.Row(equal_height=False, elem_id="row1"):
        with gr.Column(variant="panel", scale=1):
            with gr.Accordion("設定"):
                RAG_flag = gr.Radio(choices=["On", "Off"], type="value", label="RAG構成", value="On", interactive = True)
                model_name = gr.Dropdown(["mixtral-8x7b-instruct-v01", "granite-3-8b-instruct", "llama-3-2-3b-instruct"], label="LLM Model", value="mixtral-8x7b-instruct-v01")
                search_k = gr.Number(label="類似検索の結果取得数(k) 最大15",minimum=1, maximum=15, value=5, interactive = True )
                search_score_flag  = gr.Radio(["On", "Off"], label="類似度スコアの閾値を設定", value="On", interactive = True)
                search_score = gr.Slider(minimum=0.7, maximum=0.9, step=0.01, value=0.8, label="類似度スコアの閾値" ,interactive = True, visible=True)
            gr.Markdown("- 類似度スコアの閾値を大きくすると類似検索結果件数が0件となり、「文書中に質問に対する回答が明記されていません。」という回答やLLMによっては回答を独自に作ってしまう場合があります 。その場合は類似度スコアの閾値下げて調整してください。")
            gr.Markdown("- Graniteは最大トークン数が少ないため,　取得数を上げたり類似度スコアの閾値を下げるとエラーになることがあります。その場合は取得数を少なくしたり類似度スコアの閾値上げて調整してください。")
            
        with gr.Column(scale=2):
            chatbot = gr.ChatInterface(fn=chat_streaming, type="messages", 
                            additional_inputs =[RAG_flag, model_name, search_k, search_score_flag, search_score], 
                            title="TechXchange Bot", fill_height=True)
            with gr.Accordion("類似検索のみ実施", open=False):
                msg = gr.Textbox(placeholder="類似検索したい文字列を入力してEnter",submit_btn=True, container=False)
                search_result = gr.Text(label="検索結果",container=True, interactive=False, show_label=True)
                msg.submit(fn=get_search_result, inputs=[msg,search_k, search_score_flag, search_score], outputs=search_result)             
                
                                    
    search_score_flag.change(fn=change_search_score, inputs=search_score_flag, outputs=search_score)
  

demo.launch(share=share_flag)
```

watsonx.ai Studio使用の場合は`public URL`の値をクリックします
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/960dfcc4-e3c8-252c-b1ec-6473d49148f3.png)

以下のようなGUI画面がWebブラウザーに表示されます:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294516/6ff7c59c-00f1-cddf-fcef-474df714aeb6.png)

LLMや類似度スコアを変更したりしていろいろ試してみてください。RAGではない直接LLMに質問する形式も設定できます。
回答がおかしい場合は、そもそも設定した条件でどのような類似検索結果が返ってきているかを「類似検索の実施」で確認してみてください。

類似検索結果が期待と違う場合は、LLMのプロンプトの変更、ベクトル化する元の文章の内容の再考(切り方(chunking)やJSON化したことなど)、Embeddingモデルの変更、検索方法を変えてみるなど様々なRAGの検索精度向上のためのチューニングや調整で対応が必要となってきます（ここでは扱いません)。

以上で「Part4: ベクトルDB Milvusとwatsonx.ai LLMでRAGを構成して、チャットアプリを作成してみよう!」は完了です。

watsonx.data Milvus + watsonx.ai RAGハンズオンも全て完了となります。
おつかれさまでした👏