6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BedrockとAmazon Q Developerで簡単要約アプリを作ってみた

Posted at

はじめに

Bedrockを試してみたく、超簡単なWebアプリケーションを作成しました。
アーキ図は以下の画像のように構成しました。フロントエンドのWebアプリケーションでpdfファイルをアップロードすると、簡単な要約を返してくれるアプリケーションです。
image.png

構築の流れ

下記のパートに分けて構築手順を説明します。

  • 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」を開いてください。左のツールバーから「モデルアクセス」を選択し、次に「モデルアクセスを変更」を選択します。

image.png

今回のリクエスト対象である「Claude 3.5 Sonnet」を選択し、「次へ」を押下します。

image.png

確認画面で内容に問題がなければ「送信」を押下します。AWS側からまもなく有効化の完了通知が来るかと思います。

image.png

※Anthropicの利用が初回の場合には、以下のようにユースケースを入力される画面が表示されます。ここで内容に日本語や「コンマ(,)」をいれると、「Invalid from data」エラーが発生するのでご注意ください。( このエラーで大きく躓き、AWSサポートに問い合わせをすることになりました :sweat:

image.png

API Gateway + Lambdaの構築

Lambdaの構築

Lambda実行ロール作成

  1. 検索ボックスから「IAM」を選択し、ダッシュボードを表示
  2. 左側のバーで「ロール」を選択し、「ロールを作成」を押下
  3. ユースケースのサービスで「Lambda」を選択し、「次へ」を押下
    image.png
  4. 許可ポリシーに「AWSLambdaBasicExecutionRole」を設定し、「次へ」を押下
    image.png
  5. ロールの詳細の設定で、ロール名「pdf-summarizer-lambda-role」を設定し、画面をスクロールして、右下の「ロールを作成」を押下
    image.png

カスタムポリシーの作成・Lambdaロールへのアタッチ

  1. 左側のバーで「ポリシー」を選択し、「ポリシーの作成」を押下
  2. JSONタブで以下のポリシーを入力し、「次へ」を押下
  3. ポリシー名を「BedrockInvokePolicy」とする
  4. 作成したポリシー「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レイヤーの配置

  1. 検索ボックスで「Lambda」を入力し、ダッシュボードを表示
  2. 左側のバーで「レイヤー」を選択し、「レイヤーを作成」を押下
  3. レイヤー設定で下記の項目を入力し、「作成」を押下
     ・API名:pdf-summarizer-dependencies
     ・ファイルを選択:先ほど作成した「dependencies.zip」
     ・互換性のあるランタイム:Python 3.9

image.png

Lambda関数の作成

  1. 検索ボックスで「Lambda」を入力し、ダッシュボードを表示
  2. 左側のバーで「関数」を選択し、「関数を作成」を押下
  3. 関数の作成で下記の項目を入力し、「関数の作成」を押下
     ・名前:pdf-summarizer
     ・ランタイム:Python 3.9
     ・デフォルト実行ロールの変更:「既存のロールを使用する」を選択し、Lambdaの実行ロール「pdf-summarizer-lambda-role」を選択

image.png

Lambdaのコード記述

念のため、タイムアウト値を伸ばしておく

  1. 「設定」タブから「一般設定」を選択し、「編集」を押下

  2. タイムアウトをデフォルトの3秒から「5分」に変更しておく(PDF処理やBedrockへのリクエストに時間がかかる可能性があるため)
    image.png

  3. Lambdaのコードを「lambda_fuction.py」に貼り付けて、「Deploy」を押下
    ※ Lambda関数はAmazon Q Developerに生成してもらいました。(後のWebアプリケーション作成のところでAmaon Q Developerについて触れます。)

lambda.function.py
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の作成

  1. 検索ボックスで「API Gateway」を入力し、ダッシュボードを表示させてください

  2. 画面右上の「APIの作成」を押下してください
    image.png

  3. REST APIの「構築」を押下してください
    image.png

  4. APIの詳細で下記の項目をすべて設定し、「APIを作成」を押下してください
     ・API名:pdf-summarizer-api
     ・説明(任意)
     ・APIのエンドポイントタイプ:リージョン(デフォルト)
    api01_2.PNG

リソースの作成

  1. 左側のバーで「リソース」を選択し、「/」(ルート)が選択されていることを確認してから「リソースを作成」を押下してください
    image.png

  2. リソースの詳細で下記の項目をすべて設定し、「リソースを作成」を押下してください
     ・リソース名:summarize
     ・CROS(クロスオリジンリソース共有):チェックON
    image.png
    ※リソースパスは「summarize」が自動で入力されます。

メソッドの作成

  1. リソースツリーから「/summarize」を選択し、「メソッドを作成」を押下してください
    image.png

  2. メソッドの詳細で下記の項目をすべて設定し、「メソッドを作成」を押下してください
     ・メソッドタイプ:POST
     ・統合タイプ:Lambda関数
     ・Lambdaプロキシ統合:チェックON
     ・Lambda関数:上で作成した「pdf-summarizer」を選択
     ・統合のタイムアウト:29000(デフォルト)
    image.png
    ※権限追加の確認ダイアログが表示されたら「OK」を押下してください

CROSの有効化

  1. リソースツリーから「/summarize」を選択し、「CROSを有効にする」を押下してください
    image.png

  2. 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:*(デフォルト)
    image.png

APIのデプロイ

  1. リソースツリーから「POST」を選択し、「APIをデプロイ」を押下してください
    image.png

  2. Deploy APIの画面で下記の項目をすべて設定し、「デプロイ」を押下してください
     ・ステージ:新しいステージ
     ・ステージ名:dev
     ・デプロイメントの説明:初回デプロイ(任意)
    image.png

エンドポイントURLの取得

  1. 左側のバーで「ステージ」を選択してください
  2. ステージツリーから「dev/summarize」の配下にある「POST」を選択し、「URLを呼び出す」のリンクをコピーを控えてください
    image.png
    ※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

index.html
<!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に大容量のファイルをアップロードできません。
image.png

要約が返ってきました🎉
プロンプト設計を工夫すると、もう少し要約の精度が増すかもしれませんね。
image.png

注意事項
本ブログに掲載している内容は、私個人の見解であり、
所属する組織の立場や戦略、意見を代表するものではありません。​
あくまでエンジニアとしての経験や考えを発信していますので、ご了承ください。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?