ChatGPTを使ったPython関数エミュレーション

# やりたいこと
* プロンプトエンジニアリングの一種として、実装のない関数をChatGPTに渡して実装を推測してもらうというやり方がある
* これをもうちょっと使いやすくしたい。具体的には普通の(Python)関数と同じ感じで使えるようにしたい

https://twitter.com/8kpx/status/1637284615126695937

# 元ネタ
marvinというライブラリで同じようなことができる

https://www.askmarvin.ai/guide/concepts/ai_functions/

# 改善したい点
* ChatGPTへ渡す時のテンプレートが固定
    * 拡張性の観点で、ここは自分で触れるようにしたい

# やってみた

こちらの実装もmarvinの実装を相当程度参考にしています

https://github.com/PrefectHQ/marvin/blob/main/src/marvin/bots/ai_functions.py#L72

まずは、ChatGPTに関数実行をエミュレートさせる部分

```excute_by_ai.py
from typing import Dict, Any
import json

def excute_by_ai(fn_def: str, arguments: Dict[str, Any]):
    function_name = fn_def.lstrip("def").split("(")[0].strip()
    kv = ", ".join(["=".join([key, json.dumps(value, ensure_ascii=False)]) for key, value in arguments.items()])

    # `の前の\はQiitaのsyntax highlightのためなので、本来は不要
    context = f"""
\```python
{fn_def.strip()}
\```
Estimate the return value when it was called with follwing arguments.
Only returns the return value of the function. No explanation is required.
{function_name}({kv})
"""
    # print(context)
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "system", "content": "You are a python simulator, which simulates the return value from definition of function."},
            {"role": "user", "content": context[1:-1]}
        ],
        max_tokens=1024,
        temperature=0
    )

    result = response.choices[0]['message']['content']

    # 戻り値は ->と:の間にある
    return_type: str = fn_def.split("->")[1].split(":")[0].strip()
    # print(return_type)

    ### 戻り値の型に応じて、パース可能なように変換する

    # 戻り値がstrなら""で囲む
    if (return_type == "str" or return_type == "Optional[str]") and result != "None":
        if not result.startswith('"'):
            result = f'"{result}'
        if not result.endswith('"'):
            result = f'{result}"'

    try:
        return eval(result)
    except:
        print(response.choices)
        print(result)
        raise AssertionError
```

この関数を普通の関数っぽく使えるようにするためのデコレータ

```ai_decorator.py
import inspect
from typing import Callable, Optional, List
import excute_by_ai

def ai_decorator(fn: Callable):
    # 必要に応じてつけてください
    # @wraps(fn)
    def ai_wrapper(*args, **kwargs):
        # 関数定義の取得
        # @ai_decoratorの行を消したいので2行目以降だけにしている
        fn_def = "\n".join(inspect.getsource(fn).split("\n")[1:])

        # 関数の引数を取得
        sig = inspect.signature(fn)
        bound_args = sig.bind(*args, **kwargs)
        return excute_by_ai(fn_def, bound_args.arguments)

    return ai_wrapper
```

利用例
```python
import ai_decorator

# type hintやdocstringはChatGPT実行時のヒントになるので書くことを推奨します

@ai_decorator
def extract_song_title(video_title: str, description: str) -> Optional[str]:
    """動画のタイトルと概要欄から、曲名だけを抽出する。取得できない場合はNoneを返す"""


song_title = extract_song_title(video_title=video_title, description=description)
```

# どう動くの？

## ai_decoratorについて

デコレータの動きについては下記記事などを参考にしてください

https://zenn.dev/ryo_kawamata/articles/learn_decorator_in_python

結局、以下のように変換された結果、ChatGPTを呼び出す関数（excute_by_ai）が呼ばれます
```python
# 元々の記述
@ai_decorator
extract_song_title(video_title=video_title, description=description)

# デコレータを展開する
ai_decorator(extract_song_title)(video_title=video_title, description=description)

# ai_decorator(extract_song_title)を実行してai_wrapperが返ってくる
# ただしai_wrapper内の変数fnはextract_song_titleにbindされている
ai_wrapper(video_title=video_title, description=description)

# 最終的に呼ばれる関数
excute_by_ai(extract_song_titleの定義, extract_song_titleの引数)
```

## excute_by_ai

色々やってますが、最終的に以下のようなプロンプトがChatGPTに渡されます
```python
# 上のコードと同じく、\はQiita用に追加したものなので実際には送られません
\```python
def extract_song_title(video_title: str, description: str) -> Optional[str]:
    """動画のタイトルと概要欄から、曲名だけを抽出する。取得できない場合はNoneを返す"""
\```
Estimate the return value when it was called with follwing arguments.
Only returns the return value of the function. No explanation is required.
extract_song_title(video_title="[video_title]", description="[description]")
```

# 使い道

以前記事にしたような、コード化しにくいデータ処理をしたい時などに使えそうかなと思います。

https://qiita.com/miyatsuki/items/04ae3c2810c3aadf72c7

# おまけ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176335/ee9611c3-ebf9-2a6b-49c4-ab0991a7d9f2.png" width="500" />

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176335/23f457d9-d6dd-33fe-b51b-e7ccaa016801.png" width="500" />