LangChainでToolを作ってアニメランキングを表示させてみた

みなさんがよく使っているChatGPTで、あ～これネットの最新の情報取ってきたいんだよなぁみたいな事ありませんか？ChatGPTは、モデルが作られたときの情報しか持っていないので、最新の情報を取得できません。。。

でも、ネットから情報を取ってきていい感じに表示してほしいんだけどみたいなこともあると思います。

そこで今回は、「アニメランキング」の最新を取得する機能（Tool）を作って解説してみたいと思います。

### 今回の仕様は、下記となります。

- dアニメからの情報をランキング化する（300位までのデータ）
    - プロンプトで順位の指定があった場合、その順位のアニメを提示する。
    - プロンプトで順位指定がなければ、アニメランキングのTOP10を提示する。
    - 300位以上のデータを希望された場合は、格納していないのでダメです！と警告を返す。

## まずは下準備

dアニメのランキングから、ランキング情報を叩いているAPIが多分あるはずなので、Developerツールを使って探してみます。※色々探したのですが、アニメランキングはdアニメがいちばん簡単そうでした。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/f110ea77-4ba3-c6f9-0d32-f20af689c1d0.png)

お！ちゃんと、jsonデータで格納されてますね！ありがたく使わせていただきます・・

```bash
https://animestore.docomo.ne.jp/animestore/rest/WS000103?rankingType=01
```

ここに、GETメソッドでアクセスするとアニメランキングのJSONが帰ってきます。

すご～くシンプル！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/4af27145-64c2-ed8a-11ea-448ef777c389.png)

とりあえず、アニメ情報を取得して配列に格納する簡単なコードを書いてみました。ちゃんと配列に入ってますね！いい感じ。。

## LangChainのTool構造について理解する

そもそもLangChainのツールってなんぞやってところなんですが、GPTにいろんなところとやり取りするための機能です。例えば、Wikipediaから情報を持ってくる事もできますし、GPTは計算が苦手なので、計算用のToolを作って正しく計算させるといったことができたりします。

では早速、Toolの構造からみていきましょう。。

実は私Tool全然作ったこと無くてどういう中身かわかっていないので、Langchain公式のWikipedia検索Toolのコードを参考にして作ってみたいと思います。

[langchain/libs/langchain/langchain/tools/wikipedia/tool.py at master · langchain-ai/langchain · GitHub](https://github.com/langchain-ai/langchain/blob/master/libs/langchain/langchain/tools/wikipedia/tool.py)

コードを見てみると、説明にこんな感じで書いてあって

```python
description = (
        "A wrapper around Wikipedia. "
        "Useful for when you need to answer general questions about "
        "people, places, companies, facts, historical events, or other subjects. "
        "Input should be a search query."
    )
```

日本語にすると下記のように書いてあります。

- ウィキペディアのラッパー
- 以下に関する一般的な質問に答える必要がある場合に便利です。
- 人物、場所、企業、事実、歴史的出来事、その他のテーマ
- 入力は検索 query でなければならない。

```python
def _run(
        self,
        query: str,
        run_manager: Optional[CallbackManagerForToolRun] = None,
    ) -> str:
        """Use the Wikipedia tool."""
        return self.api_wrapper.run(query)
```

デフォルトで、_runメソッドが呼び出された際に引数queryがありますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/914d2ef5-8968-4e1b-fa17-2e62f672837b.png)


つまりは、descriptionで使い方とメソッドに与える引数や値を書いておけばその値が入って来るようになるわけです。

ほ～～なんて便利なんだろう・・・この簡単さにはびっくりです！

では、簡単にToolの構造がわかったところで、WikipediaToolを参考にアニメランキングToolを作っていこうと思います。

## Toolの実装

今回もいつも通りGoogleColabを使います。GoogleColabすごく便利・・

とりあえず、使うライブラリをインストールします。

```bash
#必要ファイルインストール
!pip install langchain
!pip install openai
```

OPENAI API KEYをセットします！

```bash
import os
import openai
os.environ["OPENAI_API_KEY"]="" #自分のOPENAIのAPIKEYを入れてください
openai.api_key = os.environ["OPENAI_API_KEY"]
```

# **早速Toolを作ります。**

名前はAnimeRankingToolという名前にしました。

```python
class AnimeRankingTool(BaseTool):
    name = 'AnimeRanking'
    '''
      'アニメのランキングを探す場合に有用です.'
      'インプットは、1つのキー"rank"を持っておりアニメの順位が入ります。もし順位の指定がない場合は"rank"にはNoneが入ります。'
    '''
    description = (
      'Useful if you are looking for anime rankings.'
      'The input has one key "rank" and contains the ranking of the anime. If no rank is specified, then "rank" will be set to None.'
    )
```

クラス名はAnimeRankingToolで設定します。

アニメのランキングを取得するためのツールクラスです。

- クラス変数
    - name: ツールの名前を表す文字列です。「AnimeRanking」に設定しました。
    - description: ツールの説明を表す文字列です。ここにツールの説明を記載します。

ここでは、下のようにdescriptionを記載しました。

```python
 description = (
      'Useful if you are looking for anime rankings.'
      'The input has one key "rank" and contains the ranking of the anime. If no rank is specified, then "rank" will be set to None.'
    )
```

▼日本語訳

```
'アニメのランキングを探す場合に有用です.'
'インプットは、1つのキー"rank"を持っておりアニメの順位が入ります。もし順位の指定がない場合は"rank"にはNoneが入ります。'
```

メソッド：

- _runメソッド：
    - 引数：rank（アニメの順位を指定する整数またはNone）
    - 戻り値：文字列
    - メソッドの説明
        - アニメのランキングを取得し、指定された順位のアニメの情報を返します。
        - もし順位が指定されていない場合Noneである場合は、dアニメランキングのトップ10を返します。
        - 300位以上のランキングは取得していないため、それ以上の順位を指定すると警告が返されます。
    
    ```python
    def _run(self, rank):
            #ランキングを格納する配列を生成
            anime_rank_array = []
            anime_rank_array.clear()
            return_prompt =""
    
            #アニメランキングを取得する
            url = requests.get("https://anime.dmkt-sp.jp/animestore/rest/WS000103?rankingType=01")
            text = url.text
            data = json.loads(text)
     
            #アニメランキングを0番目の配列から順番に格納
            for i in range(0, 300):
              title = data["data"]['workList'][i]['workInfo']['workTitle']
              #アニメのタイトルを格納します。
              anime_rank_array.append(title)
            if rank != None:
              # return_prompt = f"dアニメランキングでは'{anime_rank_array[rank-1]}'が{rank}位です。"
              return_prompt = f"According to the dアニメランキング, '{anime_rank_array[rank-1]}' is No. {rank}."
    
            elif rank == None:
              #順位の指定がない場合はTOP10を表示する。
              return_prompt="The top 10 in the dアニメランキング ranking are as follows.\n"
    
              #0から順にサーベイするので、anime_rank_arrayはそのままindex値を格納し、順位は+1する。
              for i in range(0,10):
                return_prompt = return_prompt + f"'{anime_rank_array[i]}'is No. {i+1}.\n"
            #300位以上のランキングは格納していないので、警告を返す        
            elif rank > 300:
              return "The total number of rankings stored in dAnime Ranking is up to 300. No ranking beyond that can be provided."
              exit
            print(return_prompt)
            return return_prompt
    ```
    

- _arunメソッド：
    - 引数：query（クエリ、使用されていません）
    - 戻り値：なし（このメソッドは非同期に対応していないため、例外が発生します）
        - このメソッドは非同期に対応していないため、非同期処理を行う場合は例外が発生します。
    
    ```python
    async def _arun(self, query):
            raise NotImplementedError("AnimeRankingTool does not support async")
    ```

# 最終的に完成したコード
```python
import requests
import json

class AnimeRankingTool:
    name = 'AnimeRanking'
    description = (
      'Useful if you are looking for anime rankings.'
      'The input has one key "rank" and contains the ranking of the anime. If no rank is specified, then "rank" will be set to None.'
    )

    def _run(self, rank):
        print("--------------------------------"+str(rank))
        #ランキングを格納する配列を生成
        anime_rank_array = []
        anime_rank_array.clear()
        return_prompt =""

        #アニメランキングを取得する
        url = requests.get("https://anime.dmkt-sp.jp/animestore/rest/WS000103?rankingType=01")
        text = url.text
        data = json.loads(text)

        #アニメランキングを0番目の配列から順番に格納
        for i in range(0, 300):
            title = data["data"]['workList'][i]['workInfo']['workTitle']
            #アニメのタイトルを格納します。
            anime_rank_array.append(title)
        if rank != None:
            # return_prompt = f"dアニメランキングでは'{anime_rank_array[rank-1]}'が{rank}位です。"
            return_prompt = f"dアニメランキングによると、'{anime_rank_array[rank-1]}' は No. {rank}です。"

        elif rank == None:
            #順位の指定がない場合はTOP10を表示する。
            return_prompt="dアニメランキングのトップ10は以下の通りです。\n"

            #0から順にサーベイするので、anime_rank_arrayはそのままindex値を格納し、順位は+1する。
            for i in range(0,10):
                return_prompt = return_prompt + f"'{anime_rank_array[i]}'is No. {i+1}.\n"
        #300位以上のランキングは格納していないので、警告を返す        
        elif rank > 300:
            return "dAnime Rankingに保存されているランキングの総数は最大300までです。それを超えるランキングは提供できません。"
            exit
        # print(return_prompt)
        return return_prompt
```

# Toolはできたんで、呼び出してみますか～

呼び出しはこんな感じ、いつもの感じですね。。

initialize_agentで今回作った、AnimeRankingToolクラスを指定しています。initialize_agentの第一引数はツール郡です。

```python
from langchain.agents import initialize_agent, AgentType
from langchain.chat_models import ChatOpenAI
# AnimeRankingTool=AnimeRankingTool()
chat = ChatOpenAI(temperature=0)
agent = initialize_agent([AnimeRankingTool()], chat, agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
result = agent.run("アニメランキングを教えてください")
print(result)
```

ここで、agentに対して質問を投げかけています。

```python
result = agent.run("アニメランキングを教えてください")
```

## 結果は・・・・・・・

```python
> Entering new AgentExecutor chain...
Thought: The user is asking for anime rankings. I can use the AnimeRanking tool to provide the rankings.

Action:
```
{
  "action": "AnimeRanking",
  "action_input": {
    "rank": null
  }
}
```The top 10 in the dアニメランキング ranking are as follows.
'無職転生Ⅱ ～異世界行ったら本気だす～'is No. 1.
'英雄教室'is No. 2.
'ゾン100～ゾンビになるまでにしたい100のこと～'is No. 3.
'るろうに剣心 －明治剣客浪漫譚－'is No. 4.
'TVアニメ『レベル1だけどユニークスキルで最強です』'is No. 5.
'BLEACH 千年血戦篇'is No. 6.
'呪術廻戦 懐玉・玉折／渋谷事変（第2期）'is No. 7.
'Lv1魔王とワンルーム勇者'is No. 8.
'七つの魔剣が支配する'is No. 9.
'好きな子がめがねを忘れた'is No. 10.

Observation: The top 10 in the dアニメランキング ranking are as follows.
'無職転生Ⅱ ～異世界行ったら本気だす～'is No. 1.
'英雄教室'is No. 2.
'ゾン100～ゾンビになるまでにしたい100のこと～'is No. 3.
'るろうに剣心 －明治剣客浪漫譚－'is No. 4.
'TVアニメ『レベル1だけどユニークスキルで最強です』'is No. 5.
'BLEACH 千年血戦篇'is No. 6.
'呪術廻戦 懐玉・玉折／渋谷事変（第2期）'is No. 7.
'Lv1魔王とワンルーム勇者'is No. 8.
'七つの魔剣が支配する'is No. 9.
'好きな子がめがねを忘れた'is No. 10.

Thought:I have obtained the current top 10 rankings in the dアニメランキング. 

Final Answer: The top 10 anime rankings in the dアニメランキング are as follows:
1. 無職転生Ⅱ ～異世界行ったら本気だす～
2. 英雄教室
3. ゾン100～ゾンビになるまでにしたい100のこと～
4. るろうに剣心 －明治剣客浪漫譚－
5. TVアニメ『レベル1だけどユニークスキルで最強です』
6. BLEACH 千年血戦篇
7. 呪術廻戦 懐玉・玉折／渋谷事変（第2期）
8. Lv1魔王とワンルーム勇者
9. 七つの魔剣が支配する
10. 好きな子がめがねを忘れた

> Finished chain.
The top 10 anime rankings in the dアニメランキング are as follows:
1. 無職転生Ⅱ ～異世界行ったら本気だす～
2. 英雄教室
3. ゾン100～ゾンビになるまでにしたい100のこと～
4. るろうに剣心 －明治剣客浪漫譚－
5. TVアニメ『レベル1だけどユニークスキルで最強です』
6. BLEACH 千年血戦篇
7. 呪術廻戦 懐玉・玉折／渋谷事変（第2期）
8. Lv1魔王とワンルーム勇者
9. 七つの魔剣が支配する
10. 好きな子がめがねを忘れた
```

おぉ～～～～できた～～～！！！！！！！！！

# 別の聞き方も試してみます。

```python
result = agent.run("アニメランキングの1位を教えてください")
```

結果は下記のようになりました。

```python
> Entering new AgentExecutor chain...
Thought: The user is asking for the top-ranked anime. I can use the AnimeRanking tool to find the answer.

Action:
```
{
  "action": "AnimeRanking",
  "action_input": {
    "rank": 1
  }
}
```According to the dアニメランキング, '無職転生Ⅱ ～異世界行ったら本気だす～' is No. 1.

Observation: According to the dアニメランキング, '無職転生Ⅱ ～異世界行ったら本気だす～' is No. 1.
Thought:The top-ranked anime is '無職転生Ⅱ ～異世界行ったら本気だす～'.
Final Answer: 無職転生Ⅱ ～異世界行ったら本気だす～

> Finished chain.
無職転生Ⅱ ～異世界行ったら本気だす～
```

ちゃんと「無職転生Ⅱ ～異世界行ったら本気だす～」が1位になってますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/bf2b2c4f-7eae-544e-94d2-fe41a5896994.png)

# ひねった聞き方をしてみました。

```python
result = agent.run("アニメランキングの1位と2位を教えてください")
```

下記のような結果になりました。

```python
> Entering new AgentExecutor chain...
Question: Please tell me the rankings for 1st and 2nd place in the anime ranking.

Thought: I can use the AnimeRanking tool to get the rankings.

Action:
```
{
  "action": "AnimeRanking",
  "action_input": {
    "rank": 1
  }
}
```

According to the dアニメランキング, '無職転生Ⅱ ～異世界行ったら本気だす～' is No. 1.

Observation: According to the dアニメランキング, '無職転生Ⅱ ～異世界行ったら本気だす～' is No. 1.
Thought:I have obtained the ranking for the 1st place anime. Now I need to get the ranking for the 2nd place.

Action:
```
{
  "action": "AnimeRanking",
  "action_input": {
    "rank": 2
  }
}
```

According to the dアニメランキング, '英雄教室' is No. 2.

Observation: According to the dアニメランキング, '英雄教室' is No. 2.
Thought:I have obtained the rankings for both 1st and 2nd place in the anime ranking.

Final Answer: The anime ranked 1st is '無職転生Ⅱ ～異世界行ったら本気だす～' and the anime ranked 2nd is '英雄教室'.

> Finished chain.
The anime ranked 1st is '無職転生Ⅱ ～異世界行ったら本気だす～' and the anime ranked 2nd is '英雄教室'.
```

agentが2回ツールを使ってくれて、最終的な答えは「The anime ranked 1st is '無職転生Ⅱ ～異世界行ったら本気だす～' and the anime ranked 2nd is '英雄教室'.」となっていますね！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/418662/3d5e0202-b7a5-f1bd-e921-eb6455f9f0cf.png)


# ちゃんと合ってる！！

英語で返ってくるのはツールの返答を私が英語にしてるからですね。

日本語で帰ってくるようにする必要がある場合は「日本語で回答してください」などの文章を付けてあげると日本語で返ってきます。

以上、LangChainでToolを使って遊んでみました。

もちろん仕事でも使えますよ！自動化API用意しておいて上げて、Toolをこんな感じで作ってあげると、agentに問いかければ自動化処理やってくれたりもできます。