ChatGPT Assistants API の使い方
本記事では、2024年4月18日からVersion 2 になったAssistants APIの使い方を1から解説します。
この記事は、OpenAI公式サイトの内容と、実際に手元で実践した結果を混合したものになっています。
まだ探りきれていないので、この記事をアップデートしていく予定です。
まず、以下が新機能の概要です:
そもそも、Assistants APIとはなんでしょうか?
Assistants APIは、OpenAI の最新のAPIシステムで、多くの革新的な機能を持っています。
Assistants APIの概要
OpenAIのAssistants APIを使用すると、独自のアプリケーション内でAIアシスタントを構築することができます。
アシスタントには指示を与え、モデル、ツール、ファイルを活用してユーザーのクエリに応答させることができます。
現在、Assistants APIでは、Code Interpreter、File Search、Function callingの3種類のツールがサポートされています。
Assistants APIの機能は、Assistantsプレイグラウンドを使用するか、このガイドに概説されているステップバイステップの統合を構築することで探索できます。
概要
Assistants APIの一般的な統合には、以下のようなフローがあります:
アシスタントの作成:カスタム指示を定義し、モデルを選択します。必要に応じて、ファイルを追加し、Code Interpreter、File Search、Function callingなどのツールを有効にします。
スレッドの作成:ユーザーが会話を開始したときにスレッドを作成します。
メッセージの追加:ユーザーが質問をするたびに、スレッドにメッセージを追加します。
アシスタントの実行:モデルとツールを呼び出してスレッド上でアシスタントを実行し、応答を生成します。
まず、Code Interpreterを使用するアシスタントを作成して実行するための主要なステップを説明します。
この例では、Code Interpreterツールが有効になっている個人的な数学チューターであるアシスタントを作成します。
Assistants APIを呼び出すには、beta HTTPヘッダーを渡す必要があります。OpenAIの公式PythonまたはNode.js SDKを使用している場合、これは自動的に処理されます。
OpenAI-Beta: assistants=v2
ステップ1:アシスタントの作成
アシスタントは、モデル、指示、ツールなどのいくつかのパラメータを使用してユーザーのメッセージに応答するように設定します。
アシスタントを作成するPythonコード例:
from openai import OpenAI
client = OpenAI(api_key="ここにAPIキーを入れます")
assistant = client.beta.assistants.create(
name="数学教師",
instructions="数学の質問に答えるために、コードを書いて実行してください。",
tools=[{"type": "code_interpreter"}],
model="gpt-4-turbo",
)
ステップ2:スレッドの作成
スレッドは、ユーザーと1つまたは複数のアシスタントとの会話を表します。ユーザー(またはAIアプリケーション)がアシスタントとの会話を開始したときに、スレッドを作成できます。
スレッドを作成するPythonコード例:
thread = client.beta.threads.create()
ステップ3:スレッドへのメッセージの追加
ユーザーやアプリケーションが作成するメッセージの内容は、メッセージオブジェクトとしてスレッドに追加されます。
メッセージにはテキストとファイルの両方を含めることができます。スレッドに追加できるメッセージの数に制限はありません。
モデルのコンテキストウィンドウに収まらないコンテキストは、スマートに切り捨てられます。
スレッドにメッセージを追加するPythonコード例:
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="方程式 `3x + 11 = 14` を解いてほしいです。手伝ってもらえますか?",
)
ステップ4:実行の作成(ストリームなしの場合)
実行は非同期で行われるため、終了ステータスに達するまでRunオブジェクトをポーリングすることでステータスを監視する必要があります。
便宜上、「作成してポーリングする」関数は、実行の作成とその完了のポーリングの両方を支援します。
Runを作成するPythonコード例:
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="ユーザーを佐藤太郎として扱ってください。ユーザーはプレミアムアカウントを持っています。",
)
実行が完了すると、アシスタントによってスレッドに追加されたメッセージを一覧表示できます:
if run.status == "completed":
messages = client.beta.threads.messages.list(thread_id=thread.id)
for message in messages.data:
prefix = "ユーザー: " if message.role == "user" else "アシスタント: "
print(prefix + message.content[0].text.value)
else:
print(run.status)
この実行中に行われたツールの呼び出しを確認したい場合は、この実行のRun Stepsを一覧表示することもできます。
上記、全てのコードをまとめたものがこちらです:
from openai import OpenAI
client = OpenAI(api_key="ここにAPIキーを入れます")
assistant = client.beta.assistants.create(
name="数学教師",
instructions="数学の質問に答えるために、コードを書いて実行してください。",
tools=[{"type": "code_interpreter"}],
model="gpt-4-turbo",
)
thread = client.beta.threads.create()
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="方程式 `3x + 11 = 14` を解いてほしいです。手伝ってもらえますか?",
)
run = client.beta.threads.runs.create_and_poll(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="ユーザーを佐藤太郎として扱ってください。ユーザーはプレミアムアカウントを持っています。",
)
if run.status == "completed":
messages = client.beta.threads.messages.list(thread_id=thread.id)
for message in messages.data:
prefix = "ユーザー: " if message.role == "user" else "アシスタント: "
print(prefix + message.content[0].text.value)
else:
print(run.status)
上記コードの実行結果がこちらです:
なお、上記のコードを実行すると、以下の、Assistants専用のOpenAI のコンソール上に、Assistantsが作成されます。
https://platform.openai.com/assistants
クリックすると詳細が見えます:
上記で設定されている内容(Code interprerter、GPT-4-Turbo)は、上記の Python コードで設定したものです。
このように、Assistants API を使うと、GPTsのような、AIエージェントがコードから量産できてしまいます。
右上のCreateボタンから手動で作成することや、すでに作られたAssistantsを編集することもコンソール上から直感的にできます。
次に、ステップ4の別の例として、ストリームを使用して継続的な出力を得る方法も示します。
ステップ4:実行の作成(ストリームの場合)
すべてのユーザーメッセージがスレッドに追加されたら、任意のアシスタントでスレッドを実行できます。実行を作成すると、アシスタントに関連付けられているモデルとツールを使用して応答が生成されます。
これらの応答は、アシスタントメッセージとしてスレッドに追加されます。
PythonおよびNode SDKの「作成してストリーミングする」関数を使用して、実行を作成し、応答をストリーミングできます。
実行を作成し、ストリーミングするPythonコード例:
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)
def on_tool_call_delta(self, delta, snapshot):
if delta.type == "code_interpreter":
if delta.code_interpreter.input:
print(delta.code_interpreter.input, end="", flush=True)
if delta.code_interpreter.outputs:
print(f"\n\noutput >", flush=True)
for output in delta.code_interpreter.outputs:
if output.type == "logs":
print(f"\n{output.logs}", flush=True)
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="ユーザーを佐藤太郎として扱ってください。ユーザーはプレミアムアカウントを持っています。",
event_handler=EventHandler(),
) as stream:
stream.until_done()
Assistantsストリーミングイベントの完全なリストは、APIリファレンスで確認できます。
全てのコードをまとめたものがこちらです:
from openai import OpenAI
from typing_extensions import override
from openai import AssistantEventHandler
client = OpenAI(api_key="ここにAPIキーを入れます")
assistant = client.beta.assistants.create(
name="数学教師",
instructions="数学の質問に答えるために、コードを書いて実行してください。",
tools=[{"type": "code_interpreter"}],
model="gpt-4-turbo",
)
thread = client.beta.threads.create()
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="方程式 `3x + 11 = 14` を解いてほしいです。手伝ってもらえますか?",
)
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)
def on_tool_call_delta(self, delta, snapshot):
if delta.type == "code_interpreter":
if delta.code_interpreter.input:
print(delta.code_interpreter.input, end="", flush=True)
if delta.code_interpreter.outputs:
print(f"\n\noutput >", flush=True)
for output in delta.code_interpreter.outputs:
if output.type == "logs":
print(f"\n{output.logs}", flush=True)
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="ユーザーを佐藤太郎として扱ってください。ユーザーはプレミアムアカウントを持っています。",
event_handler=EventHandler(),
) as stream:
stream.until_done()
実行結果が以下です。これらがストリーミングされて徐々に出力されます:
Assistantsの仕組み
Assistants APIは、開発者が多様なタスクを実行できる強力なAIアシスタントを構築するのに役立つように設計されています。
アシスタントの特徴
アシスタントは、特定の指示を与えてパーソナリティと機能をチューニングすることで、OpenAIのモデルを呼び出すことができます。
アシスタントは、`code_interpreter`や`file_search`などのOpenAIがホストするツール、または開発者が構築/ホストするツール(関数呼び出しを介して)を含む、複数のツールに並行してアクセスできます。
アシスタントは、永続的なスレッドにアクセスできます。スレッドは、メッセージ履歴を保存し、会話がモデルのコンテキスト長を超えた場合に切り捨てることで、AIアプリケーション開発を簡素化します。スレッドを一度作成すれば、ユーザーの返信に応じてメッセージを追加するだけです。
アシスタントは、作成時またはアシスタントとユーザー間のスレッドの一部として、さまざまな形式のファイルにアクセスできます。ツールを使用する際、アシスタントは画像やスプレッドシートなどのファイルを作成し、作成したメッセージで参照したファイルを引用することもできます。
オブジェクト
Assistants APIでは複数のオブジェクトが利用されています。
以下はそれぞれの解説になります。
Assistant
OpenAIのモデルを使用し、ツールを呼び出す目的に特化したAI
Thread
Assistantとユーザー間の会話セッション。メッセージを保存し、モデルのコンテキストに収まるように自動的に切り捨てを処理する
Message
Assistantまたはユーザーによって作成されたメッセージ。テキスト、画像、その他のファイルを含むことができる。Threadにリストとして保存される
Run
Threadに対するAssistantの呼び出し。Assistantは、その設定とThreadのメッセージを使用して、モデルとツールを呼び出してタスクを実行する。実行の一部として、AssistantはMessageをThreadに追加する
Run Step
実行の一部としてAssistantが実行した手順の詳細なリスト。Assistantは、実行中にツールを呼び出したりMessageを作成したりできる。Run Stepを調べることで、Assistantが最終結果にどのように到達したかを把握できる
アシスタントの作成
最良の結果と最大限の互換性を得るために、Assistants APIではOpenAIの最新モデルを使用することをお勧めしています。
開始するには、使用するモデルを指定するだけでアシスタントを作成できます。
アシスタントの動作をさらにカスタマイズすることができます。
`instructions`パラメータを使用して、アシスタントのパーソナリティを誘導し、その目標を定義します。指示は、Chat Completions APIのシステムメッセージと似ています。
`tools`パラメータを使用して、アシスタントに最大128のツールへのアクセスを与えます。`code_interpreter`や`file_search`などのOpenAIがホストするツールへのアクセスを与えたり、関数呼び出しを介してサードパーティのツールを呼び出したりできます。
`tool_resources`パラメータを使用して、`code_interpreter`や`file_search`などのツールにファイルへのアクセスを与えます。ファイルは、ファイルアップロードエンドポイントを使用してアップロードされ、このAPIで使用するには`purpose`を`assistants`に設定する必要があります。
例えば、.csvファイルに基づいてデータの可視化を作成できるアシスタントを作成するには、まずファイルをアップロードします。
file = client.files.create(
file=open("収益予測.csv", "rb"),
purpose='assistants'
)
次に、`code_interpreter`ツールを有効にしてアシスタントを作成し、ツールへのリソースとしてファイルを提供します。
assistant = client.beta.assistants.create(
name="データ可視化ボット",
description="あなたは.csvファイルのデータを分析し、傾向を理解して、その傾向に関連する美しいデータ可視化を作成するのが得意です。また、観察された傾向の簡単なテキスト要約も共有します。",
model="gpt-4-turbo",
tools=[{"type": "code_interpreter"}],
tool_resources={
"code_interpreter": {
"file_ids": [file.id]
}
}
)
`code_interpreter`には最大20個、`file_search`(`vector_store`オブジェクトを使用)には最大10,000個のファイルを添付できます。
各ファイルの最大サイズは512 MBで、最大5,000,000トークンです。デフォルトでは、組織がアップロードしたすべてのファイルの合計サイズは100 GBを超えることはできませんが、サポートチームに連絡してこの制限を増やすことができます。
スレッドとメッセージの管理
スレッドとメッセージは、アシスタントとユーザー間の会話セッションを表します。スレッドに保存できるメッセージの数に制限はありません。
メッセージのサイズがモデルのコンテキストウィンドウを超えると、スレッドはメッセージをスマートに切り捨てようとし、最も重要でないと判断したものを完全に削除します。
次のように、初期メッセージのリストを使用してスレッドを作成できます。
thread = client.beta.threads.create(
messages=[
{
"role": "user",
"content": "このファイルの傾向に基づいて、3つのデータ可視化を作成してください。",
"attachments": [
{
"file_id": file.id,
"tools": [{"type": "code_interpreter"}]
}
]
}
]
)
メッセージには、テキスト、画像、またはファイル添付を含めることができます。メッセージ添付は、ファイルをスレッドの`tool_resources`に追加するヘルパーメソッドです。
`thread.tool_resources`に直接ファイルを追加することもできます。
現時点では、ユーザーが作成したメッセージに画像ファイルを含めることはできませんが、将来的にはサポートを追加する予定です。
コンテキストウィンドウの管理
Assistants APIは、モデルの最大コンテキスト長内に収まるように自動的に切り捨てを管理します。
実行で使用する最大トークン数や、実行に含める最近のメッセージの最大数を指定することで、この動作をカスタマイズできます。
最大完了トークンと最大プロンプトトークン
1回の実行でのトークン使用量を制御するには、実行の作成時に`max_prompt_tokens`と`max_completion_tokens`を設定します。
これらの制限は、実行のライフサイクル全体で使用されるすべての完了におけるトークンの合計数に適用されます。
例えば、`max_prompt_tokens`を500、`max_completion_tokens`を1000に設定して実行を開始すると、最初の完了ではスレッドを500トークンに切り捨て、出力を1000トークンに制限します。
最初の完了で使用されたプロンプトトークンが200、完了トークンが300だった場合、2回目の完了では、使用可能な制限がプロンプトトークン300、完了トークン700になります。
完了が`max_completion_tokens`の制限に達すると、実行は`incomplete`ステータスで終了し、Runオブジェクトの`incomplete_details`フィールドに詳細が提供されます。
切り捨て戦略
スレッドをモデルのコンテキストウィンドウにレンダリングする方法を制御するために、切り捨て戦略を指定することもできます。
`auto`タイプの切り捨て戦略を使用すると、OpenAIのデフォルトの切り捨て戦略が使用されます。
`last_messages`タイプの切り捨て戦略を使用すると、コンテキストウィンドウに含める最新のメッセージ数を指定できます。
メッセージのアノテーション
アシスタントによって作成されたメッセージには、オブジェクトの`content`配列内にアノテーションが含まれている場合があります。
アノテーションは、メッセージ内のテキストにどのようにアノテーションを付けるべきかについての情報を提供します。
アノテーションには2つのタイプがあります。
`file_citation`:ファイル引用は`file_search`ツールによって作成され、アシスタントがレスポンスを生成するためにアップロードして使用した特定のファイルへの参照を定義します。
`file_path`:ファイルパスアノテーションは`code_interpreter`ツールによって作成され、ツールによって生成されたファイルへの参照を含みます。
メッセージオブジェクトにアノテーションが存在する場合、テキスト内に、アノテーションに置き換えるべきモデルが生成した判読不能な部分文字列が表示されます。これらの文字列は、【13†source】やsandbox:/mnt/data/file.csvのような形式になっている可能性があります。
これらの文字列をアノテーションに存在する情報に置き換えるPythonコードスニペットの例を以下に示します。
# メッセージオブジェクトを取得
message = client.beta.threads.messages.retrieve(
thread_id="...",
message_id="..."
)
# メッセージの内容を抽出
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}] {file_citation.quote} from {cited_file.filename}')
elif (file_path := getattr(annotation, 'file_path', None)):
cited_file = client.files.retrieve(file_path.file_id)
citations.append(f'[{index}] Click <here> to download {cited_file.filename}')
# 注: 簡潔にするため、上記のファイルダウンロード機能は実装されていません
# ユーザーに表示する前にメッセージの最後に脚注を追加
message_content.value += '\n' + '\n'.join(citations)
実行と実行ステップ
ユーザーからスレッドに必要なコンテキストがすべて揃ったら、選択したアシスタントでスレッドを実行できます。
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id
)
デフォルトでは、実行はアシスタントオブジェクトで指定されたモデルとツール構成を使用しますが、柔軟性を高めるために、実行の作成時にこれらのほとんどをオーバーライドできます。
run = client.beta.threads.runs.create(
thread_id=thread.id,
assistant_id=assistant.id,
model="gpt-4-turbo",
instructions="New instructions that override the Assistant instructions",
tools=[{"type": "code_interpreter"}, {"type": "file_search"}]
)
実行のライフサイクル
実行オブジェクトには、複数のステータスがあります。
以下に全て解説します:
queued
実行が最初に作成されたとき、または`required_action`を完了したときに、キューに入れられたステータスに移行します。ほぼ即座に`in_progress`に移行するはずです。
in_progress
`in_progress`の間、アシスタントはモデルとツールを使用してステップを実行します。実行ステップを調べることで、実行によって行われた進捗状況を確認できます。
completed
実行が正常に完了しました!これで、アシスタントがスレッドに追加したすべてのメッセージと、実行が行ったすべてのステップを確認できます。また、スレッドにさらにユーザーメッセージを追加し、別の実行を作成することで、会話を続けることができます。
requires_action
関数呼び出しツールを使用する場合、モデルが呼び出す関数の名前と引数を決定すると、実行は`required_action`状態に移行します。その後、それらの関数を実行し、実行が続行される前に出力を送信する必要があります。出力が`expires_at`タイムスタンプ(作成から約10分後)を過ぎる前に提供されない場合、実行は`expired`ステータスに移行します。
expired
`expires_at`の前に関数呼び出しの出力が送信されなかった場合や、実行の実行に時間がかかりすぎて`expires_at`に記載された時間を超えた場合に発生します。
cancelling
実行のキャンセルエンドポイントを使用して、`in_progress`の実行のキャンセルを試みることができます。キャンセルの試行が成功すると、実行のステータスは`cancelled`に移行します。キャンセルは試行されますが、保証はされません。
cancelled
実行が正常にキャンセルされました。
failed
実行の`last_error`オブジェクトを調べることで、失敗の理由を確認できます。失敗のタイムスタンプは`failed_at`に記録されます。
incomplete
`max_prompt_tokens`または`max_completion_tokens`に達したため、実行が終了しました。実行の`incomplete_details`オブジェクトを調べることで、具体的な理由を確認できます。
更新のポーリング
ストリーミングを使用していない場合、実行のステータスを最新の状態に保つには、実行オブジェクトを定期的に取得する必要があります。
実行オブジェクトを取得するたびに、実行のステータスを確認して、アプリケーションが次に何をすべきかを判断できます。
必要に応じて、NodeおよびPython SDKのポーリングヘルパーを使用して、これを支援することができます。これらのヘルパーは、実行オブジェクトを自動的にポーリングし、終了状態になったときに実行オブジェクトを返します。
スレッドのロック
実行が`in_progress`で終了状態でない場合、スレッドはロックされます。つまり、以下のようになります。
新しいメッセージをスレッドに追加できません。
スレッドで新しい実行を作成できません。
実行ステップ
実行ステップのステータスは、実行のステータスと同じ意味を持ちます。
実行ステップオブジェクトの興味深い詳細のほとんどは、`step_details`フィールドにあります。ステップの詳細には、次の2つのタイプがあります。
`message_creation`:アシスタントがスレッドにメッセージを作成したときに、この実行ステップが作成されます。
`tool_calls`:アシスタントがツールを呼び出したときに、この実行ステップが作成されます。これに関する詳細は、ツールガイドの関連セクションで説明されています。
データアクセスのガイダンス
現在、APIを介して作成されたアシスタント、スレッド、メッセージ、ベクトルストアは、作成されたプロジェクトにスコープされています。
そのため、そのプロジェクトへのAPIキーアクセス権を持つ人は誰でも、プロジェクト内のアシスタント、スレッド、メッセージ、実行を読み取ったり書き込んだりできます。
以下のデータアクセス制御を強くお勧めします。
認可を実装する。アシスタント、スレッド、メッセージ、ベクトルストアの読み取りや書き込みを行う前に、エンドユーザーがそれを行う権限を持っていることを確認します。例えば、エンドユーザーがアクセス権を持つオブジェクトIDをデータベースに保存し、APIでオブジェクトIDを取得する前にそれをチェックします。
APIキーアクセスを制限する。組織内の誰がAPIキーを持ち、プロジェクトに参加すべきかを慎重に検討してください。このリストを定期的に監査します。APIキーは、メッセージやファイルなどの機密情報の読み取りや変更など、幅広い操作を可能にします。
別のアカウントを作成する。複数のアプリケーション間でデータを分離するために、アプリケーションごとに別のプロジェクトを作成することを検討してください。
ファイル検索(File Search)
ファイル検索は、独自の製品情報やユーザーが提供するドキュメントなど、アシスタントの知識をモデル外の情報で拡張します。
OpenAIは自動的にドキュメントを解析してチャンク化し、埋め込みを作成して保存し、ベクトル検索とキーワード検索の両方を使用して、ユーザーのクエリに関連するコンテンツを取得します。
クイックスタート
この例では、企業の財務諸表に関する質問に答えるのに役立つアシスタントを作成します。
ステップ1:ファイル検索を有効にして新しいアシスタントを作成する
アシスタントの`tools`パラメーターで`file_search`を有効にして、新しいアシスタントを作成します。
from openai import OpenAI
client = OpenAI()
assistant = client.beta.assistants.create(
name="金融解析アシスタント",
instructions="あなたは専門的な金融アナリストです。ナレッジベースを使用して、監査済み財務諸表に関する質問に答えてください。",
model="gpt-4-turbo",
tools=[{"type": "file_search"}],
)
`file_search`ツールが有効になると、モデルはユーザーのメッセージに基づいてコンテンツを取得するタイミングを決定します。
ステップ2:ファイルをアップロードしてベクトルストアに追加する
`file_search`ツールがファイルにアクセスするには、ベクトルストアオブジェクトを使用します。ファイルをアップロードし、それらを格納するベクトルストアを作成します。
ベクトルストアが作成されたら、すべてのコンテンツの処理が完了したことを確認するために、すべてのファイルが`in_progress`状態から外れるまでそのステータスをポーリングする必要があります。SDKには、アップロードとポーリングを一括して行うヘルパーが用意されています。
# "財務諸表"というベクトルストアを作成
vector_store = client.beta.vector_stores.create(name="財務諸表")
# OpenAIへのアップロード用にファイルを準備
file_paths = ["edgar/goog-10k.pdf", "edgar/brka-10k.txt"]
file_streams = [open(path, "rb") for path in file_paths]
# アップロードとポーリングのSDKヘルパーを使用して、ファイルをアップロードし、
# ベクトルストアに追加し、ファイルバッチのステータスが完了するまでポーリングします。
file_batch = client.beta.vector_stores.file_batches.upload_and_poll(
vector_store_id=vector_store.id, files=file_streams
)
# バッチのステータスとファイル数を出力して、この操作の結果を確認できます。
print(file_batch.status)
print(file_batch.file_counts)
ステップ3:新しいベクトルストアを使用するようにアシスタントを更新する
ファイルをアシスタントがアクセスできるようにするには、アシスタントの`tool_resources`を新しいベクトルストアIDで更新します。
assistant = client.beta.assistants.update(
assistant_id=assistant.id,
tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
)
ステップ4:スレッドを作成する
スレッドにメッセージ添付としてファイルを添付することもできます。そうすることで、スレッドに関連付けられた別のベクトルストアが作成されます。
または、すでにベクトルストアがこのスレッドにアタッチされている場合は、新しいファイルを既存のスレッドベクトルストアにアタッチします。このスレッドでRunを作成すると、ファイル検索ツールは、アシスタントのベクトルストアとスレッドのベクトルストアの両方を検索します。
この例では、ユーザーがAppleの最新の10-K提出書のコピーを添付しています。
# ユーザーが提供したファイルをOpenAIにアップロード
message_file = client.files.create(
file=open("edgar/aapl-10k.pdf", "rb"), purpose="assistants"
)
# スレッドを作成し、ファイルをメッセージに添付
thread = client.beta.threads.create(
messages=[
{
"role": "user",
"content": "2023年10月末時点のAAPLの発行済み株式数はいくつですか?",
# 新しいファイルをメッセージに添付
"attachments": [
{ "file_id": message_file.id, "tools": [{"type": "file_search"}] }
],
}
]
)
# スレッドのツールリソースにそのファイルを含むベクトルストアができました。
print(thread.tool_resources.file_search)
メッセージ添付を使用して作成されたベクトルストアには、最後にアクティブだった日(ベクトルストアが最後にRunの一部だった日と定義)から7日後に期限切れになるデフォルトの有効期限ポリシーがあります。
このデフォルトは、ベクトルストレージのコストを管理するのに役立ちます。これらの有効期限ポリシーはいつでも上書きできます。詳細はこちらをご覧ください。
ステップ5:Runを作成し、出力を確認する
ここで、Runを作成し、モデルがファイル検索ツールを使用してユーザーの質問に回答していることを確認します。
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_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(message_content.value)
print("\n".join(citations))
# 次に、ストリームSDKヘルパーを使用して
# EventHandlerクラスでRunを作成し、
# レスポンスをストリーミングします。
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
instructions="ユーザーにはJane Doeと呼びかけてください。ユーザーはプレミアムアカウントを持っています。",
event_handler=EventHandler(),
) as stream:
stream.until_done()
新しいアシスタントは、添付された両方のベクトルストア(goog-10k.pdfとbrka-10k.txtを含むものと、aapl-10k.pdfを含むもの)を検索し、aapl-10k.pdfからこの結果を返します。
仕組み
`file_search`ツールは、ファイルから適切なデータを抽出し、モデルの応答を拡張するために、いくつかの検索のベストプラクティスをそのまま実装しています。
`file_search`ツールは以下のことを行います。
検索に最適化するためにユーザークエリを書き換えます。
複雑なユーザークエリを、並列に実行できる複数の検索に分割します。
アシスタントとスレッドの両方のベクトルストアでキーワード検索とセマンティック検索の両方を実行します。
最終的な応答を生成する前に、検索結果を再ランク付けして最も関連性の高いものを選択します。
デフォルトでは、`file_search`ツールは以下の設定を使用します。
チャンクサイズ:800トークン
チャンクの重複:400トークン
埋め込みモデル:text-embedding-3-large(256次元)
コンテキストに追加されるチャンクの最大数:20(少なくなる可能性あり)
既知の制限事項
今後数ヶ月のうちにサポートを追加する予定の既知の制限事項がいくつかあります。
チャンク化、埋め込み、その他の検索設定の変更のサポート。
カスタムメタデータを使用した決定論的な検索前フィルタリングのサポート。
ドキュメント内の画像(チャート、グラフ、表などの画像を含む)の解析のサポート。
構造化ファイル形式(csvやjsonlなど)に対する検索のサポート。
要約のより良いサポート - 現在のツールは検索クエリに最適化されています。
ベクトルストア
ベクトルストアオブジェクトは、ファイル検索ツールにファイルを検索する機能を与えます。
ベクトルストアにファイルを追加すると、自動的にファイルが解析、チャンク化、埋め込まれ、キーワード検索とセマンティック検索の両方が可能なベクトルデータベースに保存されます。
各ベクトルストアには最大10,000個のファイルを保持できます。
ベクトルストアは、アシスタントとスレッドの両方にアタッチできます。現在、アシスタントには最大1つ、スレッドには最大1つのベクトルストアをアタッチできます。
ベクトルストアの作成とファイルの追加
1回のAPIコールでベクトルストアを作成し、ファイルを追加できます。
vector_store = client.beta.vector_stores.create_and_poll(
name="製品ドキュメント",
file_ids=['file_1', 'file_2', 'file_3', 'file_4', 'file_5']
)
ベクトルストアへのファイルの追加は非同期操作です。
操作が完了したことを確認するには、公式SDKの「作成してポーリング」ヘルパーを使用することをお勧めします。SDKを使用していない場合は、ベクトルストアオブジェクトを取得し、そのfile_countsプロパティを監視して、ファイル取り込み操作の結果を確認できます。
ファイルは、作成後にベクトルストアファイルを作成することで、ベクトルストアに追加することもできます。
file = client.beta.vector_stores.files.create_and_poll(
vector_store_id="vs_abc123",
file_id="file-abc123"
)
または、最大500個のファイルのバッチを作成することで、複数のファイルをベクトルストアに追加できます。
batch = client.beta.vector_stores.fileBatches.create_and_poll(
vector_store_id="vs_abc123",
file_ids=['file_1', 'file_2', 'file_3', 'file_4', 'file_5']
)
同様に、これらのファイルは、以下のいずれかの方法でベクトルストアから削除できます。
ベクトルストアファイルオブジェクトを削除する
基礎となるファイルオブジェクトを削除する(組織内のすべてのアシスタントとスレッドのすべてのベクトルストアとcode_interpreter構成からファイルを削除します)
ファイルの最大サイズは512 MBです。各ファイルには、ファイルあたり最大5,000,000トークンを含める必要があります(ファイルをアタッチすると自動的に計算されます)。
ファイル検索は、.pdf、.md、.docxなど、さまざまなファイル形式をサポートしています。
サポートされているファイル拡張子(およびそれに対応するMIMEタイプ)の詳細は、以下のサポートされているファイルのセクションで確認できます。
ベクトルストアのアタッチ
tool_resourcesパラメーターを使用して、アシスタントまたはスレッドにベクトルストアをアタッチできます。
assistant = client.beta.assistants.create(
instructions="あなたは役立つ製品サポートアシスタントであり、提供されたファイルに基づいて質問に答えます。",
model="gpt-4-turbo",
tools=[{"type": "file_search"}],
tool_resources={
"file_search": {
"vector_store_ids": ["vs_1"]
}
}
)
thread = client.beta.threads.create(
messages=[ { "role": "user", "content": "サブスクリプションをキャンセルするにはどうすればよいですか?"} ],
tool_resources={
"file_search": {
"vector_store_ids": ["vs_2"]
}
}
アシスタントやスレッドの作成後に、適切な`tool_resources`で更新することで、ベクトルストアをアタッチすることもできます。
実行前のベクトルストアの準備の確認
実行を作成する前に、ベクトルストア内のすべてのファイルが完全に処理されていることを確認することを強くお勧めします。これにより、ベクトルストア内のすべてのデータが検索可能になります。
SDKのポーリングヘルパーを使用するか、ベクトルストアオブジェクトを手動でポーリングしてステータスが`completed`であることを確認することで、ベクトルストアの準備状況を確認できます。
フォールバックとして、スレッドのベクトルストアに処理中のファイルが含まれている場合、実行オブジェクトに60秒の最大待機時間を設定しています。これは、ユーザーがスレッドにアップロードしたファイルが、実行が進む前に完全に検索可能になるようにするためです。このフォールバック待機は、アシスタントのベクトルストアには適用されません。
有効期限ポリシーによるコストの管理
`file_search`ツールは、そのリソースとしてベクトルストアオブジェクトを使用し、作成されたベクトルストアオブジェクトのサイズに基づいて課金されます。
ベクトルストアオブジェクトのサイズは、ファイルから解析されたすべてのチャンクとそれに対応する埋め込みの合計です。
最初の1GBは無料で、それを超えると、ベクトルストレージの使用量に応じて1GB/日あたり0.10ドルが請求されます。ベクトルストア操作に関連する他のコストはありません。
これらのベクトルストアオブジェクトに関連するコストを管理するために、ベクトルストアオブジェクトに有効期限ポリシーのサポートを追加しました。
ベクトルストアオブジェクトの作成または更新時にこれらのポリシーを設定できます。
vector_store = client.beta.vector_stores.create_and_poll(
name="製品ドキュメント",
file_ids=['file_1', 'file_2', 'file_3', 'file_4', 'file_5'],
expires_after={
"anchor": "last_active_at",
"days": 7
}
)
スレッドベクトルストアにはデフォルトの有効期限ポリシーがある
スレッドヘルパー(スレッドの`tool_resources.file_search.vector_stores`やメッセージの`message.attachments`など)を使用して作成されたベクトルストアには、最後にアクティブだった日(ベクトルストアが最後に実行の一部だった日と定義)から7日後に期限切れになるデフォルトの有効期限ポリシーがあります。
ベクトルストアの有効期限が切れると、そのスレッドでの実行は失敗します。これを修正するには、同じファイルを使用して新しいベクトルストアを再作成し、それをスレッドに再アタッチするだけです。
all_files = list(client.beta.vector_stores.files.list("vs_expired"))
vector_store = client.beta.vector_stores.create(name="rag-store")
client.beta.threads.update(
"thread_abc123",
tool_resources={"file_search": {"vector_store_ids": [vector_store.id]}},
)
for file_batch in chunked(all_files, 100):
client.beta.vector_stores.file_batches.create_and_poll(
vector_store_id=vector_store.id, file_ids=[file.id for file in file_batch]
)
サポートされているファイル
テキスト/MIMEタイプの場合、エンコーディングはutf-8、utf-16、またはasciiのいずれかである必要があります。
サポートされているファイル形式とMIMEタイプ:
.c (text/x-c)
.cs (text/x-csharp)
.cpp (text/x-c++)
.doc (application/msword)
.docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
.html (text/html)
.java (text/x-java)
.json (application/json)
.md (text/markdown)
.pdf (application/pdf)
.php (text/x-php)
.pptx (application/vnd.openxmlformats-officedocument.presentationml.presentation)
.py (text/x-python, text/x-script.python)
.rb (text/x-ruby)
.tex (text/x-tex)
.txt (text/plain)
.css (text/css)
.js (text/javascript)
.sh (application/x-sh)
.ts (application/typescript)
コードインタープリター(Code Interpreter)
コードインタープリターを使用すると、アシスタントがサンドボックス化された実行環境でPythonコードを書いて実行できます。
このツールは、多様なデータや書式のファイルを処理し、データやグラフの画像を含むファイルを生成できます。
コードインタープリターを使用すると、アシスタントは反復的にコードを実行して、難しいコードや数学の問題を解決できます。
アシスタントが実行に失敗するコードを書いた場合、コードの実行が成功するまで別のコードを実行しようとすることで、このコードを反復処理できます。
仕組み
コードインタープリターは、1セッションあたり0.03ドルで課金されます。
アシスタントが2つの異なるスレッド(例えば、エンドユーザーごとに1つのスレッド)で同時にコードインタープリターを呼び出すと、2つのコードインタープリターセッションが作成されます。
各セッションは、デフォルトで1時間アクティブになります。
つまり、ユーザーが最大1時間、同じスレッドでコードインタープリターとやり取りしても、1セッションの料金しか支払わないことになります。
コードインタープリターの有効化
アシスタントオブジェクトの`tools`パラメーターに`code_interpreter`を渡すことで、コードインタープリターを有効にします。
assistant = client.beta.assistants.create(
instructions="あなたは個人的な数学講師です。数学の質問をされたら、コードを書いて実行し、質問に答えてください。",
model="gpt-4-turbo",
tools=[{"type": "code_interpreter"}]
)
その後、モデルは、ユーザーリクエストの性質に基づいて、実行時にコードインタープリターを呼び出すタイミングを決定します。
この動作は、アシスタントの指示でプロンプトを与えることで促進できます(例えば、「この問題を解決するコードを書いてください」)。
コードインタープリターへのファイルの受け渡し
アシスタントレベルで渡されたファイルは、このアシスタントを使用するすべての実行でアクセスできます。
# "assistants"の目的でファイルをアップロード
file = client.files.create(
file=open("mydata.csv", "rb"),
purpose='assistants'
)
# ファイルIDを使用してアシスタントを作成
assistant = client.beta.assistants.create(
instructions="あなたは個人的な数学講師です。数学の質問をされたら、コードを書いて実行し、質問に答えてください。",
model="gpt-4-turbo",
tools=[{"type": "code_interpreter"}],
tool_resources={
"code_interpreter": {
"file_ids": [file.id]
}
}
)
ファイルは、スレッドレベルでも渡すことができます。これらのファイルは、特定のスレッドでのみアクセスできます。
ファイルアップロードエンドポイントを使用してファイルをアップロードし、メッセージ作成リクエストの一部としてファイルIDを渡します。
thread = client.beta.threads.create(
messages=[
{
"role": "user",
"content": "方程式 `3x + 11 = 14` を解く必要があります。助けていただけますか?",
"attachments": [
{
"file_id": file.id,
"tools": [{"type": "code_interpreter"}]
}
]
}
]
)
ファイルの最大サイズは512 MBです。
コードインタープリターは、.csv、.pdf、.jsonなど、さまざまなファイル形式をサポートしています。サポートされているファイル拡張子(およびそれに対応するMIMEタイプ)の詳細は、以下のサポートされているファイルのセクションで確認できます。
コードインタープリターによって生成された画像とファイルの読み取り
APIのコードインタープリターは、画像図、CSV、PDFなどのファイルも出力します。生成されるファイルには、次の2つのタイプがあります。
画像
データファイル(例えば、アシスタントが生成したデータを含むcsvファイル)
コードインタープリターが画像を生成すると、アシスタントメッセージレスポンスの`file_id`フィールドでこのファイルを検索してダウンロードできます。
{
"id": "msg_abc123",
"object": "thread.message",
"created_at": 1698964262,
"thread_id": "thread_abc123",
"role": "assistant",
"content": [
{
"type": "image_file",
"image_file": {
"file_id": "file-abc123"
}
}
]
# ...
}
その後、ファイルAPIにファイルIDを渡すことで、ファイルの内容をダウンロードできます。
from openai import OpenAI
client = OpenAI()
image_data = client.files.content("file-abc123")
image_data_bytes = image_data.read()
with open("./my-image.png", "wb") as file:
file.write(image_data_bytes)
コードインタープリターがファイルパス(例えば、「このcsvファイルをダウンロードしてください」)を参照する場合、ファイルパスはアノテーションとしてリストされます。
これらのアノテーションを、ファイルをダウンロードするためのリンクに変換できます。
{
"id": "msg_abc123",
"object": "thread.message",
"created_at": 1699073585,
"thread_id": "thread_abc123",
"role": "assistant",
"content": [
{
"type": "text",
"text": {
"value": "CSVファイルの行がシャッフルされ、新しいCSVファイルに保存されました。シャッフルされたCSVファイルは、次のリンクからダウンロードできます。\n\n[シャッフルされたCSVファイルをダウンロード](sandbox:/mnt/data/shuffled_file.csv)",
"annotations": [
{
"type": "file_path",
"text": "sandbox:/mnt/data/shuffled_file.csv",
"start_index": 167,
"end_index": 202,
"file_path": {
"file_id": "file-abc123"
}
}
]
}
}
]
}
コードインタープリターの入力と出力ログ
コードインタープリターを呼び出した実行のステップをリストすることで、コードインタープリターのコード入力と出力ログを確認できます。
run_steps = client.beta.threads.runs.steps.list(
thread_id=thread.id,
run_id=run.id
)
{
"object": "list",
"data": [
{
"id": "step_abc123",
"object": "thread.run.step",
"type": "tool_calls",
"run_id": "run_abc123",
"thread_id": "thread_abc123",
"status": "completed",
"step_details": {
"type": "tool_calls",
"tool_calls": [
{
"type": "code",
"code": {
"input": "# 2 + 2 の計算\nresult = 2 + 2\nresult",
"outputs": [
{
"type": "logs",
"logs": "4"
}
]
}
}
]
}
}
]
}
サポートされているファイル
テキスト/MIMEタイプの場合、エンコーディングはutf-8、utf-16、またはasciiのいずれかである必要があります。
サポートされているファイル形式とMIMEタイプ:
.c (text/x-c)
.cs (text/x-csharp)
.cpp (text/x-c++)
.doc (application/msword)
.docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
.html (text/html)
.java (text/x-java)
.json (application/json)
.md (text/markdown)
.pdf (application/pdf)
.php (text/x-php)
.pptx (application/vnd.openxmlformats-officedocument.presentationml.presentation)
.py (text/x-python, text/x-script.python)
.rb (text/x-ruby)
.tex (text/x-tex)
.txt (text/plain)
.css (text/css)
.js (text/javascript)
.sh (application/x-sh)
.ts (application/typescript)
.csv (application/csv)
.jpeg (image/jpeg)
.jpg (image/jpeg)
.gif (image/gif)
.png (image/png)
.tar (application/x-tar)
.xlsx (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)
.xml (application/xml または text/xml)
.zip (application/zip)
関数呼び出し(Function Calling)
Chat Completions APIと同様に、Assistants APIは関数呼び出しをサポートしています。
呼び出しを使用すると、Assistants APIに関数を記述し、その関数が引数とともにインテリジェントに返される必要がある関数を呼び出すことができます。
クイックスタート
この例では、天気アシスタントを作成し、アシスタントが呼び出すことができるツールとして、`get_current_temperature`と`get_rain_probability`の2つの関数を定義します。
ユーザーのクエリに応じて、2023年11月6日以降にリリースされた最新のモデルを使用している場合、モデルは並列関数呼び出しを呼び出します。
並列関数呼び出しを使用する例では、サンフランシスコの今日の天気と降水確率をアシスタントに尋ねます。
また、ストリーミングを使用してアシスタントの応答を出力する方法も示します。
ステップ1:関数の定義
アシスタントを作成する際、まずアシスタントの`tools`パラメーターで関数を定義します。
from openai import OpenAI
client = OpenAI()
assistant = client.beta.assistants.create(
instructions="あなたは天気ボットです。提供された関数を使用して質問に答えてください。",
model="gpt-4-turbo",
tools=[
{
"type": "function",
"function": {
"name": "get_current_temperature",
"description": "特定の場所の現在の温度を取得します",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市と州、例えば、サンフランシスコ、カリフォルニア州"
},
"unit": {
"type": "string",
"enum": ["Celsius", "Fahrenheit"],
"description": "使用する温度単位。ユーザーの場所から推測してください。"
}
},
"required": ["location", "unit"]
}
}
},
{
"type": "function",
"function": {
"name": "get_rain_probability",
"description": "特定の場所の降水確率を取得します",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市と州、例えば、サンフランシスコ、カリフォルニア州"
}
},
"required": ["location"]
}
}
}
]
)
ステップ2:スレッドの作成とメッセージの追加
ユーザーが会話を開始したときにスレッドを作成し、ユーザーが質問をするたびにスレッドにメッセージを追加します。
thread = client.beta.threads.create()
message = client.beta.threads.messages.create(
thread_id=thread.id,
role="user",
content="今日のサンフランシスコの天気と降水確率を教えてください。",
)
ステップ3:実行の開始
1つ以上の関数を呼び出すユーザーメッセージを含むスレッドで実行を開始すると、実行は保留状態になります。処理後、実行のステータスを確認することで、実行が`requires_action`状態になったことを確認できます。
これは、ツールを実行し、その出力をアシスタントに送信して、実行を継続する必要があることを示しています。この例では、2つの`tool_calls`が表示されます。これは、ユーザーのクエリが並列関数呼び出しを引き起こしたことを示しています。
実行は作成から10分後に期限切れになることに注意してください。10分の時点までにツールの出力を送信するようにしてください。
`required_action`内に2つの`tool_calls`が表示されます。これは、ユーザーのクエリが並列関数呼び出しを引き起こしたことを示しています。
{
"id": "run_qJL1kI9xxWlfE0z1yfL0fGg9",
...
"status": "requires_action",
"required_action": {
"submit_tool_outputs": {
"tool_calls": [
{
"id": "call_FthC9qRpsL5kBpwwyw6c7j4k",
"function": {
"arguments": "{"location": "San Francisco, CA"}",
"name": "get_rain_probability"
},
"type": "function"
},
{
"id": "call_RpEDoB8O0FTL9JoKTuCVFOyR",
"function": {
"arguments": "{"location": "San Francisco, CA", "unit": "Fahrenheit"}",
"name": "get_current_temperature"
},
"type": "function"
}
]
},
...
"type": "submit_tool_outputs"
}
}
ここでは、読みやすくするために、実行オブジェクトを切り捨てています。
実行を開始し、`tool_calls`を送信する方法は、ストリーミングを使用するかどうかによって異なりますが、いずれの場合も、すべての`tool_calls`を同時に送信する必要があります。
その後、呼び出した関数からのツール出力を送信することで、実行を完了できます。`required_action`オブジェクトで参照されている各`tool_call_id`を渡して、出力を各関数呼び出しに一致させます。
ストリーミングの場合、レスポンスストリームのイベントを処理するために`EventHandler`クラスを作成し、PythonおよびNode SDKの「ツール出力ストリームの送信」ヘルパーを使用してすべてのツール出力を一度に送信します。
from typing_extensions import override
from openai import AssistantEventHandler
class EventHandler(AssistantEventHandler):
@override
def on_event(self, event):
# 'requires_action'と示されているイベントを取得します
# これらにはtool_callsが含まれます
if event.event == 'thread.run.requires_action':
run_id = event.data.id # イベントデータから実行IDを取得
self.handle_requires_action(event.data, run_id)
def handle_requires_action(self, data, run_id):
tool_outputs = []
for tool in data.required_action.submit_tool_outputs.tool_calls:
if tool.function.name == "get_current_temperature":
tool_outputs.append({"tool_call_id": tool.id, "output": "57"})
elif tool.function.name == "get_rain_probability":
tool_outputs.append({"tool_call_id": tool.id, "output": "0.06"})
# すべてのtool_outputsを同時に送信
self.submit_tool_outputs(tool_outputs, run_id)
def submit_tool_outputs(self, tool_outputs, run_id):
# submit_tool_outputs_streamヘルパーを使用
with client.beta.threads.runs.submit_tool_outputs_stream(
thread_id=self.current_run.thread_id,
run_id=self.current_run.id,
tool_outputs=tool_outputs,
event_handler=EventHandler(),
) as stream:
for text in stream.text_deltas:
print(text, end="", flush=True)
print()
with client.beta.threads.runs.stream(
thread_id=thread.id,
assistant_id=assistant.id,
event_handler=EventHandler()
) as stream:
stream.until_done()
Assistants APIは、とんでもなく機能の多いAPIですね。
Assistants API は深掘り要素が多いため、活用事例含めて、今後も研究していきます。