AWSでAIチャットボット

## AIチャットボットを立ち上げ、運用する
(ネット上に散在する情報を繋ぎ合わせて、とりあえず動くようにしてみた)

AWS上でAIチャットボットを動作させるための手順。

先ず、以下の記事を参考にAWSのサーバーを立ち上げる。

https://qiita.com/quittardis/items/62caf057fb499648859a


このAWSサーバーにアクセスし、AIチャットボットの
環境を設定する。

wsl2のubuntuウィンドウ内で作業する。
aws上のECのユーザー名がubuntu、AWSのアクセスキーが作業ディレクトリにあるとし、そのファイル名がxxxx.pemとすると、
#### AWSにログイン
```
ssh -i "xxxx.pem" ubuntu@<awsドメイン名>
```
#### バーチャル環境
以下のコマンドで環境を設定
```
source api/bin/activate
```
これによって、コマンドラインに左側に環境名が表示される、
（この場合はapi）
```
(api) ubuntu@ip-xxxxxxxxx
```

#### OpenAI APIキーの設定
　ユーザーのホームディレクトリにある、.bashrcの最後に以下のようにOpen_AIのAPIキーを設定する。APIキーはOpen-AIのサイトから取得する。たま、Opan-AIのAPIの使用は有料なので、クレジットカードで、10$以上チャージしておく。
```
export OPENAI_API_KEY='sk-proj-........................'
````
#### ファイルの転送（ロカール <->　リモート）
AWS上でAPIサーバーとして動作するPythonコードをローカルからAWSへ転送するか、AWSのエディタで作成する。

#### AWSからファイルを受け取る場合
```
scp -i xxxx.pem ubuntu@<AWSドメイン名>:<ファイル名> .
```
“.”はカレントディレクトリを表す

#### AWSへファイルを送る場合

```
scp -i xxxx.pem test.pdf  ubuntu@<AWSドメイン名>:~/
```
“~/”は、ホームディレクトリを表す

### AIサーバー(pythonコード）の作成と設定

現在、チャットボットのよう用途の場合、'gpt-4o-mini'が高速で格安。

```
# Flask imports
from flask import Flask, request, jsonify, render_template
# Langchain imports
from langchain_openai.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain, LLMChain
from langchain_openai.embeddings import OpenAIEmbeddings
from langchain.memory import ConversationSummaryMemory
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader
# Other imports
import os
#from dotenv import load_dotenv
from langchain.prompts import PromptTemplate
from langchain.chains.question_answering import load_qa_chain
import json
from settings.setting import API_KEY
# My Retrieval
from  langchain.chains.qa_with_sources.base import BaseQAWithSourcesChain
#from my_retrieval import MyRetrievalQAWithSourcesChain, my_summarize_content

def load_pdf_document(file_path):
        loader = PyPDFLoader(file_path)
        return loader.load()


def split_documents_into_chunks(documents, chunk_size=1000, chunk_overlap=0):
        text_splitter = CharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
        return text_splitter.split_documents(documents)

app = Flask(__name__)

template_qg = """
次の会話に対しフォローアップの質問があるので、フォローアップの質問を独立した質問に言い換えなさい。
        
チャットの履歴:
{chat_history}
                       
フォローアップの質問:
{question}
                                          
言い換えられた独立した質問:"""

prompt_qg = PromptTemplate(
          template=template_qg,
          input_variables=["chat_history", "question"],
          output_parser=None,
          partial_variables={},
          template_format='f-string',
          validate_template=True,
)

prompt_template_qa = """あなたの名前は XXXX　YYYYのマスコットキャラクター.　ユーザーからの入力があった際には、必ず知識源ファイルを丁寧に調べてXXXXの言葉として返答します。返答は日本語で行います。話口調で、親し気な口調で話しかけます。「知識源」という言葉は使わないで、自分のことは「僕」と表現します
写真を見せるときはリンクを次のように提示します。 <img src ="link address">

{context}

Question: {question} Answer in Japanese:"""

prompt_qa = PromptTemplate(
       template=prompt_template_qa, 
       input_variables=["context", "question"]
)
chain_type_kwargs = {"prompt": prompt_qa}


# ChatOpenAIモデルの設定
LLM = ChatOpenAI(
            #model_name='gpt-3.5-turbo',
            model_name='gpt-4o-mini',
            temperature=0,
            api_key = API_KEY
)

from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import JSONLoader

data = load_pdf_document("pdf_data/knowledge.pdf")
#docs = split_documents_into_chunks(data, chunk_size=1000, chunk_overlap=0)
text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=1000,
        chunk_overlap=100,
        length_function=len,
        separators=["\n\n", "\n", "。", " ", "、", ""],
      )
docs = text_splitter.split_documents(data)
#docs = split_documents_into_chunks(data)
vectorstore = Chroma.from_documents(documents=docs, embedding=OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
question_generator = LLMChain(llm=LLM, prompt=prompt_qg)
doc_chain = load_qa_chain(llm=LLM, chain_type="stuff", prompt=prompt_qa)
#
qa = ConversationalRetrievalChain(
                retriever=retriever,
                question_generator=question_generator,
                combine_docs_chain=doc_chain,
         )
chat_history = []

# Flask routes
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/ask', methods=['POST'])
def ask(): 
    body = request.get_data(as_text=True)
    data = json.loads(body)
    app.logger.info("Request body: " + body)
    prompt = data["question"] 
    result = qa.invoke({"question": prompt, "chat_history": chat_history})['answer']
    print("Q ",prompt)
    print("A ",result)
    chat_history.append((prompt, result))
    dummy = chat_history
    if len(chat_history) > 5:
            chat_history.pop(0)
    #print(chat_history)
    #print("### ",  chat_history)
    return jsonify({"answer": result})
# Run application
if __name__ == '__main__':
#
    app.run()


```

### 会話が表示されるWebの設定

AIチャットボットとの会話が表示されWebページの出力の設定。
このファイルはtemplatesフォルダに、index.htmlとしてセーブ
表示されるキャラクターのイメージ chara.pngをstaticの下の
imageの下に置く

```
<!DOCTYPE html>
<html>
<head>
    <title>LangChain ChatBot</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <style>
        body, html {
            height: 100%;
            margin: 0;
            display: flex;
            flex-direction: column;
        }
        #chat {
            max-width: 100%;
            flex-grow: 1;
            overflow: auto;
        }
        .question,
        .answer {
            max-width: 80%;
            padding: 10px;
            margin: 5px;
            border-radius: 10px;
        }
        .question {
            background-color: #f1f1f1;
            float: right;
            clear: both;
        }
        .answer {
            background-color: #e6ffe6;
            float: left;
            clear: both;
        }
        .error {
            color: red;
            font-weight: bold;
        }
        #input-area {
            text-align: center;
            padding: 10px;
            background-color: #93e9e0;
        }

         /* ここから追加 */
         #input-area input[type="text"] {
            font-size: 1.2em; /* 文字の大きさを1.5倍に */
            padding: 15px 10px; /* 上下のパディングを増やして入力エリアを高く */
            width: 50%; /* 幅を2/3に設定 */
        }
        #input-area button {
        cursor: pointer; /* ホバー時にカーソルを指に変更 */
        height: 100%; 
        padding: 15px 10px; /* 上下のパディングを増やして入力エリアを高く */
        background-color: #93e9e0;
        }
        #page-title {
        display: flex;
        justify-content: center; /* 水平方向の中央揃え */
        /*align-items: center;*/ /* 垂直方向の中央揃え */
        height: 5vh; /* ビューポートの高さを100%に設定 */
        text-align: center; /* テキストを中央揃え */
        background-color: #93e9e0;
        }
        #title-icon {
        height: 2.8em; /* 画像の高さをh1のテキストの高さに合わせる */
        width: 2.8em; /* 画像の横幅もh1のテキストの高さに合わせる */
        vertical-align: left; /* 画像をテキストの中央に配置 */
        margin-right: 10px; /* 画像とテキストの間に余白を追加 */
}

h1 {
    margin: 0; /* デフォルトのマージンを取り除く */
}

        /* ここまで追加 */
    </style>
</head>
<body>
    <!-- ここから追加 -->
    <div id="page-title">
    <img  id="title-icon" src="/static/images/chara.png" alt="Garlic">
    <h1>キャラクターの名前など</h1> <!-- タイトルのテキスト -->
    </div>
    <!-- ここまで追加 -->
    <div id="chat">
        <!-- Chat history will go here -->
    </div>
    <div id="input-area">
        <input type="text" id="question" placeholder="Type your question here..." minlength="2" maxlength="200" size="100">
        <button id="submit">Submit</button>
    </div>
    <script>
        $(document).ready(function() {
            $("#submit").click(function() {
                const question = $("#question").val();
                $("#chat").append("<p class='question'>Q: " + question + "</p>");
                $('#chat').scrollTop($('#chat')[0].scrollHeight); // Scroll to the bottom of #chat

                // Clear the text input field
                $("#question").val('');
                
                $.ajax({
                    url: '/ask',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify({ "question": question }),
                    success: function(data) {
                        const answer = data.answer;
                        $("#chat").append("<p class='answer'>A: " + answer + "</p>");
                        $('#chat').scrollTop($('#chat')[0].scrollHeight); // Scroll to the bottom of #chat

                        // Re-enable the submit button
                        $("#submit").prop("disabled", false);
                    },
                    error: function() {
                        $("#chat").append("<p class='answer error'>エラーが発生しました。申し訳ありません。もう一度質問してください。</p>");
                        
                    }
                });
            });
        });

        $(document).ready(function() {
    $("#question").keypress(function(event) {
        if (event.which == 13) { // 13はエンターキーのキーコード
            event.preventDefault(); // フォームの自動送信を防ぐ
            $("#submit").click(); // Submitボタンのclickイベントをトリガする
        }
    });

    
});
    </script>
</body>
</html>

```


### 知識源の準備

　知識源はそのキャラクターのSNS上での会話やブログやWEBページの記事をword上にコピペする。このwordファイルをpdf化し、フォルダpdf_dataにknowledge.pdfとして保存

### 起動

　以下のコマンドでサーバーを起動する。pythonコードが、CharaAI.pyとすると、
```
uwsgi --socket=/tmp/uwsgi.sock --wsgi-file=CharaAI.py  --callable=app --chmod-socket=666
```