Function callingで検索エンジンを切り替えてみる

表題の通り、[Function calling](https://platform.openai.com/docs/guides/gpt/function-calling)でYahoo検索とYoutube検索を切り替えて遊んでみました。

https://platform.openai.com/docs/guides/gpt/function-calling

[Jupyter Notebook on Visual Studio Code](https://python-work.com/vscode-ext-jupyter/)で実験した内容なので見辛い点などは何卒ご了承くださいませ...🙇‍♂️

## さっそくやってみよう
必要なモジュールをインストール
```python:python
#%%
pip install openai
pip install google-search-results
```
WEB検索するAPIはこちらを使います。
無料利用枠もあるので登録してAPIキーを取得しておきます。

https://serpapi.com/

```python:python
#%%
import json
import os
import sys
import openai
from typing import Dict
from serpapi import GoogleSearch

openai.api_key="********" #Openaiのkey
search_api_key="********" #serpapiのKey
```

yahoo検索用の関数を作成しておきます。
必要そうなプロパティの上位3つだけ適当に格納しておきます。

https://serpapi.com/yahoo-search-api

```python:python
#%%
def yahoo_search(query: str) -> Dict[str, str]:  
  search = GoogleSearch({
    "engine": "yahoo",
    "p": query, 
    "api_key": search_api_key
  })
  result = search.get_dict()
  top3_result = result["organic_results"][:3]

  res = {
    "snippet":"",
    "link":""
    }

  for i in top3_result:
    res["snippet"] += i["snippet"] + ","
    res["link"] += i["link"] + " "

  return res
```


次にyoutube検索用の関数です。

https://serpapi.com/youtube-search-api

```python:python
#%%
def youtube_search(query: str) -> Dict[str, str]:
  
  search = GoogleSearch({
    "engine": "youtube",
    "search_query": query, 
    "api_key": search_api_key
  })
  result = search.get_dict()
  top3_result = result["video_results"][:3]

  res = {
    "title":"",
    "link":""
    }

  for i in top3_result:
    res["title"] += i["title"] + ","
    res["link"] += i["link"] + " "

  return res
```
関数の準備ができたので、AIにどの関数が使えるのかを教えます。
```python:python
#%%
# AIが、質問に対してどの関数を使うかどうかの判断基準、
# また使う時の引数は何にするかを判断するための情報を与える。
functions = [
    {
        "name": "yahoo_search",
        # 通常では答えられないような情報が必要な場合は、ウェブで情報を検索してください。
        # ユーザーの質問に「ウェブ検索」などの言葉が含まれている場合は、必ずこの機能を使用してください。
        "description": "If you need information that cannot normally be answered, search the web for information. If a user's question includes words such as 「search the web.」 be sure to use this function.",
        "parameters": {
            "type": "object",
            "properties": {
                # 引数の情報
                "query": {
                    "type": "string",
                    "description": "Search information from users. Split by the word to be searched.",
                },
            },
            "required": ["query"],
        },
    },
    {
        "name": "youtube_search",
        # 動画に関する情報が必要な場合は、youtube_searchで検索してください。
        # ユーザーの質問に「youtube検索」や「youtubeで検索」という言葉が含まれている場合は、
        # 必ずこの機能を使ってください。
        "description": "If you need information on a video, please use youtube_search to search for information. If the user's question includes the words 「youtube search」 or 「search on youtube」, be sure to use this function.",
        "parameters": {
            "type": "object",
            "properties": {
                # 引数の情報
                "query": {
                    "type": "string",
                    "description": "Search information from users. Split by the word to be searched.",
                },
            },
            "required": ["query"],
        },
    }
]
```
さて、ここまでで役者(？)は揃いました！！
質問はこちらにします。

```python:python
#%%
query = "推しの子ってどんなアニメ？関連する動画を教えて"
```
関連動画をおしえてほしいと懇願します🙏
```python:python
#%%
# AIが質問に対して使う関数と、その時に必要な引数を決める
# 特に関数を使う必要がなければfunction_callプロパティは返されない
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": query},
    ],
    functions=functions,
    function_call="auto",
)
```
こんな感じで返ってきます。
```json:json
{
  "id": "chatcmpl-7UsLEEaXynP2MSC4Sn6cv835nZUYK",
  "object": "chat.completion",
  "created": 1687593084,
  "model": "gpt-3.5-turbo-0613",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "function_call": {
          "name": "youtube_search",
          "arguments": {"query": "推しの子 アニメ"}
        }
      },
      "finish_reason": "function_call"
    }
  ],
  "usage": {
    "prompt_tokens": 186,
    "completion_tokens": 20,
    "total_tokens": 206
  }
}
```
どの関数が選ばれたかをチェックします。
もしどの関数も選ばれなければ通常実行させときます。
```python:python
#%%
# 選ばれた関数名をチェック
function_call = message.get("function_call")
function_name = ""

if function_call:
  function_name = message["function_call"]["name"]
  print(function_name)
# 何も選ばれてなければ通常実行
else:
  print("function_call is None")
  response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": query},
    ],
    )
  print(response.choices[0]["message"]["content"].strip())
```
選ばれた関数名は…
```
youtube_search
```
引数はどうなっているかというと…
```python:python
#%%
arguments = json.loads(message["function_call"]["arguments"])
print(arguments)
```
```
{'query': '推しの子 アニメ'}
```
ちゃんと単語で分割してくれてる。偉い！
それでは、選ばれた関数を実行しましょう。

```python:python
#%%
# 関数の実行
if function_name == "youtube_search":
  function_response = youtube_search(
      query=arguments.get("query"),
  )
  func_content = function_response["title"] + ' You can read more at: ' + function_response["link"]

elif function_name == "yahoo_search":
  function_response = yahoo_search(
    query=arguments.get("query"),
  )
  func_content = function_response["snippet"] + ' You can read more at: ' + function_response["link"]
```
`func_content`の中身はこんな状態です。
```
TVアニメ『【推しの子】』本予告【2023年4月より放送開始】,YOASOBI「アイドル」 Official Music Video,MＡＤ ライブ音響【推しの子】Ｂ小町「サインはＢ」４Ｋ６０ｆｐｓ高画質, You can read more at: https://www.youtube.com/watch?v=BQ28u-8c-hI https://www.youtube.com/watch?v=ZRtdQ81jPUQ https://www.youtube.com/watch?v=WI8NiVbVF1Q
```
最後に、関数の実行結果を渡してもう一度質問してみましょう。
```python:python
#%%
# 関数実行結果を使ってもう一度質問
second_response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo-0613",
    messages=[
        {"role": "user", "content": query},
        {"role": "assistant", "content": f"You respond with the information {func_content} given. If the information has a url, you will also provide the url as the source of the information."},
        message,
        {
            "role": "function",
            "name": function_name,
            "content": func_content,
        },
    ],
)

print(second_response.choices[0]["message"]["content"].strip())
```
```
『推しの子』は、2023年4月に放送が開始されるテレビアニメです。具体的な内容やストーリーについては、本予告をご覧ください。

本予告：TVアニメ『【推しの子】』本予告【2023年4月より放送開始】
URL：https://www.youtube.com/watch?v=BQ28u-8c-hI

また、アニメの中で使用される主題歌「アイドル」はYOASOBIによって歌われています。公式のミュージックビデオも公開されています。

YOASOBI「アイドル」 Official Music Video
URL：https://www.youtube.com/watch?v=ZRtdQ81jPUQ

さらに、推しの子関連のエピソード1から8までのアニメ動画も視聴することができます。

推しの子 アニメ 1-8話
URL：https://www.youtube.com/watch?v=ZIWy3FVkTwM

詳細情報は上記のURLからご確認ください。
```
なるほど！
ここまで動かしてみて、なんとなく流れがわかりました！
`Function calling`とはLLMにどの関数を実行させるか判断させ、かつ引数の加工をおこなってくれる感じなんですねえ。偉い。

ちなみに、以下のように尋ねると、
通常通りの実行となり、思ってたんとちがーう！という答えが返ってきました👀
```python:python
query = "推しの子ってどんなアニメ？"
```
```
「推しの子」は、ユーザーがアイドル育成ゲームをプレイしながら応援しているキャラクターを指すことが一般的です。
これに関連して、2020年には「推しが武道館いってくれたら死ぬ」（通称：推し武道）というテレビドラマも放送されました。
ただし、アニメとしては「推しの子」という具体的な作品は存在しないようです。
もし推しの子が登場するアニメを探しているのであれば、具体的なキャラクター名や作品名などを教えていただければ、情報を提供できるかもしれません。
```
そこで、「ウェブで検索して答えて」と付け足す事で、Yahoo検索を選択してくれるようになりました。
```python:python
query = "推しの子ってどんなアニメ？ウェブで検索して答えて"
```
```
「推しの子」は、2023年4月から放送が開始されたTVアニメです。Blu-ray&DVDも発売されており、関連イベントや劇場上映も行われています。

ストーリーは、赤坂アカ氏と横槍メンゴ氏による人気漫画作品です。詳細な情報については、以下のウェブサイトで確認することができます。

- 公式ウェブサイト: https://ichigoproduction.com/
- YouTubeチャンネル: https://www.youtube.com/@anime_oshinoko
- Abema TIMES: https://times.abema.tv/articles/-/10056142

以上が「推しの子」に関する情報です。
```
おおお、これは、なんだか楽しいぞ😆
あらゆるものに応用できそうなので、今後も色々試してみます！

## 参考にさせて頂いた記事
https://platform.openai.com/docs/guides/gpt/function-calling
https://dev.classmethod.jp/articles/understand-openai-function-calling/