📖
Bedrock Agent でAWSドキュメントを検索する
はじめに
阿河です。
AWSの公式ドキュメントは膨大で、必要な情報を素早く見つけるのは意外と大変ですよね。特に新しいサービスや機能についての最新情報を探すとき、「このドキュメントのどこに書いてあるんだろう?」と迷った経験は誰にでもあると思います。
そこで今回は、Amazon Bedrock Agentを活用して、自然言語でAWSドキュメントを検索できるAIエージェントを構築してみました。
目次
- 前提
- Bedrock Agentの構築
- 検証
1. 前提
- 事前にAmazon Bedrockでモデルアクセスを有効化しておくこと
- 本記事では、Claude 3.7 Sonnetを利用します。
2. Bedrock Agentの構築
Bedrock Agentの設定
-
名前: aws-docs-search
-
モデル: Claude 3.7 Sonnet
-
エージェント指示文例
あなたはAWSドキュメント専門のAIアシスタントです。
AWSサービスについて質問されたら、以下の手順で回答してください:
1. まず search_aws_docs 機能を使って関連ドキュメントを検索
2. 取得した情報を基に、わかりやすく日本語での回答を生成
回答時は以下を心がけてください:
- 最新の公式情報を重視する
- 具体的な手順や設定方法を含める
- 参考ドキュメントURLを記載する
重要: 同じ検索を繰り返さないでください。1回の検索で十分な情報を提供してください。
Action Groupの設定
- Action Group type: Define with function details
- Quick create a new Lambda function
- Action group function 1
- Name: aws_docs_search
- Description: AWS公式ドキュメントの検索と読み込み
- Parameters
- action
- Description: 実行するアクション (search/read)
- Type: string
- Required: True
- query
- Description: 検索キーワード (actionがsearchの場合)
- Type: string
- Required: False
- url
- Description: 読み込むAWSドキュメントのURL (actionがreadの場合)
- Type: string
- Required: False
- action
結果的にLambda関数が作成されます。
Lambdaの実装
続いてLambda関数を作成します。
この関数が、AWS公式ドキュメントの検索と取得を担当します。
import logging
import json
import urllib.request
import urllib.parse
import re
from typing import Dict, Any, List, Optional
from html import unescape
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event: Dict[str, Any], context: Any) -> Dict[str, Any]:
try:
logger.info(f"Received event: {json.dumps(event)}")
action_group = event['actionGroup']
function = event['function']
message_version = event.get('messageVersion', 1)
parameters = event.get('parameters', [])
param_dict = {}
for param in parameters:
param_dict[param.get('name')] = param.get('value')
logger.info(f"Parsed parameters: {param_dict}")
if function == 'aws_docs_search':
result = execute_aws_docs_search(param_dict)
else:
result = f'Unknown function: {function}'
response_body = {'TEXT': {'body': result}}
action_response = {
'actionGroup': action_group,
'function': function,
'functionResponse': {'responseBody': response_body}
}
return {'response': action_response, 'messageVersion': message_version}
except Exception as e:
logger.error(f'Error in lambda_handler: {str(e)}')
return create_error_response(
event.get('actionGroup', ''),
event.get('function', ''),
event.get('messageVersion', 1),
str(e)
)
def execute_aws_docs_search(params: Dict[str, Any]) -> str:
try:
action = params.get('action', '').lower()
logger.info(f"Executing MCP action: {action}")
if action == 'search':
query = params.get('query', '')
logger.info(f"MCP search for: {query}")
return mcp_search_aws_docs(query)
elif action == 'read':
url = params.get('url', '')
logger.info(f"MCP read document: {url}")
return mcp_read_aws_doc(url)
else:
return f"サポートされていないアクション: {action}. 'search' または 'read' を指定してください。"
except Exception as e:
logger.error(f'Error in execute_aws_docs_search: {str(e)}')
return f"MCP処理中にエラーが発生しました: {str(e)}"
def mcp_search_aws_docs(query: str) -> str:
if not query.strip():
return "検索クエリが指定されていません。"
try:
logger.info(f"Starting MCP search for: {query}")
search_results = call_aws_search_api(query)
if not search_results:
return create_fallback_search_result(query)
result = f"【{query}の検索結果】\n\n"
for i, doc in enumerate(search_results, 1):
result += f"{i}. {doc['title']}\n"
result += f"概要: {doc['snippet']}\n"
result += f"URL: {doc['url']}\n"
result += f"更新日: {doc.get('lastModified', '不明')}\n\n"
result += f"検索完了: {len(search_results)}件のドキュメントが見つかりました。\n"
result += "詳細が必要な場合は、action='read' でURLを指定してください。"
logger.info(f"MCP search completed. Found {len(search_results)} results.")
return result
except Exception as e:
logger.error(f'Error in mcp_search_aws_docs: {str(e)}')
# エラー時のフォールバック
return create_fallback_search_result(query)
def call_aws_search_api(query: str) -> List[Dict[str, Any]]:
try:
base_url = "https://docs.aws.amazon.com/search/doc-search.html"
params = {
'searchPath': 'documentation',
'searchQuery': query,
'facet_doc_product': 'documentation',
'this_doc_product': 'documentation',
'doc-locale': 'en_us'
}
query_string = urllib.parse.urlencode(params)
search_url = f"{base_url}?{query_string}"
logger.info(f"Calling AWS search API: {search_url}")
req = urllib.request.Request(
search_url,
headers={
'User-Agent': 'Mozilla/5.0 (compatible; AWS-Docs-Agent/1.0)',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
)
with urllib.request.urlopen(req, timeout=15) as response:
if response.status == 200:
html_content = response.read()
# gzip解凍が必要な場合
if response.info().get('Content-Encoding') == 'gzip':
import gzip
html_content = gzip.decompress(html_content)
content_str = html_content.decode('utf-8')
logger.info("AWS search API response received")
return parse_aws_search_html(content_str)
else:
logger.warning(f"AWS search API returned status: {response.status}")
return []
except Exception as e:
logger.error(f"Error calling AWS search API: {str(e)}")
return []
def parse_aws_search_html(html_content: str) -> List[Dict[str, Any]]:
try:
results = []
result_pattern = r'<div[^>]*class="[^"]*search-result[^"]*"[^>]*>(.*?)</div>'
title_pattern = r'<h[1-6][^>]*>\s*<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>\s*</h[1-6]>'
snippet_pattern = r'<p[^>]*class="[^"]*search-result-summary[^"]*"[^>]*>(.*?)</p>'
result_blocks = re.findall(result_pattern, html_content, re.DOTALL | re.IGNORECASE)
for block in result_blocks[:5]:
title_match = re.search(title_pattern, block, re.DOTALL | re.IGNORECASE)
if title_match:
url = title_match.group(1)
title = clean_html_text(title_match.group(2))
if url.startswith('/'):
url = f"https://docs.aws.amazon.com{url}"
snippet_match = re.search(snippet_pattern, block, re.DOTALL | re.IGNORECASE)
snippet = clean_html_text(snippet_match.group(1)) if snippet_match else ""
results.append({
'title': title,
'url': url,
'snippet': snippet[:200] + "..." if len(snippet) > 200 else snippet,
'lastModified': extract_last_modified(block)
})
logger.info(f"Parsed {len(results)} search results from HTML")
return results
except Exception as e:
logger.error(f"Error parsing AWS search HTML: {str(e)}")
return []
def clean_html_text(html_text: str) -> str:
text = re.sub(r'<[^>]+>', '', html_text)
text = unescape(text)
text = re.sub(r'\s+', ' ', text).strip()
return text
def extract_last_modified(html_block: str) -> str:
try:
date_pattern = r'(\d{4}-\d{2}-\d{2}|\d{2}/\d{2}/\d{4})'
date_match = re.search(date_pattern, html_block)
return date_match.group(1) if date_match else "不明"
except:
return "不明"
def create_fallback_search_result(query: str) -> str:
encoded_query = urllib.parse.quote(query)
search_url = f"https://docs.aws.amazon.com/search/doc-search.html?searchPath=documentation&searchQuery={encoded_query}"
common_services = {
's3': ('Amazon S3', 'https://docs.aws.amazon.com/s3/'),
'lambda': ('AWS Lambda', 'https://docs.aws.amazon.com/lambda/'),
'ec2': ('Amazon EC2', 'https://docs.aws.amazon.com/ec2/'),
'rds': ('Amazon RDS', 'https://docs.aws.amazon.com/rds/'),
'iam': ('AWS IAM', 'https://docs.aws.amazon.com/iam/'),
'bedrock': ('Amazon Bedrock', 'https://docs.aws.amazon.com/bedrock/'),
'dynamodb': ('Amazon DynamoDB', 'https://docs.aws.amazon.com/dynamodb/'),
'cloudformation': ('AWS CloudFormation', 'https://docs.aws.amazon.com/cloudformation/')
}
query_lower = query.lower()
matched_services = []
for key, (service_name, base_url) in common_services.items():
if key in query_lower or service_name.lower() in query_lower:
matched_services.append({
'title': f'{service_name} ユーザーガイド',
'url': f'{base_url}latest/userguide/',
'snippet': f'{service_name}の包括的なユーザーガイドです。'
})
if matched_services:
result = f"【{query}の検索結果】(フォールバック結果)\n\n"
for i, doc in enumerate(matched_services[:3], 1):
result += f"{i}. {doc['title']}\n"
result += f"概要: {doc['snippet']}\n"
result += f"URL: {doc['url']}\n\n"
result += f"より詳細な検索は: {search_url}"
return result
else:
return f"""【{query}の検索結果】
AWS公式ドキュメントで'{query}'を検索しています。
直接検索: {search_url}
一般的なAWSドキュメント:
1. AWS全般ドキュメント
URL: https://docs.aws.amazon.com/
2. AWSサービス一覧
URL: https://aws.amazon.com/products/
より具体的なAWSサービス名での検索をお試しください。"""
def mcp_read_aws_doc(url: str) -> str:
if not url.strip():
return "ドキュメントURLが指定されていません。"
try:
if not (url.startswith('https://docs.aws.amazon.com') or
url.startswith('https://aws.amazon.com')):
return "セキュリティ上の理由により、AWS公式ドメインのURLのみサポートしています。"
logger.info(f"Reading AWS document: {url}")
req = urllib.request.Request(
url,
headers={
'User-Agent': 'Mozilla/5.0 (compatible; AWS-Docs-Agent/1.0)',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5'
}
)
with urllib.request.urlopen(req, timeout=20) as response:
if response.status == 200:
html_content = response.read()
if response.info().get('Content-Encoding') == 'gzip':
import gzip
html_content = gzip.decompress(html_content)
content_str = html_content.decode('utf-8')
logger.info("AWS document content retrieved")
return extract_document_content(content_str, url)
else:
return f"ドキュメント取得エラー: HTTP {response.status}"
except Exception as e:
logger.error(f'Error in mcp_read_aws_doc: {str(e)}')
return f"ドキュメント読み込みエラー: {str(e)}"
def extract_document_content(html_content: str, url: str) -> str:
try:
content_patterns = [
r'<div[^>]*class="[^"]*awsui-util-container[^"]*"[^>]*>(.*?)</div>',
r'<main[^>]*>(.*?)</main>',
r'<article[^>]*>(.*?)</article>',
r'<div[^>]*id="[^"]*main[^"]*"[^>]*>(.*?)</div>'
]
main_content = ""
for pattern in content_patterns:
matches = re.findall(pattern, html_content, re.DOTALL | re.IGNORECASE)
if matches:
main_content = matches[0]
break
if not main_content:
title_match = re.search(r'<title[^>]*>(.*?)</title>', html_content, re.IGNORECASE)
desc_match = re.search(r'<meta[^>]*name="description"[^>]*content="([^"]*)"', html_content, re.IGNORECASE)
title = clean_html_text(title_match.group(1)) if title_match else "AWS ドキュメント"
description = clean_html_text(desc_match.group(1)) if desc_match else "詳細情報については直接URLを参照してください。"
return f"""【{title}】
URL: {url}
概要:
{description}
このドキュメントの詳細内容を表示するには、直接URLにアクセスしてください。"""
text_content = clean_html_text(main_content)
lines = text_content.split('\n')
cleaned_lines = [line.strip() for line in lines if line.strip()]
content_lines = []
char_count = 0
for line in cleaned_lines[:50]:
if char_count + len(line) > 3000:
break
content_lines.append(line)
char_count += len(line)
final_content = '\n'.join(content_lines)
service_name = extract_service_name_from_url(url)
result = f"【{service_name} ドキュメント詳細】\n\n"
result += f"URL: {url}\n\n"
result += f"内容:\n{final_content}\n\n"
if char_count >= 3000:
result += "※ 内容が長いため一部のみ表示しています。完全な内容は上記URLを参照してください。"
logger.info(f"Extracted {len(final_content)} characters from document")
return result
except Exception as e:
logger.error(f"Error extracting document content: {str(e)}")
return f"ドキュメント内容の抽出に失敗しました: {str(e)}"
def extract_service_name_from_url(url: str) -> str:
try:
parts = url.split('/')
if 'docs.aws.amazon.com' in url and len(parts) >= 4:
service_key = parts[3]
service_mapping = {
's3': 'Amazon S3',
'lambda': 'AWS Lambda',
'ec2': 'Amazon EC2',
'rds': 'Amazon RDS',
'iam': 'AWS IAM',
'bedrock': 'Amazon Bedrock',
'dynamodb': 'Amazon DynamoDB',
'cloudformation': 'AWS CloudFormation',
'vpc': 'Amazon VPC',
'cloudwatch': 'Amazon CloudWatch',
'apigateway': 'Amazon API Gateway'
}
return service_mapping.get(service_key, service_key.upper() + ' Service')
return 'AWS Service'
except:
return 'AWS Service'
def create_error_response(action_group: str, function: str, message_version: int, error_message: str) -> Dict[str, Any]:
return {
'response': {
'actionGroup': action_group,
'function': function,
'functionResponse': {
'responseBody': {
'TEXT': {
'body': f'MCP Serverエラー: {error_message}'
}
}
}
},
'messageVersion': message_version
}
3. 検証
今回はシンプルに、Bedrockコンソールのテスト画面から動作テストを行います。

ドキュメントURL付きで回答が帰ってきました。
精度面でまだまだ課題はありますが、必要な情報が要約されて返ってきています。
おわりに
AIとクラウドサービスの組み合わせにより、従来は困難だった情報検索が手軽に実現できる時代になりました。
今回の実装が、皆さんの業務効率化やシステム開発の参考になれば幸いです。
Discussion