FlaskでGPTモデルにデータ検索・回答させるWebアプリを作ってみた
生成AIを利用したサービスを構築するのにFlaskを利用するのが一番簡単だと思ったので、実際に作って動作確認してみました。詳細に説明していきます。
完成したWebアプリの使用例がこちら。

ユーザの質問に対して、アプリはデータを検索して回答してくれます。
検索できるデータは各種テキスト、ワード・エクセル・パワーポイントといったOfficeファイルに加えて、PDFファイルも文字抽出して検索してくれます。
アプリの動作フローがこちら。

1回の動作でGPTモデルを2回(検索クエリの生成、回答文書の作成)使用します。
Flaskの説明に移ります。ファイルの階層がこちら。
flaskr.zip
├ flaskr
│ ├ __init__.py
│ ├ main.py
│ ├ wsgi.py
│ └ templates
│ └ index.html
└ requirements.txtWeb画面の部分をindex.htmlに記載し、後方の処理はmain.pyに記載しています。
index.htmlは入力ボックスと送信ボタンを配置し、ユーザメッセージと回答メッセージを表示するシンプルな作りとしています。
main.pyは動作フローで書いた通り、以下3つのステップを順に進めていく流れとなります。
Step 1: 検索クエリの生成
Step 2: Azure AI Searchを使用して検索
Step 3: 最終回答の生成
__init__.pyはアプリの初期処理を行うコードで、wsgi.pyはFlaskアプリをインポートするコードです。
requirements.txtにはインストールするソフトとバージョンを記載しています。
このflaskr.zipをデプロイすればWebアプリとして動作させることができます。
次回はterraformを用いてこのflaskr.zipをAzureにデプロイし、アプリを起動させる方法について説明します。
最後に今回ご紹介したファイルの内容を記載しておきます。お読み頂きありがとうございました。
次回記事はこちら
__init__.py
from flask import Flask
# Flaskアプリケーションの初期化
app = Flask(__name__)
# mainモジュールのインポート
import flaskr.mainmain.py
from flaskr import app
from flask import request, jsonify, render_template
import os
import openai
import requests
import logging
import sys
import time
# 環境変数の読み込み
SEARCH_ENDPOINT = os.getenv('SEARCH_ENDPOINT')
SEARCH_API_KEY = os.getenv('SEARCH_API_KEY')
OPENAI_API_BASE = os.getenv('OPENAI_API_BASE')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
# OpenAI APIの設定
openai.api_type = "azure"
openai.api_base = OPENAI_API_BASE
openai.api_key = OPENAI_API_KEY
openai.api_version = "2024-05-01-preview"
# ログの設定
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(levelname)s: %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
@app.route('/')
def index():
# フロントエンドのテンプレートを表示
return render_template('index.html')
@app.route("/chat", methods=["POST"])
def chat():
user_input = request.json.get("message")
if not user_input:
logging.warning("No message provided in request")
return jsonify({"error": "メッセージが必要です"}), 400
logging.info(f"User input: {user_input}")
# Step 1: 検索クエリの生成
try:
query_generation_response = openai.ChatCompletion.create(
engine="my-chat-model",
messages=[
{
"role": "system",
"content": (
"あなたは検索クエリ生成アシスタントです。"
"ユーザーの入力をもとに、単語を分割し、"
"以下の形式で検索クエリを生成してください。\n"
"検索クエリ: \"単語1\" または \"単語2\" または \"単語3\"\n"
"単語は5つまでとします。"
)
},
{
"role": "user",
"content": f"ユーザー入力: {user_input}。これをもとに検索クエリを生成してください。"
}
],
timeout=10,
)
search_query = query_generation_response.choices[0].message["content"].strip()
# クエリの単語数チェック(ダブルクオートで囲まれた単語単位で処理)
words = [word.strip('"') for word in search_query.split('または') if word.strip()]
if len(words) > 5:
logging.warning(f"Generated query has too many words, truncating: {search_query}")
search_query = ' または '.join([f'"{word}"' for word in words[:5]])
# クエリが空の場合のエラーハンドリング
if not search_query.strip():
logging.error("Generated query is empty after truncation")
return jsonify({"error": "生成されたクエリが無効です"}), 500
logging.info(f"Final search query: {search_query}")
except Exception as e:
logging.error(f"[STEP 1] Error in query generation: {str(e)}")
return jsonify({"error": "検索クエリの生成中にエラーが発生しました"}), 500
# Step 2: Azure AI Searchを使用して検索
try:
search_url = f"{SEARCH_ENDPOINT}/indexes/blob-index/docs/search?api-version=2021-04-30-Preview"
headers = {
"api-key": SEARCH_API_KEY,
}
search_payload = {
"search": search_query,
"top": 5
}
search_response = requests.post(search_url, headers=headers, json=search_payload)
search_response.raise_for_status()
search_results = search_response.json().get("value", [])
if not search_results:
logging.warning("No search results found")
retrieved_texts = "検索結果に関連する情報が見つかりませんでした。"
else:
# 文書の本文とURLを取得
retrieved_texts = "\n".join([
f"{result.get('content', '')} (URL: {result.get('url', 'URLがありません')})"
for result in search_results
])
# タイトルとリンクを組み込み(最大3件)
references_with_links = "\n".join([
f"- <a href='{result.get('url', '#')}' target='_blank'>{result.get('title', 'タイトルなし')}</a>"
for result in search_results[:3] # 最大3件に制限
])
logging.info(f"Retrieved search results: {retrieved_texts}")
except (requests.exceptions.RequestException, ValueError) as e:
logging.error(f"Error in Azure AI Search: {str(e)}")
return jsonify({"error": "検索中にエラーが発生しました"}), 500
# Step 3: 最終回答の生成
max_retries = 3
retry_delay = 10
for attempt in range(max_retries):
try:
# 検索結果をフォーマット(本文とURL)
formatted_results = "\n".join([
f"- {result.get('content', '')} (URL: {result.get('url', 'URLがありません')})"
for result in search_results
])
# チャットモデルへのリクエスト
final_response = openai.ChatCompletion.create(
engine="my-chat-model",
temperature=0.7,
timeout=10,
messages=[
{"role": "system", "content": "あなたは外部データソースを基に正確で詳細な回答を提供するアシスタントです。"},
{"role": "user", "content": user_input},
{"role": "assistant", "content": f"以下の情報が取得されました:\n{retrieved_texts}\nこれを基に、具体的な回答を日本語で提供してください。"},
],
)
# 最終回答を生成
final_answer = final_response.choices[0].message["content"].strip()
logging.info(f"Final GPT response: {final_answer}")
# ユーザーへの最終返答にURLを追加
response_with_references = f"{final_answer}\n\n参考リンク:\n{references_with_links}"
return jsonify({
"response": response_with_references,
"references": references_with_links
})
except openai.error.RateLimitError:
logging.warning(f"Rate limit hit. Retrying in {retry_delay} seconds... (Attempt {attempt + 1}/{max_retries})")
time.sleep(retry_delay)
except Exception as e:
logging.error(f"Error in generating final response: {str(e)}")
return jsonify({"error": "最終回答の生成中にエラーが発生しました"}), 500
logging.error("Exceeded maximum retry attempts for generating final response.")
return jsonify({"error": "レート制限を超えました。後でもう一度お試しください。"}), 429wsgi.py
from flaskr import appindex.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Chat App</title>
<style>
body {
font-family: 'Meiryo UI', sans-serif; /* Meiryo UIを指定 */
}
#chat-box {
max-height: 600px;
overflow-y: auto;
border: 1px solid #ccc;
padding: 10px;
}
.user-message {
text-align: right;
color: blue;
}
.bot-message {
text-align: left;
color: green;
}
/* 入力ボックスのスタイル */
#user-input {
width: calc(100% - 100px); /* ボタンを除いた幅を自動調整 */
font-size: 20px; /* 文字サイズ */
padding: 10px; /* 内側の余白 */
margin-top: 10px; /* 上部余白 */
box-sizing: border-box; /* ボックスの計算にパディングを含む */
}
/* 送信ボタンのスタイル */
button {
font-size: 20px; /* 文字サイズ */
padding: 10px 20px; /* ボタンの内側余白 */
margin-top: 10px; /* 上部余白 */
}
</style>
</head>
<body>
<div id="chat-box"></div>
<input type="text" id="user-input" placeholder="メッセージを入力してください">
<button onclick="sendMessage()">送信</button>
<script>
async function sendMessage() {
const message = document.getElementById('user-input').value;
if (message.trim() === '') return;
// ユーザーメッセージを表示
document.getElementById('chat-box').innerHTML += `<p class="user-message">${message}</p>`;
document.getElementById('user-input').value = '';
try {
// サーバーにメッセージを送信
const response = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
if (data.response) {
// 改行を<br>タグに変換
const formattedResponse = data.response.replace(/\n/g, '<br>');
document.getElementById('chat-box').innerHTML += `<p class="bot-message">${formattedResponse}</p>`;
} else if (data.error) {
document.getElementById('chat-box').innerHTML += `<p class="bot-message">Error: ${data.error}</p>`;
}
} catch (error) {
document.getElementById('chat-box').innerHTML += `<p class="bot-message">Error: ${error.message}</p>`;
}
// チャットボックスを最新メッセージにスクロール
document.getElementById('chat-box').scrollTop = document.getElementById('chat-box').scrollHeight;
}
</script>
</body>
</html>requirements.txt
Flask==2.0.3
Werkzeug==2.0.3
gunicorn==20.1.0
python-dotenv
openai==0.28.1