はじめに
Bedrockを試してみたく、超簡単なWebアプリケーションを作成しました。
アーキ図は以下の画像のように構成しました。フロントエンドのWebアプリケーションでpdfファイルをアップロードすると、簡単な要約を返してくれるアプリケーションです。
構築の流れ
下記のパートに分けて構築手順を説明します。
- Bedrockの設定
- API Gateway + Lambdaの構築
- Webアプリケーションの作成(by Amazon Q Developer)
Bedrockの設定
Bedrockでは複数の基盤モデルが提供されています。今回はpdf処理や文章要約に強い、Anthropic社が提供する「Claude 3.5 Sonnet」を使用します。
Bedrockの有効化
Bedrockは、デフォルトで選択できる基盤モデルは、現時点でAmazon社が提供する「Titan Text G1 - Express」のみです。その他の基盤モデルを選択したい場合には、「アクセスのリクエスト」を行う必要があります。なお、申請自体には料金は発生しませんが、実際にモデルの利用を開始すると従量課金が発生します。
AWSコンソールにログインし、検索ボックスから「Bedrock」を開いてください。左のツールバーから「モデルアクセス」を選択し、次に「モデルアクセスを変更」を選択します。
今回のリクエスト対象である「Claude 3.5 Sonnet」を選択し、「次へ」を押下します。
確認画面で内容に問題がなければ「送信」を押下します。AWS側からまもなく有効化の完了通知が来るかと思います。
※Anthropicの利用が初回の場合には、以下のようにユースケースを入力される画面が表示されます。ここで内容に日本語や「コンマ(,)」をいれると、「Invalid from data」エラーが発生するのでご注意ください。( このエラーで大きく躓き、AWSサポートに問い合わせをすることになりました )
API Gateway + Lambdaの構築
Lambdaの構築
Lambda実行ロール作成
- 検索ボックスから「IAM」を選択し、ダッシュボードを表示
- 左側のバーで「ロール」を選択し、「ロールを作成」を押下
- ユースケースのサービスで「Lambda」を選択し、「次へ」を押下
- 許可ポリシーに「AWSLambdaBasicExecutionRole」を設定し、「次へ」を押下
- ロールの詳細の設定で、ロール名「pdf-summarizer-lambda-role」を設定し、画面をスクロールして、右下の「ロールを作成」を押下
カスタムポリシーの作成・Lambdaロールへのアタッチ
- 左側のバーで「ポリシー」を選択し、「ポリシーの作成」を押下
- JSONタブで以下のポリシーを入力し、「次へ」を押下
- ポリシー名を「BedrockInvokePolicy」とする
- 作成したポリシー「BedrockInvokePolicy」を先ほど作成したLambdaロール「pdf-summarizer-lambda-role」にアタッチ
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": [
"arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"
]
}
]
}
Lambdaレイヤーの作成
「PyPDF2」をLambdaレイヤーに準備します。PyPDF2はPDFからテキストを抽出するために一般的に使用されるPythonライブラリです。以下、ローカルPC上にPyPDF2ライブラリをインストールしてzip化したファイルを、AWSコンソール上にアップロードする方法をご紹介します。
① pythonフォルダを作成します。
mkdir python
② PyPDF2をインストールします。(Anaconda3環境で実行)
pip install typing_extensions PyPDF2 boto3 -t python/
③ pythonファイルをZip化します。ファイ名は任意です。ただし、以下のフォルダ構成であることに注意してください。
zip -r dependencies.zip python/
dependencies.zip (ファイル名は任意)
└── python/
├── _pyache_/
・・・
└── typing_extensions.py
Lambdaレイヤーの配置
- 検索ボックスで「Lambda」を入力し、ダッシュボードを表示
- 左側のバーで「レイヤー」を選択し、「レイヤーを作成」を押下
- レイヤー設定で下記の項目を入力し、「作成」を押下
・API名:pdf-summarizer-dependencies
・ファイルを選択:先ほど作成した「dependencies.zip」
・互換性のあるランタイム:Python 3.9
Lambda関数の作成
- 検索ボックスで「Lambda」を入力し、ダッシュボードを表示
- 左側のバーで「関数」を選択し、「関数を作成」を押下
- 関数の作成で下記の項目を入力し、「関数の作成」を押下
・名前:pdf-summarizer
・ランタイム:Python 3.9
・デフォルト実行ロールの変更:「既存のロールを使用する」を選択し、Lambdaの実行ロール「pdf-summarizer-lambda-role」を選択
Lambdaのコード記述
念のため、タイムアウト値を伸ばしておく
-
「設定」タブから「一般設定」を選択し、「編集」を押下
-
タイムアウトをデフォルトの3秒から「5分」に変更しておく(PDF処理やBedrockへのリクエストに時間がかかる可能性があるため)
-
Lambdaのコードを「lambda_fuction.py」に貼り付けて、「Deploy」を押下
※ Lambda関数はAmazon Q Developerに生成してもらいました。(後のWebアプリケーション作成のところでAmaon Q Developerについて触れます。)
import json
import base64
import boto3
from typing import Dict, Any
import io
from PyPDF2 import PdfReader
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
try:
# Bedrock クライアント作成(東京リージョン)
bedrock = boto3.client('bedrock-runtime', region_name='ap-northeast-1')
# リクエストボディの解析
if isinstance(event.get('body'), str):
try:
body_data = json.loads(event['body'])
except:
body_data = {'body': event['body']}
else:
body_data = event.get('body', {})
# PDF Base64データを取得
pdf_base64 = body_data.get('body', '')
if not pdf_base64:
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': 'PDFデータが見つかりません'}, ensure_ascii=False)
}
# Base64デコード
try:
pdf_data = base64.b64decode(pdf_base64)
except Exception as e:
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': f'Base64デコードエラー: {str(e)}'}, ensure_ascii=False)
}
# PDFからテキスト抽出
try:
pdf_file = io.BytesIO(pdf_data)
reader = PdfReader(pdf_file)
text = ""
for page in reader.pages:
text += page.extract_text()
if not text.strip():
return {
'statusCode': 400,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': 'PDFからテキストを抽出できませんでした'}, ensure_ascii=False)
}
except Exception as e:
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': f'PDF解析エラー: {str(e)}'}, ensure_ascii=False)
}
# Bedrock で要約生成
try:
# 正しいモデルIDを使用
model_id = "anthropic.claude-3-5-sonnet-20240620-v1:0"
prompt = f"以下のPDFから抽出したテキストを日本語で簡潔に要約してください:\n\n{text[:4000]}"
response = bedrock.invoke_model(
modelId=model_id,
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"messages": [
{"role": "user", "content": prompt}
]
})
)
# レスポンス処理
response_body = json.loads(response['body'].read())
summary = response_body['content'][0]['text']
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'POST,OPTIONS'
},
'body': json.dumps({
'summary': summary,
'original_length': len(text),
'summary_length': len(summary),
'region': 'ap-northeast-1'
}, ensure_ascii=False)
}
except Exception as e:
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': f'Bedrock呼び出しエラー: {str(e)}'}, ensure_ascii=False)
}
except Exception as e:
return {
'statusCode': 500,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({'error': f'予期しないエラー: {str(e)}'}, ensure_ascii=False)
}
API Gatewayの構築
REST APIの作成
-
検索ボックスで「API Gateway」を入力し、ダッシュボードを表示させてください
-
APIの詳細で下記の項目をすべて設定し、「APIを作成」を押下してください
・API名:pdf-summarizer-api
・説明(任意)
・APIのエンドポイントタイプ:リージョン(デフォルト)
リソースの作成
-
左側のバーで「リソース」を選択し、「/」(ルート)が選択されていることを確認してから「リソースを作成」を押下してください
-
リソースの詳細で下記の項目をすべて設定し、「リソースを作成」を押下してください
・リソース名:summarize
・CROS(クロスオリジンリソース共有):チェックON
※リソースパスは「summarize」が自動で入力されます。
メソッドの作成
-
メソッドの詳細で下記の項目をすべて設定し、「メソッドを作成」を押下してください
・メソッドタイプ:POST
・統合タイプ:Lambda関数
・Lambdaプロキシ統合:チェックON
・Lambda関数:上で作成した「pdf-summarizer」を選択
・統合のタイムアウト:29000(デフォルト)
※権限追加の確認ダイアログが表示されたら「OK」を押下してください
CROSの有効化
-
CROSの設定で下記の項目をすべて設定し、「保存」を押下してください
・Access-Control-Allow-Methods:「OPTIONS」と「POST」の両方をチェックON
・Access-Control-Allow-Headers:Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token(デフォルト)
・Access-Control-Allow-Origin:*(デフォルト)
APIのデプロイ
エンドポイントURLの取得
- 左側のバーで「ステージ」を選択してください
- ステージツリーから「dev/summarize」の配下にある「POST」を選択し、「URLを呼び出す」のリンクをコピーを控えてください
※URL形式は「https:// リソースID .execute-api.us-east-1.amazonaws.com/dev/summarize」となるはずです。
Webアプリケーションの作成(by Amazon Q Developer)
VscodeにAmazon Q developerを導入し、pdfをアップロードすると簡単な要約を返すアプリケーションを作成しました。
Amazon Q developerの導入や使用方法は以下の記事をご参考ください。
https://qiita.com/HarukiHayashi/items/33a14124d97cec8b32cc
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF要約システム</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.upload-area { border: 2px dashed #ccc; padding: 40px; text-align: center; margin: 20px 0; }
.upload-area.dragover { border-color: #007bff; background-color: #f8f9fa; }
.summary-result { margin-top: 20px; padding: 20px; background-color: #f8f9fa; border-radius: 5px; }
.loading { display: none; text-align: center; margin: 20px 0; }
button { background-color: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; }
button:disabled { background-color: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<h1>PDF要約システム</h1>
<div class="upload-area" id="uploadArea">
<p>PDFファイルをドラッグ&ドロップするか、クリックして選択してください</p>
<input type="file" id="fileInput" accept=".pdf" style="display: none;">
<button onclick="document.getElementById('fileInput').click()">ファイルを選択</button>
</div>
<div class="loading" id="loading">
<p>要約を生成中...</p>
</div>
<div class="summary-result" id="summaryResult" style="display: none;">
<h3>要約結果</h3>
<div id="summaryContent"></div>
<div id="summaryStats" style="margin-top: 10px; font-size: 0.9em; color: #666;"></div>
</div>
<script>
const API_ENDPOINT = 'YOUR_API_GATEWAY_ENDPOINT_HERE';
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const loading = document.getElementById('loading');
const summaryResult = document.getElementById('summaryResult');
const summaryContent = document.getElementById('summaryContent');
const summaryStats = document.getElementById('summaryStats');
// ドラッグ&ドロップ機能
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type === 'application/pdf') {
processPDF(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
processPDF(e.target.files[0]);
}
});
async function processPDF(file) {
loading.style.display = 'block';
summaryResult.style.display = 'none';
try {
// ファイルをBase64に変換
const base64 = await fileToBase64(file);
// API呼び出し - JSON形式で送信
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: base64,
filename: file.name,
size: file.size
})
});
const result = await response.json();
if (response.ok) {
summaryContent.innerHTML = result.summary.replace(/\n/g, '<br>');
summaryStats.innerHTML = `ファイル名: ${file.name} | サイズ: ${(file.size/1024).toFixed(1)}KB`;
summaryResult.style.display = 'block';
} else {
alert('エラーが発生しました: ' + result.error);
}
} catch (error) {
alert('エラーが発生しました: ' + error.message);
} finally {
loading.style.display = 'none';
}
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = error => reject(error);
});
}
</script>
</body>
</html>
上記コードの「API_ENDPOINT」変数については、先ほど取得したエンドポイントURLに置き換えることをお忘なくお願いします。
# before
const API_ENDPOINT = 'YOUR_API_GATEWAY_ENDPOINT_HERE';
# after
const API_ENDPOINT = 'https://リソースID.execute-api.us-east-1.amazonaws.com/dev/summarize';
疎通確認
ローカル環境で「index.html」を実行し、ブラウザに表示しました。
今回現在のこの記事をpdf化したものをインプットとして、要約してもらいました。
※画像が含まれているとエラーになるのでご注意ください。また、API Gatewayに大容量のファイルをアップロードできません。
要約が返ってきました🎉
プロンプト設計を工夫すると、もう少し要約の精度が増すかもしれませんね。
注意事項
本ブログに掲載している内容は、私個人の見解であり、
所属する組織の立場や戦略、意見を代表するものではありません。
あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。