今更OpenAIのAPIをPythonで叩いてみる。

# はじめに
こちらの記事はChatGPTのAPIを使ってみたい！
でもドキュメントは英語で読みづらいという方に向けた入門記事です。

見慣れたChatGPTではなくOpenAIのAPIを使用して、ターミナル上でChatGPTライクなレスポンスを行うことができるアシスタントを作成することがゴールです。
Pythonの環境構築などは扱いません。もしまだできていないよという方がおられましたら、環境構築をしてからの閲覧をお勧めします。

それではやっていきましょう。


# 実装していく

まずは[OpenAI](https://openai.com/api/)のサイトにアクセスしてAPIキーを取得しましょう。
課金についてですが、今回は非常に安価で使用できるGPT-4o-miniを使用しますので、
最低額で結構です。
もし一銭も使いたくない！ということでしたら、Geminiを使用するのも良いですね。
GeminiはOpenAIのライブラリと互換性がありますので、
その場合は[こちらのドキュメント](https://ai.google.dev/gemini-api/docs/openai?hl=ja)を参考にGeminiでアプリケーションを動作させてみてください。



## 必要なパッケージ
本記事では以下のパッケージを使用しますので、`pip`で`install`しておきましょう。
- [python-dotenv](https://pypi.org/project/python-dotenv/)
- [openai](https://pypi.org/project/openai/)

# responseについて
アプリケーションを作成する前に、OpenAIのAPIResponseについて確認しましょう。
以下のようなmain.pyを実行すると`ChatCompletion`を返してきます。

```main.py
from openai import OpenAI
import os
import dotenv

dotenv.load_dotenv()

openai_api_key = os.getenv('OPENAI_API_KEY')

MODEL="gpt-4o-mini"
client = OpenAI(api_key=openai_api_key)

response = client.chat.completions.create(
    model=MODEL,
    # userとして、AIにHello, world!と話しかける
    messages=[{"role": "user", "content": "Hello, world!"}], 
)

print(response)
```

```bash:ChatCompletion
{
  "id": "chatcmpl-XXXXXXXXXXXXXXXXXXXX",
  "choices": [
      {
          "finish_reason": "stop",
          "index": 0,
          "logprobs": null,
          "message": {
              "content": "Hello! How can I assist you today?",
              "refusal": null,
              "role": "assistant",
              "audio": null,
              "function_call": null,
              "tool_calls": null
          }
      }
  ],
  "created": 1234567890,
  "model": "gpt-4o-mini-2024-07-18",
  "object": "chat.completion",
  "service_tier": "default",
  "system_fingerprint": "fp_XXXXXXXXXX",
  "usage": {
      "completion_tokens": 10,
      "prompt_tokens": 11,
      "total_tokens": 21,
      "completion_tokens_details": {
          "accepted_prediction_tokens": 0,
          "audio_tokens": 0,
          "reasoning_tokens": 0,
          "rejected_prediction_tokens": 0
      },
      "prompt_tokens_details": {
          "audio_tokens": 0,
          "cached_tokens": 0
      }
  }
}
```

ChatCompletionには、ChatGPTにおける対話スレッドを識別するための`id`や、メッセージの内容である`content`、そのメッセージがユーザーのものなのか、AIアシスタントのものなのかを判別するための`role`などが含まれます。
それ以外にも、どれくらい`token`を累計で使用しているのかといった情報も含まれていますから、`resopnse`の値に何が含まれているのかを覚えておくと、実際にアプリを作成するときに役に立つかもしれません。

例えば、resopnseで帰ってきたメッセージの文字列だけを表示したいなら次のようにすればいいですね。
```python
print(response.choices[0].message.content)
```

# アプリのコード
APIの中身がなんとなくわかったところで次は実際にアプリを作ってみます。

適当な名前のディレクトリを作成し、その中に`main.py`と`.env`を作成します。
OpenAIのAPIキーは`.env`に配置しましょう。
```.env
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

次は`main.py`です。
まずはコードを全て表示して、その後各部位ごとに解説します。

挙動を確認するためにコピーアンドペーストして貼り付けてもいいですし、
解説を読みながらひとつひとつ確認しても良いです。

```main.py
from openai import OpenAI
import os
import dotenv

dotenv.load_dotenv()

openai_api_key = os.getenv('OPENAI_API_KEY')
client = OpenAI(api_key=openai_api_key)
MODEL = "gpt-4o-mini"

# 会話履歴を保持するリスト
messages = [
    {"role": "developer", "content": "あなたは親切なアシスタントです。"}
]

print("チャットを開始します。終了するには 'quit' または 'exit' と入力してください。")

while True:
    # ユーザー入力を受け取る
    user_input = input("\nあなた: ")
    
    # 終了コマンドのチェック
    if user_input.lower() in ['quit', 'exit']:
        print("チャットを終了します。")
        break
    
    # ユーザーのメッセージを履歴に追加
    messages.append({"role": "user", "content": user_input})
    
    try:
        # APIリクエストを送信
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages
        )
        
        # アシスタントの応答を取得
        assistant_message = response.choices[0].message
        
        # 応答を履歴に追加
        messages.append({
            "role": assistant_message.role,
            "content": assistant_message.content
        })
        
        # 応答を表示
        print("\nアシスタント:", assistant_message.content)
        
    except Exception as e:
        print(f"\nエラーが発生しました: {str(e)}")
        messages.pop()  # エラーの場合、最後のメッセージを削除

```

## 細かい解説

以下のコードではパッケージやモジュール、モデルの宣言をしています。

```main.py
from openai import OpenAI
import os
import dotenv

dotenv.load_dotenv()

openai_api_key = os.getenv('OPENAI_API_KEY')
client = OpenAI(api_key=openai_api_key)
MODEL = "gpt-4o-mini"
```


こちらはこれまでの会話履歴を保存するリストです。
`messages`に予め`role`と`content`を定義したシステムプロンプトを入力しておくことで、
AIの話し方、思考の方向性をあらかじめ決めることができます。
`content`を<b>優秀な経営コンサルタントです</b>とすればコンサルタントさながらに言葉を返してくれます。
`role`について詳しく知りたい方は[公式ドキュメント](https://platform.openai.com/docs/guides/text-generation?lang=python)を確認してください。

便利ですね。

```main.py
# 会話履歴を保持するリスト
messages = [
    {"role": "developer", "content": "あなたは親切なアシスタントです。"}
]

print("チャットを開始します。終了するには 'quit' または 'exit' と入力してください。")
```

次はwhile文です。
今回のプログラムはwhile文でループ処理を行うことで、継続的な対話を行うようにしています。
input()を使用することでターミナルに入力された文字列を、予め作成した`messages`リストに`append`しています。
ですのでこのコードの時点で`messages`には`"role":"developer"`と`"role":"user"`の2つのメッセージが追加されることになりますね。

```main.py
while True:
    # ユーザー入力を受け取る
    user_input = input("\nあなた: ")
    
    # 終了コマンドのチェック
    if user_input.lower() in ['quit', 'exit']:
        print("チャットを終了します。")
        break
    
    # ユーザーのメッセージを履歴に追加
    messages.append({"role": "user", "content": user_input})

```

最後は`try except`文です。
上記時点で作成された`messages`が`client.chat.completions.create()`でOpenAIのAPIに渡されて、一番初めに確認した`response`の形式で返ってくるという形式になります。

```main.py
try:
        # APIリクエストを送信
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages
        )
        
        # アシスタントの応答を取得
        assistant_message = response.choices[0].message
        
        # 応答を履歴に追加
        messages.append({
            "role": assistant_message.role,
            "content": assistant_message.content
        })
        
        # 応答を表示
        print("\nアシスタント:", assistant_message.content)
        
    except Exception as e:
        print(f"\nエラーが発生しました: {str(e)}")
        messages.pop()  # エラーの場合、最後のメッセージを削除
```

メッセージを一往復させるたびに`messages`リストの中身は増えていきますから、その分だけ`token`を多く使用することになりますし、AIも内容を忘れるようになっていきます。

# 最後に

いかがだったでしょうか。
この記事が皆様のよい開発ライフの一助になれれば幸いです。