【Python】ChatGPT APIでウェブサイト版のように返答を逐次受け取る方法

# はじめに
ChatGPT APIの公開後、瞬く間に様々なサービスが誕生していますね。
私も少し触ってみましたが、マルコフ連鎖的に文章を紡ぐ仕組み上、レスポンスがやや遅い点が気になります。
PythonからAPIを叩いている多くの方はOpenAIのライブラリを使用しているかと思いますが、
一般的にHTTPリクエストに使用される`requests`を使用すれば、**ウェブサイト版のChatGPTとお話するときのように、逐次返答を受け取ることができます。**

追記
`openai`ライブラリでも普通にできました。後半で解説します。

# 方法
`requests`の`post()`からAPIを叩いて、その際、引数とペイロードの`stream`を`True`にすればおｋです。
以下の実装例の後半部分が該当箇所になります。

```python
import requests, json

API_URL = "https://api.openai.com/v1/chat/completions"
API_KEY = "sk-************************************************"


def chat(text,
         messages=None,
         settings="",
         max_tokens=2000,
         temperature=1.,
         top_p=.1,
         presence_penalty=0.,
         frequency_penalty=0.):

    # やり取りの管理
    messages = messages if messages is not None else []
    if settings and not messages:
        messages.append({"role": "system", "content": settings})
    messages.append({'role': 'user', 'content': text})

    # ヘッダ
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {API_KEY}"
    }

    # ペイロード
    payload = {
        "model": "gpt-3.5-turbo",
        "messages": messages,
        "max_tokens": max_tokens,
        "temperature": temperature,
        "top_p": top_p,
        "presence_penalty": presence_penalty,
        "frequency_penalty": frequency_penalty,
        "stream": True,
    }

    # APIを叩く、streamをTrueに
    resp = requests.post(API_URL, headers=headers, json=payload, stream=True)

    # 返答を受け取り、逐次yield
    response_text = ''
    for chunk in resp.iter_lines():
        try:
            j = json.loads(chunk.decode()[6:])
            content = j['choices'][0]['delta'].get('content')
            if content:
                response_text += content
                yield content
        except:
            ...
    else:  #
        messages += [{'role': 'assistant', 'content': response_text}]

```

## 追記 `openai`ライブラリでの方法
`openai.ChatCompletion.create()`の引数`stream`を`True`にしてあげればおkです。
以下の実装例では上の`requests`の場合と同じ内容の関数にしてあります。

```python
import openai

openai.api_key = "sk-************************************************"

def chat(text,
         messages=None,
         settings="",
         max_tokens=2000,
         temperature=1.,
         top_p=.1,
         presence_penalty=0.,
         frequency_penalty=0.):

    # やり取りの管理
    messages = messages if messages is not None else []
    if settings and not messages:
        messages.append({"role": "system", "content": settings})
    messages.append({'role': 'user', 'content': text})


    # APIを叩く、streamをTrueに
    resp = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=messages,
        max_tokens=max_tokens,
        temperature=temperature,
        top_p=top_p,
        presence_penalty=presence_penalty,
        frequency_penalty=frequency_penalty,
        stream=True
    )
    
    # 返答を受け取り、逐次yield
    response_text = "" 
    for chunk in resp:
        if chunk:
            content = chunk['choices'][0]['delta'].get('content')
            if content:
                response_text += content
                yield content
    else:  #
        messages += [{'role': 'assistant', 'content': response_text}]
```

# 使い方
上記実装例ではyieldを使用し、ジェネレーターとして定義しているので、

```python
messages = []
for talk in chat('こんにちは。自己紹介してください。', messages):
    print(talk, end='')
```
のようにすれば↓のように訥々と喋ってくれます（言い方）。
![stream.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/9777a29b-9668-aa24-8fd0-6e2b5ff98c78.gif)

# さいごに
これを読んだ方が面白いサービスを作ってくれることを期待しています（他力本願）！