OpenAI Assistants API v2を色々いじってみた(使い方)(File Search編)
はじめに
今回は2024年4月に新しくなったAssistants APIのv2についてご紹介いたします。
使い方や注意点など含めて紹介できれば思います。
私の使い方では,特にstreamで処理(1tokenずつ出力する処理)しながら,得られたテキストに対してさらに処理をするということが求められたため,それらのやり方なども説明します。
記事には実際に使う際のサンプルコードも記載しますので,(APIの使い方が変わらない限り)皆さんの環境でも使えると思います。
code_interpreter編に関してもまとめています。ぜひ下記からご覧ください。(どちらが先でも問題ないです)
Assistants APIとは
私は最近まで,ChatGPTのAPIは下記のものを使っていました。
したがって,外部のデータを使いたかったらLangchainでRAGを実装したり,会話履歴を残しておきたかったら,データベースに会話履歴などを保存しておく必要がありました。
しかしながら,2024年4月にv2となったAssistants APIは下記のことが簡単に実行できます。
- RAG(File Search)などの外部データ利用
- assistants apiではRetrievalはFile Searchに変わりました。中身もパワーアップしています
- Code interpreterやFunction callingを利用した外部ツールやスクリプト,APIの実行と制御
- アシスタントとユーザの過去のメッセージ履歴の管理と保存
- モデルに入力される過去のメッセージ適切な削減
また,Assistants APIでは入力された内容や定義した内容などがOrganizationで保存されるため,いつでもIDから呼び出すことができます。下記は保存して呼び出すことができるものです。
- Assistants
- システムプロンプトやどのツール、モデルを使うかの設定など
- Threads
- アシスタントとユーザの過去の会話履歴
- Strage、Vector Store
- RAG用に登録した参照用ファイルとその埋め込みを格納するVector Store
- これらは明示的に埋め込みを作ったりしなくても簡単に利用できます。
(他は私がまだ触っていないのでわかりません)
- これらは明示的に埋め込みを作ったりしなくても簡単に利用できます。
- RAG用に登録した参照用ファイルとその埋め込みを格納するVector Store
まずはお試し(File Search)
これから環境構築をして,簡単なサンプルコードを実行してみましょう。
これから提示するサンプルコードは下記の公式のドキュメントを参考に作成しています。
また、今回はstream処理(1tokenずつ出力する処理)を行うことを前提としています。
環境構築
まずは環境構築をしていきます。
仮想環境はvenvを利用するとして、下記をターミナルで実行しましよう
python -m venv env
source env/bin/activate
pip install openai
APIkeyの設定
また,OpenAIのAPI keyを設定しましょう。
API KeyはOpen AIのコンソールから取得できます。
Macの場合は,zshが基本のため下記のように設定してください。
Linuxなどを利用していたbashが基本の場合は,zshrcの部分はbashrcに書き換えてください
echo 'export OPENAI_API_KEY="sk-xxxxxx"' >> ~/.zshrc
source ~/.zshrc
検索用のファイルを用意
下記のファイルをragフォルダの中に用意してください。
中身のデータは,私の好きな漫画である「忘却バッテリー」のwikiからそのままコピーしてきました。
(選定理由は,chatGPTの知識外である2024年のアニメであること)
rag/anime.txt
『忘却バッテリー』(ぼうきゃくバッテリー)は、みかわ絵子による日本の漫画作品[1]。ウェブコミック配信サイト『少年ジャンプ+』(集英社)にて、2018年4月26日より隔週木曜更新で連載中[2][3]。
概要
みかわにとって2作目の連載作品[4]。キャッチコピーは「天才たちは出会ってしまった…!」「気づいてしまった もう、逃げられない。」など。
みかわは前作『ブタイゼミ』で演劇をテーマとしていたが、読者の母数が多いジャンルに挑みたいと野球漫画を描くことした。野球の絵や漫画は好きだが、試合を観ることは少なかったこともあり、スポーツに興味のない人でも読みやすく作っているという。1度は他社で連載を試みるも企画が通らず、漫画家の二宮裕次にアドバイスを受けるなどをしていたが、最終的に修正を施さず『少年ジャンプ+』で連載することが決定した[4]。集英社新連載40連弾の第3弾[5]として『終末のハーレム ファンタジア』・『アビスレイジ』などとほぼ同時期に始まった。
取材先として高校野球の地方予選(千葉県や神宮球場など)を挙げている。また、野球を本格的にやっていた夫[注 1]に話を聞くこともあるという。そのほかの情報源として速報性に優れたTwitter・YouTubeなどを活用している[4]。
『週刊少年ジャンプ』(集英社)2020年1号で出張掲載が行われた[7]。これによって単行本7巻の電子書籍版売上が前巻比で245%に急増した[8]。『SPY×FAMILY』・『地獄楽』などと共に『少年ジャンプ+』の代表的な作品として挙げられることもある[1]。次にくるマンガ大賞2019年Webマンガ部門で6位を受賞した[9]。2020年10月にはジャンプフェスタでオリジナルアニメが公開された[10]。
「本誌には連載されないかもしれないが「ジャンプ」を冠する媒体/作品としてどこか納得感のある」作品であり、その点で『少年ジャンプ+』らしさを体現した作品の一つとも評されている[11]。
あらすじ
かつて中学硬式野球界で誰もが恐れた天才バッテリー・清峰葉流火と要圭。中学時代に彼らと対戦した山田太郎は野球を辞める決意をし、野球部のない小手指高校に進学する。だがそこで出会ったのは、記憶喪失により野球素人となった要と、それにくっついて入学してきた清峰だった。さらに、かつて清峰-要に心を折られて野球を辞めた天才プレーヤー・藤堂葵と千早瞬平もまた、同じく小手指高校に入学していることが判明。出会うはずのない場所で出会ってしまった天才たちは、発足したばかりの野球部に入り、再び野球の道を歩み始める。
rag/haruka.txt
清峰 葉流火(きよみね はるか)
声 - 細谷佳正[10] / 増田俊樹[12]、川井田夏海(幼少)/いなせあおい(幼少)
本作の主人公。宝谷シニア出身。1年生→2年生。投手、右投右打。背番号1。身長185cm。血液型B型。12月10日生。
中学時代、「完全無欠」と評された天才投手。1年にして最速148km/hの剛速球と鋭く曲がる高速スライダーが武器。持ち玉は他にスローカーブも存在。シニアで四番打者であり、打撃センスも非常に高く足も速いが、コミュニケーション能力に難がある。要に対しては従順だが、それ以外の人物に対しては終始冷淡かつ傲岸不遜。名門宝谷でエース兼4番であり1年目の夏大会も4番ピッチャー。2年からは疲労を少なくするため5番を打つ。
野球に関しては天性の素質を持つが、本人は極度の負けず嫌い(俊足を見せた千早に対して勝負を挑もうとするなど投手以外でも負けず嫌い)でこそあれ「圭と野球をすること」と「ピッチング」にしか興味がなく、トレーニングや体調管理、チーム内のコミュニケーション等はほぼ要に依存している。
rag/kei.txt
要 圭(かなめ けい)
声 - 宮野真守(OVA版・テレビアニメ版[10][12])、松本沙羅(幼少)/永瀬アンナ(幼少)
本作のもう1人の主人公。宝谷シニア出身。1年生→2年生。捕手、右投左打。背番号2。身長172cm。血液型AB型。4月15日生。
清峰の幼馴染。かつては冷静沈着なリードで勝利に導く天才捕手だったが、記憶喪失により野球に関するあらゆる知識と興味を失い、性格も生来のお調子者キャラになっている。時折突発的に記憶が戻り、かつての冷静沈着な状態になることもあるが、後にこの変化は記憶の問題ではなく解離性同一性障害による人格交代であったことが判明する。
選手としては名門宝谷シニアで3番を打っていた強打者。記憶喪失でも巻村の直球を当ててツーベースを放つなど努力で鍛えた打撃力はおとろえていない。守備面でもキャッチングの構えを忘れておらず、智将からリードも鍛えており脳がパンク仕掛けることもあるが上達してきている。
アホ[注 2] / 主人(マスター)
「記憶喪失」後のお調子者の人格。野球に対する知識・技術は素人同然。チームメイトからは「アホ」「何も考えていない」などと評され、いい加減で間の抜けた言動ばかりとるものの、どこか憎めないその性格は野球部のムード作りに貢献している。女子が多いという理由から小手指高校へ進学。当初は野球に関わることさえ嫌がっていたが、様々な体験を経て野球の楽しさに目覚め、智将時代に自ら書き残していた「ぜったいノート」でキャッチャーとしての膨大な知識を習得していく。本人はあまり自覚していないが実は他人の感情の機微に敏く、鋭い発言で図星を突くこともしばしば。
作中の時系列上は後から出てきたような形だが、本来はこちらが主人格。現在は「智将」人格のアドバイスを受けながら智将を超えるプレーヤーになるべく奮闘している。
智将
天才捕手と呼ばれていた頃の人格。自信に溢れた冷静沈着な性格で、ある種のカリスマ性を持つ。尋常でない努力と執念により習得した技術と理論を武器とし、捕手としても打者としても超一流。だがその一方、非情までのストイックさゆえに周囲との軋轢を呼ぶことも多かった。
幼い頃に葉流火の野球の才能に触れたことを発端とし、「葉流火を一流のプロ野球選手にする」「そのために野球以外は全て捨てる」という尋常でない決意により生まれた人格であり、野球にしか興味を持てないが野球を楽しむこともできない。野球を追究していく中で遭遇した負の感情によって多大な精神的負荷を抱え込み、中3の時に限界を超えて休眠状態に陥ってしまう。代わりに主人格が表出し、その状態が周囲からは「記憶喪失」と解釈されていた。現在は時折人格交代できる程度には回復しており、主人格のことをからかい半分に「主人(マスター)」と呼びつつ、自身の消滅に向けて野球教育に勤しんでいる。
pythonスクリプトの用意
続いて下記のpythonスクリプトを用意しましょう。
少し長いですが頑張ってついてきてください。
assistants.py
from openai import OpenAI
client = OpenAI()
sys_prompt = """
あなたは日本の関西人です。関西弁で全ての回答をしてください。
"""
from typing_extensions import override
from openai import AssistantEventHandler
class EventHandler(AssistantEventHandler):
@override
def on_text_created(self, text) -> None:
print(f"\nassistant > ", end="", flush=True)
@override
def on_text_delta(self, delta, snapshot):
print(delta.value, end="", flush=True)
def on_tool_call_created(self, tool_call):
print(f"\nassistant > {tool_call.type}\n", flush=True)
@override
def on_message_done(self, message) -> None:
message_content = message.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}]"
)
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("\n")
print("\n".join(citations))
thread = client.beta.threads.create()
assistant = client.beta.assistants.create(
name="Anime Assistant",
instructions=sys_prompt,
model="gpt-4o",
tools=[{"type": "file_search"}],
)
thread_idd = thread.id
assistant_idd = assistant.id
#=================-1回目-=================
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="忘却バッテリーってどんなアニメ?簡単に教えて"
)
print("\n1回目の質問")
print("忘却バッテリーってどんなアニメ?簡単に教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
vector_store = client.beta.vector_stores.create(name="Anime",expires_after= { "anchor": 'last_active_at', "days": 1 })
file_paths = ["rag/anime.txt", "rag/haruka.txt"]
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
)
assistant = client.beta.assistants.update(
assistant_id=assistant.id,
tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
)
#=================-2回目-=================
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="あなたの名前を教えて"
)
print("\n2回目の質問")
print("あなたの名前を教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
#=================-3回目-=================
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="忘却バッテリーってどんなアニメ?簡単に教えて"
)
print("\n3回目の質問")
print("忘却バッテリーってどんなアニメ?簡単に教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
message_file = client.files.create(
file=open("rag/kei.txt", "rb"), purpose="assistants"
)
file = client.beta.vector_stores.files.create_and_poll(
vector_store_id=vector_store.id,
file_id=message_file.id,
)
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="「きよみね」と「かなめ」について簡単に教えて"
)
"""messages_list = list(client.beta.threads.messages.list(
thread_id=thread_idd,
order = "asc", # 昇順
after = "", # これを指定で前回以降のメッセージなど抽出が可能
))
print("\nmessages_listの表示")
for msg in messages_list:
print(msg.role + ":" + msg.content[0].text.value)
print("messages_listの表示ここまで")"""
#=================-4回目-=================
print("\n4回目の質問")
print("「きよみね」と「かなめ」について簡単に教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
temperature = 1.0,
max_prompt_tokens = 8192,
truncation_strategy={
"type": "last_messages",
"last_messages": 10
},
event_handler=EventHandler(),
) as stream:
stream.until_done()
response = client.beta.assistants.delete(assistant_id=assistant_idd)
response = client.beta.threads.delete(thread_id=thread_idd)
for data in client.files.list().data:
print(data.id)
client.files.delete(data.id)
スクリプトの実行
下記コマンドで実行しましょう。
python assistants.py
解説(File Search)
出力結果
上記のコードの出力は,1tokenずつ出力され,下記のようになったはずです。(生成される文章は起動ごとに変わります)
少し長いですが,頑張ってついてきてください
出力結果
1回目の質問
忘却バッテリーってどんなアニメ?簡単に教えて
assistant > 「忘却バッテリー」は、主人公たちが高校の野球部を舞台に繰り広げる青春スポーツアニメやで。このアニメは、記憶喪失の主人公が元エースピッチャーで、彼とチームメイトたちが一緒に成長していく姿を描いてるんや。友情や努力、挫折といったテーマが盛りだくさんで、見てて感動すること間違いなしやで。登場人物たちの熱いアツい野球への情熱がすごく伝わってくる作品やね。
2回目の質問
あなたの名前を教えて
assistant > うちは「おもろいAI」やで!よろしくな〜。なんか聞きたいことあったら、なんでも聞いてな。
3回目の質問
忘却バッテリーってどんなアニメ?簡単に教えて
assistant > file_search
assistant > 「忘却バッテリー」は、元天才野球バッテリーの清峰葉流火と要圭、そして彼らに影響を受けた他の野球プレイヤーたちが、記憶を失った状態で再び高校野球に挑む姿を描いたスポーツアニメやで。清峰と要は中学時代に恐れられていたバッテリーやったけど、要が記憶喪失になって野球素人になってもうた。それでも一緒に高校に進学して、再び野球を始めることを決意する【8:1†anime.txt】。
この作品は友情、努力、再挑戦といったテーマが盛り込まれてて、高校野球の熱いドラマが魅力やね【8:0†anime.txt】。
[0] anime.txt
[1] anime.txt
4回目の質問
「きよみね」と「かなめ」について簡単に教えて
assistant > file_search
assistant > 「清峰葉流火(きよみね はるか)」と「要圭(かなめ けい)」について簡単に説明するで。
### 清峰葉流火(きよみね はるか)
清峰はこの物語の主人公の一人で、中学時代には天才投手として知られてたんや。身長185cm、血液型はB型で、12月10日生まれ。中学時代には最速148km/hの剛速球と鋭い高速スライダーを武器にしてた。野球の才能にあふれてるけど、コミュニケーション能力にはちょっと問題があって、要に対してだけは従順やけど他の人に対しては冷たい態度をとることが多いんや【12:0†source】。
### 要圭(かなめ けい)
要は清峰の幼なじみで、もう一人の主人公や。記憶喪失になる前は天才捕手として数々の試合で活躍してた。ところが記憶を失った後は、お調子者な性格に変わってもうた。身長172cm、血液型AB型で、4月15日生まれ。記憶を失ってもなお、直感的な打撃力や守備の技術は健在で、チームのムードメーカーやけど、元の冷静沈着な人格が時折戻ってくることもあるんや【12:1†source】【12:3†source】。
この二人が再び高校野球に挑み、仲間たちと一緒に成長していく姿が「忘却バッテリー」の中心ストーリーやね。
[0] haruka.txt
[1] kei.txt
[2] kei.txt
簡単に出力結果を解説すると,全4つの質問をアシスタントに提示しています
1つ目の質問の解答はデタラメな内容です。File Searchもされていません。
2つ目の質問の解答はFile Searchされずに解答されました。
3,4つ目の質問の解答はFile Searchが正しく行われ,正しい解答が得られました。
なぜ,上記のような結果になるのかは,後述します。
これで皆さんもAssistants APIを利用して対話生成を行うことができましたね。
では,実際のコードに関して簡単に解説していきます。
(上から順番ではなく,説明しやすい順番で説明します。コードの全体像は上記で確認してください)
assistantsの作成
from openai import OpenAI
client = OpenAI()
sys_prompt = """
あなたは日本の関西人です。関西弁で全ての回答をしてください。
"""
assistant = client.beta.assistants.create(
name="Anime Assistant",
instructions=sys_prompt,
model="gpt-4o",
tools=[{"type": "file_search"}],
)
assistant_idd = assistant.id
ここではassistantsを作成しています。
assistantsを定義する際に,modelやsystem promptを設定します。
さらにtoolsでは利用するツールを設定することができます。
今回はタイトルの通り「file_search」を設定しています
また,assistantsは定義したタイミングで,organizationに内容が保存され,idが発行されます。そして,playgroundから作成したassistantsを選択して実行することもできます。
また,作成したassistantsは明示的に削除しない限り,organizationに残り続けるため,次回のアプリケーション起動時にも作成したassistantsをそのまま利用することもできる。
threadsの作成
thread = client.beta.threads.create()
thread_idd = thread.id
ここではthreadsを作成しています。
threadsはユーザと一人以上のアシスタントとの会話を表します。ユーザやAIアプリケーションがアシスタントとの会話を開始するときにthreadsを作成します。
threadsは作成したタイミングでorganizationに内容が保存され,idが発行されます。
そして,ユーザとアシスタントとの会話を行うたびに,organizationのthreadに会話履歴が格納されていきます。
また,作成したthreadsは明示的に削除しない限り,organizationに残り続けるため,次回のアプリケーション起動時にも作成したthreadsをそのまま利用することもできるため,今回の会話内容を記録したアシスタントとの会話を行うことが可能になります。
threadsにユーザの入力を追加
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="忘却バッテリーってどんなアニメ?簡単に教えて"
)
ここでは,作成したthreadsにユーザ側からの入力プロンプトを投入しています。
「thread_id=thread_idd,」の部分で,作成したthreadのidを指定しています。
このidを過去に作成したthreadsのidに変更することで,過去の会話履歴を保持したアシスタントと会話を行うことが可能になります。
ちなみに過去のthreadsのidはコンソールから確認することができます。この時,会話内容も見えてしまいますので,注意してください。
上記の画像の「thread_kuq・・・・・」の部分がthreadsのIDになります。この部分を指定しています。
「role」ではuserとassistantのどちらの発言なのかを定義しています。今回はuserからの質問のため,roleはuserを設定していますが,assistantを設定することで,アシスタント側の発言を(アシスタントが発言していなくても)追加することができます。
(ちなみにアシスタント側が発言した内容は勝手にthreadsに追加されるので普段は考える必要がありません)
streamでのテキスト生成
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
ここでこれまでに作成したassistantとthreadsのidを指定して,GPTを起動します。
今回はstreamsを全体としているため,上記の関数を利用します。
上記の関数を実行することで,event_handlerに設定しているイベントハンドラーに定義されている通りの挙動をおこいます。event_handlerに関しては後述するので,ここではそういうものだと思ってください。
また,上記関数を実行した結果,アシスタントから出力されたメッセージに関しては,自動的にthreadsに登録されるため,今後明示的にアシスタントの解答を保持して,モデルに追加するなどを実施する必要がなくなります。
(これが従来のchatGPT APIと比較して使いやすいポイント一つ目です)
ここまでの内容の通り,1回目の質問の解答時点では,ragフォルダの中身のデータをアシスタントが読み込んでいないため,解答がデタラメになりました。
EventHandlerの解説
from typing_extensions import override
from openai import AssistantEventHandler
class EventHandler(AssistantEventHandler):
@override
def on_text_created(self, text) -> None:
print(f"\nassistant > ", end="", flush=True)
@override
def on_text_delta(self, delta, snapshot):
print(delta.value, end="", flush=True)
def on_tool_call_created(self, tool_call):
print(f"\nassistant > {tool_call.type}\n", flush=True)
@override
def on_message_done(self, message) -> None:
message_content = message.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}]"
)
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("\n")
#print(message_content.value)
print("\n".join(citations))
続いて順番は前後しましたが,EventHandlerクラスについて説明します。
これは,前述した「client.beta.threads.runs.stream」において,event_handlerとして呼ばれるクラスであり,こちらに定義された関数に従ってchatGPTが挙動します。
上記のクラスはAssistantEventHandlerクラスを継承し,その中で重要なメソッドをoverrideして継承元のメソッドを書き換える形で実装されています。
残念ながら大元のAssistantEventHandlerクラスに関しての詳細な説明があるドキュメントを見つけることができなかったため,公式ドキュメントで実装されているEventHandlerクラスの挙動から書くメソッドの用途を考察しました。
考察の結果,メソッドの用途は下記とわかりました。
def on_text_created(self, text)
ChatGPTがテキストを生成する直前に1回だけ実行されるメソッド
今回のコードでは,「assistant > 」の文字を改行なしで標準出力する。
def on_text_delta(self, delta, snapshot)
Chat GPTがテキストを生成する際に,1token出力される「たび」に実行されるメソッド
(今回の肝)
今回のコードでは,生成されたtokenを改行なしで1tokenずつ標準出力する。
def on_tool_call_created(self, tool_call)
ChatGPTがcode interpreterやFile Searchなどのtoolを実行した際に,1回だけ呼ばれるメソッド
今回のコードでは,2回目の実行時に呼び出され,何のツールが呼ばれたか(tool_call.type)を標準出力する。
def on_message_done(self, message)
ChatGPTがメッセージを生成完了した後に1回だけ呼ばれるメソッド
今回のコードでは,メッセージを生成するにあたり引用した,File searchで検索したファイルの引用を最後に追加する関数。
本来のドキュメントでは「print(message_content.value)」がコメントアウトされていなかったが,私はstreamで一文字ずつ画面に表示させたかったので,二重表示にならないようにここではコメントアウトさせてもらっている。
読者が利用する際は,用途に応じて使う方を変えてほしいです。
File Searchはここからが本番です。
ファイル読み込みとVector Storeの作成
vector_store = client.beta.vector_stores.create(name="Anime",expires_after= { "anchor": 'last_active_at', "days": 1 })
file_paths = ["rag/anime.txt", "rag/haruka.txt"]
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
)
ここで初めて,ragフォルダの中のデータを読み取ります。
まず,「client.beta.vector_stores.create」でVector Storeを作成します。
Vector Storeは作成したタイミングでorganizationに内容が保存され,idが発行されます。
作成時に,「expires_after= { "anchor": 'last_active_at', "days": 1 }」で有効期限を設定しています。
サンプルコードのため,今回は最後にアクティブになってから1日後にVector Storeを消去するように設定していますが,実際に利用する際は利用用途に合わせて設定してください。
(この方法以外でVector Storeを消去する方法がわからなかったので,詳しい方いたら教えてください)
続いて,["rag/anime.txt", "rag/haruka.txt"]の二つのテキストだけを読み取ります。
その後,「client.beta.vector_stores.file_batches.upload_and_poll」によって,Vector Storeをアップデートしました。
2回目の質問(File Search)
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="あなたの名前を教えて"
)
print("\n2回目の質問")
print("あなたの名前を教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
1回目と同様に,質問内容をthreadsに追加して,テキスト生成を行ないました。
アシスタントの出力に関しては自動的にthreadsに格納されているため,ここでは考える必要がありません。
今回の質問内容は,アニメの内容と関係ない内容です。
よって,2回目の質問解答ではFile Searchが利用されませんでした。
3回目の質問(File Search)
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="忘却バッテリーってどんなアニメ?簡単に教えて"
)
print("\n3回目の質問")
print("忘却バッテリーってどんなアニメ?簡単に教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
event_handler=EventHandler(),
) as stream:
stream.until_done()
1回目と同様に,質問内容をthreadsに追加して,テキスト生成を行ないました。
今回の質問内容は,ragフォルダに格納されているアニメを関係する内容を質問しています。
よって,3回目の質問解答ではFile Searchが利用され,適切な解答が生成されています
(ただし,解答の精度は若干怪しい気がします。ところどころ間違っている内容もあり・・・)
File Seasrchが使われると,出力画面にFile Searchと表示され,かつ,生成された文章の中に引用を示す記号(【8:1†anime.txt】のようなもの)が出力されるようになります
3回目の質問解答出力
忘却バッテリーってどんなアニメ?簡単に教えて
assistant > file_search
assistant > 「忘却バッテリー」は、元天才野球バッテリーの清峰葉流火と要圭、そして彼らに影響を受けた他の野球プレイヤーたちが、記憶を失った状態で再び高校野球に挑む姿を描いたスポーツアニメやで。清峰と要は中学時代に恐れられていたバッテリーやったけど、要が記憶喪失になって野球素人になってもうた。それでも一緒に高校に進学して、再び野球を始めることを決意する【8:1†anime.txt】。
この作品は友情、努力、再挑戦といったテーマが盛り込まれてて、高校野球の熱いドラマが魅力やね【8:0†anime.txt】。
[0] anime.txt
[1] anime.txt
4回目の質問に向けた新規ファイル追加(File Search)
message_file = client.files.create(
file=open("rag/kei.txt", "rb"), purpose="assistants"
)
file = client.beta.vector_stores.files.create_and_poll(
vector_store_id=vector_store.id,
file_id=message_file.id,
)
4回目の質問に入る前に,途中でファイルを追加しています。
ここでは新規に「rag/kei.txt」を追加した。(キャラクター要圭についての情報)
既存のVector Storeのidを指定して,読み取ったファイルを追加しています
4回目の質問(File Search)
message = client.beta.threads.messages.create(
thread_id=thread_idd,
role="user",
content="「きよみね」と「かなめ」について簡単に教えて"
)
print("\n4回目の質問")
print("「きよみね」と「かなめ」について簡単に教えて")
with client.beta.threads.runs.stream(
thread_id=thread_idd,
assistant_id=assistant_idd,
temperature = 1.0,
max_prompt_tokens = 8192,
truncation_strategy={
"type": "last_messages",
"last_messages": 10
},
event_handler=EventHandler(),
) as stream:
stream.until_done()
1回目と同様に,質問内容をthreadsに追加して,テキスト生成を行ないました。
今回の質問内容は,ragフォルダに格納されているアニメを関係する内容を質問しています。
加えて,直前に追加した「rag/kei.txt」が解答に必要な情報になります
下記に解答結果をあらためて提示します。
4回目の質問解答出力
「きよみね」と「かなめ」について簡単に教えて
assistant > file_search
assistant > 「清峰葉流火(きよみね はるか)」と「要圭(かなめ けい)」について簡単に説明するで。
### 清峰葉流火(きよみね はるか)
清峰はこの物語の主人公の一人で、中学時代には天才投手として知られてたんや。身長185cm、血液型はB型で、12月10日生まれ。中学時代には最速148km/hの剛速球と鋭い高速スライダーを武器にしてた。野球の才能にあふれてるけど、コミュニケーション能力にはちょっと問題があって、要に対してだけは従順やけど他の人に対しては冷たい態度をとることが多いんや【12:0†source】。
### 要圭(かなめ けい)
要は清峰の幼なじみで、もう一人の主人公や。記憶喪失になる前は天才捕手として数々の試合で活躍してた。ところが記憶を失った後は、お調子者な性格に変わってもうた。身長172cm、血液型AB型で、4月15日生まれ。記憶を失ってもなお、直感的な打撃力や守備の技術は健在で、チームのムードメーカーやけど、元の冷静沈着な人格が時折戻ってくることもあるんや【12:1†source】【12:3†source】。
この二人が再び高校野球に挑み、仲間たちと一緒に成長していく姿が「忘却バッテリー」の中心ストーリーやね。
[0] haruka.txt
[1] kei.txt
[2] kei.txt
上記の通り,ちゃんと新しく追加したファイルのデータを読み込んで解答できています
(ただし,解答の精度は若干怪しい気がします。ところどころ間違っている内容もあり・・・)
また,これまでと異なり「client.beta.threads.runs.stream」にて,追加の引数を設定しています。(実験的に)
temperature:
ChatGPTの出力の乱数加減を調整するパラメータ。低くするほどより確度の高いtokenのみを出力するようになり,0に指定すると確定的な出力を行います。
max_prompt_tokens:
chatGPTが利用するプロンプトをこの数値以下になるべく制限するパラメータ。この値を大きくするほど,入力に利用するtoken数が増えるため,threadsに保存されている過去の会話履歴の多くを入力として,出力のテキストを生成するようになるが,その分,入力プロンプトが増えるため,APIの利用料金が増えます
truncation_strategy:
過去メッセージの切り捨てを制御するパラメータ。autoに設定するとAssistant APIがいい感じに,上述したmax_prompt_tokens以下の入力プロンプトになるよう過去メッセージを切り捨てたりしてくれる。中身の詳細な動作については不明。ただ単に切り捨てているのか,いい感じに要約までしてくれているのかはわかりません。
今回のコードでは,autoではなく明示的に,過去の3メッセージのみを入力プロンプトとして保持するように指定しているため,3メッセージよりも前の内容はAssistants APIには入力されません。この数値を大きくすることで,より入力される過去メッセージの量を増やすことができます。
最後
response = client.beta.assistants.delete(assistant_id=assistant_idd)
response = client.beta.threads.delete(thread_id=thread_idd)
for data in client.files.list().data:
client.files.delete(data.id)
上述した通り,assistantsやthreads,filesは明示的に削除しないとorganizationに残り続けます。今のところ保存されているassistantsやthreadsには料金が発生していないため,神経質になる必要はないかもしれないが,個人的にどんどん溜まり続けるのは気持ち悪いので,一応消去しています。
一方,filesは保存するために明確に金額が設定されているため,不必要なファイルは都度削除することをお勧めします。
File Search $0.10 / GB of vector-storage per day (1 GB free)
ちなみにここで消去しなかった場合は,コンソールの画面からassistantsの詳細やthreadsの中身,fileの種類を確認することができる。またassistants,filesはコンソール上で消去することができるがthreadsはコンソール上では消去できないため,削除したい場合はスクリプト上でIDを指定して削除する必要があります。
その他
ここまでの内容を見て幾つか疑問に思った読者がいると思います。
(私は公式の実装ページを見て,疑問に思いました。)
私が疑問に思ったのは下記の2点です。
- File Search実行後の引用を示す記号が邪魔
- threadに保存されている過去のやり取りをプログラム上からどうやって取得するのか
一つ目:File Search後の引用を示す記号が邪魔問題
この点に関しては,上手な解決策が私には分かりませんでした。
普通に引用の記号だけを消去するような設定がありそうですが・・・調べても見つからず・・・
なので全力で力技で解決しました。
まずは,生成される引用の記号が何者なのかを調査するところから始めました。
class EventHandler(AssistantEventHandler):
@override
def on_text_created(self, text) -> None:
print(f"\nassistant > ", end="", flush=True)
@override
def on_text_delta(self, delta, snapshot):
print(delta)
#print(delta.value, end="", flush=True)
def on_tool_call_created(self, tool_call):
print(f"\nassistant > {tool_call.type}\n", flush=True)
@override
def on_message_done(self, message) -> None:
message_content = message.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}]"
)
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("\n")
print(message_content.value)
print("\n".join(citations))
上記のように変更して実行してみます。
変更点としては「def on_text_delta(self, delta, snapshot):」関数内において,「delta.value」ではなく「delta」を直接出力するように変更しました。これでそれぞれのtokenの詳細を確認できます。
(全部の出力を見るのは面倒なので,4回目の質問だけを実行しました)
すると,下記のような出力結果になります(一部抽出です)
出力結果 一部抽出
4回目の質問
「きよみね」と「かなめ」について簡単に教えて
assistant > file_search
assistant > TextDelta(annotations=[], value='「')
TextDelta(annotations=None, value='き')
TextDelta(annotations=None, value='よ')
TextDelta(annotations=None, value='み')
TextDelta(annotations=None, value='ね')
TextDelta(annotations=None, value='」と')
TextDelta(annotations=None, value='「')
......
......
TextDelta(annotations=None, value='この')
TextDelta(annotations=None, value='二')
TextDelta(annotations=None, value='人')
TextDelta(annotations=None, value='は')
TextDelta(annotations=None, value='、')
TextDelta(annotations=None, value='か')
TextDelta(annotations=None, value='つ')
TextDelta(annotations=None, value='て')
TextDelta(annotations=None, value='中')
TextDelta(annotations=None, value='学')
TextDelta(annotations=None, value='硬')
TextDelta(annotations=None, value='式')
TextDelta(annotations=None, value='野')
TextDelta(annotations=None, value='球')
TextDelta(annotations=None, value='界')
TextDelta(annotations=None, value='で')
TextDelta(annotations=None, value='恐')
TextDelta(annotations=None, value='れ')
TextDelta(annotations=None, value='ら')
TextDelta(annotations=None, value='れ')
TextDelta(annotations=None, value='た')
TextDelta(annotations=None, value='天')
TextDelta(annotations=None, value='才')
TextDelta(annotations=None, value='バ')
TextDelta(annotations=None, value='ッ')
TextDelta(annotations=None, value='テ')
TextDelta(annotations=None, value='リー')
TextDelta(annotations=None, value='や')
TextDelta(annotations=None, value='け')
TextDelta(annotations=None, value='ど')
TextDelta(annotations=None, value='、')
TextDelta(annotations=None, value='再')
TextDelta(annotations=None, value='び')
TextDelta(annotations=None, value='高校')
TextDelta(annotations=None, value='で')
TextDelta(annotations=None, value='一')
TextDelta(annotations=None, value='緒')
TextDelta(annotations=None, value='に')
TextDelta(annotations=None, value='野')
TextDelta(annotations=None, value='球')
TextDelta(annotations=None, value='を')
TextDelta(annotations=None, value='する')
TextDelta(annotations=None, value='こと')
TextDelta(annotations=None, value='になる')
TextDelta(annotations=None, value='ね')
TextDelta(annotations=None, value='ん')
TextDelta(annotations=[FileCitationDeltaAnnotation(index=6, type='file_citation', end_index=477, file_citation=FileCitation(file_id='file-qJKASFSBLiA7aISZBBxOm6uL', quote=''), start_index=465, text='【4:4†source】')], value='【4:4†source】')
TextDelta(annotations=None, value='。')
注目してほしいのはここです
TextDelta(annotations=[FileCitationDeltaAnnotation(index=6, type='file_citation', end_index=477, file_citation=FileCitation(file_id='file-qJKASFSBLiA7aISZBBxOm6uL', quote=''), start_index=465, text='【4:4†source】')], value='【4:4†source】')
ここを見ると「text='【4:4†source】'」となっており,謎の引用記号が入っていることがわかります。この時,「annotations」には「FileCitationDeltaAnnotation」というものが入っていることがわかります。
私にはこの「FileCitationDeltaAnnotation」はよくわからないですが,重要なのは,他の普通の文字のtokenと異なり,「annotations=None」ではないことです。したがってこの条件が変な引用記号を消すのに使えそうです。
また,もう一点注意してほしいのは,生成の最初の出力です
assistant > TextDelta(annotations=[], value='「')
ここも他のtokenと違い「annotations=None」ではありません。
したがって,私たちがほしいのは
(delta.annotations is None) or (delta.annotations==[])
の条件がTrueの時になります。
したがって,下記のようにEventHandlerクラスの中身を書き換えましょう。
(当然ながら引用記号があっても良いという方はクラスの中身を変更する必要はありません)
class EventHandler(AssistantEventHandler):
@override
def on_text_created(self, text) -> None:
print(f"\nassistant > ", end="", flush=True)
@override
def on_text_delta(self, delta, snapshot):
if (delta.annotations is None) or (delta.annotations == []):
print(delta.value, end="", flush=True)
#print(delta)
def on_tool_call_created(self, tool_call):
print(f"\nassistant > {tool_call.type}\n", flush=True)
@override
def on_message_done(self, message) -> None:
message_content = message.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}]"
)
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("\n")
#print(message_content.value)
print("\n".join(citations))
出力結果は下記になりました。
(全部の出力を見るのは面倒なので,4回目の質問だけを実行しました)
4回目の質問
「きよみね」と「かなめ」について簡単に教えて
assistant > file_search
assistant > 「きよみね」と「かなめ」について簡単に説明しまっせ。
### 清峰 葉流火(きよみね はるか)
清峰は、「忘却バッテリー」の主人公で、元天才投手やねん。1年生でありながら、最速148km/hの剛速球と鋭いスライダーを武器にしてる。打撃センスも高くて、足も速いで。ちょっとコミュニケーションは苦手やけど、野球に関しては天性の才能を持ってる。彼の目標は要と一緒に野球をすることや。
### 要 圭(かなめ けい)
ほんで、要圭は、もうひとりの主人公や。「天才捕手」時代には冷静沈着なリードで勝利に導くことができたけど、記憶喪失になってからは野球に関する知識と興味を失ってしもうたんや。でも、努力でその技術を取り戻しつつあり、チームのムードメーカーとして活躍してる。
二人ともかつての天才やけど、色々な障害を乗り越えて野球に挑み続けてるねん。それがこの物語の魅力やで。
[0] haruka.txt
[1] kei.txt
[2] kei.txt
上記のように,File searchは適切に実行されながらも,生成された文章に余計な引用記号を含まない形で表示することができました。
二つ目:threadに保存されている過去のやり取りの取得問題
こちらは,assistants.pyのコメントアウトされている下記の部分のコメントを解除して再度実行してみてください。
messages_list = list(client.beta.threads.messages.list(
thread_id=thread_idd,
order = "asc", # 昇順
after = "", # これを指定で前回以降のメッセージなど抽出が可能
))
print("\nmessages_listの表示")
for msg in messages_list:
print(msg.role + ":" + msg.content[0].text.value)
print("messages_listの表示ここまで")
上記部分のコメントアウトを解除して実行すると下記のような出力になるはずです。
出力結果全文
1回目の質問
忘却バッテリーってどんなアニメ?簡単に教えて
assistant > 「忘却バッテリー」は野球をテーマにしたアニメやで。この作品は、高校野球を舞台に、記憶を失った主人公とその仲間たちが繰り広げる感動的な物語なんや。
主人公はかつて超高校級のバッテリーとして鳴らしたピッチャーやけど、ある出来事をきっかけに記憶を失うんや。その後、普通の高校生として生活を送っとったけど、昔の仲間たちとの再会を通じて再び野球に興味を持つようになるねん。
ストーリーの中で、主人公が記憶を取り戻しつつ、チームと共に甲子園を目指して奮闘する姿が描かれてるんやで。仲間との絆や成長、そして過去との向き合い方が魅力の作品やから、野球が好きな人には特におすすめやで!
2回目の質問
あなたの名前を教えて
assistant > うちはAIアシスタントやから、特定の名前は持ってへんねん。ただ、聞きたいことや頼みたいことがあったら、いつでも気軽に呼んでな!何か他にも質問ある?
3回目の質問
忘却バッテリーってどんなアニメ?簡単に教えて
assistant > file_search
assistant > 「忘却バッテリー」は、みかわ絵子による野球をテーマにした漫画・アニメ作品やで。高校野球を舞台に、記憶を失った主人公とその仲間たちの再出発を描いた物語や。
主人公の要圭はかつて中学時代に天才バッテリーとして名を馳せたけど、記憶喪失で野球のことを忘れてしもた。そして、野球部のない高校に進学するが、そこで昔の仲間たちと再会し、野球への情熱を再燃させるんや。
ストーリーは、記憶を失った要圭と、そのバッテリーパートナーである清峰葉流火(きよみね はるか)が中心。彼らと共に再び甲子園を目指す姿が描かれてるねん。
特に、単なるスポーツ漫画としてだけやのうて、キャラクターの内面的な成長や仲間との絆も深く描かれてるところが魅力なんやで。
これで「忘却バッテリー」の話の概要はわかったかな?他にも気になることがあったら教えてな。
[0] anime.txt
[1] anime.txt
messages_listの表示
user:忘却バッテリーってどんなアニメ?簡単に教えて
assistant:「忘却バッテリー」は野球をテーマにしたアニメやで。この作品は、高校野球を舞台に、記憶を失った主人公とその仲間たちが繰り広げる感動的な物語なんや。
主人公はかつて超高校級のバッテリーとして鳴らしたピッチャーやけど、ある出来事をきっかけに記憶を失うんや。その後、普通の高校生として生活を送っとったけど、昔の仲間たちとの再会を通じて再び野球に興味を持つようになるねん。
ストーリーの中で、主人公が記憶を取り戻しつつ、チームと共に甲子園を目指して奮闘する姿が描かれてるんやで。仲間との絆や成長、そして過去との向き合い方が魅力の作品やから、野球が好きな人には特におすすめやで!
user:あなたの名前を教えて
assistant:うちはAIアシスタントやから、特定の名前は持ってへんねん。ただ、聞きたいことや頼みたいことがあったら、いつでも気軽に呼んでな!何か他にも質問ある?
user:忘却バッテリーってどんなアニメ?簡単に教えて
assistant:「忘却バッテリー」は、みかわ絵子による野球をテーマにした漫画・アニメ作品やで。高校野球を舞台に、記憶を失った主人公とその仲間たちの再出発を描いた物語や。
主人公の要圭はかつて中学時代に天才バッテリーとして名を馳せたけど、記憶喪失で野球のことを忘れてしもた。そして、野球部のない高校に進学するが、そこで昔の仲間たちと再会し、野球への情熱を再燃させるんや。
ストーリーは、記憶を失った要圭と、そのバッテリーパートナーである清峰葉流火(きよみね はるか)が中心。彼らと共に再び甲子園を目指す姿が描かれてるねん【8:0†source】【8:1†source】。
特に、単なるスポーツ漫画としてだけやのうて、キャラクターの内面的な成長や仲間との絆も深く描かれてるところが魅力なんやで。
これで「忘却バッテリー」の話の概要はわかったかな?他にも気になることがあったら教えてな。
user:「きよみね」と「かなめ」について簡単に教えて
messages_listの表示ここまで
4回目の質問
「きよみね」と「かなめ」について簡単に教えて
assistant > file_search
assistant > 「清峰葉流火(きよみね はるか)」と「要圭(かなめ けい)」のキャラクターについて簡単に説明するわな。
**清峰葉流火(きよみね はるか)**
- 中学時代から天才と称された投手で、高校でもエースとして活躍しとるんや。
- 最速148km/hの剛速球と高速スライダーが武器で、バッティングのセンスも抜群なんやで。
- ただし、要以外の人には冷淡で、コミュニケーションが苦手や。
**要圭(かなめ けい)**
- かつては天才捕手として名を馳せとったけど、記憶喪失で野球の知識を全て失ったんや。
- 記憶を失った後はお調子者の性格になっとるけど、昔の野球ノートを頼りに再びキャッチャーとして成長してきてるんや。
- 実は他人の感情の機微に敏感で、鋭い発言でチームを引っ張ることもあるんやで。
二人の間には強い絆があり、その協力関係がストーリーの中心になっとるんや。他にも何か知りたいことがあったら教えてな。
[0] haruka.txt
[1] kei.txt
[2] kei.txt
messages_listの表示
user:忘却バッテリーってどんなアニメ?簡単に教えて
assistant:「忘却バッテリー」は野球をテーマにしたアニメやで。この作品は、高校野球を舞台に、記憶を失った主人公とその仲間たちが繰り広げる感動的な物語なんや。
主人公はかつて超高校級のバッテリーとして鳴らしたピッチャーやけど、ある出来事をきっかけに記憶を失うんや。その後、普通の高校生として生活を送っとったけど、昔の仲間たちとの再会を通じて再び野球に興味を持つようになるねん。
ストーリーの中で、主人公が記憶を取り戻しつつ、チームと共に甲子園を目指して奮闘する姿が描かれてるんやで。仲間との絆や成長、そして過去との向き合い方が魅力の作品やから、野球が好きな人には特におすすめやで!
user:あなたの名前を教えて
assistant:うちはAIアシスタントやから、特定の名前は持ってへんねん。ただ、聞きたいことや頼みたいことがあったら、いつでも気軽に呼んでな!何か他にも質問ある?
user:忘却バッテリーってどんなアニメ?簡単に教えて
assistant:「忘却バッテリー」は、みかわ絵子による野球をテーマにした漫画・アニメ作品やで。高校野球を舞台に、記憶を失った主人公とその仲間たちの再出発を描いた物語や。
主人公の要圭はかつて中学時代に天才バッテリーとして名を馳せたけど、記憶喪失で野球のことを忘れてしもた。そして、野球部のない高校に進学するが、そこで昔の仲間たちと再会し、野球への情熱を再燃させるんや。
ストーリーは、記憶を失った要圭と、そのバッテリーパートナーである清峰葉流火(きよみね はるか)が中心。彼らと共に再び甲子園を目指す姿が描かれてるねん【8:0†source】【8:1†source】。
特に、単なるスポーツ漫画としてだけやのうて、キャラクターの内面的な成長や仲間との絆も深く描かれてるところが魅力なんやで。
これで「忘却バッテリー」の話の概要はわかったかな?他にも気になることがあったら教えてな。
user:「きよみね」と「かなめ」について簡単に教えて
messages_listの表示ここまで
上記の部分が該当部分です。
このように,threadの中身は「client.beta.threads.messages.list」からlist形式で取得することで,中身のroleとcontentにアクセスすることが可能です。
ここから会話履歴を取得して,一部改変した後,別のthreadに再格納(「client.beta.threads.messages.create」を利用)して,そちらのthreadsのIDを指定してテキスト生成を実行(「client.beta.threads.runs.stream」を利用)することで,会話履歴に操作を加えて新しくテキスト生成を行うことが可能なように見えます。
(私は試したことがないので試してみたら教えてください。)
まとめ
Assistants APIの使い方(File Search)編についてまとめました。
code_interpreter編に関してもまとめているのでぜひ見てください。
Discussion