AnthropicでFunction Calling
LiteLLMでFunction Callingもうまく抽象化してくれないかなーと思ってやってみたけど、Parallelの場合だけうまくいかなかった。
ドキュメントを見る限りはできるのではないかという気がするので、一旦Anthropic単体でのFunciton Callingを使ったコードを書いてみる。
まず以下のnotebookが基本的なFunction Callingの紹介のようなので、これをまずやってみる。
パッケージインストールとAPIキーの設定がないので、セットする。
!pip install -qq anthropic
from google.colab import userdata
import os
os.environ["ANTHROPIC_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')
あとはnotebookにしたがって実行していくだけ。日本語化してみたのでそれでやってみる。
クライアント初期化
from anthropic import Anthropic
import re
client = Anthropic()
MODEL_NAME = "claude-3-opus-20240229"
Function Callingを使わない普通のpromptの場合
multiplication_message = {
"role": "user",
"content": "1,984,135 かける 9,343,116 は?"
}
message = client.messages.create(
model=MODEL_NAME,
max_tokens=1024,
messages=[multiplication_message]
).content[0].text
print(message)
1,984,135 × 9,343,116 を計算しましょう。
まず、1,984,135 と 9,343,116 を縦に並べて、筆算形式で書きます。
1984135
× 9343116
--------
11904810
19841350
17857215
15873080
11904810
3968270
---------
18532943208060
したがって、1,984,135 × 9,343,116 = 18,532,943,208,060 となります。
確認してみる。
answer = 1984135 * 9343116
print(f"{answer:,}")
残念ながら間違っていた。ちなみにうまくいくときもある。
18,538,003,464,660
ではFunction Callingでやってみる。
まず実行する関数を定義
def do_pairwise_arithmetic(num1, num2, operation):
if operation == '+':
return num1 + num2
elif operation == "-":
return num1 - num2
elif operation == "*":
return num1 * num2
elif operation == "/":
return num1 / num2
else:
return "Error: Operation not supported."
プロンプトに含める関数の定義を書く
def construct_format_tool_for_claude_prompt(name, description, parameters):
constructed_prompt = (
"<tool_description>\n"
f"<tool_name>{name}</tool_name>\n"
"<description>\n"
f"{description}\n"
"</description>\n"
"<parameters>\n"
f"{construct_format_parameters_prompt(parameters)}\n"
"</parameters>\n"
"</tool_description>"
)
return constructed_prompt
tool_name = "calculator"
tool_description = """基本的な計算関数。足し算、引き算、掛け算に対応"""
def construct_format_parameters_prompt(parameters):
constructed_prompt = "\n".join(f"<parameter>\n<name>{parameter['name']}</name>\n<type>{parameter['type']}</type>\n<description>{parameter['description']}</description>\n</parameter>" for parameter in parameters)
return constructed_prompt
parameters = [
{
"name": "first_operand",
"type": "int",
"description": "最初のオペランド(演算子の前)"
},
{
"name": "second_operand",
"type": "int",
"description": "2番目のオペランド(演算子の後)"
},
{
"name": "operator",
"type": "str",
"description": "実行する操作。必ず +, -, *, / のいずれかを指定する。"
}
]
tool = construct_format_tool_for_claude_prompt(tool_name, tool_description, parameters)
print(tool)
こういう風になる
<tool_description>
<tool_name>calculator</tool_name>
<description>
基本的な計算関数。足し算、引き算、掛け算に対応
</description>
<parameters>
<parameter>
<name>first_operand</name>
<type>int</type>
<description>最初のオペランド(演算子の前)</description>
</parameter>
<parameter>
<name>second_operand</name>
<type>int</type>
<description>2番目のオペランド(演算子の後)</description>
</parameter>
<parameter>
<name>operator</name>
<type>str</type>
<description>実行する操作。必ず +, -, *, / のいずれかを指定する。</description>
</parameter>
</parameters>
</tool_description>
これを使ってシステムプロンプトを作る。
def construct_tool_use_system_prompt(tools):
tool_use_system_prompt = (
"この環境では、ユーザーの質問に答えるために使用できる一連のツールにアクセスできる。\n"
"\n"
"以下のように呼ぶことができる。:\n"
"<function_calls>\n"
"<invoke>\n"
"<tool_name>$TOOL_NAME</tool_name>\n"
"<parameters>\n"
"<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>\n"
"...\n"
"</parameters>\n"
"</invoke>\n"
"</function_calls>\n"
"\n"
"利用可能なツールは以下の通り:\n"
"<tools>\n"
+ '\n'.join([tool for tool in tools]) +
"\n</tools>"
)
return tool_use_system_prompt
system_prompt = construct_tool_use_system_prompt([tool])
print(system_prompt)
できたシステムプロンプト
この環境では、ユーザーの質問に答えるために使用できる一連のツールにアクセスできる。
以下のように呼ぶことができる。:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
利用可能なツールは以下の通り:
<tools>
<tool_description>
<tool_name>calculator</tool_name>
<description>
基本的な計算関数。足し算、引き算、掛け算に対応
</description>
<parameters>
<parameter>
<name>first_operand</name>
<type>int</type>
<description>最初のオペランド(演算子の前)</description>
</parameter>
<parameter>
<name>second_operand</name>
<type>int</type>
<description>2番目のオペランド(演算子の後)</description>
</parameter>
<parameter>
<name>operator</name>
<type>str</type>
<description>実行する操作。必ず +, -, *, / のいずれかを指定する。</description>
</parameter>
</parameters>
</tool_description>
</tools>
このシステムプロンプトをモデルに送る。
function_calling_message = client.messages.create(
model=MODEL_NAME,
max_tokens=1024,
messages=[multiplication_message],
system=system_prompt,
stop_sequences=["\n\nHuman:", "\n\nAssistant", "</function_calls>"]
).content[0].text
print("==== system====\n", system_prompt, "\n")
print("==== {} ====\n{}".format(multiplication_message["role"], multiplication_message["content"]), "\n")
print("==== assistant ====\n", function_calling_message, "\n")
全体のプロンプトとレスポンス
==== system====
この環境では、ユーザーの質問に答えるために使用できる一連のツールにアクセスできる。
以下のように呼ぶことができる。:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
利用可能なツールは以下の通り:
<tools>
<tool_description>
<tool_name>calculator</tool_name>
<description>
基本的な計算関数。足し算、引き算、掛け算に対応
</description>
<parameters>
<parameter>
<name>first_operand</name>
<type>int</type>
<description>最初のオペランド(演算子の前)</description>
</parameter>
<parameter>
<name>second_operand</name>
<type>int</type>
<description>2番目のオペランド(演算子の後)</description>
</parameter>
<parameter>
<name>operator</name>
<type>str</type>
<description>実行する操作。必ず +, -, *, / のいずれかを指定する。</description>
</parameter>
</parameters>
</tool_description>
</tools>
==== user ====
1,984,135 かける 9,343,116 は?
==== assistant ====
<function_calls>
<invoke>
<tool_name>calculator</tool_name>
<parameters>
<first_operand>1984135</first_operand>
<second_operand>9343116</second_operand>
<operator>*</operator>
</parameters>
</invoke>
正しくパラメータを取得できている。
では関数にこれを渡す。
def extract_between_tags(tag: str, string: str, strip: bool = False) -> list[str]:
ext_list = re.findall(f"<{tag}>(.+?)</{tag}>", string, re.DOTALL)
if strip:
ext_list = [e.strip() for e in ext_list]
return ext_list
first_operand = int(extract_between_tags("first_operand", function_calling_message)[0])
second_operand = int(extract_between_tags("second_operand", function_calling_message)[0])
operator = extract_between_tags("operator", function_calling_message)[0]
result = do_pairwise_arithmetic(first_operand, second_operand, operator)
print(f"{result:,}")
18,538,003,464,660
この結果を送信するためのプロンプトを作る。
def construct_successful_function_run_injection_prompt(invoke_results):
constructed_prompt = (
"<function_results>\n"
+ '\n'.join(
f"<result>\n<tool_name>{res['tool_name']}</tool_name>\n<stdout>\n{res['tool_result']}\n</stdout>\n</result>"
for res in invoke_results
) + "\n</function_results>"
)
return constructed_prompt
formatted_results = [{
'tool_name': 'do_pairwise_arithmetic',
'tool_result': result
}]
function_results = construct_successful_function_run_injection_prompt(formatted_results)
print(function_results)
<function_results>
<result>
<tool_name>do_pairwise_arithmetic</tool_name>
<stdout>
18538003464660
</stdout>
</result>
</function_results>
これをモデルに送信する。
partial_assistant_message = function_calling_message + "</function_calls>" + function_results
messages = [
multiplication_message,
{
"role": "assistant",
"content": partial_assistant_message
}
]
final_message = client.messages.create(
model=MODEL_NAME,
max_tokens=1024,
messages=messages,
system=system_prompt
).content[0].text
print("==== system====\n", system_prompt, "\n")
for m in messages:
print("==== {} ====\n{}".format(m["role"], m["content"]), "\n")
print("==== assistant ====\n", final_message, "\n")
プロンプトと応答
==== system====
この環境では、ユーザーの質問に答えるために使用できる一連のツールにアクセスできる。
以下のように呼ぶことができる。:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
利用可能なツールは以下の通り:
<tools>
<tool_description>
<tool_name>calculator</tool_name>
<description>
基本的な計算関数。足し算、引き算、掛け算に対応
</description>
<parameters>
<parameter>
<name>first_operand</name>
<type>int</type>
<description>最初のオペランド(演算子の前)</description>
</parameter>
<parameter>
<name>second_operand</name>
<type>int</type>
<description>2番目のオペランド(演算子の後)</description>
</parameter>
<parameter>
<name>operator</name>
<type>str</type>
<description>実行する操作。必ず +, -, *, / のいずれかを指定する。</description>
</parameter>
</parameters>
</tool_description>
</tools>
==== user ====
1,984,135 かける 9,343,116 は?
==== assistant ====
<function_calls>
<invoke>
<tool_name>calculator</tool_name>
<parameters>
<first_operand>1984135</first_operand>
<second_operand>9343116</second_operand>
<operator>*</operator>
</parameters>
</invoke>
</function_calls><function_results>
<result>
<tool_name>do_pairwise_arithmetic</tool_name>
<stdout>
18538003464660
</stdout>
</result>
</function_results>
==== assistant ====
1,984,135 と 9,343,116 の積は 18,538,003,464,660 です。
期待した通りの挙動になっている。SonnetでもHaikuでも動作することを確認した。
OpenAIだと、使用する関数の推論結果をmessagesに含めて送る感じなんだけど、少なくともこの例ではそれが違うように思える。
まだα版っぽいけど、公式のfunction calling用のツールがある。
次はこれを使ってみる。
PyPIのパッケージはまだないみたいなので、依存パッケージをインストールし、クローンしたレポジトリのファイルから直接インポートする形になる。なお、依存パッケージインストール後、Colaboratoryだとランタイムの再起動が必要だった。再起動すると移動していたディレクトリが元に戻るので注意。
!git clone https://github.com/anthropics/anthropic-tools
%cd anthropic-tools
!pip install -r requirements.txt
再起動後に再度ディレクトリを移動。
%cd anthropic-tools
APIキーをセット
from google.colab import userdata
import os
os.environ["ANTHROPIC_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')
anthropic-toolsでは、ツールの定義に2つのクラスを使うらしい。
BaseTool
ToolUser
まずBaseToolは、ツールを定義するためのクラスで、これを継承して自分のツールを定義し、use_tool
メソッドを実装する。
from tool_use_package.tools.base_tool import BaseTool
import datetime, zoneinfo
from tool_use_package.tools.base_tool import BaseTool
class AdditionTool(BaseTool):
"""2つの数字を足し合わせるツール"""
def use_tool(self, a, b):
return a + b
class SubtractionTool(BaseTool):
"""ある数字から別の数字を引くツール"""
def use_tool(self, a, b):
return a - b
上記のクラスからインスタンスを作成する。
addition_tool_name = "perform_addition"
addition_tool_description = "aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。"
addition_tool_parameters = [
{"name": "a", "type": "float", "description": "足しあわせる最初の数字。例えば、5。"},
{"name": "b", "type": "float", "description": "足しあわせる2番目の数字。例えば、4.6。"}
]
subtraction_tool_name = "perform_subtraction"
subtraction_tool_description = "ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。"
subtraction_tool_parameters = [
{"name": "a", "type": "float", "description": "被減数。例えば、5。"},
{"name": "b", "type": "float", "description": "減数。例えば、9。"}
]
addition_tool = AdditionTool(addition_tool_name, addition_tool_description, addition_tool_parameters)
subtraction_tool = SubtractionTool(subtraction_tool_name, subtraction_tool_description, subtraction_tool_parameters)
これでツールの定義ができた。今度はこれをClaudeが使えるようにする。ToolUser
にツールインスタンスのリストを渡してToolUser
インスタンスを作成する。
from tool_use_package.tool_user import ToolUser
math_tool_user = ToolUser([addition_tool, subtraction_tool])
ToolUser
インスタンスにはuse_tools
メソッドがあるので、これにプロンプトというかメッセージのオブジェクトを渡す。execution_mode
をautomatic
にすると関数が実行される。
messages = [{'role': 'user', 'content': '5 たす 6 は?'}]
math_tool_user.use_tools(messages, execution_mode='automatic')
ツールが実行され、その結果を元に出力が生成されている。
\n\nしたがって、5 + 6 = 11 です。
execution_mode
をmanual
にすると関数に渡すための引数が生成されるまでになる。
math_tool_user.use_tools(messages, execution_mode='manual')
{'role': 'tool_inputs',
'content': 'こちらは5と6を足し算した結果を計算します:\n\n',
'tool_inputs': [{'tool_name': 'perform_addition',
'tool_arguments': {'a': 5.0, 'b': 6.0}}]}
ちょっとOpenAI風味に似てきた感がある。
また、anthropic-toolsではプロンプトのフォーマットが構造化されたものになっている。
{
"role": str,
"content": str,
"tool_inputs": list[dict],
"tool_outputs": list[dict],
"tool_error": str
}
-
role
- メッセージの役割。以下の4種類が記載されている。
-
user
: ユーザーからのメッセージ -
assistant
: アシスタントからのメッセージ -
tool_inputs
: アシスタントからのツールの使用要求 -
tool_outputs
: 指定されたツールを指定された方法で使用した結果を含む tool_inputs メッセージへの応答。
-
- メッセージの役割。以下の4種類が記載されている。
-
content
- メッセージのコンテンツ
- ロールが
user
/assitant
/tool_inputs`のメッセージには必ず含まれている必要がある。 - ロールが
tool_outputs
のメッセージには含まれていてはいけない。
-
tool_inputs
- ツールの実行に必要な情報を辞書のリストにしたもの。
- ロールが
tool_inputs
のメッセージには必須。指定されなければならない。
-
tool_outputs
- ツールの実行結果を辞書のリストにしたもの。
- ロールが
tool_outputs
のメッセージでは、tool_outputs
かtool_error
のどちらか1つに結果を出力して、もう片方はNone
として指定する必要がある。
-
tool_error
- 最初にエラーとなったツールのエラーメッセージ。
- 指定の仕方については
tool_outputs
を参照。
tool_inputsの例。関数名とその関数に渡す引数が定義されている。
{
'role': 'tool_inputs',
'content': '',
'tool_inputs': [
{
'tool_name': 'perform_addition',
'tool_arguments': {'a': 9, 'b': 1}
},
{
'tool_name': 'perform_subtraction',
'tool_arguments': {'a': 6, 'b': 4}
}
]
}
上の方の説明を読む限り、content
は空でも定義されている必要がある。content
が定義されている場合、Claudeには、{content}{tool_inputs}
の順でレンダリングされる。
で、以下のようなコードが紹介されているが、tool_inputs
を受けて、ツールをチェックしつつ取得、そして引数を実際に関数に渡す例だと思う(なのでこのままでは動かない。)
tool = next((t for t in your_ToolUser_instance.tools if t.name == tool_name), None)
if tool is None:
return "No tool named <tool_name>{tool_name}</tool_name> available."
return tool.use_tool(**tool_arguments)
で、use_tool
を実行した結果として以下のようなtool_outputs
メッセージになる。
{
'role': 'tool_outputs',
'tool_outputs': [
{
"tool_name": 'perform_addition',
'tool_result': 10
},
{
"tool_name": 'perform_subtraction',
'tool_result': 2
}
],
'tool_error': None
}
実行結果がエラーの場合はtool_outputs
メッセージを、以下のようにtool_error
にエラー内容を記述して渡す。なお、tool_outputs
とtool_error
両方に内容を記載して渡すとエラーになるらしい。
{
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': 'Missing required parameter "b" in tool perform_addition.'
}
細かいやり取りを抑えたところで、全部実行したコードが以下。
import datetime, zoneinfo
from tool_use_package.tools.base_tool import BaseTool
from tool_use_package.tool_user import ToolUser
class AdditionTool(BaseTool):
"""2つの数字を足し合わせるツール"""
def use_tool(self, a, b):
return a + b
class SubtractionTool(BaseTool):
"""ある数字から別の数字を引くツール"""
def use_tool(self, a, b):
return a - b
addition_tool_name = "perform_addition"
addition_tool_description = "aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。"
addition_tool_parameters = [
{"name": "a", "type": "float", "description": "足しあわせる最初の数字。例えば、5。"},
{"name": "b", "type": "float", "description": "足しあわせる2番目の数字。例えば、4.6。"}
]
subtraction_tool_name = "perform_subtraction"
subtraction_tool_description = "ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。"
subtraction_tool_parameters = [
{"name": "a", "type": "float", "description": "被減数。例えば、5。"},
{"name": "b", "type": "float", "description": "減数。例えば、9。"}
]
addition_tool = AdditionTool(addition_tool_name, addition_tool_description, addition_tool_parameters)
subtraction_tool = SubtractionTool(subtraction_tool_name, subtraction_tool_description, subtraction_tool_parameters)
math_tool_user = ToolUser([addition_tool, subtraction_tool])
messages = [
{
'role': 'user',
'content': (
'サリーはリンゴを17個持っている。'
'彼女は9個をジムにあげる。'
'その日のうちに、ピーターはバナナ6本をサリーにあげる。'
'その日の終わりに、サリーが持っている果物の数は何個?'
)
}
]
math_tool_user.use_tools(messages, execution_mode='automatic')
\n\nよって、その日の終わりにサリーが持っている果物の総数は、リンゴ8個とバナナ6本の合計14個になります。
正解、、、ではあるが、何が起きているかがこれではわからない。
ということでベタベタにやってみる。
まず、ツールの定義。ここは上と同じ。
import datetime, zoneinfo
from tool_use_package.tools.base_tool import BaseTool
from tool_use_package.tool_user import ToolUser
class AdditionTool(BaseTool):
"""2つの数字を足し合わせるツール"""
def use_tool(self, a, b):
return a + b
class SubtractionTool(BaseTool):
"""ある数字から別の数字を引くツール"""
def use_tool(self, a, b):
return a - b
addition_tool_name = "perform_addition"
addition_tool_description = "aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。"
addition_tool_parameters = [
{"name": "a", "type": "float", "description": "足しあわせる最初の数字。例えば、5。"},
{"name": "b", "type": "float", "description": "足しあわせる2番目の数字。例えば、4.6。"}
]
subtraction_tool_name = "perform_subtraction"
subtraction_tool_description = "ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。"
subtraction_tool_parameters = [
{"name": "a", "type": "float", "description": "被減数。例えば、5。"},
{"name": "b", "type": "float", "description": "減数。例えば、9。"}
]
addition_tool = AdditionTool(addition_tool_name, addition_tool_description, addition_tool_parameters)
subtraction_tool = SubtractionTool(subtraction_tool_name, subtraction_tool_description, subtraction_tool_parameters)
math_tool_user = ToolUser([addition_tool, subtraction_tool])
ではまず最初のユーザクエリ。
from pprint import pprint
messages = [
{
'role': 'user',
'content': (
'サリーはリンゴを17個持っている。'
'彼女は9個をジムにあげる。'
'その日のうちに、ピーターはバナナ6本をサリーにあげる。'
'その日の終わりに、サリーが持っている果物の数は何個?'
)
}
]
response = math_tool_user.use_tools(messages, execution_mode='manual')
messages.append(response)
pprint(messages)
tool_inputs
が返ってくる。
[
{
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?',
'role': 'user'
},
{
'content': 'この問題を解くために、以下の手順で進めていきましょう。\n''\n''まず、サリーの最初のリンゴの数を確認します。\n''<bot_reflection>\n''サリーは最初17個のリンゴを持っていた。\n''</bot_reflection>\n''\n''次に、サリーがジムにあげたリンゴの数を引きます。\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 17.0,
'b': 9.0
},
'tool_name': 'perform_subtraction'
}
]
}
]
tool_inputs
を元に関数を実行して、その結果をtool_output
としてメッセージを作る。
tool_name = response["tool_inputs"][0]["tool_name"]
tool_args = response["tool_inputs"][0]["tool_arguments"]
try:
tool = next((t for t in math_tool_user.tools if t.name == tool_name), None)
if tool is None:
messages.append({
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': "No tool named <tool_name>{tool_name}</tool_name> available."
})
else:
tool_result = tool.use_tool(**tool_args)
messages.append({
'role': 'tool_outputs',
'tool_outputs': [
{
"tool_name": tool_name,
'tool_result': tool_result,
},
],
'tool_error': None,
})
except Exception as e:
messages.append({
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': e,
})
pprint(messages)
[
{
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?',
'role': 'user'
},
{
'content': 'この問題を解くために、以下の手順で進めていきましょう。\n''\n''まず、サリーの最初のリンゴの数を確認します。\n''<bot_reflection>\n''サリーは最初17個のリンゴを持っていた。\n''</bot_reflection>\n''\n''次に、サリーがジムにあげたリンゴの数を引きます。\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 17.0,
'b': 9.0
},
'tool_name': 'perform_subtraction'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_subtraction',
'tool_result': 8.0
}
]
}
]
これを再度モデルに送る。
response = math_tool_user.use_tools(messages, execution_mode='manual')
messages.append(response)
pprint(messages)
さらにtool_inputs
が返ってくる。
[
{
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?',
'role': 'user'
},
{
'content': 'この問題を解くために、以下の手順で進めていきましょう。\n''\n''まず、サリーの最初のリンゴの数を確認します。\n''<bot_reflection>\n''サリーは最初17個のリンゴを持っていた。\n''</bot_reflection>\n''\n''次に、サリーがジムにあげたリンゴの数を引きます。\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 17.0,
'b': 9.0
},
'tool_name': 'perform_subtraction'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_subtraction',
'tool_result': 8.0
}
]
},
{
'content': '\n''\n''<bot_reflection>\n''サリーは17個のリンゴから9個をジムにあげたので、8個のリンゴが残った。\n''</bot_reflection>\n''\n''最後に、ピーターからサリーがもらったバナナの数を足します。\n''\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 8.0,
'b': 6.0
},
'tool_name': 'perform_addition'
}
]
}
]
再度これを関数に渡して実行結果をtool_output
で渡す。
tool_name = response["tool_inputs"][0]["tool_name"]
tool_args = response["tool_inputs"][0]["tool_arguments"]
try:
tool = next((t for t in math_tool_user.tools if t.name == tool_name), None)
if tool is None:
messages.append({
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': "No tool named <tool_name>{tool_name}</tool_name> available."
})
else:
tool_result = tool.use_tool(**tool_args)
messages.append({
'role': 'tool_outputs',
'tool_outputs': [
{
"tool_name": tool_name,
'tool_result': tool_result,
},
],
'tool_error': None,
})
except Exception as e:
messages.append({
'role': 'tool_outputs',
'tool_outputs': None,
'tool_error': e,
})
pprint(messages)
[
{
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?',
'role': 'user'
},
{
'content': 'この問題を解くために、以下の手順で進めていきましょう。\n''\n''まず、サリーの最初のリンゴの数を確認します。\n''<bot_reflection>\n''サリーは最初17個のリンゴを持っていた。\n''</bot_reflection>\n''\n''次に、サリーがジムにあげたリンゴの数を引きます。\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 17.0,
'b': 9.0
},
'tool_name': 'perform_subtraction'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_subtraction',
'tool_result': 8.0
}
]
},
{
'content': '\n''\n''<bot_reflection>\n''サリーは17個のリンゴから9個をジムにあげたので、8個のリンゴが残った。\n''</bot_reflection>\n''\n''最後に、ピーターからサリーがもらったバナナの数を足します。\n''\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 8.0,
'b': 6.0
},
'tool_name': 'perform_addition'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_addition',
'tool_result': 14.0
}
]
}
]
再度モデルに送信
response = math_tool_user.use_tools(messages, execution_mode='manual')
messages.append(response)
pprint(messages)
最終的な回答が得られる。
[
{
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?',
'role': 'user'
},
{
'content': 'この問題を解くために、以下の手順で進めていきましょう。\n''\n''まず、サリーの最初のリンゴの数を確認します。\n''<bot_reflection>\n''サリーは最初17個のリンゴを持っていた。\n''</bot_reflection>\n''\n''次に、サリーがジムにあげたリンゴの数を引きます。\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 17.0,
'b': 9.0
},
'tool_name': 'perform_subtraction'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_subtraction',
'tool_result': 8.0
}
]
},
{
'content': '\n''\n''<bot_reflection>\n''サリーは17個のリンゴから9個をジムにあげたので、8個のリンゴが残った。\n''</bot_reflection>\n''\n''最後に、ピーターからサリーがもらったバナナの数を足します。\n''\n',
'role': 'tool_inputs',
'tool_inputs': [
{
'tool_arguments': {
'a': 8.0,
'b': 6.0
},
'tool_name': 'perform_addition'
}
]
},
{
'role': 'tool_outputs',
'tool_error': None,
'tool_outputs': [
{
'tool_name': 'perform_addition',
'tool_result': 14.0
}
]
},
{
'content': '\n''\n''\n''<bot_reflection>\n''ピーターからバナナ6本をもらったので、サリーが最終的に持っている果物の数は、\n''リンゴ8個とバナナ6本の合計14個になった。\n''</bot_reflection>\n''\n''<result>\n''よって、その日の終わりにサリーが持っている果物の数は、リンゴ8個とバナナ6本の合計14個です。\n''</result>',
'role': 'assistant'
}
]
これを一連の処理にまとめるための関数のサンプルが用意されている。
def handle_manual_claude_res(messages, claude_res, tool_user):
"""
- messagesはclaude_resを含まない
- tool_user は以前のメッセージに使用した ToolUser インスタンスでなければならない。
"""
# メッセージに Claude の応答を追加する。
messages.append(claude_res)
if claude_res['role'] == "assistant":
# もしメッセージがツールを使おうとしていないのであれば、自動的に Claudeに応答するのではなく、ユーザーに入力を求める
return {"next_action": "user_input", "messages": messages}
elif claude_res['role'] == "tool_inputs":
# もしメッセージがツールを使おうとしているのであれば、ツールと引数を解析、ツールを実行して、その結果からtool_outputsメッセージを作成、そのメッセージをmessagesに追加する
tool_outputs = []
for tool_input in claude_res['tool_inputs']:
tool = next((t for t in tool_user.tools if t.name == tool_input['tool_name']), None)
if tool is None:
messages.append({"role": "tool_outputs", "tool_outputs": None, "tool_error": f"No tool named <tool_name>{tool_name}</tool_name> available."})
return {"next_action": "auto_respond", "messages": messages}
tool_result = tool.use_tool(**tool_input['tool_arguments'])
tool_outputs.append({"tool_name": tool_input['tool_name'], "tool_result": tool_result})
messages.append({"role": "tool_outputs", "tool_outputs": tool_outputs, "tool_error": None})
return {"next_action": "auto_respond", "messages": messages}
else:
raise ValueError(f"Provided role should be assistant or tool_inputs, got {claude_res['role']}")
実際に使う場合はこんな感じで。
messages = [
{
'role': 'user',
'content': (
'サリーはリンゴを17個持っている。'
'彼女は9個をジムにあげる。'
'その日のうちに、ピーターはバナナ6本をサリーにあげる。'
'その日の終わりに、サリーが持っている果物の数は何個?'
)
}
]
claude_res = math_tool_user.use_tools(messages, execution_mode='manual')
while True:
result = handle_manual_claude_res(messages, claude_res, math_tool_user)
if result["next_action"] == "user_input":
for m in messages:
print(m)
break
else:
claude_res = math_tool_user.use_tools(messages, execution_mode='manual')
結果
{
'role': 'user',
'content': 'サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?'
}
{
'role': 'tool_inputs',
'content': '問題を解いていきましょう。\n\nサリーが最初に持っていたリンゴの数は 17 個でした。\n\n',
'tool_inputs': [
{
'tool_name': 'perform_subtraction',
'tool_arguments': {
'a': 17.0,
'b': 9.0
}
}
]
}
{
'role': 'tool_outputs',
'tool_outputs': [
{
'tool_name': 'perform_subtraction',
'tool_result': 8.0
}
],
'tool_error': None
}
{
'role': 'tool_inputs',
'content': '\n\nサリーがジムにあげた後のリンゴの数は 17 - 9 = 8 個です。\n\nその後、ピーターからバナナ 6 本をもらっています。\nバナナの数を先ほどのリンゴの数に足しましょう。\n\n',
'tool_inputs': [
{
'tool_name': 'perform_addition',
'tool_arguments': {
'a': 8.0,
'b': 6.0
}
}
]
}
{
'role': 'tool_outputs',
'tool_outputs': [
{
'tool_name': 'perform_addition',
'tool_result': 14.0
}
],
'tool_error': None
}
{
'role': 'assistant',
'content': '\n\n\n\nその日の終わりに、サリーが持っているリンゴはもともとの17個から9個減った8個で、\nそこにピーターからもらったバナナ6本を足すと、\n8 + 6 = 14 個 になります。\n\nよって、その日の最後にサリーが持っている果物の合計は リンゴ8個とバナナ6本の 合計14個 です。'
}
気付いたことをいくつか。
- デフォルトで"opus"が使用される。モデルを変えたい場合は
ToolUser
インスタンス作成時に指定すればよい。
math_tool_user = ToolUser([addition_tool, subtraction_tool],model="claude-3-haiku-20240307")
-
今回の例だとシーケンシャルに処理がされているけども、これは今回のクエリがツールをシーケンシャルに実行しないと判断できないためそうなってるだけで、コードを見る限りは、複数の関数が用意されていて、同時というかそれぞれ独立して個別に実行できるならば、
tool_inputs
に複数の関数定義が返されて、tool_outputs
に複数の実行結果を送信するのだと思う。つまりParallel Function Callingには対応しているように思える。 -
use_tools
にverbose=True
をつけると、プロンプトのやり取りが全部表示される
from pprint import pprint
messages = [
{
'role': 'user',
'content': (
'サリーはリンゴを17個持っている。'
'彼女は9個をジムにあげる。'
'その日のうちに、ピーターはバナナ6本をサリーにあげる。'
'その日の終わりに、サリーが持っている果物の数は何個?'
)
}
]
response = math_tool_user.use_tools(messages, execution_mode='automatic', verbose=True)
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>perform_addition</tool_name>
<description>
aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>足しあわせる最初の数字。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>足しあわせる2番目の数字。例えば、4.6。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>perform_subtraction</tool_name>
<description>
ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>被減数。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>減数。例えば、9。</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?
Assistant:
----------COMPLETION----------
よし、問題を整理して順番に考えていきましょう。
最初にサリーが持っている果物:
<function_calls>
<invoke>
<tool_name>perform_subtraction</tool_name>
<parameters>
<a>17</a>
<b>9</b>
</parameters>
</invoke>
</function_calls>
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>perform_addition</tool_name>
<description>
aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>足しあわせる最初の数字。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>足しあわせる2番目の数字。例えば、4.6。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>perform_subtraction</tool_name>
<description>
ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>被減数。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>減数。例えば、9。</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?
Assistant:よし、問題を整理して順番に考えていきましょう。
最初にサリーが持っている果物:
<function_calls>
<invoke>
<tool_name>perform_subtraction</tool_name>
<parameters>
<a>17</a>
<b>9</b>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>perform_subtraction</tool_name>
<stdout>
8.0
</stdout>
</result>
</function_results>
----------CLAUDE GENERATION----------
サリーは9個のリンゴをジムにあげたので、残りは8個のリンゴです。
次に、ピーターからバナナ6本をもらっています。
サリーの持っている果物の合計は:
<function_calls>
<invoke>
<tool_name>perform_addition</tool_name>
<parameters>
<a>8</a>
<b>6</b>
</parameters>
</invoke>
</function_calls>
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>perform_addition</tool_name>
<description>
aとbの2つの数値を足し合わせる。例えば、add_numbers(a=10, b=12) -> 22。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>足しあわせる最初の数字。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>足しあわせる2番目の数字。例えば、4.6。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>perform_subtraction</tool_name>
<description>
ある数値(a)から別の数値(b)を引いて、a-bを得る。例えば、subtract_numbers(a=8, b=5) -> 3。数字はどんな有理数でもよい。
</description>
<parameters>
<parameter>
<name>a</name>
<type>float</type>
<description>被減数。例えば、5。</description>
</parameter>
<parameter>
<name>b</name>
<type>float</type>
<description>減数。例えば、9。</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: サリーはリンゴを17個持っている。彼女は9個をジムにあげる。その日のうちに、ピーターはバナナ6本をサリーにあげる。その日の終わりに、サリーが持っている果物の数は何個?
Assistant:よし、問題を整理して順番に考えていきましょう。
最初にサリーが持っている果物:
<function_calls>
<invoke>
<tool_name>perform_subtraction</tool_name>
<parameters>
<a>17</a>
<b>9</b>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>perform_subtraction</tool_name>
<stdout>
8.0
</stdout>
</result>
</function_results>
サリーは9個のリンゴをジムにあげたので、残りは8個のリンゴです。
次に、ピーターからバナナ6本をもらっています。
サリーの持っている果物の合計は:
<function_calls>
<invoke>
<tool_name>perform_addition</tool_name>
<parameters>
<a>8</a>
<b>6</b>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>perform_addition</tool_name>
<stdout>
14.0
</stdout>
</result>
</function_results>
----------CLAUDE GENERATION----------
よって、その日の終わりにサリーが持っている果物は、リンゴ8個とバナナ6本の合計14個です。
今回の例だとシーケンシャルに処理がされているけども、これは今回のクエリがツールをシーケンシャルに実行しないと判断できないためそうなってるだけで、コードを見る限りは、複数の関数が用意されていて、同時というかそれぞれ独立して個別に実行できるならば、
tool_inputs
に複数の関数定義が返されて、tool_outputs
に複数の実行結果を送信するのだと思う。つまりParallel Function Callingには対応しているように思える。
ということで、Parallelで実行できるはずの関数とクエリに変えてみた。
import json
from tool_use_package.tools.base_tool import BaseTool
from tool_use_package.tool_user import ToolUser
def handle_manual_claude_res(messages, claude_res, tool_user):
"""
- messagesはclaude_resを含まない
- tool_user は以前のメッセージに使用した ToolUser インスタンスでなければならない。
"""
# メッセージに Claude の応答を追加する。
messages.append(claude_res)
if claude_res['role'] == "assistant":
# もしメッセージがツールを使おうとしていないのであれば、自動的に Claudeに応答するのではなく、ユーザーに入力を求める
return {"next_action": "user_input", "messages": messages}
elif claude_res['role'] == "tool_inputs":
# もしメッセージがツールを使おうとしているのであれば、ツールと引数を解析、ツールを実行して、その結果からtool_outputsメッセージを作成、そのメッセージをmessagesに追加する
tool_outputs = []
for tool_input in claude_res['tool_inputs']:
tool = next((t for t in tool_user.tools if t.name == tool_input['tool_name']), None)
if tool is None:
messages.append({"role": "tool_outputs", "tool_outputs": None, "tool_error": f"No tool named <tool_name>{tool_name}</tool_name> available."})
return {"next_action": "auto_respond", "messages": messages}
tool_result = tool.use_tool(**tool_input['tool_arguments'])
tool_outputs.append({"tool_name": tool_input['tool_name'], "tool_result": tool_result})
messages.append({"role": "tool_outputs", "tool_outputs": tool_outputs, "tool_error": None})
return {"next_action": "auto_respond", "messages": messages}
else:
raise ValueError(f"Provided role should be assistant or tool_inputs, got {claude_res['role']}")
class GetTemperatureTool(BaseTool):
"""指定された都市の現在の気温を取得する"""
def use_tool(self, location, unit="fahrenheit"):
if "tokyo" in location.lower():
return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
elif "san francisco" in location.lower():
return json.dumps({"location": "San Francisco", "temperature": "72", "unit": unit})
elif "paris" in location.lower():
return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
else:
return json.dumps({"location": location, "temperature": "unknown"})
class GetCurrentWeatherTool(BaseTool):
"""指定された都市の現在の天気を取得する"""
def use_tool(self, location):
if "tokyo" in location.lower():
return json.dumps({"location": "Tokyo", "weather": "sunny"})
elif "san francisco" in location.lower():
return json.dumps({"location": "San Francisco", "weather": "rain"})
elif "paris" in location.lower():
return json.dumps({"location": "Paris", "weather": "cloudy"})
else:
return json.dumps({"location": location, "weather": "unknown"})
temperature_tool_name = "get_current_temperature"
temperature_tool_description = "指定された都市の現在の天気を取得する"
temperature_tool_parameters = [
{"name": "location", "type": "str", "description": "都市名を英語で指定。例) San Francisco, Tokyo."},
{"name": "unit", "type": "str", "enum": ["celsius", "fahrenheit"], "description": "気温の単位。指定された都市名から判断する。"}
]
weather_tool_name = "get_current_weather"
weather_tool_description = "指定された都市の現在の気温を取得する"
weather_tool_parameters = [
{"name": "location", "type": "str", "description": "都市名を英語で指定。例) San Francisco, Tokyo."},
]
temperature_tool = GetTemperatureTool(temperature_tool_name, temperature_tool_description, temperature_tool_parameters)
weather_tool = GetCurrentWeatherTool(weather_tool_name, weather_tool_description, weather_tool_parameters)
tools = [temperature_tool, weather_tool]
mytool_user = ToolUser(tools, model="claude-3-haiku-20240307")
messages = [
{
'role': 'user',
'content': "東京の天気と気温を教えて。"
}
]
claude_res = mytool_user.use_tools(messages, execution_mode='manual')
while True:
result = handle_manual_claude_res(messages, claude_res, mytool_user)
if result["next_action"] == "user_input":
for m in messages:
print(m)
break
else:
claude_res = mytool_user.use_tools(messages, execution_mode='manual')
{'role': 'user', 'content': '東京の天気と気温を教えて。'}
{'role': 'tool_inputs', 'content': 'わかりました。東京の天気と気温を調べます。\n\n', 'tool_inputs': [{'tool_name': 'get_current_weather', 'tool_arguments': {'location': 'Tokyo'}}]}
{'role': 'tool_outputs', 'tool_outputs': [{'tool_name': 'get_current_weather', 'tool_result': '{"location": "Tokyo", "weather": "sunny"}'}], 'tool_error': None}
{'role': 'tool_inputs', 'content': '\n\n', 'tool_inputs': [{'tool_name': 'get_current_temperature', 'tool_arguments': {'location': 'Tokyo', 'unit': 'celsius'}}]}
{'role': 'tool_outputs', 'tool_outputs': [{'tool_name': 'get_current_temperature', 'tool_result': '{"location": "Tokyo", "temperature": "10", "unit": "celsius"}'}], 'tool_error': None}
{'role': 'assistant', 'content': '\n\n東京の現在の天気は晴れで、気温は摂氏10度です。'}
ちなみに、関数のプロパティの定義で、文字列だからって"string"にしたら怒られた。
AttributeError: module 'builtins' has no attribute 'strings'
ここはpythonのbuiltinsで使えるデータ型、つまり"str"と指定する必要があった、
ただ、これはanthropic-toolsのコードがそうなっているだけだと思う。
(snip)
# TODO: This only handles the outer-most type. Nested types are an unimplemented issue at the moment.
@staticmethod
def _convert_value(value, type_str):
"""Convert a string value into its appropriate Python data type based on the provided type string.
Arg:
value: the value to convert
type_str: the type to convert the value to
Returns:
The value converted into the requested type or the original value
if the conversion failed.
"""
if type_str in ("list", "dict"):
return ast.literal_eval(value)
type_class = getattr(builtins, type_str)
try:
return type_class(value)
except ValueError:
return value
(snip)
プロンプトの流れ。
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>get_current_temperature</tool_name>
<description>
指定された都市の現在の天気を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
<parameter>
<name>unit</name>
<type>str</type>
<description>気温の単位。指定された都市名から判断する。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>get_current_weather</tool_name>
<description>
指定された都市の現在の気温を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: 東京の天気と気温を教えて。
Assistant:
----------COMPLETION----------
では、東京の天気と気温を確認しましょう。
<function_calls>
<invoke>
<tool_name>get_current_weather</tool_name>
<parameters>
<location>Tokyo</location>
</parameters>
</invoke>
</function_calls>
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>get_current_temperature</tool_name>
<description>
指定された都市の現在の天気を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
<parameter>
<name>unit</name>
<type>str</type>
<description>気温の単位。指定された都市名から判断する。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>get_current_weather</tool_name>
<description>
指定された都市の現在の気温を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: 東京の天気と気温を教えて。
Assistant:では、東京の天気と気温を確認しましょう。
<function_calls>
<invoke>
<tool_name>get_current_weather</tool_name>
<parameters>
<location>Tokyo</location>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>get_current_weather</tool_name>
<stdout>
{"location": "Tokyo", "weather": "sunny"}
</stdout>
</result>
</function_results>
----------CLAUDE GENERATION----------
<function_calls>
<invoke>
<tool_name>get_current_temperature</tool_name>
<parameters>
<location>Tokyo</location>
<unit>celsius</unit>
</parameters>
</invoke>
</function_calls>
----------CURRENT PROMPT----------
In this environment you have access to a set of tools you can use to answer the user's question.
You may call them like this:
<function_calls>
<invoke>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
...
</parameters>
</invoke>
</function_calls>
Here are the tools available:
<tools>
<tool_description>
<tool_name>get_current_temperature</tool_name>
<description>
指定された都市の現在の天気を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
<parameter>
<name>unit</name>
<type>str</type>
<description>気温の単位。指定された都市名から判断する。</description>
</parameter>
</parameters>
</tool_description>
<tool_description>
<tool_name>get_current_weather</tool_name>
<description>
指定された都市の現在の気温を取得する
</description>
<parameters>
<parameter>
<name>location</name>
<type>str</type>
<description>都市名を英語で指定。例) San Francisco, Tokyo.</description>
</parameter>
</parameters>
</tool_description>
</tools>
Human: 東京の天気と気温を教えて。
Assistant:では、東京の天気と気温を確認しましょう。
<function_calls>
<invoke>
<tool_name>get_current_weather</tool_name>
<parameters>
<location>Tokyo</location>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>get_current_weather</tool_name>
<stdout>
{"location": "Tokyo", "weather": "sunny"}
</stdout>
</result>
</function_results>
<function_calls>
<invoke>
<tool_name>get_current_temperature</tool_name>
<parameters>
<location>Tokyo</location>
<unit>celsius</unit>
</parameters>
</invoke>
</function_calls>
<function_results>
<result>
<tool_name>get_current_temperature</tool_name>
<stdout>
{"location": "Tokyo", "temperature": "10", "unit": "celsius"}
</stdout>
</result>
</function_results>
----------CLAUDE GENERATION----------
東京の天気は晴れで、気温は摂氏10度です。
厳密にはParallelではないのかな?
WorkbenchではParallelにはできた。
ならば、XMLをきちんとパースできればやれるはず。
import re
import xml.etree.ElementTree as ET
text = """
<function_calls>
<invoke>
<tool_name>get_current_weather</tool_name>
<parameters>
<location>Tokyo</location>
</parameters>
</invoke>
</function_calls>
<function_calls>
<invoke>
<tool_name>get_current_temperature</tool_name>
<parameters>
<location>Tokyo</location>
<unit>Celsius</unit>
</parameters>
</invoke>
</function_calls>
東京の現在の天気は<get_current_weather>で、気温は<get_current_temperature>度です。
"""
functions_xml = re.findall(r'<function_calls>.*?</function_calls>', text, re.DOTALL)
function_calls = []
for xml_part in functions_xml:
root = ET.fromstring(xml_part)
function_call = {}
for child in root.iter('invoke'):
tool_name = child.find('tool_name').text
parameters = {}
for param in child.find('parameters'):
parameters[param.tag] = param.text
function_call = {'tool_name': tool_name, 'parameters': parameters}
function_calls.append(function_call)
print(function_calls)
[
{'tool_name': 'get_current_weather', 'parameters': {'location': 'Tokyo'}},
{'tool_name': 'get_current_temperature', 'parameters': {'location': 'Tokyo', 'unit': 'Celsius'}}
]
これを関数に渡して、実行結果をまとめてメッセージで渡せばParallelになるはず。anthropic-toolsの出力を見る限りは、現状はまだそういうふうにはなっていないってことなのかなと思った。
こういうリリースが。
あれ?元々対応してるじゃん?と思ったんだけど、上のanthropic-toolsがプログラム側インタフェースの裏でプロンプト作ってやっていたのが、エンドポイント側で対応したのでJSONでそのまま投げれるってことね。
おー、XML方式はLegacy扱いなのね
ということで、新しい形式で書いてみた。一応まだβ扱いみたい。
!pip install anthropic
!pip freeze | grep anthropic
anthropic==0.23.1
APIキー設定
from google.colab import userdata
import os
os.environ["ANTHROPIC_API_KEY"] = userdata.get('ANTHROPIC_API_KEY')
関数を作成してツール定義
import json
def get_current_temperature(location, unit="fahrenheit"):
"""与えられた地域の気温予報を取得する"""
location = location.lower()
if location == "tokyo":
temperature = 55.4
elif location == "san francisco":
temperature = 65
elif location == "paris":
temperature = 68
else:
return json.dumps({"location": location.title(), "temperature": "unknown"})
if unit == "celsius":
temperature = round((temperature - 32) * 5 / 9, 1)
return json.dumps({"location": location.title(), "temperature": str(temperature), "unit": unit})
def get_current_weather(location):
"""指定された都市の現在の天気を取得する"""
location = location.lower()
if location == "tokyo":
weather = "sunny"
elif location == "san francisco":
weather = "rain"
elif location == "paris":
weather = "cloudy"
else:
return json.dumps({"location": location.title(), "weather": "unknown"})
return json.dumps({"location": location.title(), "weather": weather})
tools = [
{
"name": "get_current_temperature",
"description": "指定された都市の現在の気温を取得する",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名を英語で指定。例) San Francisco, Tokyo.",
},
"unit": {
"type": "string",
"enum": ["fahrenheit", "celsius"],
"description": "気温の単位。ユーザの言語や位置情報から判断する。",
},
},
"required": ["location", "unit"],
},
},
{
"name": "get_current_weather",
"description": "指定された都市の現在の天気を取得する",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名を英語で指定。例) San Francisco, Tokyo.",
},
},
"required": ["location"]
},
}
]
tools_mapping = {
"get_current_temperature": get_current_temperature,
"get_current_weather": get_current_weather,
}
やりとりするところ
import anthropic
from pprint import pprint
client = anthropic.Anthropic()
messages = [
{
"role": "user", "content": "サンフランシスコの天気と気温を教えて?"
}
]
max_iterations = 5
iteration_count = 0
while iteration_count < max_iterations:
response = client.beta.tools.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1024,
tools=tools,
messages=messages,
)
response = response.dict()
if response["stop_reason"] == "tool_use":
messages.append(
{
"role": "assistant",
"content": response["content"]
}
)
tool_results = []
for content in response["content"]:
if content["type"] == "tool_use":
tool_use_id = content["id"]
tool_name = content["name"]
tool_to_call = tools_mapping[tool_name]
tool_input = content["input"]
tool_result = tool_to_call(**tool_input)
tool_results.append(
{
"tool_use_id": tool_use_id,
"type": "tool_result",
"content": tool_result
}
)
messages.append(
{
"role": "user",
"content": tool_results
}
)
else:
messages.append(
{
"role": "assistant",
"content": response["content"][0]["text"]
}
)
break
iteration_count += 1
pprint(messages)
こんな感じ
[
{
'content': 'サンフランシスコの天気と気温を教えて?',
'role': 'user'
},
{
'content': [
{
'text': 'わかりました。サンフランシスコの天気と気温を取得します。',
'type': 'text'
},
{
'id': 'toolu_01McYNUZiWmCUBhG2ZZr6pyB',
'input': {
'location': 'San Francisco'
},
'name': 'get_current_weather',
'type': 'tool_use'
}
],
'role': 'assistant'
},
{
'content': [
{
'content': '{"location": "San Francisco", "weather": "rain"}',
'tool_use_id': 'toolu_01McYNUZiWmCUBhG2ZZr6pyB',
'type': 'tool_result'
}
],
'role': 'user'
},
{
'content': [
{
'id': 'toolu_01CP3qXUdVBWqnmGFfuM6Hpn',
'input': {
'location': 'San Francisco',
'unit': 'celsius'
},
'name': 'get_current_temperature',
'type': 'tool_use'
}
],
'role': 'assistant'
},
{
'content': [
{
'content': '{"location": "San Francisco", "temperature": ''"18.3", "unit": "celsius"}',
'tool_use_id': 'toolu_01CP3qXUdVBWqnmGFfuM6Hpn',
'type': 'tool_result'
}
],
'role': 'user'
},
{
'content': 'サンフランシスコの天気は現在雨が降っています。気温は摂氏18.3度となっています。',
'role': 'assistant'
}
]
んー、同時に実行できる場合でも、パラレルにはならなくてシーケンシャルっぽい動きに見える。
ただし、ドキュメントにはこうある。
ツール使用のベストプラクティスと制限
Claude でツールを使用する場合、以下の制限とベストプラクティスに留意してください:
- 複雑なツールを使用する場合はClaude 3 Opusを使用し、簡単なツールを扱う場合はHaikuを使用してください: Opusは、他のモデルと比べて、最も多くのツールを同時に扱うことができ、引数の欠落を検出するのに優れています。引数が明示的に与えられていない曖昧なケースや、ユーザーリクエストを完了するためにツールが必要でない可能性がある場合に、説明を求める可能性が高いでしょう。Haikuは、(クエリに関連していなくても)より頻繁にツールを使用しようとすることをデフォルトとし、パラメータが明示的に与えられていない場合、不足しているパラメータを推測します。
Opusに変えてみたらこうなった。
[
{
'content': 'サンフランシスコの天気と気温を教えて?',
'role': 'user'
},
{
'content': [
{
'text': '<thinking>\n''ユーザーはサンフランシスコの天気と気温について尋ねています。これに答えるために、以下の2つの関数が関連しています:\n''\n''get_current_weather:\n''- location パラメーター: ユーザーが "サンフランシスコ" と明示的に指定しているので、"San ''Francisco" を使用できます。\n''よって、この関数を呼び出すのに必要な情報は揃っています。\n''\n''get_current_temperature:\n''- location パラメーター: 上記と同様、"San Francisco" が使用できます。 \n''- unit パラメーター: これは optional ''なので、ユーザーが明示的に指定していなくても問題ありません。ユーザーの言語や位置情報から華氏か摂氏かを判断してくれます。\n''よって、この関数も呼び出し可能です。\n''\n''以上より、ユーザーの質問に答えるのに必要な情報は十分そろっていると判断できます。\n''</thinking>',
'type': 'text'
},
{
'id': 'toolu_0141SWHc27ccpiNMez4T8tGF',
'input': {
'location': 'San Francisco'
},
'name': 'get_current_weather',
'type': 'tool_use'
},
{
'id': 'toolu_01GbzEdQDD8UHbbqekkhRJkQ',
'input': {
'location': 'San Francisco',
'unit': 'fahrenheit'
},
'name': 'get_current_temperature',
'type': 'tool_use'
}
],
'role': 'assistant'
},
{
'content': [
{
'content': '{"location": "San Francisco", "weather": "rain"}',
'tool_use_id': 'toolu_0141SWHc27ccpiNMez4T8tGF',
'type': 'tool_result'
},
{
'content': '{"location": "San Francisco", "temperature": "65", ''"unit": "fahrenheit"}',
'tool_use_id': 'toolu_01GbzEdQDD8UHbbqekkhRJkQ',
'type': 'tool_result'
}
],
'role': 'user'
},
{
'content': '現在のサンフランシスコの天気は雨です。気温は華氏 65 度 (摂氏約 18 度) です。',
'role': 'assistant'
}
]
なるほど、Opusだと確かにやりとりが少なくなっている。
ただ、そもそもOpusのレスポンス自体が遅いので、トータルでもシーケンシャルなHaikuのほうが速かった。
いくつか気になったこと
- Best practices for tool definitions
- 例よりも説明が優先される、つまりfew shotよりもpromptでしっかり書けってことか。
- Forcing tool use
- 特定のツールを矯正したい場合は、userメッセージに"Use the get_weather tool in your response."というふうに書く。
-
tools
はあくまでも推奨されるツールリストであって、必ずしも実行されるわけではないということか。
-
- 特定のツールを矯正したい場合は、userメッセージに"Use the get_weather tool in your response."というふうに書く。
- JSON output
- example参照
- Error handling
- ツール実行エラーを返す場合
-
tool_result
で、"is_error": true
をつけてcontentにエラー内容を書く
-
- ツールへのパラメータが間違っている・足りない場合
- descriptionを修正する
-
tool_result
で、"is_error": true
をつけてcontentにその旨を書くと会話を続けれる
- ツール実行エラーを返す場合
- Chain of thought tool use
- Claude-3はCoTに沿ってどのツールを使うかを考えることが多い。
- Opusは常にそうする
- Sonnet/Haikuでそうしたい場合にはプロンプトでそれを促す
-
<thinking>
タグはそのためのものだが、変更される可能性がある
-
- Claude-3はCoTに沿ってどのツールを使うかを考えることが多い。
- Tool use best practices and limitations
- スキーマは複雑なものよりもフラットな方がうまく機能する
- Claudeは傾向として一度に一つのツールを使用して、その結果出力を次のアクションに使用する傾向がある。
- プロンプト等でパラレルにさせることができるが、その場合、依存関係のあるツール(ツールBはツールAの結果に依存する)を使う際に(ツールBの)パラメータにダミー値を入れてしまう可能性がある。
- パラメータの不備でリトライする場合は2、3回程度でそれを超えるとClaudeは諦める
- 検索を行うツールの場合、出力結果に
<search_quality_reflection>
タグとスコアが含まれる場合がある。不要な場合はpromptの最後に"Do not reflect on the quality of the returned search results in your response. "を追加する
exampleが用意されている
JSONモードだけ気になるのでちょっと試してみる。
与えられた画像のフォーマットをJSONで出力するというもの。
import base64
import httpx
image_url = "https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg"
image_media_type = "image/jpeg"
image_data = base64.b64encode(httpx.get(image_url).content).decode("utf-8")
tools=[
{
"name": "record_summary",
"description": "画像を構造化されたJSONに要約して記録する",
"input_schema": {
"type": "object",
"properties": {
"key_colors": {
"type": "array",
"items": {
"type": "object",
"properties": {
"r": {
"type": "number",
"description": "RGBの赤の値 [0, 255]",
},
"g": {
"type": "number",
"description": "RGBの緑の値 [0, 255]",
},
"b": {
"type": "number",
"description": "RGBの青の値 [0, 255]",
},
"name": {
"type": "string",
"description": "人間が理解できる色の名前を snake_case で。例: \"olive_green\" or \"turquoise\""
},
},
"required": ["r", "g", "b", "name"],
},
"description": "画像のキーカラー。4色以内に抑える。",
},
"description": {
"type": "string",
"description": "画像の簡潔な説明。最大1~2文以内。",
},
"estimated_year": {
"type": "integer",
"description": "写真の場合は、画像が撮影された推定年。画像がフィクションではない場合のみ設定してください。大まかな推定で構いません!",
},
},
"required": ["key_colors", "description"],
},
}
]
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": image_media_type,
"data": image_data,
},
},
{"type": "text", "text": "`record_summary`を使って、この画像を説明して下さい。"},
],
}
]
response = anthropic.Anthropic().beta.tools.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1024,
tools=tools,
messages=messages,
)
print(response.dict()["content"][0]["input"])
結果
{
'description': 'この画像は、地面に座っている小さな黒いアリの写真です。アリは大きな頭部と細長い脚を持っており、興味深い姿勢で撮影されています。画像全体がオレンジがかった茶色の色調を持っており、アリの詳細が際立っています。',
'key_colors': [
{
'name': 'dark_brown',
'r': 87,
'g': 57,
'b': 42
},
{
'name': 'tan',
'r': 210,
'g': 180,
'b': 140
},
{
'name': 'sienna',
'r': 160,
'g': 82,
'b': 45
},
{
'name': 'black',
'r': 0,
'g': 0,
'b': 0
}
],
'estimated_year': 2023
}
なるほど。
前回まで使っていたサンプルコードでも試してみる。format_json
という実行はしない定義だけのツールを用意。
import json
def get_current_temperature(location, unit="fahrenheit"):
"""与えられた地域の気温予報を取得する"""
location = location.lower()
if location == "tokyo":
temperature = 55.4
elif location == "san francisco":
temperature = 65
elif location == "paris":
temperature = 68
else:
return json.dumps({"location": location.title(), "temperature": "unknown"})
if unit == "celsius":
temperature = round((temperature - 32) * 5 / 9, 1)
return json.dumps({"location": location.title(), "temperature": str(temperature), "unit": unit})
def get_current_weather(location):
"""指定された都市の現在の天気を取得する"""
location = location.lower()
if location == "tokyo":
weather = "sunny"
elif location == "san francisco":
weather = "rain"
elif location == "paris":
weather = "cloudy"
else:
return json.dumps({"location": location.title(), "weather": "unknown"})
return json.dumps({"location": location.title(), "weather": weather})
tools = [
{
"name": "get_current_temperature",
"description": "指定された都市の現在の気温を取得する",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名を英語で指定。例) San Francisco, Tokyo.",
},
"unit": {
"type": "string",
"enum": ["fahrenheit", "celsius"],
"description": "気温の単位。ユーザの言語や位置情報から判断する。",
},
},
"required": ["location", "unit"],
},
},
{
"name": "get_current_weather",
"description": "指定された都市の現在の天気を取得する",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "都市名を英語で指定。例) San Francisco, Tokyo.",
},
},
"required": ["location"]
},
},
{
"name": "format_json",
"description": "指定された都市の天気と気温をJSON形式にフォーマットする。",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "ユーザが指定した都市名。",
},
"weather": {
"type": "string",
"description": "ユーザが指定した都市の現在の天気。",
},
"temperature": {
"type": "string",
"description": "ユーザが指定した都市の現在の気温。",
},
"temperature_unit": {
"type": "string",
"enum": ["fahrenheit", "celsius"],
"description": "ユーザが指定した都市の現在の気温の単位。",
},
},
"required": ["location", "weather", "temperature", "temperature_unit"]
},
}
]
tools_mapping = {
"get_current_temperature": get_current_temperature,
"get_current_weather": get_current_weather,
}
システムプロンプトでこのツールを使うように定義。少しループ内の処理等はいじっている。
import anthropic
from pprint import pprint
client = anthropic.Anthropic()
system_prompt = "あなたは世界のお天気ガイドです。与えられたツールを使って、ユーザが指定した都市の天気や気温などを答えます。最終的な回答は`format_json`ツールを使ってください。"
messages = [
{
"role": "user", "content": "サンフランシスコの天気と気温を教えて?"
}
]
max_iterations = 5
iteration_count = 0
while iteration_count < max_iterations:
braked=False # while内のforループでbrakeした場合にwhileからもbreakさせるためのフラグ
response = client.beta.tools.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1024,
system=system_prompt,
tools=tools,
messages=messages,
)
response = response.dict()
if response["stop_reason"] == "tool_use":
messages.append(
{
"role": "assistant",
"content": response["content"]
}
)
tool_results = []
for content in response["content"]:
if content["type"] == "tool_use":
tool_use_id = content["id"]
tool_name = content["name"]
tool_input = content["input"]
if tool_name in tools_mapping:
tool_to_call = tools_mapping[tool_name]
tool_result = tool_to_call(**tool_input)
tool_results.append(
{
"tool_use_id": tool_use_id,
"type": "tool_result",
"content": tool_result
}
)
else:
braked=True
break
if braked == True:
break
messages.append(
{
"role": "user",
"content": tool_results
}
)
else:
messages.append(
{
"role": "assistant",
"content": response["content"][0]["text"]
}
)
break
iteration_count += 1
pprint(messages)
結果
[
{
'content': 'サンフランシスコの天気と気温を教えて?',
'role': 'user'
},
{
'content': [
{
'text': 'わかりました。サンフランシスコの天気と気温を確認しましょう。',
'type': 'text'
},
{
'id': 'toolu_01GLkFkqRHFfxz7jpniZnnE4',
'input': {
'location': 'San Francisco'
},
'name': 'get_current_weather',
'type': 'tool_use'
}
],
'role': 'assistant'
},
{
'content': [
{
'content': '{"location": "San Francisco", "weather": "rain"}',
'tool_use_id': 'toolu_01GLkFkqRHFfxz7jpniZnnE4',
'type': 'tool_result'
}
],
'role': 'user'
},
{
'content': [
{
'id': 'toolu_014N1CUadM1yfT6vJtNpy61Z',
'input': {
'location': 'San Francisco',
'unit': 'fahrenheit'
},
'name': 'get_current_temperature',
'type': 'tool_use'
}
],
'role': 'assistant'
},
{
'content': [
{
'content': '{"location": "San Francisco", "temperature": "65", ''"unit": "fahrenheit"}',
'tool_use_id': 'toolu_014N1CUadM1yfT6vJtNpy61Z',
'type': 'tool_result'
}
],
'role': 'user'
},
{
'content': [
{
'text': '以上の結果から、サンフランシスコの現在の天気は「rain」(雨)、気温は65°Fとなっています。',
'type': 'text'
},
{
'id': 'toolu_01DpA8tWYWW9GyjXvSZsNv4G',
'input': {
'location': 'San Francisco',
'temperature': '65',
'temperature_unit': 'fahrenheit',
'weather': 'rain'
},
'name': 'format_json',
'type': 'tool_use'
}
],
'role': 'assistant'
}
]
ちょい最終レスポンスの処理がイマイチイケてない書き方だけど、一応出来てる。