🌏

LangChainチャットボットをAzure App Serviceで動かしてみる

2023/09/03に公開

先日、「LangChainチャットボットのベクターストアにAzure Cognitive Searchを使ってみる」というエントリーで、ベクターストアにPaaSを使うようにしました。それでは一連の検証の最後に、ウェブアプリケーションもPaaSで動かしてみることにします。ベクターストアにAzure Cognitive Searchを使ったので、アプリケーション実行環境にはAzure App Serviceを使います。

なお、ローカルで実行する分にはあまり気にすることではなかったのですが、インターネットアクセス可能にすると、意図しないアクセスでOpenAIのAPI費用を無駄に消費する問題が気になります。そこで、最低限の対策としてベーシック認証をかけておくことにします。

また、他の人にも触って試してもらう場合、どんな質問と回答のやり取りがされていたか追跡可能となるようにログ保存しておきたいところです。そこで、Application Insightsでログ保存をしておくことにします。

Flaskアプリケーションにベーシック認証をかける

ちゃんとしたシステムで複数の方に使ってもらうには、App ServiceのEasy Auth認証を使ってAzure Active Directory(Microsoft Entra ID)などと連携させて認証させるのがいいとは思いますが、今回はデモ目的レベルなので、固定ユーザーとパスワードのベーシック認証だけかけておくことにします。

Flaskアプリケーションでは、Flask-HTTPAuthというモジュールがありますので、これを使います。ユーザー名はdemoという名前にします。サンプルコードではPythonのコードの中にパスワードも埋め込まれているのですが、.envファイルとしてUSER_DEMO_PASSWORDとしてパスワードを環境変数にします(なお、このエントリーにおけるパスワードやAPIキーなどの文字列は、スクリーンショットも含めてすべてダミーです)。

.env
OPENAI_API_KEY="D7f9G2uMEK1lvaoA8eB0xjXVNyR5rFQ63LhtSgZWmPc"
AZURE_COGNITIVE_SEARCH_ENDPOINT="https://langchaintest-acs.search.windows.net/"
AZURE_COGNITIVE_SEARCH_ADMIN_KEY="9X8rZ4FgH2D6uOJQsV7WnTzC5pL1vByEwRiY0aNx3klMbm"
AZURE_COGNITIVE_SEARCH_INDEX_NAME="langchaintest-index"
USER_DEMO_PASSWORD="A9b8-c7D6-E2f1-G403"

アプリケーションの追加箇所はこちらです。

app.py
# Flask imports
from flask import Flask, request, jsonify, render_template

# ~~~中略~~~

# HTTP Authentication
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

# Other imports
import os
from dotenv import load_dotenv

app = Flask(__name__)

# Load environment variables
load_dotenv()

# Configure Authentication
auth = HTTPBasicAuth()

users = {
    "demo": generate_password_hash(os.environ['USER_DEMO_PASSWORD']),
}

# ~~~中略~~~

# Flask routes
@auth.verify_password
def verify_password(username, password):
    if username in users and \
            check_password_hash(users.get(username), password):
        return username

@app.route('/')
@auth.login_required
def index():
    return render_template('index.html')

@app.route('/ask', methods=['POST'])
def ask():
    question = request.json['question']
    answer = qa(question)['answer']
    return jsonify({"answer": answer})

# Run application
if __name__ == '__main__':
    app.run()

ローカルで実行した際に、ベーシック認証が求められることを確認しましょう。

アプリケーションログの保管にApplication Insightsを使う

Azureでアプリケーションのログを保管するソリューションとしては、Azure Monitorの中のApplication Insightsという機能が使われます。

まずは、Azure Portal上でリソースを作成します。「Application Insights」で検索し、作成から下記の画面を開きます。サービス名を指定すること以外は、基本的にウィザードの初期値に従って設定するだけで良いと思います。

リソースの作成が完了したら、リソースの概要画面に移動し、画面右側の「接続文字列」の値(この画面だとInstrumentationKeyから始まる値)をコピーしておきます。

Application Insightsにログ保存する際の接続用に、Pythonコードがこちらの文字列を知っていることが必要ですので、.envファイルにAZURE_APPLICATION_INSIGHTS_CONNECTIONとして指定しておきます。

.env
OPENAI_API_KEY="D7f9G2uMEK1lvaoA8eB0xjXVNyR5rFQ63LhtSgZWmPc"
AZURE_COGNITIVE_SEARCH_ENDPOINT="https://langchaintest-acs.search.windows.net/"
AZURE_COGNITIVE_SEARCH_ADMIN_KEY="9X8rZ4FgH2D6uOJQsV7WnTzC5pL1vByEwRiY0aNx3klMbm"
AZURE_COGNITIVE_SEARCH_INDEX_NAME="langchaintest-index"
USER_DEMO_PASSWORD="A9b8-c7D6-E2f1-G403"
AZURE_APPLICATION_INSIGHTS_CONNECTION="InstrumentationKey=1234a567-8901-2b34-5c6d-7e8ff901g23h
;IngestionEndpoint=https://japaneast-1.in.applicationinsights.azure.com/;LiveEndpoint=https://japaneast.livediagnostics.monitor.azure.com/"

PythonコードからApplication Insightsに接続するには、OpenCensus Azure Monitor Exporter(opencensus-ext-azure)というモジュールを使用します。

以下のコードのようにLoggerを構成し、ユーザーからINPUTを受け取ったタイミングと、チャットボットが応答を返したタイミングでログを書き出すように設定します。

app.py
# Flask imports
from flask import Flask, request, jsonify, render_template

# ~~~中略~~~

# HTTP Authentication
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

# ~~~中略~~~

app = Flask(__name__)

# Load environment variables
load_dotenv()

# ~~~中略~~~

# Configure logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
logger.addHandler(AzureLogHandler(connection_string=os.environ['AZURE_APPLICATION_INSIGHTS_CONNECTION']))

# ~~~中略~~~

@app.route('/ask', methods=['POST'])
def ask():
    question = request.json['question']
    logger.info("[user] %s" % question)
    answer = qa(question)['answer']
    logger.info("[bot] %s" % answer)
    return jsonify({"answer": answer})

# ~~~以下略~~~

Azure App Service (Web App)にアプリケーションをデプロイする

アプリケーションの修正は完了したので、Azure App SericeのWeb Appリソースを作成します。Azure Portalで、「App Service」で検索し、作成で「Webアプリ」を選択します。

インスタンスの名前を指定して、ランタイムスタックはローカルのPythonバージョンに合わせて選んでください(このエントリーを書いている時点では、Python 3.8から3.11までがサポートされていました)。価格プランは私は動作検証だけだったので「Free F1」にしましたが、一日中動かし続けるのであれば「Basic B1」以上を検討してください。地域は「Japan East」(東日本リージョン)にしていますが、これも好みです。他は基本的にデフォルトで良いと思います。リソースを作成します。

作成が完了したら、リソースの画面に移動し、設定セクション配下の構成画面を開きます。この画面で、App Serviceの環境変数(アプリ設定)を設定します。今まではpython-dotenvを使って、.envファイルに定義していたのですが、App Serviceのアプリ設定では保存時に暗号化もされるので、.envの利用は行わず、アプリ設定を利用します。

「+新しいアプリケーション設定」をクリックし、.envに設定されていた内容を一つずつ設定していきます。このタイミングで、SCM_DO_BUILD_DURING_DEPLOYMENT=trueも一緒に設定しておいてください。この後でアプリをzipファイルでデプロイするのですが、ビルド自動化が必要な手順のためです。デプロイ方法が別の方法であれば必要ありません。

環境変数の設定方法を変えたので、アプリケーションコードの不要箇所を削除します。

app.py
# Flask imports
from flask import Flask, request, jsonify, render_template

# Langchain imports
from langchain.chat_models import ChatOpenAI
from langchain.chains import ConversationalRetrievalChain, LLMChain
from langchain.embeddings import OpenAIEmbeddings
from langchain.memory import ConversationSummaryMemory
from langchain.vectorstores.azuresearch import AzureSearch

# Application Insights import
from opencensus.ext.azure.log_exporter import AzureLogHandler
import logging

# HTTP Authentication
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import generate_password_hash, check_password_hash

# Other imports
import os

app = Flask(__name__)

# Configure Authentication
auth = HTTPBasicAuth()

users = {
    "demo": generate_password_hash(os.environ['USER_DEMO_PASSWORD']),
}

# Configure logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler())
logger.addHandler(AzureLogHandler(connection_string=os.environ['AZURE_APPLICATION_INSIGHTS_CONNECTION']))

# Initialize Langchain model
llm = ChatOpenAI()

# Connect to Azure Cognitive Search
embeddings = OpenAIEmbeddings(deployment="text-embedding-ada-002", chunk_size=1)
vector_store = AzureSearch(
    azure_search_endpoint=os.environ['AZURE_COGNITIVE_SEARCH_ENDPOINT'],
    azure_search_key=os.environ['AZURE_COGNITIVE_SEARCH_ADMIN_KEY'],
    index_name=os.environ['AZURE_COGNITIVE_SEARCH_INDEX_NAME'],
    embedding_function=embeddings.embed_query,
)

# Initialize memory and retriever
memory = ConversationSummaryMemory(llm=llm, memory_key="chat_history", return_messages=True)
retriever = vector_store.as_retriever()
qa = ConversationalRetrievalChain.from_llm(llm, retriever=retriever, memory=memory, verbose=True)

# Flask routes
@auth.verify_password
def verify_password(username, password):
    if username in users and \
            check_password_hash(users.get(username), password):
        return username

@app.route('/')
@auth.login_required
def index():
    return render_template('index.html')

@app.route('/ask', methods=['POST'])
def ask():
    question = request.json['question']
    logger.info("[user] %s" % question)
    answer = qa(question)['answer']
    logger.info("[bot] %s" % answer)
    return jsonify({"answer": answer})

# Run application
if __name__ == '__main__':
    app.run()

次に、App Serviceにデプロイした時に必要なPythonモジュールをインストールしてもらうために、アプリケーションルートにrequirements.txtを作成して、追加インストールが必要なモジュールを指定します。

requirements.txt
flask
openai
langchain
tiktoken
azure-search-documents == 11.4.0b8
azure-identity
opencensus-ext-azure
Flask-HTTPAuth

今回は上記で作成したrequirements.txtと、ウェブアプリケーション部分のPythonファイル(app.py)と、HTMLファイル(templates/index.html)だけがあれば、以下のファイル一式についてzipファイルに固めます(app.zipという名前にしました)。

/your_directory
|-- app.py
|-- requirements.txt
|-- templates/
|   |-- index.html

Azure CLIを使用して、zipファイル化したアプリケーションをデプロイします。

zipファイルを保存したディレクトリに移動します。az loginを実行し、Azureにサインインしてください。ブラウザ画面でログインを行います。続いて、az webapp deploy --name langchaintest-app --resource-group LangChain --src-path ./app.zip --type zipで、ローカルのカレントディレクトリにあるapp.zipをデプロイします。Web Appの名前が「langchaintest-app」、リソースグループ名が「LangChain」から変わっている場合など、適宜環境に合わせて変更してください(参考:「az webapp deployリファレンス」)

$ az login
A web browser has been opened at https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
(ブラウザでログインを行います。成功するとサブスクリプション情報がJSONで出力されます)

$ az webapp deploy --name langchaintest-app --resource-group LangChain --src-path ./app.zip --type zip
This command is in preview and under development. Reference and support levels: https://aka.ms/CLI_refstatus
(しばらく待つとデプロイ結果がJSONで出力されます)

デプロイ完了後に、Web AppのURL(https://{your-webapp-name}.azurewebsites.net/)にアクセスしてみます。ベーシック認証が求められ、ユーザー名にdemo、パスワードにA9b8-c7D6-E2f1-G403と入力したところ、チャットボットの画面が表示できることが確認できました。

続いて、実際にいくつか質問してみます。回答が返ってくることを確認できました。

最後にログが保存されているか確認しましょう。Azure PortalのAppliation Insightsのリソース画面に移動し、監視セクション配下のログ画面を開きます。クエリモーダルは閉じて、クエリとして「traces」を入力し、実行ボタンを押します。先程の動作検証時に実行した内容のログが出力されていることを確認できました。

以上のように、LangChainチャットボットを作成し、Azureで簡易的なチャットボットアプリとして動かすことができました。

最後に不要になったリソースを削除します。すべて同じリソースグループで作っている場合は、リソースグループごと削除すると簡単に削除ができます。

参考サイト

Discussion