OpenAI Assistants APIの "File Search" を使ってみる ～小林製薬編～ ベクトルストア

# Assistants APIがV2にアップデート

OpenAIのAssistants APIが[V2にアップデートしました](https://platform.openai.com/docs/assistants/whats-new)！
アップデートと言っても既存の機能の修正より、追加の機能が増えたような気がします。

アップデートの内容はやはりnpakaさんの記事が大変わかりやすいと思います。

https://note.com/npaka/n/n4438384c96d0


# File Searchを使ってみる
V2になり<b>"File Search"</b>という機能が増えました。
と言っても元は<b>"Retrieval"</b>という機能が今回リニューアルしたようです。

旧Retrieval、新File Searchはざっくり言うと<b>「ドキュメントを使ってアシスタントに外部からの知識を加えられる機能」</b>という感じです。

AIもの全般に言えることですが、うだうだ書くより試す方がどんな機能かがわかりやすいので使って見たいと思います。
<br>

ということで外部知識の基になる何らかの資料が必要なのですが、色々探し回った結果

### 紅麹
<br>

に行き着いたので、この資料を利用したいと思います。
<b>※小林製薬さんには何の恨みもありません</b>

https://www.mhlw.go.jp/stf/seisakunitsuite/bunya/kenkou_iryou/shokuhin/daietto/index.html

<br>
こちらの「リコール情報」「原因究明」「よくある質問」の3つのPDFを利用したいと思います。

File SearchではPDFだけではなく、docxなど他の形式でも読み込ますことができます。詳細は公式を見て下さい。

https://platform.openai.com/docs/assistants/tools/file-search/supported-files

<br>

アシスタントに厚生労働省の職員になってもらい、紅麹について<b>「何がリコールの商品か？」「どういう症状が出るのか？」</b>を聞いて回答できることを期待します。
<br>

# ベクトルストア作成

<b>※基本的に[公式の解説](https://platform.openai.com/docs/assistants/tools/file-search/quickstart?context=without-streaming)に則り、順序などは変えています。今回はストリーミングなしで行います。（GPTが完全に回答しきった後に操作）</b>

<br>
最初に資料から<b>ベクトルストア</b>を作成します。ベクトルストアは「テキストなどのデータをベクトルとして保存するデータベースのことで、意味的に類似する情報を探せるのが特徴…」だそうです。

https://atmarkit.itmedia.co.jp/ait/articles/2402/29/news042.html

<br>
とにかくAIが探しやすくするように、料理でいう<b>下処理</b>のようなことをまず行います。


:::note 
openaiはバージョン：1.25.0 です。
（インストール済みということで進めます）
:::


```Python:初期処理
import os
os.environ["OPENAI_API_KEY"] = "OpenAIKEYをセット"

from openai import OpenAI
client = OpenAI()
```
```Python:PDFアップロード
vs_name = "Benikoji info"
vector_store = client.beta.vector_stores.create(name=vs_name)

file_paths = ["紅麹を含む食品に関するリコール情報.pdf" ,"原因物質の特定について.pdf" ,"小林製薬の紅麹を含む健康食品に関するQA.pdf"]
file_streams = [open(path, "rb") for path in file_paths]

file_batch = client.beta.vector_stores.file_batches.upload_and_poll(
  vector_store_id=vector_store.id, files=file_streams
)
```
```Python:バッチ状況確認
print(file_batch.status)
print(file_batch.file_counts)
```
```:出力結果
completed
FileCounts(cancelled=0, completed=3, failed=0, in_progress=0, total=3)
```
<br>
vector_stores.createでベクトルストアを新規作成。（名前は適当）
PDFを読み込んでそれをupload_and_pollでアップロードを行います。

よほど重いファイルや大量のファイルでない限り、すぐに完了するようです。
作成されたベクトルストアはOpenAIマイページの[Storage]で確認できます。

![スクリーンショット 2024-05-03 221521.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3549938/1a0b398e-28fe-359c-9174-f38d015f616b.png)
<br>
1つのベクトルストアに複数のPDFがぶら下がっている感じです。（追加・削除できます）
PDF名はわかりやすい日本語の名前が推奨です。（後の表示のため）

# FileSearch用 アシスタント作成

次にアシスタント作成ですが、これは以前とほぼ変わらずtoolsに"file_search"をセットしてFile Searchができるようにするだけです。（既存のアシスタントの更新でも構いません。）

```Python:アシスタント作成
assistant = client.beta.assistants.create(
  name="File Search Benikoji",
  instructions="あなたは厚生労働省の局員です。与えられたナレッジベースから「紅麹」に関する健康被害の質問について答えて下さい。",
  model="gpt-4-turbo",
  tools=[{"type": "file_search"}],
)
```
```Python:リソースにベクトルストア追加
assistant = client.beta.assistants.update(
  assistant_id=assistant.id,
  tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
)
```

アシスタントにベクトルストアIDを指示します。（複数可）
アシスタント作成時にベクトルストアを追加できるようですが、公式がこのようにしていたのでそうしました。

# 問い合わせてみる
これで準備完了ですので、あとは問い合わせるだけです。
基本的なAssistants APIの流れはこちらをご参照ください。

https://qiita.com/nekoniii3/items/7128e1d0cec06e3ff6a4

※[公式](https://platform.openai.com/docs/assistants/tools/file-search/quickstart?context=without-streaming)とは若干書き方が異なります。前回に合わせたためこうしましたが、追加のファイルがある場合などは公式の方が使いやすいと思います。
<br>

```Python:スレッドの作成①
thread = client.beta.threads.create()
```


```Python:メッセージの作成②
prompt = "入院・通院をしている人の症状について教えてください。"

message = client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content=prompt,
)
```
```Python:run作成（ポーリングバージョン）③
run = client.beta.threads.runs.create_and_poll(
  thread_id=thread.id,
  assistant_id=assistant_id
)
```
```Python:メッセージの取得④
messages = list(client.beta.threads.messages.list(thread_id=thread.id, run_id=run.id))

message_content = messages[0].content[0].text
annotations = message_content.annotations
citations = []
for index, annotation in enumerate(annotations):
    message_content.value = message_content.value.replace(annotation.text, f"[{index}]")

    # file_citationがある場合の処理
    if file_citation := getattr(annotation, "file_citation", None):
        cited_file = client.files.retrieve(file_citation.file_id)
        citations.append(f"[{index}] {cited_file.filename}")

print(message_content.value)
print("\n".join(citations))
```
```:出力結果
入院や通院している人の報告された症状には、「浮腫（むくみ）」「頭痛」「倦怠感（だるさ）」「食欲不振」「吐気・嘔吐」「動悸・息切れ」「めまい・ふらつき」が含まれています[0]。
[0] 小林製薬の紅麹を含む健康食品に関するQA.pdf
```
<br>

紅麹による健康被害の回答をしてもらいました。わかりづらいですが横にスクロールすると"[0]"とあり、それが<b>2行目の「小林製薬の紅麹を含む健康食品に関するQA.pdf」</b>からの引用ということを現わしています。

コードとしては<b>annotations</b>にどの資料（PDF）を利用したかがセットされていてそれを表示させています。（元々回答内に資料名があるのですが、それは[n]で置換しています。）

前回のAssistants APIの利用と異なる点として、<b>runs.create_and_poll</b>を使っています。前回は回答までのステータスを手動で確認していましたが、このcreate_and_pollは完了するまでコードが実行されるので、ステータスの確認が不要となります。

<br>

今度は別の問い合わせをしてみます。

```Python:メッセージの作成②（回収対象商品問い合わせ）
prompt = "「内脂ブロッカー 」は回収対象商品でしょうか。"

message = client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content=prompt,
)
```
（③、④は同じコードを実行）
```:出力結果
はい、「内脂ブロッカー」は回収対象の商品です。[0]
[0] 紅麹を含む食品に関するリコール情報.pdf
```

<br>

```Python:メッセージの作成②（原因物質問い合わせ）
prompt = "健康被害となる原因物質はなんでしょうか。"

message = client.beta.threads.messages.create(
    thread_id=thread.id,
    role="user",
    content=prompt,
)
```
（③、④は同じコードを実行）
```:出力結果
健康被害の原因とされる物質は、プベルル酸と特定されています。高速液体クロマトグラフィー（HPLC）による分析でこの物質が同定されました[0]。
[0] 原因物質の特定について.pdf
```

<br>

与えた資料がバラバラで、どれも<b>「小林製薬の商品でこういうことが起こって～」</b>と説明していないのですが、ちゃんと回答できています。かなり優秀だと思います！

<br>

以前以下の能登半島地震支援チャットボットを見かけて、ソースの資料まで表示されてすごいなと思っていましたが、このFile Searchを使えば実装が出来そうです。

https://buddy-talk.nan-naru.com/

OpenAIは5月14日に新しいAIの発表があるようで今から楽しみです。