まるっとワーク

データ分析・電子工作など気になることを残していきます

langChainを用いた簡単チャットアプリの作成

LangChainは、Large Language Models(LLM)を使ったアプリケーション開発を容易にするためのフレームワークです。LLMと会話をする際に会話履歴の保管しておく機能や、Agentと呼ばれる機能等、自身で色々と書かないと実現できなかったことが簡単にできる所がとても良いです。(詳細以下)
www.langchain.com

今回は、このLangChainとGoogle Gemini API(今は無料で使えるので)を用いて簡単なチャットアプリとpdfを読み込んで、そのpdfの内容に応じて答えを出してくれるアプリを作ったので、その記録を残します。


目次


構成について

LLMとの会話はpython, LangChainライブラリ、アプリ部分はStreamlitライブラリ、実装はGCP CloudRunを使用しています。
基本的な構成は以下ブログで記載と差が無く、app.pyのコードのみ変更します。

dango-study.hatenablog.jp

構成詳細

│─ Dockerfile
│─ requirements.txt
└─ src/
   └─ app.py (WEBアプリのコード)

コード詳細

簡単なチャットアプリ

LangChainのConversationChainの機能を使用しています。
以下はapp.pyのコードの抜粋です。

import streamlit as st
from streamlit_lottie import st_lottie
from langchain.chat_models import ChatVertexAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import AIMessage, HumanMessage, SystemMessage
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.cache import InMemoryCache
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
import requests
from dotenv import load_dotenv
import os

GOOGLE_API_KEY = "自身で取得したAPI KEYを入力"

langchainllm_cache = InMemoryCache()
llm = ChatGoogleGenerativeAI(model="gemini-pro",
                             temperature=0.3, convert_system_message_to_human=True,google_api_key=GOOGLE_API_KEY)

def get_state(): 
     if "state" not in st.session_state: 
         st.session_state.state = {"memory": ConversationBufferMemory(return_messages=True, k=5)} 
     return st.session_state.state 
state = get_state()
print(state)

chain = ConversationChain(
            llm=llm, 
            memory=state['memory']            
        )

def load_lottieurl(url:str):
    r = requests.get(url)
    url_json = dict()
    if r.status_code == 200:
        url_json = r.json()
        return url_json
    else:
        None

load_bot = load_lottieurl("https://lottie.host/dffb1e31-78af-4548-9e7c-30fd1cbbb704/lUvwHha1IZ.json")
col1,col2 = st.columns(2)
with col1:
    st.markdown('')
    st.markdown('')
    st.title(":violet[AI Bot]")
with col2:
    st_lottie(load_bot,height=200,width=200,speed=1,loop=True,quality='high',key='bot')

# Set a default model
if "vertexai_model" not in st.session_state:
    st.session_state["vertexai_model"] = "chat-bison"

# Initialize chat history
if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat messages from history on app rerun
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# Accept user input
if human := st.chat_input("Please ask assistant"):
    # Add user message to chat history
    st.session_state.messages.append({"role": "user", "content": human})
    # Display user message in chat message container
    with st.chat_message("user"):
        st.markdown(human)
    # Display assistant response in chat message container
    with st.chat_message("assistant"):
        message_placeholder = st.empty()
        with st.spinner("Processing..."):
            #full_response = gen_response(human)
            full_response = chain(human)

        try :
            message_placeholder.markdown(full_response["response"])
            st.session_state.messages.append({"role": "assistant", "content": full_response["response"]})
            print(full_response)
            #print()
        except:
            print("error")

実行結果

デプロイ方法は以下の記載と同じなので、ここでは割愛します。
WEBアプリをクラウドでデプロイ(Cloud Run + Streamlit or Flask) - まるっとワーク

実行結果として、会話が正常に行え、その記録もしっかり残せています。
streamlitは、変数保持に対応が必要なので、会話履歴に対しては保持するための対応をしています。
【Streamlit】Session Stateで変数の値を保持する方法 #Python - Qiita



pdfの内容に応じて答えを出してくれるアプリ

同様にLangChainのConversationChainの機能を使用しています。
ただし、特定の命令(プロンプト)を与えています。
以下はapp.pyのコードの抜粋です。

構成詳細

import streamlit as st
from streamlit_lottie import st_lottie
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain import PromptTemplate
from langchain.cache import InMemoryCache
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain.text_splitter import CharacterTextSplitter
from langchain.agents import AgentType, AgentExecutor, ZeroShotAgent, Tool, initialize_agent, load_tools
import requests
from langchain.tools import WriteFileTool
import PyPDF2
import io

GOOGLE_API_KEY = "自身で取得したAPI KEYを入力"

langchainllm_cache = InMemoryCache()
llm = ChatGoogleGenerativeAI(model="gemini-pro",
                             temperature=0.3, convert_system_message_to_human=True,google_api_key=GOOGLE_API_KEY)

def get_state(): 
    if "state" not in st.session_state: 
        st.session_state.state = {"memory": ConversationBufferMemory(return_messages=True, k=5)} 
        st.session_state.state["count"] = 0 
    return st.session_state.state 
state = get_state()
print(state)

tools = load_tools(
    [],
    llm=llm
)

tools.append(WriteFileTool())

chain = ConversationChain(
            llm=llm, 
            memory=state['memory']            
        )

if state["count"] >= 0:
    prompt = PromptTemplate(
        input_variables=["system_input", "document_input", "user_input"],
        template="""System Prompt Here: {system_input}
        Document Input: {document_input}
        User Prompt: {user_input}"""
    )
else:
    prompt = PromptTemplate(
        input_variables=["user_input"],
        template="""
        User Prompt: {user_input}"""
    )
    print("Not First")

system_input = """####Document Input#####の内容を踏まえて、####User Prompt####の命令に従って回答してください。これを厳守してください"""

agent = initialize_agent(
    tools,
    llm,
    memory=state["memory"],
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True
)

def load_lottieurl(url:str):
    r = requests.get(url)
    url_json = dict()
    if r.status_code == 200:
        url_json = r.json()
        return url_json
    else:
        None

def get_pdf_text():
    st.markdown('Managed by: <a href="https://dango-study.hatenablog.jp/">Kazuma Tokuda</a>',unsafe_allow_html=True)
    st.markdown('')
    st.title(":violet[Document AI Search]")
    uploaded_file = st.file_uploader(
        label='Upload your PDF here😇',
        type='pdf'
    )
    if uploaded_file:
        # ファイルをメモリに読み込む
        file_buffer = io.BytesIO(uploaded_file.getvalue())

        # PyPDF2を使用してPDFを読み込む
        pdf_reader = PyPDF2.PdfReader(file_buffer)
        text = ''
        for page in range(len(pdf_reader.pages)):
            text += pdf_reader.pages[page].extract_text() + '\n'
        text_splitter = CharacterTextSplitter(
            separator = "\n\n",
            chunk_size = 100,
            chunk_overlap = 0,
            length_function = len,
        )
        text_list = text_splitter.split_text(text)
        document_list = text_splitter.create_documents([text])

        st.session_state.pdf_text = text_list 
        st.write(text_list)

def page_ask_my_pdf():        
    load_bot = load_lottieurl("https://lottie.host/dffb1e31-78af-4548-9e7c-30fd1cbbb704/lUvwHha1IZ.json")
    col1,col2 = st.columns(2)
    with col1:
        st.markdown('')
        st.title(":violet[Document AI Search]")

    if "pdf_text" in st.session_state:
        pdf_text = st.session_state.pdf_text
        # PDFテキストを使用して何か処理を行う
        # 例: st.write(pdf_text)
        with col2:
            st_lottie(load_bot,height=200,width=200,speed=1,loop=True,quality='high',key='bot')

        # Set a default model
        if "vertexai_model" not in st.session_state:
            st.session_state["vertexai_model"] = "chat-bison"

        # Initialize chat history
        if "messages" not in st.session_state:
            st.session_state.messages = []

        # Display chat messages from history on app rerun
        for message in st.session_state.messages:
            with st.chat_message(message["role"]):
                st.markdown(message["content"])

        # Accept user input
        if human := st.chat_input("Please ask assistant"):
            # Add user message to chat history
            st.session_state.messages.append({"role": "user", "content": human})
            # Display user message in chat message container
            with st.chat_message("user"):
                st.markdown(human)
            # Display assistant response in chat message container
            with st.chat_message("assistant"):
                message_placeholder = st.empty()
                with st.spinner("Processing..."):
                    custom_prompt = prompt.format(system_input=system_input, document_input= pdf_text,user_input=human)
                    print(custom_prompt)
                    print(state["memory"])
                    full_response = agent(custom_prompt)
                    state["count"] += 1
                    try :                
                        #print("full_response", full_response)
                        message_placeholder.markdown(full_response["output"])
                        st.session_state.messages.append({"role": "assistant", "content": full_response["output"]})
                        #print("st.session_state", st.session_state)
                    except Exception as e:
                        st.error(f"An error occurred: {str(e)}")
                        print(e)
    else:
        st.markdown("まずpdfファイルを読み込んでください")

def main():
    selection = st.sidebar.radio("Go to", ["PDF Upload", "Ask My PDF(s)"])
    if selection == "PDF Upload":
        get_pdf_text()
    elif selection == "Ask My PDF(s)":
        page_ask_my_pdf()

if __name__ == '__main__':
    main()


実行結果

pdfを読み込むページと会話をするページを分けています。


pdfを読み込むと読み込んだ結果が表示されます。


会話をするページでは、聞いた内容及びpdfの内容も踏まえて回答してくれています。
ただ、まだ制御が怪しくきれいに出すことができていません・・
また、プロンプトインジェクションにも弱く、なかなか難しいですね・・



まとめ

今回は、LangChainとGoogle Gemini APIを用いて、簡単なチャットアプリを作ってみました。
現在Google Gemini APIは無料で利用できるため、このような試みを行うには絶好の機会です。
LangChainは更新頻度が高く、その進化に大きな期待が持てます。今後も注目していきたいと思います。

WEBアプリをクラウドでデプロイ(Cloud Run + Streamlit or Flask)

WEBアプリを色々な方に使って貰うようにするには、クラウド等にアプリをデプロイする必要があります。
過去 AWSのサービスを使って、アプリのデプロイを試みてみましたが、Google CloudのCloud Runというサービスが、非常に良いなぁと思ったので、こちらを使ってやったことを備忘録として残します。

ちなみに、、PythonライブラリであるStreamlitをAWSでデプロイをしようとして、AWS AppRunnerというサービスを使ってみたのですが、、デプロイ完了、起動まではできるが、その後画面が、遷移せず(動かず)という感じで断念しました。
→調べてみると、、WebSocketが使えないから駄目だとか。コンテナ化をしてFargateを使ったらできるのかな?と思っています。


目次


Cloud Runについて

GCP Cloud Run(クラウド ラン)は、Google Cloud Platform(GCP)上で提供されているサーバーレスなコンテナ実行環境を構築できるサービスです。
"コンテナ"か、と思って最初は構えていたのですが、GCPのCloud Buildサービスと組み合し、Pythonコードをコンテナ化する作業もクラウド側で実行してもらえることには感動しました。(Dockerを使うのは過去苦労した経験があるので・・・WindowsのPCに入れたくない)
つまり、WEBアプリのPythonコードを書いて、(Docker fileは作るけど)、それをクラウドのサービスに渡すだけでデプロイできるのです。

実装

前準備

GCP CLIではデプロイができなかった件

詰まったところの紹介ですが、以下公式のチュートリアルに従って、進めたところ、デプロイ部分でエラーが出てしまいました。
https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-python-service?hl=ja

エラー画面

エラーの対処について、以下ブログ等の情報を探ると、リージョンをグローバル(非リージョン)にすれば解決するとのこと。
ただし、CLIではうまく設定が分からなかったので、今回はコンソールで実行しています。

↓参考になったブログ
zenn.dev

コード関係準備

コードや必要なファイルはすべてGithubにあげているので、詳細は割愛します。
構成は以下の通りで、ローカルで実行する時との差分はDockerfileが入る程度かなと思います。
github.com

│─ Dockerfile
│─ requirements.txt
└─ src/
   └─ app.py (WEBアプリのコード)


Dockerfileだけ、詳細を載せておきます。
pip が無いとエラーが出たことがあったので、"RUN python -m ensurepip"という行を入れています。

FROM python:3.11.1

WORKDIR /app

RUN python -m ensurepip
RUN python -m pip install --upgrade pip

COPY requirements.txt ./requirements.txt
RUN pip install -r requirements.txt
COPY . .

EXPOSE 8080

CMD streamlit run --server.port 8080 src/app.py


デプロイ

作成したソースやファイルたちは、Githubにあげた後、
Cloud Runのページで、ソースリポジトリからのデプロイを選択、Githubの対象リポジトリを選択します。



今回の作業は以下のページも参考にして対応しています。(感謝)
zenn.dev

これでデプロイは完了になります。

動作確認

デプロイ後に吐き出されるURLにアクセスをすると、Streamlitのアプリが無事動いていることを確認しました。


デプロイしたアプリのコンテナイメージは、Artifact Registryに保存されています。
デプロイ後、使い終えたら、アプリは削除しましょうね!(無料枠はあるけど、、ちょっとお金かかりそうで怖いので)

その他詰まったところ

IAMのエラー

エラーで吐き出されたURLにアクセスして、許可をすることで解決

generic::permission_denied: Identity and Access Management (IAM) API has not been used in project ○○before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/iam.googleapis.com/overview?project=○○ then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.


Flask HelloWorldもやってみた

上と準備することが殆ど同じなので、コードのみの紹介になりますが、Google公式のチュートリアルも無事こなすことができました。
github.com

まとめ

今回は、Google Cloud(GCP)のサービスを使ってWEBアプリをデプロイしてみました。
私は、Dockerを自身のPCに入れて作業するのに抵抗があったので、コンテナ化までを実行してくれるサービスはとても良いなと思いました。
また、Cloud Runは月々の無料枠も結構あるため、身内で使う程度のものなら、無料で運用できそうですし、他の方にもおすすめしようと思います。

ChatGPT My GPTsを作って気になったこと



個人のニーズに合わせてカスタマイズ可能なChatGPTであるGPTs(ジーピーティーズ)、かなり簡単に作成できるのですが、制御は結構難しい・・
色々と作成して気になったこと、気を付けるべきポイントについて、この記事でまとめます。

目次

GPTsの作り方/共有方法(他サイト参照)

一応作り方についてもまとめようと思いましたが、多くのサイトで共有されているので割愛します。
以下はご紹介です。

chatgpt-lab.com

自身で作成してみたGPTsについての紹介

とりあえず、自身で作ってみよう!ということで、作ってみました。
作成したMy GPTs君たちは以下です。画像を作らせたり、検索させたり、ファイルを読み込んで分析させたり等、それぞれが違うアプローチ(作業)になるように作ってみました。
これらGPTs は、ChatGPT Plus ユーザーならば誰でも無料で使うことができます。

No. GPTs Name 概要 URL
1 GPT use guide for JPN ChatGPTの使い方、シナリオを考えて複数個を紹介します。 [Link](https://chat.openai.com/g/g-4aQp2tI1D-gpt-use-guide-for-jpn)
2 Search MyGPTs for JPN 日本人向けのMy GPTsを検索して紹介します。 [Link](https://chat.openai.com/g/g-od9NiNtEy-search-mygpts-for-jpn)
3 Japanese Picture Book Creator for JPN 赤ちゃん向けの絵本を作って、1ページごとにページをめくってもらえます [Link](https://chat.openai.com/g/g-JpRZXiFJO-japanese-picture-book-creator-for-jpn)
4 Group Discussion Coach For JPN グループディスカッションのシミュレーター。終了後に評価シートを使って採点します。 [Link](https://chat.openai.com/g/g-WicD3sdXv-group-discussion-coach-for-jpn)
5 PinterestSearch for JPN 日本語のPinterest検索結果を表形式でまとめます [Link](https://chat.openai.com/g/g-2WH0qXcqG-pinterestsearch-for-jpn)
6 English Vocabulary Tutor For JPN TOEICレベル(550点, 720点, 800点)に合わせて、英単語の理解度を確認するテストを出題します。 [Link](https://chat.openai.com/g/g-YEpEGD9vw-english-vocabulary-tutor-for-jpn)
7 Prompt Reviser プロンプトを整理修正してくれます。重複の削除や強調表現の追加など、入力されたプロンプトを修正して出力します。 [Link](https://chat.openai.com/g/g-0K4EEaJwJ-prompt-reviser)
8 GIF Creator ユーザーの要望に従った動作のGIF画像を作成します。 [Link](https://chat.openai.com/g/g-kinaLWgLv-gif-creator)
9 DocuSummarize 入力した(添付した)ドキュメントを要約して、要約結果をテキスト出力します。 [Link](https://chat.openai.com/g/g-ED65z7m4v-docusummarize)
10 Thumbnail Creator サムネイル用の画像を作成します。意図に沿った画像を作成するため、画像設定を細かく決めてから画像生成するしようにしています。 [Link](https://chat.openai.com/g/g-UUsJyel6B-thumbnail-creator)
11 iPhone User Helper Apple公式ページの情報を参照して、iPhone等のApple製品の困りごとに応えてくれます。 [Link](https://chat.openai.com/g/g-3QnTaBOgF-iphone-user-helper)

作ってみた感想

対話型で作れるのは良いが、プロンプトがごちゃごちゃする

以下画像が、GPTsのConfigureの画面であるが、プロンプトが入力される"Instructions"が、ユーザーの要望に従って修正される。
「修正してくれる」ことがありがたいのではあるが、、プロンプトを整理した後にごちゃごちゃにされてしまうのが困った


使用制限に結構引っかかる

chatGPT課金ユーザーにも使用制限があり、GPTsを作るためにchatGPTを使うことでも制限がかかってしまう・・・そんな
短時間で何度も修正をすると、結構引っかかってしまう印象。


GPTs作成で気を付けるべきと感じたこと

「GPTs作成で気を付けるべきと感じたこと」というよりは、「GPTに役割を与えるためのプロンプト作成で気を付けた方が良いこと」になるかもしれないが、感じたことをまとめる。
かなり主観が入っている為、ご注意ください。

明確で具体的なプロンプトとする

目的があやふやだと、やはり出力結果がぶれてしまいます。
「文脈」「前後関係」「事情」「背景」「状況」を細かく、100人が100人同じ答えを出せるように書くことが望ましかもしれません。
以下は例になります。

失敗例: 天気はどうですか?
成功例: 2024年1月6日の東京 赤坂の午後9時以降の天気予報は何ですか?

失敗例: 地球温暖化について教えて下さい。
成功例: 地球温暖化について、2023年に報告された研究結果を基に教えて下さい。

失敗例: 最低賃金について教えて。
成功例: 経済学の観点から、最低賃金の引き上げが労働市場に与える影響について、事実に基づいた分析を行い、結果をtextにまとめてください。


また、日本語でプロンプトを書くよりも英語でプロンプトを書いた方が、出力結果にぶれがないような・・・やはり英語で学習をしているから?

シンプルな言い回しに努める

複雑に書くと分かりにくいのか、どうも意図通りに動作しないときがありました。
シンプルで直接的な表現が良いのかもしれません。

失敗例: 経済的な均衡理論における供給と需要の相互作用に関する詳細な説明をしてください。
成功例: 経済学において、供給と需要はどのように働きますか?


作業フローの記載を入れる

スタートからゴールまでの1つ1つの作業を細かく記載することで、途中で動作が止まる等、意図しない動作がなくなりました。

【例】
1. @@@を検索して△△△の情報を取得する
2. 情報をスプレッドシートに記載する
3. スプレッドシートのダウンロードリンクを作成する


プロンプトを整理する

役割に関する情報、作業に関する情報、ユーザーの対応に関する情報など、項目毎にプロンプトを整理して記載した方が、意図しない動作が少なくなると感じました。
以下は赤ちゃん向けの絵本を作成するためのプロンプトであり、記載を分けることで安定した出力ができるようになりました。

【例】
[Objective & Goal]
The objective is to generate Japanese picture books specifically designed for infants aged 0, 1, and 2 years. These picture books will have images presented in a 16:9 aspect ratio, accompanied by separate story text below each image. The goal is to create engaging, age-appropriate content that aligns with infants' developmental stages, avoiding complex language or concepts. The interaction will be entirely in Japanese, including narrations and user conversations. The tone and personality are yet to be defined, guiding the interaction style.

[Flow]
1. Generate images and story text suitable for infants aged 0, 1, and 2 years.
2. Present each image along with its corresponding story text in a 16:9 aspect ratio.
3. Narrate the story text in Japanese.
4. Prompt the user to input "続きは" to move on to the next page.
Continue the process, ensuring that each page of the picture book is engaging and appropriate for the target age group.
Maintain a consistent and engaging narrative style throughout the picture book.

[Adaptation Strategy]
Culturally Sensitive and Inclusive Content: Be aware of cultural sensitivities and strive to include diverse elements in the content. This can involve using a variety of characters and settings that reflect different backgrounds and cultures, fostering inclusivity from an early age.
Safety and Comfort: Always prioritize the safety and comfort of the infant audience in every aspect of the content. This includes avoiding any elements that could be potentially startling or uncomfortable for young children, such as loud noises or overly complex imagery.

プロンプトインジェクション対策をする

プロンプトが見られてしまう自体は、現状別に・・と思っていますが、不適切な回答をするのは、AIとしての役割設定がうまくできていないということになるので、避けたいなと思っています。

以下がかなり参考になりましたが、もう少しメカニズム的な解決の仕方が無いのかなぁと思う今日この頃です



chat-gpt.school

プロンプトエンジニアリングに関して読んだ文献

プロンプト記載の原則についてまとめた文献
かなり詳しく書いているので、おススメです。
arxiv.org

プロンプトを自動的に改善するアルゴリズムについてが記載された文献
arxiv.org


まとめ

GPTsの作成が比較的容易であるものの、その制御が難しいというのは多くの人が感じているのではないでしょうか?
プロンプトの最適化については、研究もされているようなので、目的に沿ったプロンプトが自動で作れる時代もすぐに来るのではないかな‥と、私も勉強しつつ期待をしています。

できるだけPythonだけでWEBアプリを作る④(Streamlit, Gradio, Dash, Reflex)

「できるだけPythonだけでWEBアプリを作る第4弾」として複数のPython ライブラリの比較を行っていきたいと思います。
各ライブラリの使い方はそれぞれまとめている方がいらっしゃるので、比較結果についてのみをまとめます。

第1弾, 第2弾, 第3弾記事は以下の通りです。
できるだけPythonだけでWEBアプリを作る①(Python + AWS + HTML/Java) - まるっとワーク
できるだけPythonだけでWEBアプリを作る②(Python flask/zappa + AWS) - まるっとワーク
できるだけPythonだけでWEBアプリを作る③(Python Streamlit) - まるっとワーク

目次


PythonでWEBアプリが作れるライブラリについて

丁寧にまとめて下さっている方がいらっしゃいました。(感謝です)
まさかこんなにあるなんて・・・最近まで全然知りませんでした。
これらはPyhtonのみでWebアプリが作れるライブラリであり、html, cssを書く必要があるFlask, Djangoは入っていませんね。
zenn.dev

ご参考↓


今回はまとめて下さっているライブラリの中で、GitHub star数が相対的に多めである4つ(Streamlit, Gradio, Dash, Reflex)の比較を行いました。

ライブラリの比較結果

比較検討のために作成したコードは、以下 Githubで公開をしています。
github.com

結果はかなり所感が入っているので、ご注意下さい。
まとめると、以下の通りの使い分けになると思っています。
とりあえずシンプルなUIでよい!素早くアプリを出したい!:Streamlit, Gradio
UIのカスタマイズをしたい!オリジナリティを出す必要がある!:Dash, Reflex


比較結果を表でまとめています。

観点 説明 Streamlit Gradio Dash Reflex
用途 フレームワークの主な利用シナリオ。 データ分析や機械学習モデルの可視化、プロトタイピング、簡単なWebアプリケーション 機械学習モデルのデプロイ、モデルの入力と出力の簡単な設定 データダッシュボード、高度な可視化、企業向けアプリケーション リアルタイムアプリケーション、コラボレーション、高度な対話型アプリケーション
インタラクティブ ユーザーとの対話性能力(反応性)。 高い 中程度 高い 高い
UIカスタマイズ ユーザーインターフェースの外観と振る舞いを調整する能力。 限定的(デフォルトのウィジェットをカスタマイズ可能) 限定的(デフォルトのウィジェットをカスタマイズ可能 カスタマイズ可能(HTML / CSS / JavaScriptを使用してUIをカスタマイズ) カスタマイズ可能(HTML / CSS / JavaScriptを使用してUIをカスタマイズ)
シンプルさ 使用の簡便さ。 高い(シンプルなコードでアプリケーションを構築できる) 高い(簡潔なAPIでモデルデプロイが可能) 中程度(高度なカスタマイズが必要な場合がある) 中程度(カスタマイズが必要な場合がある)
コミュニティサポート オンラインコミュニティのサポートとリソースの利用可能性。 高い 中程度 高い 中程度
ドキュメンテーション フレームワークの公式ドキュメントとチュートリアルの品質と充実度。 良い 良い 良い 良い


それぞれ特徴がありますが、シンプルなUIで良いからとりあえずアプリを出したいということであれば、Streamlit, Gradioという選択になり、後はカスタマイズの希望に応じて、Dash, Reflex, Flask, Django等・・・という感じかなと

GUI比較(ご参考程度ですが)

同じ目的のアプリを作成した時の見た目の差の比較です。
ただし、見た目についての調整はほとんどしておらず、、、最初からある程度デザイン性があるStreamlit, Gradio, Reflexの方が見た目は良いかもですね。

Streamlit


Gradio


Dash


Reflex



まとめ

簡単ですが、以上です。
PythonだけでWEBアプリを作ることができるライブラリがこんなにあるなんて、知りませんでした。
これだけあると、どれを使えば良いのか分からなくなってくるので、特徴などを比較しながら適切に選択していきたいなと思います。

できるだけPythonだけでWEBアプリを作る③(Python Streamlit)

できるだけPythonだけで・・・と進めてきましたが、画期的なフレームワークの存在最近まで知らず・・・
「できるだけPythonだけでWEBアプリを作る第3弾」としてPython ライブラリのStreamlitとStreamlit Cloudを使用した方法をまとめていきます。
この方法は他の方も多くまとめているので、備忘録として簡単なフローだけを示しています。

第1弾, 第2弾記事は以下の通り。
dango-study.hatenablog.jp
dango-study.hatenablog.jp

目次


Streamlitでできること

  • PythonだけでWebアプリが作れる
  • フロントエンド(UI)の開発スキルが無くても簡単にUIを作ることができる
  • 作成したアプリをStreamlit Cloudに(無料)デプロイできる

デプロイ方法は、Streamlit Cloud意外に色々ありますが、Streamlit Cloudではセキュリティーが担保できないので、機密データを処理したい時などは、ローカルサーバーやセキュリティ保護されたサーバーでの運用が望ましいです。

Streamlitを用いたWEBアプリ作成

開発環境

[ローカル]
Microsoft Windows 10 Pro
Python 3.9.0

ライブラリのインストール

以下を実行して必要なライブラリをインストールします。

$ pip install streamlit

アプリ構成

基本的な構成は、「処理等を記載した.pyファイル」「必要ライブラリとバージョン情報を記載したrequirements.txt」の2つの構成になります。

@@@.py
requirements.txt

コード作成

任意の名前の.pyファイルには、最低限streamlitのモジュールインポート記載をしておけば、実行確認ができます。

$ import streamlit as st 

ローカルで実行/確認

コンソールで以下コードを実行して、
エラーが出なければ「http://localhost:8555」にアクセスして実行結果を確認できます。

$ streamlit run @@@.py

→.pyファイルの記載がstreamlitのモジュールインポート記載のみであれば、白紙のページが表示されるだけです。

コードの書き方などは、ありがたいことに・・先人の皆様のページで詳しく説明されているので割愛します。

  • 参考コード(私)

https://github.com/Mya-mori/streamlit_csv/blob/main/test.py

  • 参考ページ①

Streamlit入門+応用 ~ データ分析Webアプリを爆速で開発する - Qiita

  • 参考ページ②

PythonでDX「Streamlit」簡単ダッシュボード(前編) | コードファミリー

  • 参考ページ③

Streamlitで手軽にWebアプリ開発 - アルファテックブログ

アプリをStreamlit Cloudにデプロイ

Streamlit Cloudにサインイン

https://share.streamlit.io/signup
Googleアカウント、Githubアカウント、メールアドレスのいずれかを利用してアカウントを作成します。

Githubアカウント連携

Settingsをクリックし、Connect Github accountをクリックして、Githubと連携する。
(Githubアカウントがない場合は、作成する)

GithubリポジトリにWebアプリのソースコードをアップロード

作成したソースコードGithubにアップロードします
アップロードするファイルは、最低限以下構成のファイル

@@@.py
requirements.txt

requirements.txtは以下コードを実行することで、出力することができる。
必要最低限の情報でないとエラーが出る場合がある為、余計なライブラリ情報は削除する。(ライブラリが多すぎることが原因のエラーが出ました)

$pip freeze > requirements.txt

アプリのデプロイ

Streamlit CloudにStreamlitで作成したWebアプリをデプロイします。
Streamlitにログインして、"New app"と記載されたボタンを押す。

以下の通り設定をして、デプロイボタンを押すことで、デプロイされる。

何か問題があれば、エラーが吐き出されます。

動作確認

デプロイされるアプリURLにアクセスすると、デプロイしたアプリの動作確認ができます。
簡単にデプロイして利用できるのがStereamlit Cloudを用いた利点ですね。
参考までに私が作成したアプリは以下↓

https://webapplicationsample1-rwb1ydqchif.streamlit.app/
システム同定シミュレーターアプリ】
csvデータを入力して、線形システムのシステム同定シミュレーションができます。
Githubアイコンを押すとコードも参照頂けます。

まとめ

今回は、前回とは違う方法でWEBアプリを作ってみました。
正直、セキュリティーとかなんやら何も気にしなくてよいのであれば、この方法が一番簡単だなと思いました。今までの努力は・・笑
次は、こういったフレームワーククラウドを組み合わせてかつ、セキュリティも考慮したアプリを作成していこうと思います。

Amazon API Gatewayを理解する

Amazon API Gatewayは、AWS Lambdaと組み合わせて使用することが一般的で、サーバーレスアーキテクチャを構築するための基盤として利用されています。WEBアプリを色々と作ろうとしている段階で、このAmazon API Gatewayを避けて通ることはないと思うので、この機能を理解して、使う時に引っかかるであろうポイントをまとめようと思います。

目次

Application Programming Interface(API)とは

分かっているようで分かっていない気がするので改めて、確認します。
APIは、アプリケーションやサービスとやり取りをするための標準的なインターフェースのことです。プログラムが他のプログラムやサービスと情報を共有するために使用され、APIを使用することで、異なるプログラムやサービス間でデータを共有することができ、相互運用性や拡張性を向上させることができます。

APIの種類

APIは、異なる種類のアプリケーションやサービスで使用されます。Web APIREST APISOAP API等の多様な種類のAPIがあります。APIは、標準化された規格に従って設計されることが多く、APIの利用者がアクセスしやすいように、ドキュメント化され、公開されることが一般的です。

Web API

APIというとWeb APIを指すことが大部分で、Webブラウザ経由でアクセスするAPIのことを指します。
HTTPプロトコルを使用してリクエストを送信し、JSONXML、またはプレーンテキストの形式で応答を受け取ることができます。

APIというと「アプリケーションやサービスとやり取りをするための標準的なインターフェース」のことなので、ローカル環境でもこのワードに該当するサービスを作れば、APIと呼ぶものになるので、単純なAPIとWeb APIだとワードの範囲が違うんだなと理解しました。

Representational State Transfer(REST API)

Web APIと並列で考えてよいのかが怪しいのですが、RESTアーキテクチャスタイルであるWEB APIを指します。

  • アドレス指定可能なURIで公開されていること
  • インターフェース(HTTPメソッドの利用)の統一がされていること
  • ステートレスであること
  • 処理結果がHTTPステータスコードで通知されること

ステートレスとは、システムが現在の状態を表すデータなどを保持せず、入力の内容によってのみ出力が決定される方式。同じ入力に対する出力は常に同じになる。

これらの原則に則ったWebサービスをRESTfulなサービスといいます。
Web APIと同じように、HTTPプロトコルを使用して通信してデータをやり取りしますが、REST APIは、リソースの操作を表現するためにHTTPメソッド(後述: POST, GET, PUTなど)を使用する点が、Web APIとは異なるようです。

詳細は以下に載っていました。
RESTful API とは? - RESTful API の説明 - AWS

Simple Object Access Protoco(SOAP API)

SOAPは、HTTPプロトコルの代わりに独自のプロトコルを使用してXML形式でデータをやり取りしてAPI操作を表現します。独自のプロトコルを使うという点とXML形式を使うという点で、Web APIREST APIと異なり、SOAPの方が複雑な設定が必要であることから、比較的重いプロトコルであり、通信速度が遅くなります。ただし、セキュリティや信頼性に優れるという利点があり、主に企業間のシステム間の通信に使用されることが多いようです。

Amazon API Gatewayとは

Amazon API GatewayAWS内でのAPIの作成および諸々の管理を行えるサービスのこと。基本これ単体で使うのではなく、AWS Lambda等とつなげて、リクエストに対して結果を返すサービスを作るの目的に使われます。

ちなみに、公式だと以下の説明が載っています。
Amazon API Gateway(規模に応じた API の作成、維持、保護)| AWS

フルマネージド型サービスの Amazon API Gateway を利用すれば、デベロッパーは規模にかかわらず簡単に API の作成、公開、保守、モニタリング、保護を行えます。API は、アプリケーションがバックエンドサービスからのデータ、ビジネスロジック、機能にアクセスするための「フロントドア」として機能します。API Gateway を使用すれば、リアルタイム双方向通信アプリケーションを実現する RESTful API および WebSocket API を作成することができます。API Gateway は、コンテナ化されたサーバーレスのワークロードやウェブアプリケーションをサポートします。

APIのタイプ

APIタイプは以下から選択ができ、それぞれの特徴は以下の通りです。

HTTP API Web Socket API REST API
HTTP APIは、HTTPリクエスト/レスポンスを使用してRESTfulなWebサービスを作成するためのAPI。機能を絞ることで、REST APIよりも低レイテンシかつ低コスト(コスト最適化)を実現できるようです。 WebSocket APIは、リアルタイムなWebアプリケーションを構築するためのAPIです。WebSocket APIは、クライアントとサーバー間で長時間の接続を確立することができ、データの双方向通信を可能にします。WebSocket APIは、リアルタイムな情報の伝達が必要なチャットアプリ、オンラインゲーム、監視システムなどに適しています。 REST APIは、HTTPプロトコルに基づいたAPIで、HTTP APIの違いは色々とあるが、HTTPメソッド(GET、POST、PUT、DELETEなど)を使用してリソースの操作を実行します点が大きな差かなと思っています。

目的に応じて選択が必要です。私の場合、JSON形式でデータ送信したいので、HTTP API/REST APIから選択ができ、HTTPメソッドを用いて簡単に開発したいならばREST APIを選ぶ感じかなと思います。

REST APIのHTTPメソッド

REST APIタイプは以下から選択ができ、それぞれの特徴は以下の通りです。

メソッド 説明
GET Webページや画像などリソースを取得するために使用する
POST リソースを送信するために使用する
PUT リソースの更新や作成をするために使用する
DELETE リソースを削除するために使用する
PATCH リソースの一部を更新するために使用する
HEAD リソースのヘッダ情報のみを取得するために使用する
OPTIONS リソースに対して許可されているHTTPメソッドの一覧を取得するために使用する
ANY 一般的なHTTPメソッドではなくAWS API Gatewayで使われる概念で、すべてのHTTPメソッドを扱うことができる

なんとなくは分かりましたが・・扱えるようになるため、実際に使ってみます。

Amazon API Gateway(REST API)を使ってみる

実際に使う前に、理解した範囲で知識をまとめます。

統合リクエスト設定

統合リクエストは、API Gatewayがバックエンドに送るHTTPリクエストで、クライアントから送信されたリクエストデータを渡して、必要に応じて変更します。今回はAWS Lambdaに渡すことを前提とします。設定方法は、以下にまとめられています。
API Gateway で Lambda プロキシ統合を設定する - Amazon API Gateway



メソッドリクエストから、クエリパラメータを受け取って、Lambdaに渡したい場合は、クエリパラメータを登録する必要がある。
統合リクエストから、マッピングテンプレートを登録する。今回は、JSON形式で受け取りたいので、以下の通り3つのパラメータ(my_param1, my_param2, my_param3)をもらう設定を追加。

クエリパラメータの設定方法

URLにクエリパラメータを付与する場合は、ルールに従って以下のように書く必要がある。

  • パラメータはURLの後に「?」をつけて記載
  • 各パラメータは「名前=値」で記載
  • 複数パラメータを付与する場合は、「&」で分ける

書き方(3つのパラメータmy_param1, my_param2, my_param3を付与するケース)
URL(https://*******)?my_param1=1&my_param2=2&my_param3=3


URLはデプロイしたHTTPメソッドのエンドポイントに設定する。


エンドポイントとは、クライアントとAPIとが通信をするための窓口であり、APIのリクエスト先になる。

プロキシ統合設定について

HTTPメソッドを作成する際に、「Lambdaプロキシ統合の使用」というチェックボックスがあり、この役割を説明する。


詳しくは以下にまとめられていたが、以下のように理解しました。

統合 非統合
API GatewayはLambda関数に直接接続され、API GatewayがHTTPリクエストをLambda関数に直接ルーティングするため、API GatewayとLambda関数の間の通信が最適化されます。Lambdaから返される値のフォーマットが決まっている API GatewayとLambdaは独立して動作します。API GatewayはHTTPリクエストを受け取り、Lambda関数を呼び出すためにLambda関数のARN(Amazonリソース名)を設定します。Lambda関数はリクエストを処理し、結果をAPI Gatewayに返します。API GatewayはLambda関数からのレスポンスを返信し、クライアントに返します。Lambdaから返される値にフォーマットはなくそのままの値が返される

qiita.com

動作を確認

環境設定に関しては、以下の通りで、Lambda関数は「statusCode」「body("Hello World")」「event(リクエスト)」を返す関数であり、API Gateway側ではANYメソッドがリクエストデータをそのままLambda関数に渡す設定になっている。プロキシ統合は非統合の設定。

Lambda関数

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps('Hello World'),
        'event': json.dumps(event)
    }


AWS API Gateway

GET

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "GET"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "GET":
    print("GET")
    response = requests.get(URL, params=payload)

data = response.json()
print ("json", json.dumps(data, indent=4))
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、eventは何もない状態で返されている。クエリとして、3つのパラメータを送付しているが、クエリパラメータを受け取る設定にしていないため、こういった現象が起きている。

GET
json {
    "statusCode": 200,
    "body": "\"Hello World\"",
    "event": "{}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{}"}'


上記で説明した通り、統合リクエスト設定にてマッピングテンプレートを設定すると、以下の通り、受け取ることができて、Lambda関数に適切に渡されていることが確認できる。

# マッピングテンプレート設定後
GET
json {
    "statusCode": 200,
    "event": "{\"my_param1\": \"1\", \"my_param2\": \"2\", \"my_param3\": \"3\"}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{\"my_param1\": \"1\", \"my_param2\": \"2\", \"my_param3\": \"3\"}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{\\"my_param1\\": \\"1\\", \\"my_param2\\": \\"2\\", \\"my_param3\\": \\"3\\"}"}'


POST

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "POST"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "POST":
    print("POST")
    response = requests.post(URL, data=json.dumps(payload))

data = response.json()
print ("json", json.dumps(data, indent=4))
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、POSTメソッドの場合はマッピングテンプレートの設定をせずパラメータを送付して、Lambda関数に適切に渡されていることが確認できる。

POST
json {
    "statusCode": 200,
    "body": "\"Hello World\"",
    "event": "{\"my_param1\": \"1\", \"my_param2\": 2, \"my_param3\": 3}"
}
status_code 200
HTML_text {"statusCode": 200, "body": "\"Hello World\"", "event": "{\"my_param1\": \"1\", \"my_param2\": 2, \"my_param3\": 3}"}
binary b'{"statusCode": 200, "body": "\\"Hello World\\"", "event": "{\\"my_param1\\": \\"1\\", \\"my_param2\\": 2, \\"my_param3\\": 3}"}'

ローカルにて以下コードを実行して、出力結果を確認した。

import requests
import json

pattern = "HEAD"
URL = r"エンドポイント"
payload = {'my_param1':"1", 'my_param2':2, 'my_param3':3}

if pattern == "HEAD":
    print("HEAD")
    response = requests.head(URL)

#data = response.json()
#print ("json", json.dumps(data, indent=4))
print(response)
print("status_code",response.status_code)    # ステータスコード取得
print("HTML_text", response.text)    # HTMLを文字列で取得
print("binary", response.content)    # HTMLをバイナリ形式で取得


出力結果は以下の通りで、HEADメソッドでは、レスポンスボディを返さないため、 response.content などでレスポンスボディを取得することはできません。HEADメソッドは、サーバーからのレスポンスボディを取得することがないため、GETメソッドよりも高速にリソースの存在確認を行うことができます。

HEAD
<Response [200]>
status_code 200
HTML_text
binary b''


他のメソッドに関しては、実際にWEBアプリを作った際に色々と確認しようと思う。一旦はここまで。

まとめ

APIに関する言葉の意味からスタートして、色々と勉強になりましたが、まだまだ奥が深く知識を浅いことを痛感しました。
HTTPメソッドは、Webアプリケーション開発において非常に重要な役割を果たしているので、使い方を勉強して、WEBアプリケーション作りに活かしていきたいと思います。

オリジン間リソース共有 (CORS)設定について

WEBアプリ(以下)を作成した時に、ブラウザのCORSというポリシーによるエラーが発生し、エラー解除に苦戦をしたので、COR2の関連知識についてまとめていきます。
dango-study.hatenablog.jp
 
目次


CORSのエラーについて

色々調べていくと、以下のように書かれていました。
Cross-Origin Resource Sharing(CORS: オリジン間リソース共有)はサーバーが同一オリジンポリシーを緩和できる標準で、異なるオリジンからのリクエストに対してサーバー側が許可を与えなかった場合に発生するエラーです。・・ちょっとまだ完全に理解ができていないので、用語などを調べながら理解を進めようと思います。

Origin(オリジン)とは

オリジンとは、ウェブコンテンツにアクセスされるために使われるURLの中のスキーム(プロトコル)ホスト(ドメイン)ポート番号を組みわせたもの

項目 詳細 備考
オリジン https://www.example.com:8080 以下3つを組み合わせたもの
スキーム(プロトコル) https:// かhttp:// 2つの違いは通信が暗号化されているかどうかで、httpsが暗号化されている
ホスト(ドメイン) www.example.com いわゆるインターネット上の住所で自身で好きなドメイン名にしたりする
ポート 8080等 ポートに関しては、httpsが443番でhttpが80番が既定番号となり、既定番号ら省略ができるので、ポート番号が付けられないこともある

同一オリジンポリシーとは

上記の通りプロトコル+ドメイン+ポートが同一の場合は同一オリジンとみなして保護をして、異なるオリジンからのアクセスを制限する仕組みのこと。

主な原則として3つがあります。

  • ドキュメントオブジェクトモデル(Document ObjectModel:DOM)は、同じオリジンのスクリプトにしかアクセスできない。

DOMは、HTMLのように文章の構造を示すものやJavaScriptのように操作を示すもの

  • XMLHttpRequest(XHR)オブジェクトを使用して、異なるオリジンからデータを取得することはできない。

XHRはサーバーとのデータ通信に使い、ウェブページの更新などを行う目的で使用される

  • クッキー(HTTP Cookie)は、同じオリジンのWebページのみに送信されます。

クッキーは、クライアントのWebブラウザとWebサーバとの間で、状態を維持・管理する仕組みで、その通信の際にクライアントのWebブラウザに保存された情報。その後のリクエストとともにサーバーに送信され、ログイン状態の維持などの役割で使用される。

つまり、あるオリジン(Webサーバー)の更新は、同一オリジン(内包したプログラム)やクライアント側でのみ行われ、異なるオリジンからの操作/情報取得等を制限する仕組みと思われます。

CORSの制限対象

同一オリジンポリシーの考え方を踏まえるとCORS(オリジン間リソース共有)の考え方は、なんとなく理解ができ、異なるオリジンからのHTTPリクエストが制限対象となります。

今回のエラーケースでいうと、アーキテクチャが以下の通りで、CORSのエラーが初期段階で出ていました。
恐らく、JavaScriptからXMLHttpRequestAPI Gatewayからデータを取得しようとしていた為、CORSエラーが発生したと思われます。
(追記)アーキテクチャ

CORSエラーの解決方法

全て網羅できていなさそうですが、対処方法を以下にまとめます。

サーバー側で必要なHTTPレスポンスヘッダーを設定する

この方法が、私がWEBアプリを作成時に対応した方法です。サーバー側で、必要なHTTPレスポンスヘッダーを設定することで、異なるオリジンからのリクエストを許可することができます。具体的には、Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-HeadersなどのHTTPレスポンスヘッダーを設定することが必要です。これらのヘッダーは、CORSエラーを回避するために必要な情報を提供するために使用されます。
詳細は、以下に記載されています。
API Gateway からの CORS エラーのトラブルシューティング | AWS re:Post
  

プロキシを使用する

プロキシを使用することで、異なるオリジン間での通信を実現することができます。プロキシとは、クライアントとサーバーの間にある中継サーバーのことで、クライアントからのリクエストを代理でサーバーに送信することができます。プロキシを使用することで、サーバー側でCORSの設定を行わなくても、異なるオリジンからのリクエストを許可することができます。

まとめ

CORSエラーは、異なるオリジン間での通信が必要な場合に発生する問題です。CORSエラーを回避するためには、サーバー側で必要なHTTPレスポンスヘッダーを設定することや、プロキシを使用することができます。開発をしていて障害になるのは、こういった制限ルールなどが多いので、こういった知識をひとつづつ学んでいかないといけないなと思いました。