これの続きです。
今の状態だと、AIのコメントがDynamoDBに格納されない設計になっています。
また、誰がこの投稿をしたのかもわからない状態です。
これを解消したいと思います。
環境イメージ
新しいコンポーネントは作成しておりません。
DynamoDBに格納するデータの追加による処理の流れを変えています。
処理フロー
こんがらがってきたのでまとめます。
Step 1: 認証と入力 (Frontend)
ユーザーがアプリにログインします(Cognito)。
コメントを入力し、「分析する」ボタンを押すと、React は Cognito から認証トークン(ID Token)を取得し、ヘッダーにセットしてリクエストの準備をします。
Step 2: 感情分析リクエスト (React → Lambda 1)
React が POST/analyze を呼び出します。
API Gateway はトークンを検証し、「誰がアクセスしたか(UserId)」という情報を付与して Lambda (Comprehend側) を起動します。
Step 3: 分析と「枠」の作成 (Lambda 1)
ID取得: Lambda は API Gateway から渡された情報から UserId(Adminなど)を取り出します。
分析: Comprehend にテキストを送り、感情(例: NEGATIVE)を取得します。
時刻生成: 日本時間(JST)で現在時刻(例: 2026-02-01T15:00:00+09:00)を作ります。
保存 (Put): DynamoDB に以下のデータを新規保存します。
UserId: Admin
Timestamp: 2026-02-01T15:00:00+09:00
Text: ...
Sentiment: NEGATIVE
AiComment: (まだ無し)
Step 4: バケツリレー (Frontend)
Lambda1 から React へ、分析結果と一緒に Timestamp (2026-02-01T15:00:00+09:00) が返されます。
React はこれを受け取り、即座に次のリクエストの引数として使用します。
Step 5: AIコメントリクエスト (React → Lambda 2)
React が POST/bedrock を呼び出します。
この時、「さっき保存したデータの時刻 (timestamp)」 を一緒に送信します。
Step 6: 生成と「追記」 (Lambda 2)
ID取得: こちらの Lambda も同様に、トークンから UserId(Admin)を特定します(これで他人によるなりすましを防ぎます)。
生成: Bedrock (Amazon Nova Micro) にテキストと感情を送り、励ましのコメントを生成してもらいます。
更新 (Update): DynamoDB に対し、以下の条件で書き込みに行きます。
「UserId が Admin で、かつ Timestamp が 2026-02-01T15:00:00+09:00 のデータを探して、AiComment 列に今回のコメントを書き込んで!」
これで、先ほど保存したデータにAIコメントが合体します。
Step 7: 完了 (Frontend)
AIコメントが React に返却され、画面に表示されます。
ハンズオン
DynamoDB
プライマリキーを現行の状態から変えるため、いったん既存のものを削除します。


削除出来たら同じ名前でDynamoDBのテーブルを作成します。下記の設定以外はデフォルトで作成します。
- テーブル名: 環境変数に設定したものと同じ名前(例: SentimentResults)
- パーティションキー: UserId (文字列 / String)
- ソートキー: Timestamp (文字列 / String)
Lambda
Comprehendと連携しているLambda(SentimentLambda)のコードを次のように更新します。
import os
import json
import boto3
from datetime import datetime, timezone, timedelta # ★変更: timedeltaを追加
from decimal import Decimal
# --- クライアント初期化 ---
dynamodb = boto3.resource("dynamodb")
comprehend = boto3.client("comprehend")
# 環境変数 TABLE_NAME (例: SentimentResults)
TABLE_NAME = os.environ.get("TABLE_NAME", "SentimentResults")
table = dynamodb.Table(TABLE_NAME)
def response(status, body):
def decimal_default(obj):
if isinstance(obj, Decimal): return float(obj)
raise TypeError
return {
"statusCode": status,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
},
"body": json.dumps(body, default=decimal_default, ensure_ascii=False)
}
def lambda_handler(event, context):
if event.get("httpMethod") == "OPTIONS": return response(200, {})
# --- 1. ユーザーID取得 (HTTP API対応) ---
try:
auth_data = event.get('requestContext', {}).get('authorizer', {})
if 'jwt' in auth_data:
claims = auth_data['jwt']['claims']
else:
claims = auth_data.get('claims', {})
user_id = claims.get('username') or claims.get('cognito:username') or claims.get('sub') or "test_user"
except Exception as e:
print(f"Auth extraction error: {e}")
user_id = "test_user"
# --- 2. リクエストボディの取得 ---
try:
body = json.loads(event.get("body")) if isinstance(event.get("body"), str) else event.get("body")
text = body.get("text", "").strip()
except:
return response(400, {"message": "Invalid JSON"})
if not text: return response(400, {"message": "text is required"})
# --- 3. Comprehendで感情分析 ---
try:
comp_res = comprehend.detect_sentiment(Text=text, LanguageCode="ja")
sentiment = comp_res["Sentiment"]
scores = comp_res["SentimentScore"]
except Exception as e:
print(f"Comprehend Error: {e}")
return response(500, {"message": "Failed to analyze sentiment"})
# --- 4. DynamoDBに保存 (JSTに変更) ---
# ★変更: 日本時間 (UTC+9) のタイムゾーンを定義して時刻生成
JST = timezone(timedelta(hours=9), 'JST')
timestamp = datetime.now(JST).isoformat()
item = {
"UserId": user_id,
"Timestamp": timestamp, # 例: 2026-02-01T15:00:00+09:00
"Text": text,
"Sentiment": sentiment,
"SentimentScore": {k: Decimal(str(v)) for k, v in scores.items()},
"Language": "ja"
}
try:
table.put_item(Item=item)
except Exception as e:
print(f"DynamoDB Put Error: {e}")
return response(500, {"message": "Failed to save data"})
# --- 5. 応答 ---
return response(200, {
"message": "Diary saved",
"timestamp": timestamp, # JSTの時間を返す
"sentiment": sentiment,
"scores": scores
})
Bedrockと連携しているLambda(SentimentLambda-AItalk)を以下のように更新します。
import json
import boto3
import os
# Bedrock (us-east-1)
bedrock = boto3.client(service_name='bedrock-runtime', region_name='us-east-1')
# DynamoDB
dynamodb = boto3.resource('dynamodb')
# 環境変数 TABLE_NAME を設定してください (例: SentimentResults)
TABLE_NAME = os.environ.get("TABLE_NAME", "SentimentResults")
def response(status, body):
return {
"statusCode": status,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type",
"Access-Control-Allow-Methods": "OPTIONS,POST"
},
"body": json.dumps(body, ensure_ascii=False)
}
def lambda_handler(event, context):
if event.get("httpMethod") == "OPTIONS": return response(200, {})
# --- 1. ユーザーID取得 (HTTP API / Payload 2.0 対応) ---
# 保存時と同じIDを使うために、ここでも同じロジックで取得します
try:
auth_data = event.get('requestContext', {}).get('authorizer', {})
if 'jwt' in auth_data:
claims = auth_data['jwt']['claims']
else:
claims = auth_data.get('claims', {})
user_id = claims.get('username') or claims.get('cognito:username') or claims.get('sub') or "test_user"
except Exception as e:
print(f"Auth extraction error: {e}")
user_id = "test_user"
# --- 2. リクエストボディの解析 ---
try:
body = json.loads(event.get("body")) if isinstance(event.get("body"), str) else event.get("body")
text = body.get("text", "")
sentiment = body.get("sentiment", "不明")
# ★重要: 前のLambdaから渡されるタイムスタンプを受け取る
timestamp = body.get("timestamp")
except:
return response(400, {"message": "Invalid JSON"})
if not text: return response(400, {"message": "text is required"})
# --- 3. Amazon Nova Micro コメント生成 ---
system_list = [
{ "text": "あなたは親切なAIカウンセラーです。ユーザーの言葉と感情分析結果を元に、50文字以内で優しく励ましやコメントをしてください。" }
]
user_message = f"ユーザー: {text}\n感情: {sentiment}"
messages_list = [
{
"role": "user",
"content": [
{"text": user_message}
]
}
]
body_payload = json.dumps({
"system": system_list,
"messages": messages_list,
"inferenceConfig": {
"max_new_tokens": 300,
"temperature": 0.7,
"top_p": 0.9
}
})
ai_comment = ""
try:
model_id = 'amazon.nova-micro-v1:0'
res = bedrock.invoke_model(
body=body_payload,
modelId=model_id,
accept='application/json',
contentType='application/json'
)
response_body = json.loads(res.get('body').read())
ai_comment = response_body.get('output').get('message').get('content')[0].get('text').strip()
except Exception as e:
print(f"Bedrock Error: {e}")
return response(500, {"message": str(e)})
# --- 4. DynamoDBへの追記処理 ---
# フロントエンドからタイムスタンプが送られてきた場合のみ実行
if timestamp:
try:
table = dynamodb.Table(TABLE_NAME)
# 該当するデータ(UserIdとTimestampで特定)に、AiCommentを追加
table.update_item(
Key={
'UserId': user_id,
'Timestamp': timestamp
},
UpdateExpression="set AiComment = :c",
ExpressionAttributeValues={
':c': ai_comment
}
)
print(f"DynamoDB updated: {user_id}, {timestamp}")
except Exception as e:
# エラーログは出すが、ユーザーにはコメントを返してあげる
print(f"DynamoDB Update Error: {e}")
return response(200, {"comment": ai_comment})
環境変数を以下のように設定を追加します。
これを追加してあげないとどのDynamoDBのテーブルに追記をしていけばLambdaは判断できません。

また、Lambdaに紐づいているIAM Roleに対してAmazonDynamoDBFullAccess_v2のpolicyも追加します。
これが無いとDynamoDBにデータを書き込みできません。

React修正
**src/components/MainApp.jsx
import { useMemo, useState } from "react";
import { fetchAuthSession } from "aws-amplify/auth";
import { useTranscribe } from "../hooks/useTranscribe";
import { ResultCard } from "./ResultCard";
import { AiFeedback } from "./AiFeedback";
// 環境設定
const API_BASE = (import.meta.env.VITE_API_BASE_URL ?? "").replace(/\/$/, "");
async function safeReadText(res) {
try { return await res.text(); } catch { return ""; }
}
export default function MainApp({ signOut, user }) {
const [text, setText] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState(null);
const [apiStatus, setApiStatus] = useState("unchecked");
// AI用ステート
const [aiComment, setAiComment] = useState("");
const [aiLoading, setAiLoading] = useState(false);
const { isRecording, startRecording, stopRecording } = useTranscribe(setText);
const canSubmit = useMemo(() => {
const len = text.trim().length;
return !loading && !aiLoading && len > 0 && len <= 1000;
}, [text, loading, aiLoading]);
const apiStatusLabel = !API_BASE ? "未設定" : apiStatus === "ok" ? "接続OK" : apiStatus === "ng" ? "接続NG" : "未確認";
// --- ロジック部分 ---
async function onSubmit(e) {
e.preventDefault();
setError("");
setResult(null);
setAiComment("");
if (!API_BASE) {
setError("API の設定がありません。");
return;
}
setLoading(true);
try {
const session = await fetchAuthSession();
const token = session.tokens?.idToken?.toString();
if (!token) throw new Error("認証トークンエラー");
// 1. 感情分析 API 呼び出し
// ここでバックエンドはデータを保存し、timestampを発行して返します
const res = await fetch(`${API_BASE}/analyze`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: token },
body: JSON.stringify({ text: text.trim() }),
});
if (!res.ok) {
const bodyText = await safeReadText(res);
throw new Error(bodyText || `HTTP ${res.status}`);
}
const data = await res.json();
setResult(data);
setApiStatus("ok");
// 2. AIコメント取得 API 呼び出し
// ★修正: Analyzeの結果に含まれる timestamp を引数に追加して渡します
// これにより、Bedrock側が「どのデータに追記すればよいか」を理解できます
if (data.timestamp) {
await fetchAiComment(text.trim(), data.sentiment, data.timestamp, token);
} else {
// 万が一タイムスタンプがない場合も、コメント生成自体は動くようにそのまま呼ぶ
await fetchAiComment(text.trim(), data.sentiment, null, token);
}
} catch (err) {
setApiStatus("ng");
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}
// ★修正: 引数に timestamp を追加
async function fetchAiComment(inputText, sentimentResult, timestamp, token) {
setAiLoading(true);
try {
const bodyParams = {
text: inputText,
sentiment: sentimentResult
};
// timestampがある場合のみbodyに追加
if (timestamp) {
bodyParams.timestamp = timestamp;
}
const res = await fetch(`${API_BASE}/bedrock`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: token },
body: JSON.stringify(bodyParams),
});
if (!res.ok) throw new Error("AI Error");
const data = await res.json();
setAiComment(data.comment);
} catch (err) {
console.error(err);
setAiComment(""); // エラー時は空にしておくか、エラーメッセージを入れる
} finally {
setAiLoading(false);
}
}
// --- 表示部分 (JSX) ---
return (
<div className="page">
<main className="card">
<div className="header">
<div>
<h1 className="title">感情分析デモ</h1>
<p className="subtitle">ユーザー: {user?.signInDetails?.loginId || user?.username}</p>
<p className="subtitle" style={{ marginTop: "4px" }}>API接続: {apiStatusLabel}</p>
</div>
<div>
<button onClick={signOut} className="btn" style={{ backgroundColor: "#4b5563", fontSize: "12px", padding: "8px 12px" }}>ログアウト</button>
</div>
</div>
<form onSubmit={onSubmit} className="form">
<textarea
className="textarea"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="ここに文章を入力してください(最大1000文字)"
/>
<div className="row" style={{ alignItems: 'center' }}>
<button
type="button"
className="btn"
onClick={isRecording ? stopRecording : startRecording}
style={{
backgroundColor: isRecording ? '#dc2626' : '#2563eb',
marginRight: 'auto', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer'
}}
>
<span style={{ fontSize: '1.2em' }}>{isRecording ? "⏹" : "🎤"}</span>
{isRecording ? "停止する" : "音声入力"}
</button>
<small className="counter" style={{ marginRight: '10px' }}>{text.length} / 1000</small>
<button type="submit" className="btn" disabled={!canSubmit}>
{loading ? "分析中..." : "分析する"}
</button>
</div>
</form>
{error && <div className="alert alert--error">{error}</div>}
{/* 結果表示 */}
<ResultCard result={result} />
{/* AiFeedbackコンポ―ネントの配置 */}
<AiFeedback
comment={aiComment}
loading={aiLoading}
isVisible={!!result || aiLoading}
/>
</main>
</div>
);
}
動作確認
npm run devでアプリケーションを稼働。
Adminユーザでコメントを追加。ComprehendとBedrock連携は問題なさそうです。

DynamoDBを確認するとユーザと、タイムスタンプ、AIのコメントが格納されていることがわかります。

ユーザを変えてテストをしてみます。こちらは音声入力を試してみます。
Transcribeとの連携も壊れていないようです。(AIが「何言ってんだコイツは?」みたいな反応を示しています)

本番へのデプロイ
C:\Users\ohtsu\Documents\AWS\SentimentRepo>git add .
C:\Users\ohtsu\Documents\AWS\SentimentRepo>git commit -m "ver1.4"
[main 0ce9ebd] ver1.4
1 file changed, 27 insertions(+), 7 deletions(-)
C:\Users\ohtsu\Documents\AWS\SentimentRepo>git push origin main
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 20 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 962 bytes | 962.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
To https://github.com/ohtsuka-shota/SentimentRepo.git
cb8c32e..0ce9ebd main -> main





