LINE Messaging APIとOpenAI APIを連携させる

# やったこと
最近はやってるOpenAIにLINE経由で質問して回答を受け取れるようにしました！
接続イメージは以下の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/1a882039-2c7c-3d7f-61d1-ae5ba32f5dad.png)

# LINE Messaging APIとOpenAIの準備
### LINE Messaging API
[公式のガイド](https://developers.line.biz/ja/docs/messaging-api/getting-started/)に沿って、[LINE Developer Consle](https://developers.line.biz/console/)上でMessaging APIを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/cb2c43da-0bc3-2f12-236d-ec9ec4b6bf53.png)

「Messasing API設定」タブからチャネルアクセストークンを確認しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/5673c707-326a-4c62-d9a2-1e5db87f3fa0.png)

Webhookの設定も必要なのですが、それはLambda構築後に実施します。

### OpenAI

OpenAIのアカウントを作成します。
[OpenAI APIのトップ](https://openai.com/api/)でサインアップを実施。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/ff160d64-5da5-769a-a6e1-9bf3db87a171.png)

ログイン後、右上の「Personal」から「View API Keys」でAPI Keysを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/3a9bbea2-4cf2-7225-cb23-d8e74878fd26.png)

以下の画面でAPI Keyを発行して確認します。
一度作ったものを画面上で再確認するのは不可能なので、別途しっかり管理する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/df5f69b1-9a02-fb43-cf59-2e221b67e8a5.png)

# Lambda
### 関数の作成
[AWS Lambda](https://ap-northeast-1.console.aws.amazon.com/lambda)関数を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/154269dc-7b27-9c0c-6875-115e9700b492.png)

詳細設定から、最近（去年くらい？）用意された関数URLを有効化します。
以前はAPI Gatewayの設定とかが必要だったので、今回みたいにサクッと外部のAPIを連携させたいときは便利だなと思いました。
認証タイプは今回はとりあえずお試しなのでNONE、CORS設定にもチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/347fc0ed-a6b3-fb03-bc51-d1e12cde3811.png)

関数の作成を実施。
右下に関数URLが表示されているので、これをコピー。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/40950da0-07fa-77e4-a431-338dd8ba78e6.png)

### LINE Developer ConsoleでWebhook URLを設定

LINE Developer Consoleで、作成したMessaging APIのWebhook URLに設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/7ee4f9ca-c994-3a81-813b-5f83c44d6342.png)

ここまで書いて思い出しましたがMessaging APIは自動で応答メッセージがあるのでこれは無効化しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/b4302041-bdca-78bb-74b2-00ee16645a54.png)

### Layerの追加
今回、[openai]()と[@line/bot-sdk]()をnode_modulesとして使うので、Lambdaレイヤーに追加します。
ローカルコンピューター上で「nodejs」ディレクトリを作成し、以下の通りnode_modulesをインストール。
```:Powershell
PS C:\Users\flets\OneDrive\node\nodejs> npm i @line/bot-sdk openai
PS C:\Users\flets\OneDrive\node\nodejs> npm ls
node@ C:\Users\flets\OneDrive\node
+-- @line/bot-sdk@7.5.2
`-- openai@3.1.0

PS C:\Users\flets\OneDrive\node\nodejs>
```
Zip圧縮してnodejs.zipを作成しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/5bc0df9c-36ee-13ed-5e80-953b14096e51.png)

Lambdaコンソールの左のナビゲーションペインで「レイヤー」を選択し、右上の「レイヤーの作成」をクリック。
以下の通りレイヤー名を適当に入力して、nodejs.zipをアップロード元に設定して「作成」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/77f16ce4-b1f9-d2e9-8957-1a09b4f436ec.png)

無事に作成されればOK。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/9053c22c-28a8-5914-a972-11283f5dafe2.png)

以下の通り、先に作った関数にLayer追加しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/c19b8f64-6829-3378-4853-104f82413c23.png)

ARN指定で追加しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/48bb68fc-6e14-5df6-5a7d-fd173fce7b90.png)

### コードの記述
以下の通り記述します。

##### モジュール呼び出し、トークン・Key設定
まず、`handler`関数の手前でLINEとOpenAIのモジュールをImportして、LINEのチャネルアクセストークンとOpenAIのAPI Keyも設定します。（直書きでごめんなさい）

```javascript
import line from "@line/bot-sdk";
import { Configuration, OpenAIApi } from "openai";

//事前に確認したものを記述
const LINE_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const GPT_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
```

##### LINE Messaging APIとのやりとり
`handler`関数の中身を書きます。
まずは、LINE Messaging APIとやりとりする枠を書きます。

```javascript:ガワを書く
export const handler = async(event) => {
  //LINEのMessaging APIに接続
  const client = new line.Client({
    channelAccessToken: LINE_TOKEN,
  });

  //LINEからWebhookでアクセスしてきたときのBodyをJSONに格納
  const reqBody = JSON.parse(event.body);
  console.log(reqBody)

  let message = {};

  //このあと、ここでOpenAIのAPIを呼び出してmessageの中身を埋めます

  //LINE Messaging APIでメッセージをPushする宛先
  const toToken = reqBody.events[0].source.userId;

  //メッセージをPush
  await client.pushMessage(toToken, message).catch((err) => {
    // error handling
    console.error(err);
  });

  //Webhookに対するResponse（200応答が必要らしい）
  const response = {
      statusCode: 200,
      body: JSON.stringify(message),
  };
  return response;
};
```
##### OpenAIのAPI呼び出し、messageへの格納
OpenAIのAPIにアクセスして、レスポンスデータをMessageに格納します。
LINEからのWebhookがテキストメッセージじゃないとダメなので、if分で分岐も一応入れておきます。

```javascript:さっき「このあと～～」と書いた部分

//WebhookがテキストだったらOpenAIになげる
if (reqBody.events[0].message.type == "text") {
    //OpenAIアクセス設定
    const configuration = new Configuration({
      apiKey: GPT_KEY,
    });
    //オブジェクト作成
    const openai = new OpenAIApi(configuration);
    //APIアクセス、レスポンス格納
    //max_tokensを変えればレスポンスの最大文字数が変化
    //ただし、max_tokensの値が純粋な最大文字数ではないので注意
    //ただしLINE、OpenAIそれぞれで上限値ありなので要確認
    const response = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: reqBody.events[0].message.text,
      max_tokens: 1000,
      temperature: 0,
    });

    message = {
      type: "text",
      text: response.data.choices[0].text.trim()
    };
  } else {
  //LINEからのWebhookがテキストメッセージじゃなかった場合
    message = {
      type: "text",
      text: "テキストを入力してください。"
    };
  }
```

##### 全文
全文は以下の通り。
```javascript:myGptFunction全文
import line from "@line/bot-sdk";
import { Configuration, OpenAIApi } from "openai";

const LINE_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const GPT_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

export const handler = async(event) => {
    const client = new line.Client({
    channelAccessToken: LINE_TOKEN,
  });

  const reqBody = JSON.parse(event.body);
  console.log(reqBody)

  let message = {};

  if (reqBody.events[0].message.type == "text") {
    const configuration = new Configuration({
      apiKey: GPT_KEY,
    });
    const openai = new OpenAIApi(configuration);
    const response = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: reqBody.events[0].message.text,
      max_tokens: 1000,
      temperature: 0,
    });

    message = {
      type: "text",
      text: response.data.choices[0].text.trim()
    };
  } else {
    message = {
      type: "text",
      text: "テキストを入力してください。"
    };
  }

  const toToken = reqBody.events[0].source.userId;

  await client.pushMessage(toToken, message).catch((err) => {
    // error handling
    console.error(err);
  });
    const response = {
        statusCode: 200,
        body: JSON.stringify(message),
    };
    return response;
};

```
これでコンソール画面上で`Deploy`を実施。
上部に「関数myGptFunctionが正常に更新されました」と表示されればOK！

# 動作確認
無事に動きました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/820066/30a85f6e-d665-8d38-fc29-10b5f4e6bf01.png)

とりあえず作ってみましたが結構面白いので、しばらくこれでChatGPTで遊んでみようと思います。

# 参考
ほぼ以下2つのコピペです。
[OpenAIのAPI Reference](https://beta.openai.com/docs/api-reference/completions/create?lang=node.js)
[LINE Messaging APIのAPI Reference](https://developers.line.biz/ja/reference/messaging-api/#send-push-message)