Open AI の Function Calling を試す

OpenAI が Function Calling https://platform.openai.com/docs/guides/gpt/function-calling という新しい機能をリリースしました。GPT というと自然言語でお願いをして自然言語で答えが返ってくるので GPT の返答を元にプログラムで何かしようとすると結構工夫が必要なのですが、Function Calling を使うと決まった形で返答を受け取る事ができてとても便利です。ためしにウェブ上のイベント情報から場所と日時を抜き出してみます。

準備:

* 適宜必要なパッケージを import してください。
* OpenAI の API Key を環境変数 OPENAI_API_KEY に入れるか Python の `openai.api_key` に入れてください。


```python
import json
from pprint import pprint
from tenacity import retry, wait_random_exponential, stop_after_attempt
import requests
from bs4 import BeautifulSoup
import openai
```


```python
GPT_MODEL = "gpt-3.5-turbo-0613"
URL = "https://bijutsutecho.com/exhibitions/11828"
```

まず便利関数を https://github.com/openai/openai-cookbook/blob/main/examples/How_to_call_functions_with_chat_models.ipynb から借用します。


```python
@retry(wait=wait_random_exponential(min=1, max=40), stop=stop_after_attempt(3))
def chat_completion_request(messages, functions=None, model=GPT_MODEL):
    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + openai.api_key,
    }
    json_data = {"model": model, "messages": messages}
    if functions is not None:
        json_data.update({"functions": functions})
    # pprint(json_data)
    try:
        response = requests.post(
            "https://api.openai.com/v1/chat/completions",
            headers=headers,
            json=json_data,
        )
        if not response.ok:
            print(f"Response: {response.json()}")
        return response
    except Exception as e:
        print("Unable to generate ChatCompletion response")
        print(f"Exception: {e}")
        return e
```

Function とは GPT が勝手に関数を呼び出すわけではなく、指定したフォーマットに従って解答を返してくれるという機能です。その後受け取った側が必要に応じて処理を行います。

ここでは iCalendar https://datatracker.ietf.org/doc/html/rfc5545 の用語に従いイベント情報を返してもらう事にします。


```python
event_functions = [
    {
        "name": "make_event",
        "description": "Create a calendar event in iCalendar format",
        "parameters": {
            "type": "object",
            "properties": {
                "SUMMARY": {
                    "type": "string",
                    "description": "a short, one-line description of the event",
                },
                "DESCRIPTION": {
                    "type": "string",
                    "description": "a more complete description of the calendar",
                    "maxLength": 400, # JSON Schema の maxLength に対応していないらしく長過ぎてたまにエラーになる
                },
                "DTSTART": {
                    "type": "string",
                    "description": "the date and time that the event begins such as 19980119T020000",
                },
                "DTEND": {
                    "type": "string",
                    "description": "the date and time that the event ends such as 19980119T030000",
                },
                "LOCATION": {
                    "type": "string",
                    "description": "the intended venue with address for the event.",
                },
            },
            "required": ["SUMMARY", "DTSTART", "DTEND"],
        },
    }
]
```

与えられた URL からページの内容をテキストで取得する部分を作ります。requests と BeautifulSoup で抜いて雑に先頭から 3000 文字抜き出します。

こだわりのある人は Auto-GPT みたいにブラウザを立ち上げても良いと思います。


```python
import re
def web_to_text(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.text, 'html.parser')
    for style_tag in soup.find_all(['style', 'script']):
        style_tag.decompose()
    
    text = soup.get_text(separator='\n', strip=True)
    return text[:3000]


print(web_to_text(URL)[:100])
```

    東弘一郎　個展「HANMA」（アートフロントギャラリー）｜美術手帖
    Sorry...
    本サイトではJavaScriptを使用しています。
    ご使用のブラウザの環境設定においてJavaScriptをオン（


実はサイトによっては schema.org のデータがあって簡単にカレンダー情報を取れるのですが、GPT がそれに惑わされてやたら長い DESCRIPTION を返しエラーになる事があるので、今回あえて本文だけを読んでいます。


```python
def extract_event_request(url):
    web_content = web_to_text(url)
    chat = []
    chat.append({"role": "user", "content": f"Could you make an event from the text?: {web_content}"})
    chat_response = chat_completion_request(chat, functions=event_functions)
    return chat_response.json()


def extract_event(url):
    response = extract_event_request(url)
    response_message = response['choices'][0]['message']
    arguments = response_message['function_call']['arguments']
    return json.loads(arguments)

```


```python
pprint(extract_event(URL))
```

    {'DESCRIPTION': '東弘一郎の初個展「HANMA」が開催されています。展示される作品は「佐原の大祭」で使われる山車の木製車輪「半間」をモチーフにしたものです。会期は2023年6月9日から7月16日までで、会場はアートフロントギャラリーです。',
     'DTEND': '2023-07-16T00:00:00',
     'DTSTART': '2023-06-09T00:00:00',
     'LOCATION': '東京都渋谷区猿楽町29-18 代官山ヒルサイドテラスA棟 アートフロントギャラリー',
     'SUMMARY': '東弘一郎 個展「HANMA」'}


せっかくなので他にも色々抜き出します。


```python
# web_to_text("https://iko-yo.net/events/49287")
# extract_event_request("https://iko-yo.net/events/49287")
pprint(extract_event("https://iko-yo.net/events/49287"))
```

    {'DTEND': '2023-08-30T17:30:00',
     'DTSTART': '2023-06-04T09:30:00',
     'LOCATION': '東京都小平市花小金井南町３丁目ミュウテック工房 花小金井教室',
     'SUMMARY': '【募集中】夏休み子ども工作教室2023～天然木材で作る動く工作に挑戦'}



```python
pprint(extract_event("https://www.mot-art-museum.jp/exhibitions/hockney/"))
```

    {'DESCRIPTION': '東京都現代美術館では、2023年7月15日(土)から11月5日(日)まで、「デイヴィッド・ホックニー展」（主催：東京都現代美術館、読売新聞社）を開催します。\n'
                    '\n'
                    'ホックニーは60年以上にわたり、絵画、ドローイング、版画、写真、舞台芸術といった分野で多彩な作品を発表し続けてきました。本展は、イギリス各地とロサンゼルスで制作された多数の代表作に加えて、近年の風景画の傑作〈春の到来〉シリーズやCOVID-19によるロックダウン中にiPadで描かれた全長90メートルにもおよぶ新作まで120点余の作品によって、ホックニーの世界を体感できる機会となるでしょう。',
     'DTEND': '2023-11-05T18:00:00',
     'DTSTART': '2023-07-15T10:00:00',
     'LOCATION': '東京都現代美術館｜MUSEUM OF CONTEMPORARY ART TOKYO',
     'SUMMARY': 'デイヴィッド・ホックニー展'}



```python
pprint(extract_event("https://www.minpaku.ac.jp/ai1ec_event/43337"))
```

    {'DESCRIPTION': '狩りは、私たちの誕生した先史の時代から現在までかわらず人を魅了し続けています。狩りの魅力とは、いったい何でしょうか。世界の隅々にくらすハンターの生きざまをとおして狩りの意味や地球の未来を考えます。',
     'DTEND': '2023-07-23T15:00:00+09:00',
     'DTSTART': '2023-07-23T14:30:00+09:00',
     'LOCATION': '国立民族学博物館\u3000本館展示場（ナビひろば）',
     'SUMMARY': '人間にとって狩猟とは何か'}

できました！日付のフォーマットが間違っていたり、多少へんな所もありますが、まあまあ実用的なレベルなのではと思います。