AWS Lambda関数とAPI GatewayでChatGPTのAPIとやり取りするサイトを作ろうとした話

3月ごろに勉強がてらAWSのLambdaを使ってChatGPT(OpenAI)のAPIとやりとりできるサイトを作った後、特に何をするでもなく放置していたのですが、改めて確認するとドメイン維持費が高くなってたので一旦削除することにしました。

で、単に削除するのもちょっと寂しいので、どんな感じで作ったのかをここに記録がてらコードや構成図を置いておきます。

【前置き】

  • 2023年12月時点でもAPIとやり取りできることを確認済み
  • 使えそうな部分があったら使ってもらっても大丈夫ですが、外部からのアクセスやセキュリティ面、プロンプトエンジニア的な面を一切考慮していないのでその辺はご了承ください。
  • 当時の記録が残っておらず一部、正確な参考元が追えなくて申し訳ないのですが、各種コード等は先人の方々のものを参考にしています。参考になるコードを残してくださった皆様ありがとうございました。

構成図(アーキテクチャ図)

簡単なものですが、構成図です。

  • サーバレス
  • S3に静的サイトをホスティング。ユーザはフォームを使ってメッセージを送信できる
  • Route53、CloudFront、ACMを用いてhttps化
  • Lambdaを用いてChatGPTのAPIとやり取り
  • セッション管理のためDynamoDBを使用(ユーザの質問が何回目であるか等をカウントするため)
    • ※セッション管理は目的がなければあんまり必要ないと思うので今回はコード等は割愛します。

細かな設定内容は今となってはさっぱり……。せっかく勉強したのに覚えていないのは情けないですが、残っているコードや微かな記憶から参考になりそうな情報を書き留めておきます。

Route53・CloudFront・ACM

静的サイトホスティング・独自ドメイン設定・https設定は下記ページを参考にさせていただいた記憶があります。

参考:【AWS】S3+CloudFront+Route53+ACMでSSL化(https)した静的Webサイトを公開する

ドメイン自体はムームードメインかスタードメインで取得しました。

S3

S3 バケットには「index.html」「main.css」「main.js」の3つのオブジェクトを用意しました。

index.html、main.css

入力フォーム付きの普通のHTMLファイルです。CSSファイルはごく普通な内容なので省略しますが、下記のような画面になるように作成しました。

<!DOCTYPE html>
<html>
    <head>
        <title>サイト名</title>
        <meta charset="UTF-8">
        <meta name="format-detection" content="telephone=no,address=no,email=no">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        (省略)
        <link rel="stylesheet" href="main.css">
        <script type="text/javascript" src="main.js"></script>
    </head>
    <body>
            <header>
                <h1>サイト名</h1>
                <p>説明文</p>
            </header>
        <section>
            <div>
                    <div id="answer_content">
                        Chat GPT:(初期メッセージ)
                    </div>
            <div>
                <h3>入力フォーム</h3>
                <form method="post" accept-charset="utf-8">
                    <textarea id="user_content" name="contents" placeholder="質問文を入力する欄です。"></textarea><br/>
                    <div class="user_button">
                        <input type="submit" id="submit_button" value="回答する">
                    </div>
                    </form>
            </div>

            <div class="how_to_use">
                <h3>使い方</h3>
                    <p>使い方についての説明。
                    </p>
                </div>

            <h3>入力する情報について</h3>
            <p>入力された情報についての注意書き(サーバで保存します、など)</p>

        </section>

        <footer>
            <address>Copyright(C)20XX XXXXXXXXX,Allright Reserved.</address>
        </footer>
    </body>
</html>

main.js

Javascriptでは以下の動きを設定しました。

  • セッションID生成(何回目のメッセージかカウントするため)
  • フォームに入力された内容を入力欄の上に反映するなど表面上チャット的な挙動にする
  • メッセージを送ってからChatGPTのAPIから返信が来るまで時差があるので「考え中」という表示を出しておく
  • フォームに入力された内容やセッションIDをAPI Gatewayへ送信→返事が戻ってきたら表示

ChatGPTのAPIから返信が来るまで時間がかかるため、上手く動かない場合はどこで引っかかっているのか調べるのに苦労した記憶があります。

  window.addEventListener('DOMContentLoaded', function () {
    // フォーム要素を取得
    const form = document.querySelector('form');
  
    form.addEventListener('submit', function (e) {
      e.preventDefault(); // フォームのデフォルトの送信動作をキャンセル
      const button = document.getElementById('submit_button');
      button.disabled = true;
  
      // HTMLフォームから入力内容を取得
      const userContent = document.getElementById('user_content').value;
      const answerContent = document.getElementById('answer_content');
      
      //ユーザ入力を反映
      const reset_target = document.getElementById("user_content");
      reset_target.value = '';
      answerContent.innerHTML += "you:" + userContent + '<br><br>ChatGPT:(考え中...)';

      //スクロールする
      let chatArea = document.getElementById('answer_content'),
      chatAreaHeight = chatArea.scrollHeight;
      chatArea.scrollTop = chatAreaHeight;
      
      // Fetch APIを使ってPOSTリクエストを送信
      const API_GATEWAY_ENDPOINT = "(AWSのAPI GatewayのエンドポイントURL)";
      const requestBody = JSON.stringify({ 
        contents: userContent
       });
      console.log('Sending JSON data:', requestBody);
      fetch(API_GATEWAY_ENDPOINT, {
        "method": "POST",
        "headers": {
          "Content-Type": "application/json", 
        },
        "body": requestBody,
      })

      .then((response) => {
        return response.json();
      })

      .then((data) => {
        const responseData = JSON.parse(data.body); 
        const thinking_delete = answerContent.innerHTML.slice(0,-8);
        answerContent.innerHTML = thinking_delete + responseData.message + '<br><br>';

        let chatArea = document.getElementById('answer_content'),
        chatAreaHeight = chatArea.scrollHeight;
        chatArea.scrollTop = chatAreaHeight;
        button.disabled = false; 
      })
        .catch((error) => {
          console.error('Error:', error);
        });
      
    });
  });

API Gateway

確か、ChatGPTから「Lambda関数とやり取りするのならAPI Gatewayを使うと良いよ!」とアドバイスされ、言われるがままに使った記憶があります。

使い方としては、APIを作成し、そのリソースにてメソッドタイプ「POST」の場合にLambda関数とやりとりできるように設定する。必要に応じてAPIキーを設定する。

ただ改めて調べたところ、API Gatewayはタイムアウト時間の最大値が29秒と低いこともあり、現在のベストプラクティスは2022年4月にリリースされたLambda関数URLというものを使う方法のようです。

下記、参考までに。

参考:AWS API GatewayとLambda 関数 URL(とChatGPT)

Lambda関数

Node.js 18.xを使用しました。確かサンプルコードをいったんダウンロードした後、「index.mjs」を調整した記憶があります。

アップロード時には「node_modulesフォルダ」「index.mjs」「package.json」「package-lock.json」をZip圧縮してアップロードしました。

APIを使用するにあたって事前にAPIキーをLambda関数の「設定」>「環境変数」に設定しておきます。

下記はindex.mjsのコードの一部です。

import fetch from 'node-fetch';
import AWS from 'aws-sdk';
const OPENAPI_CHAT_COMPLETIONS_API = 'https://api.openai.com/v1/chat/completions';

export async function handler(event) {
  console.log("event : ",event);
 
  //OpenAI APIとの通信
  const requestBody = event.contents;
  const openai_api_key = process.env.OPENAI_API_KEY;
  const body = JSON.stringify({
    model: 'gpt-3.5-turbo',
    messages: [
      {
        role :'system',
        content :'Chat GPTへの指示文'},
      {
        role: 'user',
        content: requestBody,
      },
    ],
  });

  const res = await fetch(OPENAPI_CHAT_COMPLETIONS_API, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${openai_api_key}`,
    },
    body,
  });

  if (!res.ok) {
    throw new Error(`API error: ${res.status} ${res.statusText}`);
  }

  const data = await res.json();

  const response = {
    "statusCode": 200,
    "headers": {
      "Content-Type": "application/json",
      //"Access-Control-Allow-Origin": "https://XXXXXXX",
      "Access-Control-Allow-Origin": "https://XXXXXXX", 
      "Access-Control-Allow-Headers": "Content-Type",
      "Access-Control-Allow-Methods": "OPTIONS,POST"
    },
    "body": JSON.stringify({
      "session" : session_count,
      "message": data.choices && data.choices[0] ? data.choices[0].message.content : 'エラーが発生しました。',
    })
  }
    
  console.log("Lambda response:", response); // Lambdaのレスポンスをログに出力
  return response;
};

}

DynamoDB

セッション管理のために使用。

元々は「ChatGPTがユーザから10回ヒアリングしてToDoリストを作成します」というコンセプトで作っていたのでやり取りの回数が10回に達したらChatGPT側でTodoリストを作成する……予定でした。

ただJavascriptとLambda関数でChatGPTとやり取りできるようになり、さらにセッション管理までできることを確認したら満足して放置してしまったのでTodoリスト作成までは行きつかず。

でもセッション管理について学べてなかなか楽しかった記憶があります。

おわりに

AWS Lambda関数とAPI GatewayでChatGPT(OpenAI)のAPIとやり取りできるようなサイトのコードや設定についてでした。

メジャーな構成なのであまり珍しいものはなかったかと思いますが、個人的には普段全く触ったことのないAWSやらNode.jsやらに触れられてとても楽しかったです。こうして振り返ってみて色々思い出すところがあって懐かしい。

ちなみにこのシステム構成自体もChatGPTに相談してもらい、エラーコードを投げては解決法を教えてもらいながら十日くらいで作成した記憶があります。ChatGPT、本当に便利ですね……。

外部アクセスがなければAWS費用自体は100円/月ほどです。今回はドメイン維持費が年間2千円もかかることに気付いたのでちょっといったん削除しようかな……と思い立ったのですが、勉強用に作成する分には安い費用かなと思います。

何らかの参考になったなら幸いです。

コメントする

メールアドレスが公開されることはありません。