AWS Lambda(Python)からOpenAIのGPT4のAPIを呼び出してみた

[Supership](https://supership.jp/)の名畑です。[スナックバス江](https://snackbasue.com/)は肩の力を抜いて見られるのでいいですね。年齢を重ねる毎にこういう作品が好きになっていきます。

## はじめに

[AWS(Amazon Web Services)](https://aws.amazon.com/jp/)でWebアプリを作ることがあるのですが、その過程で触れてきたものについて、せっかくなので要点だけを抽出して残しておくことにしました。

特に目新しい内容はありませんが、**AWS**や**OpenAI**のアカウント作成も含めて一通りの流れを残すので、いい具合の備忘になればと。

今回は[Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html)を経由して[OpenAI](https://openai.com/)の**API**を叩くまでをまとめます。

## Lambdaとは

> AWS Lambda は、サーバーをプロビジョニングまたは管理せずにコードを実行できるようにするコンピューティングサービスです。
> 
> Lambda は可用性の高いコンピューティングインフラストラクチャでコードを実行し、コンピューティングリソースに関するすべての管理を行います。これには、サーバーおよびオペレーティングシステムのメンテナンス、容量のプロビジョニングおよび自動スケーリング、さらにログ記録などが含まれます。Lambda で必要なことは、サポートするいずれかの言語ランタイムにコードを与えることだけです。
> 
> 参考：[AWS Lambda の概要 - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html)

## 私の環境

OSやブラウザ依存の部分はそこまで多くないですが、一応残しておきます。

**MacOS Venture**で、ブラウザは**Chrome**です。

## AWSのアカウントを作る

私はすでにAWSアカウント取得済みでしたが、初めての方は「[AWS アカウント作成の流れ](https://aws.amazon.com/jp/register-flow/)」に従ってアカウントを作成してください。

## AWS CLIをインストールする

[AWS CLI(AWS コマンドラインインターフェイス)](https://aws.amazon.com/jp/cli/)を入れておきます。

今回の内容であればなくても完結させられはするのですが、私は手元のコードをコードアップロードする等がしたいので入れました。

リンク先から自環境に適したインストーラをダウンロードします。

CLIは動かすために**リージョン**(ロケーションの単位、東京であればap-northeast-1)の登録などが必要なのですが、configureコマンドで設定するのが早いでしょうか。

詳しくは下記あたりをご覧ください。

- [AWS CLI を設定する - AWS Command Line Interface](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-configure.html)
- [【AWS】aws cliの設定方法](https://zenn.dev/akkie1030/articles/aws-cli-setup-tutorial)

## OpenAIのアカウント作成とAPI Keyの取得

**OpenAI**のアカウント作成は[OpenAI platform](https://platform.openai.com/)で行えます。

API呼び出しに用いる**API key**の取得はサインイン後の[API keys](https://platform.openai.com/account/api-keys)のページで行えます。Create new secret keyを押して、名前(optional)をつけるだけです。
**API key**は作成時しか画面に表示されませんので気をつけてください。画面上から後で知る方法はありません。

## OpenAIのSDKのダウンロード

後ほど**Lambda**で**OpenAI**のAPI呼び出しを行います。
そのために**OpenAI**のSDKをダウンロードしておきます。

ただし、普通にダウンロードして普通にアップロードをすると、**Lambda**からの呼び出しのタイミングで下記のエラーが発生します。

```shell-session
{
  "errorMessage": "Unable to import module 'LambdaFunctionOverHttps': No module named 'pydantic_core._pydantic_core'",
  "errorType": "Runtime.ImportModuleError",
  "requestId": "略",
  "stackTrace": []
}
```

どうやら[こちらのissue](https://github.com/pydantic/pydantic/issues/6557)でやりとりされているもののようです。
「[AWS Lambda Layers + OpenAI でつまづいた件](https://zenn.dev/galapagos/articles/a222e38a32f4ba)」という記事で同種のエラーを上げられている方もいらっしゃいました。

これを回避するため、今回は**fastapi**のバージョンを下げた上でダウンロードします。

まず、私の環境ではPythonは3.10です。今回は全環境を3.10で統一することとします。

```shell-session
$ python --version
Python 3.10.12
```

次にSDKのダウンロードです。**fastapi**のバージョンを指定した上で、あらかじめ用意しておいた**python**というフォルダにダウンロードします。

```shell-session
$ python -m pip install -t ./python fastapi==0.99.0 openai
```

これをzip形式で**openai.zip**として固めておきます。後ほど使います。

```shell-session
$ zip -r openai.zip ./python
```

## Lambda関数の作成

ブラウザのコンソールで**Lambda**を開いて「**関数の作成**」をクリックします。

「**一から作成**」を選び、関数名は**openai-sample**としました。
**ランタイム**は**Python 3.10**を選びました。

これで「**関数の作成**」をクリックすると**Lambda関数**が作られます。

デフォルトでのコードは下記です。

```python
import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```

## OpenAIのSDKのアップロード

**Lambda**でサイドメニューから**レイヤー**を選んで「**レイヤーの作成**」をクリックします。
名前は**openai**としておきます。

先ほど作成した**openai.zip**をアップロードするファイルとして選びます。

互換性のあるアーキテクチャは**x86_64**と**arm64**を選びます。
互換性のあるランタイムは**python3.10**とします。

最後に**作成**をクリックします。

![lambda_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/25ab6fcd-14bb-9050-2f75-425c0704d2d0.png)

スクリーンショット上では私が色々といじったのでバージョンが**8**となっていますが、実際の初期値は**1**です。バージョン番号はレイヤーを更新する度にアップデートされていきます。

この状態で先ほどのコード編集画面に戻り「**レイヤーの追加**」で**カスタムレイヤー**から**openai**を選びます。

すると下記のスクリーンショットのように追加されます。

![lambda_1b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/48cbaa0c-1b21-c762-0c15-231114cffaa5.png)

## 環境変数の設定

![lambda_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/5959952e-1df5-e91d-d403-5e2af2baf799.png)

**API\_KEY**をコードに直接記述するのはよろしくないので、**Lambda**の**設定**の**環境変数**で

- **キー**：OPENAI\_API\_KEY
- **値**：OpenAIで取得したAPI-Key

を設定しておきます。スクリーンショットの**hoge**を実際の値にしましょう。

## タイムアウトの変更

**Lambda**の「**設定**」の「**基本設定**」で「**タイムアウト値**」を3秒から伸ばしておきます。
3秒だとレスポンスがほとんど返ってこないためです。

今回は60秒としておきました。

## Pythonのコードを書く

```python
import openai
import json
import os
from openai import OpenAI
openai.api_key = os.environ['OPENAI_API_KEY']  # 環境変数から取得

def lambda_handler(event, context):
    try:
        client = OpenAI()
        completion = client.chat.completions.create(
            model="gpt-4",
            messages=event
        )
    except Exception as e:
        raise e
    
    return {
        'statusCode': 200,
        'body': json.dumps(completion.choices[0].message.content)
    }
```

受け取った内容をそのまま**OpenAI**の**GPT4**に投げるだけのコードです。
実際は受け取った内容に応じて分岐するでしょうし、エラー処理もきちんと書く必要があるでしょう。今は**Exception**をそのまま投げているだけなので無意味な処理です。

今回は本題ではないので深くは触れませんが、**OpenAI**のAPIについて詳しくは[API reference](https://platform.openai.com/docs/api-reference)をご覧ください。

コンソール上で直接このコードを貼り付けても大丈夫ですが、今回はせっかくなのでローカルからアップロードします。

**lambda_function.py**というファイル名でローカルで保存します。
ファイル名は**Lambda**でのデフォルトです。コードの画面で確認できます。

まず、zipで圧縮します。

```shell-session
$ zip function.zip ./lambda_function.py
```

次にアップロードします。

```shell-session
$ aws lambda update-function-code --function-name openai-sample --zip-file fileb://function.zip
```

**Lambda**のコードが更新されていることがコンソールでわかります。

![lambda_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/869934/feb712a6-431d-1bf0-0001-92c2fb8cdd2f.png)

## Lambda関数を呼び出してみる

下記の内容をinput.txtというファイル名で保存しておきます。**OpenAI**のAPIに渡す内容をそのまま書いたものです。

```shell-session
[
	{
		"role": "user",
		"content":"あなたはOpenAIのAPIですか？"
	}
]
```

ターミナルからコマンドを叩いてみます。**input.txt**の内容を**Lambda**に渡して、レスポンスを**output.txt**に出力するコマンドです。

```shell-session
$ aws lambda invoke --function-name openai-sample --payload file://input.txt output.txt --cli-binary-format raw-in-base64-out
```

しばらく待つとレスポンスがありました。

```shell-session
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

**output.txt**の中身を**cat**で見てみます。

```shell-session
$ cat output.txt  
{"statusCode": 200, "body": "\"\\u306f\\u3044\\u3001\\u305d\\u306e\\u901a\\u308a\\u3067\\u3059\\u3002\\u79c1\\u306fOpenAI\\u306e\\u4eba\\u5de5\\u77e5\\u80fd\\u30a2\\u30b7\\u30b9\\u30bf\\u30f3\\u30c8\\u3067\\u3001\\u8c4a\\u5bcc\\u306a\\u77e5\\u8b58\\u3068\\u6a5f\\u68b0\\u5b66\\u7fd2\\u306e\\u529b\\u3092\\u4f7f\\u3063\\u3066\\u3001\\u30e6\\u30fc\\u30b6\\u30fc\\u304b\\u3089\\u306e\\u3055\\u307e\\u3056\\u307e\\u306a\\u8cea\\u554f\\u3084\\u8981\\u6c42\\u306b\\u5fdc\\u3048\\u308b\\u3053\\u3068\\u304c\\u3067\\u304d\\u307e\\u3059\\u3002\""}%  
```

Unicodeをデコードしないと読めないですね。
もっと楽な手もあるかもしれませんが、**sed**と**nkf**でデコードして出力します。

```shell-session
$ cat output.txt | sed 's/\\\\\u\(....\)/\&#x\1;/g' | nkf --numchar-input -w
{"statusCode": 200, "body": "\"はい、その通りです。私はOpenAIの人工知能アシスタントで、豊富な知識と機械学習の力を使って、ユーザーからのさまざまな質問や要求に応えることができます。\""}%       
```

bodyの中身が

> はい、その通りです。私はOpenAIの人工知能アシスタントで、豊富な知識と機械学習の力を使って、ユーザーからのさまざまな質問や要求に応えることができます。

こうなっていますので、無事に**OpenAI**のAPIの呼び出しが行えていそうです。

デコードに用いた**nkf**はデフォルトだとインストールされていませんので、ご注意ください。もしも使うなら**brew**等でインストールしましょう。

```shell-session
$ brew install nkf 
```

## 最後に

AWSに触れる度、本当に便利な時代になったものだと実感します。

次回は今回作成した**Lambda関数**と[API Gateway](https://aws.amazon.com/jp/api-gateway/)を接続してみる予定です。

そうすることによってURLを指定して**Lambda関数**を叩けますので。今は[Lambda関数URL](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html)があるので、用途によってはこれだけで充分かもしれませんが。

## 宣伝

[SupershipのQiita Organization](https://qiita.com/organizations/supership-inc)を合わせてご覧いただけますと嬉しいです。他のメンバーの記事も多数あります。

Supershipではプロダクト開発やサービス開発に関わる方を絶賛募集しております。
興味がある方は[Supership株式会社 採用サイト](https://supership.jp/recruit/)よりご確認ください。