LangChain v0.3　その12 ~tool collingの出力を使ってtoolを実行する~

# 1. はじめに

https://qiita.com/ikedachin/items/37fe239651dba6336e37

前回、tool callingをやりましたね。
大事なことが記載されていました。
**tool calling = 引数の生成**です。あくまで引数の生成。ということで、今回は生成された引数を使って、関数を実行しながら、チャットを作っていきます。

<br><br>


# 2. バージョン情報
:::note
バージョン情報
Python 3.10.8
langchain==0.3.7
python-dotenv
langchain-openai==0.2.5
langgraph>0.2.27
langchain-core
langchain-community==0.3.5
:::

<br><br>

# 3. とにかくやってみよう
## 3-1. ライブラリインポート
```python:ライブラリインポート
import os
import datetime
from dotenv import load_dotenv
from dateutil.relativedelta import relativedelta

from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import AzureChatOpenAI
```
まぁ、ほぼいつも通り。
のちに書きますが、日付を扱うことになるのでライブラリが増えてます。

<br><br>


## 3-2. 環境変数
```python:環境変数
load_dotenv(dotenv_path='.env')

END_POINT = os.getenv("END_POINT")
API_KEY = os.getenv("API_KEY")
```
もう言うことはあるまい。（さぼった。ｗ）

<br><br>


## 3-3. モデルの用意
```python:モデルの用意
llm = AzureChatOpenAI(
    model='gpt-4o-mini',
    azure_endpoint=os.getenv("END_POINT"),
    api_version=os.getenv("API_VERSION"),
    api_key=os.getenv("API_KEY")
)
```
いつもどgpt-4o-miniだ。安いし。


<br><br>


## 3-4. 回答させたい質問
```python:回答させたい質問
query = "エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。"
```
この文章の回答をさせましょう。多分、gpt-4o-miniであれば、きちんと回答できちゃうと思いますが、あえてtoolsを使って回答させます。
ということで、この目的に合わせたtoolsを作っていきましょう！

<br><br>



## 3-5. toolsの用意
```python:
@tool
def subtract(a: int, b: int) -> int:
    """absolute difference between two integers.
    return abs value.

    Args:
        a: First integer
        b: Second integer
    """
    return abs(a - b)

@tool
def get_name_age(name: str, age: int):
    """Get name and age.

    Args:
        name: Name
        age: Age like "YYYY-MM-DD"
    """
    return name, age

@tool
def get_name_age_from_birthday(name: str, birthday: str):
    """Get name and Birthday.

    Args:
        name: Name
        agbirthdaye: Birthday like "YYYY-MM-DD"
    """
    b_day = datetime.datetime.strptime(birthday, "%Y-%m-%d")
    age = datetime.datetime.now() - b_day

    return name, age.days // 365

@tool
def date_difference(start_date: str, end_date: str) -> str:
    """二つの日付の差を「〇年〇か月〇日」として計算する関数

    Args:
        start_date: start_date
        end_date: end_date
    """
    start = datetime.datetime.strptime(start_date, "%Y-%m-%d")
    end = datetime.datetime.strptime(end_date, "%Y-%m-%d")
    
    delta = relativedelta(end, start)
    
    return f"{delta.years}年{delta.months}か月{delta.days}日"
```
四つのtoolsを用意してみました。型定義はせず、シンプルにデコレータで定義しました。

- **subtract**
二つの整数の差を計算する関数
- **get_name_age**
名前と、年齢を取得する関数
- **get_name_age_from_birthday**
名前と誕生日から取得した年齢を計算する関数 
- **date_difference**
日付けと日付を比較して、何年何か月何日を計算する関数


さて、どのtoolが使われるんでしょうね。

<br><br>



## 3-6. messagesの用意
```python:
messages = [HumanMessage(query)]
messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={})]
```
過去の会話履歴をどんどんと積み重ねていく必要があるのでmessagesを用意します。ここに入力やモデルからのレスポンスを付け加えていきます。
最初の入力は質問文なので、`HumanMessage`として入れておきます。

<br><br>


## 3-7. toolsをモデルにバインド
```python:toolsのバインド
tools = [
    get_name_age, 
    date_difference, 
    get_name_age_from_birthday, 
    subtract, 
]
llm_with_tools = llm.bind_tools(tools)
```
用意したtoolsをモデルにバインドします。前回やったやつですね。

<br><br>


## 3-8. toolsの呼び出し辞書の作成
```python:
tools_dict = {
    "get_name_age": get_name_age,
    "get_name_age_from_birthday": get_name_age_from_birthday,
    "date_difference": date_difference,
    "subtract": subtract
}
```
後で、モデルが選択した実行すべき関数を選択するために、valueに関数が入った辞書を用意します。
なぜかって？理由はtool_collingの出力をよく見るとわかりますので、ちょっとここはがまん。

<br><br>


## 3-9. tool calling
```python:tool calling
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-***********', tool_calls=[{'name': 'get_name_age_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
```
tool callingの実行結果です。この出力の`additional_kwargs`を確認しましょう。

<br>

```python:tool callingの結果
result.additional_kwargs
# {'tool_calls': [{'id': 'call_***********',
#    'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}',
#     'name': 'get_name_age_from_birthday'},
#    'type': 'function'},
#   {'id': 'call_***********',
#    'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}',
#     'name': 'get_name_age_from_birthday'},
#    'type': 'function'}],
#  'refusal': None}
```
`result.name`に呼ぶべき関数が、`result.arguments`には関数に渡す引数が取得できてます。
どうやら、`get_name_age_from_birthday`関数が疲れそうです。
<br><br>


## 3-10. tool calling後の処理1
```python:messagesへtool callingの関数名、引数を追加
messages.append(result)
messages

# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
#  AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-***********', tool_calls=[{'name': 'get_name_age_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})]
```
ひとまず、tool_callingの結果をmessagesに追加しましょう。

<br><br>


## 3-11. tool実行＆実行結果のmessagesへの追加
```python:messagesへtool実行結果を追加
for tool_call in result.tool_calls:
    choiced_tool = tools_dict[tool_call['name']] # `tool_call['name']`が呼ぶべき関数名なので、`tools_dict`から関数を選択
    messages.append(choiced_tool.invoke(tool_call)) # 選択した関数を実行＆messagesに追加

messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
#  AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_***********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_***********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-***********', tool_calls=[{'name': 'get_name_age_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_***********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_***********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#  ToolMessage(content='["エメット・ブラウン", 74]', name='get_name_age_from_birthday', tool_call_id='call_***********'),
#  ToolMessage(content='["マーティー・マクフライ", 53]', name='get_name_age_from_birthday', tool_call_id='call_***********')]
```
`tool_call['name']`が呼ぶべき関数名なので、`tools_dict`から関数を選択します。
そして、tools_callをこの関数に渡して、関数を実行します。
実行結果を`messages`に追加します。
きちんと、ドクが74歳（@2025年）、マーティーが53歳（@2025年）と抽出できています。
使われた関数は`get_name_age_from_birthday`です。

<br><br>


## 3-12. さらにmessagesをバインドされたモデルに入力
```python
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"a":74,"b":53}', 'name': 'subtract'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 436, 'total_tokens': 453, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', tool_calls=[{'name': 'subtract', 'args': {'a': 74, 'b': 53}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 436, 'output_tokens': 17, 'total_tokens': 453, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
```
求めている答えはまだ出ていません。
`messages`をバインドされたモデルにさらに入力します。
またも、`tool calling`されたようですね。今度は`subtract`関数が呼ばれそうです。


<br><br>


## 3-13. tool calling & toolsの実行結果をmessagesに追加
```python
messages.append(result) # 忘れがち！

for tool_call in result.tool_calls:
    choiced_tool = tools_dict[tool_call['name']]
    messages.append(choiced_tool.invoke(tool_call))

messages
# [HumanMessage(content='エメット・ブラウンは1950年11月4日生まれです。マーティー・マクフライは1971年5月3日生まれです。二人の年齢差を教えてください。', additional_kwargs={}, response_metadata={}),
#  AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"name": "エメット・ブラウン", "birthday": "1950-11-04"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}, {'id': 'call_**********', 'function': {'arguments': '{"name": "マーティー・マクフライ", "birthday": "1971-05-03"}', 'name': 'get_name_age_from_birthday'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 82, 'prompt_tokens': 258, 'total_tokens': 340, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', tool_calls=[{'name': 'get_name_age_from_birthday', 'args': {'name': 'エメット・ブラウン', 'birthday': '1950-11-04'}, 'id': 'call_**********', 'type': 'tool_call'}, {'name': 'get_name_age_from_birthday', 'args': {'name': 'マーティー・マクフライ', 'birthday': '1971-05-03'}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 258, 'output_tokens': 82, 'total_tokens': 340, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#  ToolMessage(content='["エメット・ブラウン", 74]', name='get_name_age_from_birthday', tool_call_id='call_**********'),
#  ToolMessage(content='["マーティー・マクフライ", 53]', name='get_name_age_from_birthday', tool_call_id='call_**********'),
#  AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_**********', 'function': {'arguments': '{"a":74,"b":53}', 'name': 'subtract'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 436, 'total_tokens': 453, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'tool_calls', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', tool_calls=[{'name': 'subtract', 'args': {'a': 74, 'b': 53}, 'id': 'call_**********', 'type': 'tool_call'}], usage_metadata={'input_tokens': 436, 'output_tokens': 17, 'total_tokens': 453, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}}),
#  ToolMessage(content='21', name='subtract', tool_call_id='call_**********')]
```
またまた同じように`tool calling`の結果を`messages`に追加し、さらに`tool calling`で選択された関数を実行＆`messages`に追加していきます。

<br><br>


```python
result = llm_with_tools.invoke(messages)
result
# AIMessage(content='エメット・ブラウンは74歳、マーティー・マクフライは53歳です。二人の年齢差は21年です。', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 35, 'prompt_tokens': 461, 'total_tokens': 496, 'completion_tokens_details': {'accepted_prediction_tokens': 0, 'audio_tokens': 0, 'reasoning_tokens': 0, 'rejected_prediction_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': 0, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-mini-2024-07-18', 'system_fingerprint': 'fp_*****', 'finish_reason': 'stop', 'logprobs': None, 'content_filter_results': {}}, id='run-**********', usage_metadata={'input_tokens': 461, 'output_tokens': 35, 'total_tokens': 496, 'input_token_details': {'audio': 0, 'cache_read': 0}, 'output_token_details': {'audio': 0, 'reasoning': 0}})
```
出力されましたね！
ドクとマーティーの年齢差は21歳。
ん？ドクってそんなに若くないよね？最初の入力文章を作り間違えたみたい。笑

<br><br>



# 4. 終わりに
もう一つ例を出そうかと思ったけど、それほど変わらないものしか思い浮かばなかったのでやめときます。
今回、`tool calling`の出力結果（使用すべき関数名と引数）を実行し、その結果を`messages`に追加して出力、さらにその出力を使ってほしい結果を得る方法がわかりました。
`tool calling`の結果を`messages`に追加するとき、入れ忘れそうになるので気を付けましょう。僕は何度もやらかしました。

きちんと型定義した方が間違いのない動作になると思うので、要件等ですね。
ということで、前回、今回で`tools`の基礎を理解することができました。
**ドキュメントは焦らず、慌てず、じっくりと**読めば何とかなるもんですね。💦

ではまた

<br><br>