背景や目的
以前の記事(→AWS Lambdaでチャットボット画面を作成)で紹介したチャット画面の改善版に取り組みました。
今回は、AIの回答に対してGOOD/BADボタンでユーザー評価を記録する機能を追加し、RAGで生成された回答に対するフィードバックを取得できるようにしました。
良質な応答とそうでない応答を見極め、改善に活かすことが目的です。
本記事では、その実装手順を紹介したいと思います。
実現したい事
AIチャットボットで、

以下のように、フィードバックボタンを設置。

システム構成は以下の通りです。

上の赤枠部を中心に紹介します。
構築手順サマリー
手順のサマリーとしては以下の通りです。
1.【準備用】VPCの構築
2.【準備用】Google認証の準備
3.ユーザー評価機能付きクライアント側Lambdaの作成
4.DynamoDBテーブル作成
5.サーバー側Lambda関数の作成
順番に紹介してゆきます。
構築1)VPCの構築
※個人的な実験でやる場合はここは無しでOKです。構築2へお進み下さい。
セキュリティー対応としてVPCの設定をします。メインVPCとサブネットを定義し、それらにLambda関数が属するようなネットワーク設定をします。
具体的な手順は以前の記事(→RAG型社内チャットボットのVPC構築例)にありますので、必要な場合はそちらを参照してください。
構築2)Google認証の準備
※ユーザー制限が目的ですが、個人的な実験ならIP制限のみや認証なしでもOKです。構築3へお進み下さい。
認証としてGoogle認証(OAuth)を使います。
OAuthクライアントIDとシークレットキーは、Google Cloudの「認証情報」画面からWebアプリ用のOAuthクライアントを作成して取得します。
具体的な手順は以前の記事(→AWS LambdaでGoogle認証)にあります。必要な場合はそちらを参照してください。
構築3)ユーザー評価機能付きクライアント側Lambdaの作成
チャット画面内回答生成後の表示に、ユーザーが評価できるGOOD/BADボタンを追加します。
・まず、Lambda関数を以下の手順で作成します。
- AWSコンソールでLambdaメニューに遷移し、トップ画面で「関数の作成」を押します。
(※AWSアカウントの作成など基本設定はここでは割愛します)
- 画面「関数の作成」では、以下の設定値で、作成します。
| 設定項目 | 値 |
|---|---|
| 関数名 | chat_client ※お好きな名称で |
| ランタイム | python3.11 |
| アーキテクチャ | x86_64(デフォルト) |
| 実行ロール | lambda_role_for_user ※お手元の環境で任意に設定 |
| 関数 URL を有効化 | チェック |
| 認証タイプ | NONE |
・続いて、Lambda関数の中身となるlambda_function.pyのコードを記述します。
チャット画面のコードで評価入力処理はHTML側で実装し、以下の関数ではCookieによるログイン状態に応じてチャット画面の表示を切り替えるだけの構成です。※個人的な実験ならcookie処理は省略してOKです。
import base64
import uuid
import urllib.parse
from http import cookies
COOKIE_KEY = 'cookie-key_ctcrag1' #※お好きな名称で
def decode_body(event_body):
body_query = base64.b64decode(event_body).decode()
body_dict = urllib.parse.parse_qs(body_query)
for key in body_dict: # Convert array values to strings
body_dict[key] = body_dict[key][0]
return body_dict
def lambda_handler(event, context):
# Initialize cookies_data
cookies_data = {}
# Check for cookies in event['cookies'] (list of cookie strings)
if 'cookies' in event:
print(f"Raw cookies from event['cookies']: {event['cookies']}")
# Combine all cookies into a single string (if multiple)
combined_cookies = "; ".join(event['cookies'])
C = cookies.SimpleCookie()
C.load(combined_cookies)
cookies_data = {k: v.value for k, v in C.items()}
# Fallback: Check for cookies in event['headers']['cookie'] (single cookie string)
elif 'headers' in event and 'cookie' in event['headers']:
cookie_header = event['headers']['cookie']
#print(f"Raw Cookie header from headers: {cookie_header}")
C = cookies.SimpleCookie()
C.load(cookie_header)
cookies_data = {k: v.value for k, v in C.items()}
#print(f"Parsed cookies_data: {cookies_data}")
# Retrieve or generate google_callback_id
google_callback_id = cookies_data.get(COOKIE_KEY, None)
if google_callback_id:
print(f"Existing {COOKIE_KEY} found: {google_callback_id}")
else:
google_callback_id = str(uuid.uuid4()) # Generate a new one if not present
#print(f"Generated new {COOKIE_KEY}: {google_callback_id}")
# Retrieve `auth_success` from cookies
auth_success = cookies_data.get('auth_success', '0') # Default to '0' if not found
#print(f"Extracted auth_success: {auth_success}")
# Retrieve `clear` from cookies
clear = cookies_data.get('clear', '0')
# Debugging: Log all cookies
#print(f"All received cookies: {cookies_data}")
# Prepare Set-Cookie headers
set_cookie_header_1 = (
f"{COOKIE_KEY}={google_callback_id}; Secure; Max-Age=3600; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
set_cookie_header_2 = (
f"auth_success={auth_success}; Secure; Max-Age=3600; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
set_cookie_header_3 = (
f"clear={clear}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
# Load HTML
try:
with open('./ctcrag_init_disp.html', 'r', encoding='utf-8') as f:
html_body = f.read()
except FileNotFoundError:
print("HTML template not found.")
# Modify HTML based on authentication state
if auth_success == '1':
# Hide login section, show message section
#print("replacing the function")
html_body = html_body.replace('<div id="login-section">', '<div id="login-section" style="display:none;">')
html_body = html_body.replace('id="message-section" style="display:none;"', 'id="message-section" ')
else:
# Show login section, hide message section
html_body = html_body.replace('<div id="login-section">', '<div id="login-section" style="display:block;">')
html_body = html_body.replace('id="message-section" style="display:block;"', 'id="message-section" style="display:none;"')
# Prepare response headers
headers = {
'Content-Type': 'text/html',
'charset': "UTF-8",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
'Set-Cookie': f"{set_cookie_header_1}, {set_cookie_header_2},{set_cookie_header_3} " # Combine cookies into a single header
}
# Return the response
return {
'statusCode': 200,
'headers': headers,
'body': html_body
}
・上のコードが参照するログイン画面(ctcrag_init_disp.html)のHTMLコードは以下の通りです。
<!DOCTYPE html>
<html lang="ja">
<head>
<title>チャット画面</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
/* General Body Style */
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #FFFFFF;
font-family: Arial, sans-serif;
}
#login-section {
display: flex;
background-color: rgb(255, 180, 13);
border-radius: 10px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
margin-top: 5px;
padding: 20px 20px;
width: 600px;
height: 350px;
}
#google-login-btn {
display: flex;
margin-left: 145px;
margin-top: 95px;
background-color: rgb(0, 169, 228);
color: white;
border: none;
padding: 50px 50px;
border-radius: 5px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
}
/* Chat Container */
#message-section {
position: relative;
display: flex;
flex-direction: column;
margin-top: 5px;
width: 600px; /* Fixed width */
height: 400px; /* Fixed height */
background-color: rgb(255, 180, 13);
border-radius: 10px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
overflow: hidden; /* Prevents scrollbars from affecting alignment */
}
/* Chat Body */
.chat-body {
flex-grow: 1; /* Occupies available space */
padding: 15px;
overflow-y: auto; /* Adds a vertical scrollbar */
display: flex;
flex-direction: column;
gap: 10px; /* Spacing between messages */
}
.chat-body::-webkit-scrollbar {
width: 6px; /* Scrollbar width for Webkit browsers */
}
.chat-body::-webkit-scrollbar-thumb {
background-color: #ccc; /* Scrollbar thumb color */
border-radius: 10px;
}
/* Message Bubbles */
.user-message-container {
display: flex;
justify-content: flex-end; /* Align user messages to the right */
}
.bot-message-container {
display: flex;
justify-content: flex-start; /* Align bot messages to the left */
}
.message {
/*max-width: 80%;*/
width: 100%;
padding: 10px;
border-radius: 15px;
word-wrap: break-word;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.user-message-container .message {
background-color: rgb(243, 243, 243);
align-self: flex-end;
}
.bot-message-container .message {
background-color: white;
align-self: flex-start;
}
.reviewDiv {
display: flex;
justify-content: center;
align-self: center;
}
/* Timestamp */
.message-time {
font-size: 12px;
color: gray;
margin-top: 5px;
text-align: right;
}
.bot-message-container .message-time {
text-align: left;
}
/* Chat Input Section */
.chat-input {
display: flex;
align-items: center;
padding: 10px 15px;
background-color: #ccc;
border-top: 1px solid #ccc;
gap: 10px;
}
.chat-input input {
flex: 1;
padding: 10px;
font-size: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
.send-button {
background-color: rgb(40, 54, 103);
color: white;
border: none;
padding: 8px 15px;
font-size: 14px;
border-radius: 5px;
cursor: pointer;
}
.send-button:hover {
background-color: #002855;
}
/* Chat Header */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgb(40, 54, 103);
color: white;
padding: 10px 15px;
font-weight: bold;
font-size: 20px;
border-radius: 10px 10px 0 0;
}
.spacer {
height: 10px;
width: 1%;
}
#bot-img {
margin-right: 1%;
}
#logout-btn {
background-color: rgb(0, 169, 228);
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
#logout-btn:hover {
background-color: #0056b3;
}
#loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 215, 0, 0.5); /* Semi-transparent yellow */
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgb(40, 54, 103);
border-top: 5px solid #ffffff; /* White border for contrast */
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#newChatButton {
position: absolute;
right: 5px;
bottom: 80px;
width: 80px;
height: 80px;
margin-right: 5px;
background-color: rgba(255, 180, 13, 0.9);
border: none;
border-radius: 50%;
box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: none; /* Initially hidden */
justify-content: center;
align-items: center;
}
#newChatButton:hover {
background-color: rgba(255, 180, 13, 0.5);
}
</style>
</head>
<body>
<!-- Login Section -->
<div id="login-section">
<button id="google-login-btn">
Google でログイン
</button>
</div>
<!-- Chat Container -->
<div id="message-section" style="display:none;">
<div class="chat-header">
<span>インフォマン</span>
<button id="logout-btn">ログアウト</button>
</div>
<!-- Chat Body -->
<div class="chat-body" id="chat-body"></div>
<!-- New Chat Button (Initially Hidden) -->
<button id="newChatButton" style="display: none;">新規質問</button>
<!-- Chat Input -->
<div class="chat-input">
<input type="text" id="question" placeholder="質問を入力してください" />
<button class="send-button" id="send">送信</button>
</div>
</div>
<div id="loader" style="display: none;">
<div class="spinner"></div>
<div class="sync-text"><h4>Now Thinking</h4></div>
</div>
<script type="text/javascript">
// Google Login button handler
document.getElementById('google-login-btn').addEventListener('click', function () {
const googleAuthUrl = "https://accounts.google.com/o/oauth2/v2/auth"
+ "?client_id=*****.apps.googleusercontent.com"
+ "&redirect_uri=https://*****.lambda-url.ap-northeast-1.on.aws/"
+ "&response_type=code"
+ "&scope=openid%20email%20profile"
+ "&state=random_state_string"
+ "&prompt=select_account";
window.location.href = googleAuthUrl;
});
// Logout button handler
document.getElementById('logout-btn').addEventListener('click', function () {
createCustomConfirm(
"全てを終了し、ログアウトします。\n よろしいでしょうか?",
function () {
createFeedbackModal(
function () {
sendEvaluation(1, 1);
document.cookie = "auth_success=; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; Path=/; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC";
setTimeout(resetSession, 500);
},
function () {
sendEvaluation(1, 2);
document.cookie = "auth_success=; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; Path=/; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC";
setTimeout(resetSession, 500);
}
);
},
function () {
// "いいえ": do nothing.
}
);
});
function resetSession() {
document.cookie = "cookie-key_ctcrag1=; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; Path=/; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC";
location.reload();
newChatButton.style.display = "none";
}
// On page load: display the chat UI and add a greeting from the bot.
window.onload = function () {
document.getElementById('message-section');
const now = new Date();
const formattedTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} `
+ `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
addChat("ご質問をどうぞ。", formattedTime, true);
};
// ◆◆◆ユーザ評価メイン部◆◆◆ (used in chat bubble and modal)
function createFeedbackContent(onLike, onDislike, customTitle) {
// Outer container
let feedbackDiv = document.createElement("div");
feedbackDiv.style.display = "flex";
feedbackDiv.style.flexDirection = "column";
feedbackDiv.style.gap = "1px";
feedbackDiv.style.marginTop = "10px";
feedbackDiv.style.alignItems = "center";
feedbackDiv.style.backgroundColor = "#fff9c4"; // Light yellow highlight
feedbackDiv.style.paddingBottom = "5px";
feedbackDiv.style.paddingTop = "5px";
// Feedback Title (Top)
let feedbackTitle = document.createElement("span");
// Use the custom title if provided; otherwise fall back to the default.
feedbackTitle.textContent = customTitle || "↓↓質問が終了した時にご評価お願いします。↓↓";
feedbackTitle.style.textAlign = "center";
feedbackTitle.style.padding = "0px 5px";
feedbackTitle.style.borderRadius = "5px";
feedbackTitle.style.fontSize = "10px";
feedbackTitle.style.marginBottom = "0px";
feedbackTitle.style.width = "100%";
// Label: "解決しましたか?"
let feedbackLabel = document.createElement("span");
feedbackLabel.textContent = "解決しましたか?";
feedbackLabel.style.padding = "0px 5px";
feedbackLabel.style.borderRadius = "5px";
feedbackLabel.style.fontSize = "10px";
feedbackLabel.style.marginTop= "0px";
feedbackLabel.style.marginBottom = "0px";
// Row for the buttons
let feedbackRow = document.createElement("div");
feedbackRow.style.display = "flex";
feedbackRow.style.alignItems = "center";
feedbackRow.style.justifyContent = "center";
feedbackRow.style.gap = "5px";
// Good (👍) Button
let likeButton = document.createElement("button");
likeButton.innerHTML = "👍";
likeButton.style.background = "#28a745";
likeButton.style.color = "white";
likeButton.style.border = "none";
likeButton.style.borderRadius = "10px";
likeButton.style.padding = "5px 10px";
likeButton.style.fontSize = "12px";
likeButton.style.cursor = "pointer";
likeButton.onclick = function () {
if (typeof onLike === 'function') onLike();
};
// Bad (👎) Button
let dislikeButton = document.createElement("button");
dislikeButton.innerHTML = "👎";
dislikeButton.style.background = "#dc3545";
dislikeButton.style.color = "white";
dislikeButton.style.border = "none";
dislikeButton.style.borderRadius = "10px";
dislikeButton.style.padding = "5px 10px";
dislikeButton.style.fontSize = "12px";
dislikeButton.style.cursor = "pointer";
dislikeButton.onclick = function () {
if (typeof onDislike === 'function') onDislike();
};
// Assemble feedback elements
feedbackRow.appendChild(likeButton);
feedbackRow.appendChild(dislikeButton);
feedbackDiv.appendChild(feedbackTitle);
feedbackDiv.appendChild(feedbackLabel);
feedbackDiv.appendChild(feedbackRow);
return feedbackDiv;
}
// General-purpose custom confirmation modal.
// Accepts an optional customButtons array to replace the default "はい" and "いいえ" buttons.
function createCustomConfirm(message, yesCallback, noCallback, customButtons = null) {
const messageSection = document.getElementById('message-section');
// Create overlay relative to messageSection
const overlay = document.createElement('div');
overlay.style.position = 'absolute';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.style.zIndex = '9999';
overlay.style.borderRadius = '10px';
// Dialog box styling
const dialogBox = document.createElement('div');
dialogBox.style.backgroundColor = '#fff';
dialogBox.style.padding = '20px';
dialogBox.style.borderRadius = '10px';
dialogBox.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.3)';
dialogBox.style.textAlign = 'center';
dialogBox.style.minWidth = '250px';
dialogBox.style.maxWidth = '90%';
// Confirmation message element
const messageElem = document.createElement('p');
messageElem.textContent = message;
//messageElem.style.fontWeight = 'bold';
messageElem.style.marginBottom = '20px';
messageElem.style.fontSize = '14px';
messageElem.style.whiteSpace = 'pre-line';
// Button container
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.justifyContent = 'center';
buttonContainer.style.gap = '20px';
if (customButtons) {
// Use custom buttons if provided
customButtons.forEach(button => buttonContainer.appendChild(button));
} else {
// Default buttons: "はい" and "いいえ"
const yesButton = document.createElement('button');
yesButton.textContent = 'はい';
yesButton.style.backgroundColor = '#28a745';
yesButton.style.color = 'white';
yesButton.style.padding = '8px 16px';
yesButton.style.border = 'none';
yesButton.style.borderRadius = '5px';
yesButton.style.cursor = 'pointer';
yesButton.onclick = function () {
messageSection.removeChild(overlay);
if (typeof yesCallback === 'function') yesCallback();
};
const noButton = document.createElement('button');
noButton.textContent = 'いいえ';
noButton.style.backgroundColor = '#dc3545';
noButton.style.color = 'white';
noButton.style.padding = '8px 16px';
noButton.style.border = 'none';
noButton.style.borderRadius = '5px';
noButton.style.cursor = 'pointer';
noButton.onclick = function () {
messageSection.removeChild(overlay);
if (typeof noCallback === 'function') noCallback();
};
buttonContainer.appendChild(yesButton);
buttonContainer.appendChild(noButton);
}
// Assemble modal elements
dialogBox.appendChild(messageElem);
dialogBox.appendChild(buttonContainer);
overlay.appendChild(dialogBox);
// Attach overlay to chat container
messageSection.appendChild(overlay);
}
// Modularized feedback modal that uses custom like/dislike buttons.
// Modified createFeedbackModal to pass a custom title
function createFeedbackModal(likeCallback, dislikeCallback) {
// Pass your custom title "最後の質問の評価をお願い致します。"
let feedbackDiv = createFeedbackContent(likeCallback, dislikeCallback, "↓↓終了する質問の評価をお願い致します。↓↓");
createCustomConfirm(
"",//★ ご評価お願い致します。★
null,
null,
[feedbackDiv]
);
}
// Chat bubble for bot messages
function addChat(botreply, ajax_time, isGreeting = false) {
const mainDiv = document.getElementById("chat-body");
let botMessageContainer = document.createElement("div");
botMessageContainer.classList.add("bot-message-container");
let botimgDiv = document.createElement("div");
botimgDiv.id = "bot-img";
botimgDiv.innerHTML = '<img src="https://ctcrag-init-disp.s3.ap-northeast-1.amazonaws.com/bot_icon_redesign.png" alt="BOTimg" width="35px" height="35px">';
let messageDiv = document.createElement("div");
messageDiv.classList.add("message");
messageDiv.innerHTML = botreply.replace(/\n/g, "<br>"); //for URL
messageDiv.style.fontSize = "10px";
messageDiv.style.paddingBottom = "15px";
let spacerDiv = document.createElement("div");
spacerDiv.classList.add("spacer");
let timeDiv = document.createElement("div");
timeDiv.classList.add("message-time");
timeDiv.textContent = ajax_time;
botMessageContainer.appendChild(botimgDiv);
botMessageContainer.appendChild(messageDiv);
botMessageContainer.appendChild(spacerDiv);
botMessageContainer.appendChild(timeDiv);
// Only add extra UI for non-greeting messages
if (!isGreeting) {
// #v2で追加: 新しい質問の案内とボタン(文中・装飾付き)
let v2NoticeDiv = document.createElement("div");
v2NoticeDiv.style.display = "flex";
v2NoticeDiv.style.flexDirection = "column";
v2NoticeDiv.style.alignItems = "center";
v2NoticeDiv.style.justifyContent = "center";
v2NoticeDiv.style.gap = "8px";
v2NoticeDiv.style.marginTop = "10px";
v2NoticeDiv.style.backgroundColor = "#fff9c4";
v2NoticeDiv.style.paddingTop = "5px";
let line1Wrapper = document.createElement("div");
line1Wrapper.style.fontSize = "10px";
line1Wrapper.style.textAlign = "center";
line1Wrapper.style.display = "flex";
line1Wrapper.style.flexWrap = "wrap";
line1Wrapper.style.justifyContent = "center";
line1Wrapper.style.alignItems = "center";
line1Wrapper.style.gap = "6px";
//line1Wrapper.style.fontWeight = "bold";
let textBeforeBtn = document.createElement("span");
textBeforeBtn.textContent = "新しい質問をする場合は、";
let inlineBtn = document.createElement("button");
inlineBtn.textContent = "新規質問";
inlineBtn.style.backgroundColor = "rgba(255, 180, 13, 0.9)";
inlineBtn.style.border = "none";
inlineBtn.style.width = "50px";
inlineBtn.style.height = "50px";
inlineBtn.style.borderRadius = "50%";
inlineBtn.style.boxShadow = "2px 4px 6px rgba(0, 0, 0, 0.3)";
inlineBtn.style.cursor = "pointer";
inlineBtn.style.fontSize = "9px";
inlineBtn.style.fontWeight = "bold";
inlineBtn.style.display = "flex";
inlineBtn.style.alignItems = "center";
inlineBtn.style.justifyContent = "center";
inlineBtn.style.transition = "background-color 0.3s";
inlineBtn.addEventListener("mouseover", function () {
inlineBtn.style.backgroundColor = "rgba(255, 180, 13, 0.5)";
});
inlineBtn.addEventListener("mouseout", function () {
inlineBtn.style.backgroundColor = "rgba(255, 180, 13, 0.9)";
});
inlineBtn.addEventListener("click", function () {
document.getElementById("newChatButton").click();
});
let textAfterBtn = document.createElement("span");
textAfterBtn.textContent = "を押してください。";
let line2 = document.createElement("div");
line2.textContent = "今の質問のまま会話を続けたい場合は、そのまま質問入力欄から入力してください。";
line2.style.fontSize = "10px";
line2.style.textAlign = "center";
//line2.style.fontWeight = "bold";
line1Wrapper.appendChild(textBeforeBtn);
line1Wrapper.appendChild(inlineBtn);
line1Wrapper.appendChild(textAfterBtn);
v2NoticeDiv.appendChild(line1Wrapper);
v2NoticeDiv.appendChild(line2);
messageDiv.appendChild(v2NoticeDiv);
let feedbackDiv = createFeedbackContent(
function () {
handleFeedback(1);
},
function () {
handleFeedback(2);
}
);
messageDiv.appendChild(feedbackDiv);
let newChatButton = document.getElementById("newChatButton");
if (newChatButton) {
newChatButton.style.display = "flex";
}
}
mainDiv.appendChild(botMessageContainer);
mainDiv.scrollTop = mainDiv.scrollHeight;
}
//質問用メッセージ追加
function addquestion(question, question_time) {
const mainDiv = document.getElementById("chat-body");
let userMessageContainer = document.createElement("div");
userMessageContainer.classList.add("user-message-container");
let messageDiv = document.createElement("div");
messageDiv.classList.add("message");
messageDiv.style.fontSize = "10px";
messageDiv.textContent = question;
let timeDiv = document.createElement("div");
timeDiv.classList.add("message-time");
timeDiv.textContent = question_time;
let spacerDiv = document.createElement("div");
spacerDiv.classList.add("spacer");
userMessageContainer.appendChild(timeDiv);
userMessageContainer.appendChild(spacerDiv);
userMessageContainer.appendChild(messageDiv);
mainDiv.appendChild(userMessageContainer);
mainDiv.scrollTop = mainDiv.scrollHeight;
if (newChatButton) {
newChatButton.style.display = "none"; // Hide till Answer is displayed
}
}
// Feedback from Good/Bad Buttons in chat bubble
function handleFeedback(useful) {
createCustomConfirm(
"この質問を終了します。\nよろしいですか?",
function () {
// "はい" clicked: Send Evaluation and End Session
sendEvaluation(1, useful);
setTimeout(() => {
document.cookie = "cookie-key_ctcrag1=; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; Path=/; SameSite=None; expires=Thu, 01 Jan 1970 00:00:00 UTC";
location.reload();
}, 500);
},
function () {
// "いいえ" clicked: Do nothing
}
);
}
// ユーザ評価 AJAX
function sendEvaluation(clear, useful) {
$.ajax({
url: 'https://****.lambda-url.ap-northeast-1.on.aws/', //サーバー側URL
type: 'POST',
data: {
'clear': clear,
'useful': useful
},
crossDomain: true,
xhrFields: { withCredentials: true },
})
.done(() => {
// Optionally notify that evaluation was sent.
})
.fail(() => {
//alert("開発者へ連絡してください。");
//alert("評価の送信に失敗しました。");
});
}
// New Chat Button handling
document.addEventListener("DOMContentLoaded", function () {
const newChatButton = document.getElementById("newChatButton");
newChatButton.onclick = function () {
// First confirmation to end the current QA session
createCustomConfirm("この質問を終了します。\nよろしいですか?",
function () {
// Second: show feedback modal using our shared feedback styling
createFeedbackModal(
function () {
sendEvaluation(1, 1);
setTimeout(resetSession, 500);
},
function () {
sendEvaluation(1, 2);
setTimeout(resetSession, 500);
}
);
},
function () {
// "いいえ": do nothing
}
);
};
});
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script>
/* global $*/ // Added as a 対策 to $ is not defined
$(function () {
// Prevent default form submit and trigger the send button
$('form').on('submit', function (e) {
e.preventDefault(); // Stop the default form submit action.
$('#send').click(); // Click AJAX button.
});
// 「Ajax通信」ボタンをクリックしたら発動
$('#send').on('click', function () {
// Get the question value
var questionValue = $('#question').val();
// Check if the question value is "0"
if (questionValue == "") {
alert("質問を入力してください。");
return; // Exit the function to prevent further execution
}
// Show the loader
$('#loader').show();
const now = new Date();
const formattedTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
// Add the user's question to the chat
addquestion(questionValue, formattedTime);
// Clear input
$('#question').val('');
// AJAX request
$.ajax({
url: 'https://***.lambda-url.ap-northeast-1.on.aws/', // Main server URL
type: 'POST',
data: {
'question': questionValue
},
sent: 1,
crossDomain: true,
xhrFields: { withCredentials: true },
})
// Ajax通信が成功したら発動
.done((data) => {
// Hide the loader
$('#loader').hide();
// Add bot's response to the chat
addChat(data.answer, data.exec_time, false);
})
// Ajax通信が失敗したら発動
.fail((jqXHR, textStatus, errorThrown) => {
// Hide the loader
$('#loader').hide();
//alert("開発者へ連絡してください。");
//alert('質問を入力してください');
location.reload();
//alert('Ajax通信に失敗しました。');
console.log("jqXHR : " + jqXHR.status); // HTTPステータスを表示
console.log("textStatus : " + textStatus); // タイムアウト、パースエラーなどのエラー情報を表示
console.log("errorThrown : " + errorThrown.message); // 例外情報を表示
})
// Ajax通信が成功・失敗のどちらでも発動
.always((data) => {
console.log($('#question').val());
console.log($('answer').val());
});
});
});
</script>
</body>
</html>
・上記のHTMLでは、ユーザーのGood/Bad評価を受け付けることができます。
主な処理流れは以下の通りです:
1.addChat()(回答表示部)
- 回答文の表示時に、評価UI(👍👎)を自動で挿入します。
2.createFeedbackContent()(◆◆◆ユーザ評価メイン部◆◆◆)
- 評価UIを生成部。上記のaddChat()から呼び出されます。
3.handleFeedback()
- 評価ボタン押下後、確認モーダルを表示します。
4.sendEvaluation()(ユーザ評価AJAX)
- 上記の確認モーダルで「はい」押下時、評価値(useful)をサーバー側Lambda関数に送信します。
このようにして、ユーザーからの評価を収集・送信できるようにしました。
構築4)DynamoDBテーブル作成
ユーザー評価(GOOD/BAD)と会話履歴を保存するために、2つのDynamoDBテーブルを作成します。
※評価(GOOD/BAD)のみ記録する場合、会話履歴テーブル作成をスキップしてOKです。
・まずは、ユーザーの評価を記録するための評価記録用テーブルを作成します。
・AWSコンソールでDynamoDBに遷移し、トップ画面で「テーブルの作成」を押します。


・画面「テーブルの作成」にて、以下を設定値としてテーブルを作成します。
| 設定項目 | 値 |
|---|---|
| テーブル名 | ctcrag_user_review ※お好きな名称で |
| パーティションキー | session_id(文字列) |
| ソートキー | exec_time(文字列) |
| デフォルト設定 | チェック |
・次は、ユーザーとの会話内容を保存する会話履歴用テーブルを作成します。
・もう一度「テーブルの作成」画面を開き、以下の設定でテーブルを作成します。
| 設定項目 | 値 |
|---|---|
| テーブル名 | ctcrag_chat_history ※お好きな名称で |
| パーティションキー | session_id(文字列) |
| ソートキー | exec_time(文字列) |
| デフォルト設定 | チェック |
構築5)サーバー側Lambda関数の作成
サーバー側でユーザーのGOOD/BAD評価を処理・保存するために、1つ関数を追加します。
・サーバー側Lambda関数を以下の手順で作成します。
- AWSコンソールでLambdaメニューに遷移し、トップ画面で「関数の作成」を押します。
(※AWSアカウントの作成など基本設定はここでは割愛します)
- 画面「関数の作成」では、以下の設定値で、作成します。
| 設定項目 | 値 |
|---|---|
| 関数名 | chat_server ※お好きな名称で |
| ランタイム | python3.9 |
| アーキテクチャ | x86_64(デフォルト) |
| 実行ロール | lambda_role_for_user ※お手元の環境で任意に設定 |
| 関数 URL を有効化 | チェック |
| 認証タイプ | NONE |
・ AWSコンソールでIAM画面に遷移し、メニュー「ロール」を開きます。
・ その画面で、Lambda関数に割り当てたロール名を押し、以下ポリシーをアタッチします。
| 設定項目 | 値 |
|---|---|
| ポリシー | AmazonDynamoDBFullAccess |
・GOOD/BAD評価を保存するため、lambda_function.py内にhandle_user_rev関数を追加します。処理としては、評価を受け取り、それをDynamoDBに記録する内容です。
・コードは以下の通りです。該当箇所は「ユーザ評価Handler」(行:496~574)です。
import json
import boto3
import logging
import base64
import uuid
from http import cookies
import urllib.parse
import urllib.request # For making HTTP requests
import textwrap
from openai import OpenAI
from datetime import datetime, timedelta
# OpenAI API Key
OPENAI_API_KEY = "******"
openai_client = OpenAI(api_key=OPENAI_API_KEY)
# Pinecone setup
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="******")
index = pc.Index(host="https://******.pinecone.io")
# Initialize the logger
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# Constants for Lambda function and VPC Lambda endpoint
VPC_ENDPOINT_DNS = "https://***.vpce.amazonaws.com"
CONSUMER_LAMBDA_NAME = "ctcrag_serve_embedding"
lambda_client = boto3.client("lambda", endpoint_url=VPC_ENDPOINT_DNS)
# Constants for Google OAuth 2.0
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
CLIENT_ID = "******.apps.googleusercontent.com"
CLIENT_SECRET = "******"
REDIRECT_URI = "https://*****.lambda-url.ap-northeast-1.on.aws/"
COOKIE_KEY = 'cookie-key_ctcrag1'
#DynamoDB関連
dynamodb_client = boto3.client('dynamodb')
CONVO_HISTORY_TABLE_NAME = 'ctcrag_chat_history'
DN_P_KEY = 'session_id'
REVIEW_TABLE_NAME = "ctcrag_user_review"
# Decodes a base64-encoded event body
def decode_body(event_body):
body_query = base64.b64decode(event_body).decode()
body_dict = urllib.parse.parse_qs(body_query)
for key in body_dict: # Convert array values to strings
body_dict[key] = body_dict[key][0]
return body_dict
################### QuestionのGluCoSEからベクター値生成 ###################
def create_embeddings(sentence_list):
try:
payload = {
"body": json.dumps({"sentences": sentence_list}, ensure_ascii=False)
}
#logger.info(f"Payload prepared: {payload}")
embed_response = lambda_client.invoke(
FunctionName=CONSUMER_LAMBDA_NAME,
InvocationType="RequestResponse",
Payload=json.dumps(payload).encode("utf-8")
)
consumer_body = json.loads(embed_response["Payload"].read().decode("utf-8"))["body"]
embeddings = json.loads(consumer_body).get("embeddings", [])
return embeddings[0]
except Exception as e:
logger.error(f"Error creating embeddings: {str(e)}", exc_info=True)
raise
################### Lambda Handler ###################
def lambda_handler(event, context):
query_params = event.get('queryStringParameters', {})
#logging.info(f" Lambda Handler Query Params: {query_params}")
request_body = decode_body(event['body']) if event.get('body') else {}
if event.get('body'):
useful,question = request_body.get('useful'),request_body.get('question')
#print(f"Lambda Handler Request body : {request_body}")
if 'code' in query_params:
return handle_google_callback(event, context) #Google認証
elif 'question' in request_body:
return handle_chat(event, context) #チャット応答
else:
return handle_user_rev(event,context) #評価記録
################### Google認証 Handler ###################
def handle_google_callback(event, context):
#print("Handling Google callback...")
#print("jsonevent:", event)
# Initialize cookies for COOKIE_KEY and auth_success only
google_callback_id = None
auth_success = "1"
# Check for cookies in event['cookies'] (list of cookie strings)
if 'cookies' in event:
#print(f"Raw cookies from event['cookies']: {event['cookies']}")
combined_cookies = "; ".join(event['cookies'])
C = cookies.SimpleCookie()
C.load(combined_cookies)
google_callback_id = C.get(COOKIE_KEY).value if C.get(COOKIE_KEY) else None
auth_success = C.get("auth_success").value if C.get("auth_success") else "0"
# Fallback: Check for cookies in event['headers']['cookie'] (single cookie string)
elif 'headers' in event and 'cookie' in event['headers']:
cookie_header = event['headers']['cookie']
#print(f"Raw Cookie header from headers: {cookie_header}")
C = cookies.SimpleCookie()
C.load(cookie_header)
google_callback_id = C.get(COOKIE_KEY).value if C.get(COOKIE_KEY) else None
auth_success = C.get("auth_success").value if C.get("auth_success") else "0"
#print(f"Extracted COOKIE_KEY during auth: {google_callback_id}")
#print(f"Extracted auth_success during auth: {auth_success}")
# Generate a new google_callback_id if not present
if not google_callback_id:
google_callback_id = str(uuid.uuid4())
#print(f"Generated new {COOKIE_KEY}: {google_callback_id}")
# Validate the authorization code
code = event.get('queryStringParameters', {}).get('code')
if not code:
print("Authorization code missing from query parameters.")
return {'statusCode': 400, 'body': 'Invalid Request'}
#print(f"Authorization code received: {code}")
# Simulate token exchange and validation
try:
token_data = get_google_token(code)
id_token = token_data.get('id_token')
auth_success = "1"
if not id_token:
print("ID token verification failed.")
return {'statusCode': 401, 'body': 'Unauthorized'}
# Decode user info (optional step for debugging)
user_info = decode_jwt(id_token)
#print(f"User info decoded: {user_info}")
user_email = user_info.get("email", "Unknown")
#print(f"User email decoded: {user_email}")
set_cookie_header_email = (
f"user_email={user_email}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
# Redirect back to 関数3
return {
'statusCode': 302,
'headers': {
'Location': "https://rc26qs5sgsdkrn4pfsxion6gme0cgoss.lambda-url.ap-northeast-1.on.aws/",
'Set-Cookie': f"auth_success={auth_success}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/, {set_cookie_header_email}",
'Access-Control-Allow-Credentials': 'true',
},
'body': 'Redirecting...'
}
except Exception as e:
#print(f"Error during token exchange or verification: {e}")
return {'statusCode': 500, 'body': 'Internal Server Error'}
def get_google_token(code):
payload = {
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
data = urllib.parse.urlencode(payload).encode('utf-8')
#print(f"Sending request to Google Token API: {payload}")
req = urllib.request.Request(GOOGLE_TOKEN_URL, data=data, headers=headers, method='POST')
try:
with urllib.request.urlopen(req) as response:
response_body = response.read().decode('utf-8')
#print(f"Google Token API Response: {response_body}")
return json.loads(response_body)
except urllib.error.HTTPError as e:
error_body = e.read().decode('utf-8')
#print(f"Error fetching token: {error_body}")
raise
def decode_jwt(jwt_token):
try:
base64_url = jwt_token.split('.')[1]
base64_bytes = base64_url + '=' * (4 - len(base64_url) % 4)
decoded_bytes = base64.b64decode(base64_bytes)
decoded_str = decoded_bytes.decode('utf-8')
return json.loads(decoded_str)
except Exception as e:
#print(f"Error decoding JWT: {e}")
return {}
################### チャット処理 Handler ###################
def handle_chat(event, context):
# Initialize cookies for COOKIE_KEY and auth_success only
google_callback_id = None
auth_success = "0"
clear = "0"
# Check for cookies in event['cookies'] (list of cookie strings)
if 'cookies' in event:
#print(f"Raw cookies from event['cookies']: {event['cookies']}")
combined_cookies = "; ".join(event['cookies'])
C = cookies.SimpleCookie()
C.load(combined_cookies)
google_callback_id = C.get(COOKIE_KEY).value if C.get(COOKIE_KEY) else None
auth_success = C.get("auth_success").value if C.get("auth_success") else "0"
clear = C.get("clear").value if C.get("clear") else "0"
# Fallback: Check for cookies in event['headers']['cookie'] (single cookie string)
elif 'headers' in event and 'cookie' in event['headers']:
cookie_header = event['headers']['cookie']
#print(f"Raw Cookie header from headers: {cookie_header}")
C = cookies.SimpleCookie()
C.load(cookie_header)
google_callback_id = C.get(COOKIE_KEY).value if C.get(COOKIE_KEY) else None
auth_success = C.get("auth_success").value if C.get("auth_success") else "0"
clear = C.get("clear").value if C.get("clear") else "0"
#print(f"Extracted COOKIE_KEY during chat: {google_callback_id}")
#print(f"Extracted auth_success during chat: {auth_success}")
# Check if the user is authenticated
if auth_success != "1":
print("User not authenticated. Redirecting to login.")
google_auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
"&response_type=code"
"&scope=openid%20email%20profile"
"&state=random_state_string"
)
#print(f"Redirecting to Google Auth: {google_auth_url}") # for debug
return {
'statusCode': 302,
'headers': {'Location': google_auth_url,'Set-Cookie': 'auth_success=0; Domain=.lambda-url.ap-northeast-1.on.aws; Path=/; HttpOnly; SameSite=Strict',},
'body': json.dumps({"message": "User not authenticated. Please log in."}, ensure_ascii=False),
}
#print("Authentication successful, proceeding to handle chat.")
session_id,continuation = '',False
print("going ahead with handling chat")
#body部のデコード
if event.get('body'):
request_body = decode_body(event['body'])
print("decoded_body",request_body)
#パラメータの受理(question,clear)
question,clear = '','0'
if event.get('queryStringParameters'):
question,clear = event.get('queryStringParameters').get('question'),event.get('queryStringParameters').get('clear')
elif event.get('body'):
question,clear = request_body.get('question'),request_body.get('clear')
print("question:",question)
print("Processing Answer: Creating GluCoSE embeddings + PineconeQuery + OpenAI")
cookie_dict = {k: v.value for k, v in C.items()}
if COOKIE_KEY not in cookie_dict:
session_id = str(uuid.uuid4())
print('IS_cookies:NO!! -> New Question?')
else:
session_id = cookie_dict[COOKIE_KEY]
print('COOKIE_KEY:Exist!!',session_id)
if is_conversation_log(session_id):
continuation=True
print('Continue Conversation')
else:
continuation=False
print('New Question!!')
print(f"Extracted clear during chat: {clear}")
if clear == '1':
session_id,continuation = str(uuid.uuid4()),False
print('conversation_clear!!')
#回答生成要求部
if clear == '1':
answer,system_prompt,exec_time,question = '新しい質問をどうぞ。','','','' #生成しない。
else:
("No need to clear. go ahead and save to DB")
answer,system_prompt,exec_time = make_answer(question,session_id,continuation) #引数3は継続mode(True:継続/False:初回)
# Prepare Set-Cookie headers
set_cookie_header_1 = (
f"{COOKIE_KEY}={session_id}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
set_cookie_header_2 = (
f"auth_success={auth_success}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
set_cookie_header_3 = (
f"clear={clear}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
#レスポンスデータ作成
message = {
"question":question,
"exec_time":exec_time,
"answer":answer
}
return {
'statusCode': 200,
'isBase64Encoded': False,
'headers': {
'Content-Type': 'application/json',
'Set-Cookie': f"{set_cookie_header_1}, {set_cookie_header_2},{set_cookie_header_3} "
},
'body': json.dumps(message, ensure_ascii=False),
}
def is_conversation_log(session_id): #会話履歴内存在確認の関数
dynamo_response = dynamodb_client.query(TableName=CONVO_HISTORY_TABLE_NAME,KeyConditionExpression= DN_P_KEY + '= :val',ExpressionAttributeValues={":val": {"S": session_id}},)
if dynamo_response['Count'] > 0 :
return True
else:
return False
def record_conversations(session_id,question,answer,system_prompt): #会話履歴保存関数
exec_time = str((datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S"))
dynamodb_client.put_item(TableName=CONVO_HISTORY_TABLE_NAME,Item={
DN_P_KEY: {"S": session_id},
"question": {"S": question},
"answer": {"S": answer},
"system_prompt": {"S": system_prompt},
"exec_time": {"S": exec_time},
})
dynamo_response = dynamodb_client.query(TableName=CONVO_HISTORY_TABLE_NAME,KeyConditionExpression= DN_P_KEY + '= :val',ExpressionAttributeValues={":val": {"S": session_id}},)
print("dynamoDB output:",dynamo_response)
return exec_time
def make_answer(question,session_id,continuation): #回答作成関数(最上位関数) question:質問文,session_id:セッションID,continuation:True継続/False初回
if not continuation :
answer,system_prompt = exec_rag(question,similarity=0.65)
else:
answer,system_prompt = continuous_genarate(question,session_id)
#exec_time = '2025-02-5 11:11:11' #for non openai debug
exec_time = record_conversations(session_id,question,answer,system_prompt)
if exec_time :
return answer,system_prompt,exec_time
else:
return '**異常終了1**','**異常終了1**',''
def exec_rag(question,similarity):#question:質問(str),similarity:類似度(-1~1)
try:
# Create embeddings for the question
question_list = [f"query: {question}"] #GLuCoSEに送る前にquery:を追加しました。
print("Creating embeddings for the question")
q_embeddings = create_embeddings(question_list)
print("Question embeddings created")
# Query Pinecone for similar vectors
print("Querying Pinecone for similarity search")
#search_results = index.query(vector=q_embeddings, top_k=10, include_values=False, include_metadata=True)
# For File IDs (is_f=True)
search_results_f = index.query(vector=q_embeddings, top_k=5,filter={"is_f": True}, include_values=False, include_metadata=True)
# For Slack IDs (is_f not True OR missing)
search_results_s = index.query(
vector=q_embeddings,
top_k=5,
filter={
"$or": [
{"is_f": {"$ne": True}}, # is_f exists but is not True
{"is_f": {"$exists": False}} # is_f not set (old Slack vectors)
]
},
include_values=False,
include_metadata=True
)
# Filter search results based on similarity
context_candidates = []
work_list = []
filtered_f = [
[val['id'], val['score'], val['metadata']['doc']]
for val in search_results_f['matches']
if val['score'] >= similarity
]
filtered_s = [
[val['id'], val['score'], val['metadata']['doc']]
for val in search_results_s['matches']
if val['score'] >= similarity
]
# Sort only by ID (descending)
filtered_f.sort(key=lambda x: int(x[0][1:]), reverse=True)
filtered_s.sort(key=lambda x: int(x[0][1:]), reverse=True)
# Merge with F first, then S
context_candidates = filtered_f + filtered_s
# Build LLM context
llm_context = ''
view_use_id = '[PineconeID:'
for candidate in context_candidates:
llm_context += candidate[2] + '\n-----------------\n'
view_use_id += candidate[0] + ','
view_use_id += ' ]'
# Prepare the system prompt
system_prompt = textwrap.dedent("""
あなたは質問に回答するテクニカルセンターのチャットbotです。
以下のコンテクストを参考にして質問に回答して下さい。
コンテクストの中に質問に対する答えがない場合や、わからない場合、不確かな情報で回答しないでください。
わからない場合は正直に「わかりませんでした。CTCまでお問い合わせください。」と答えてください。
なお、回答文で”コンテクスト”という表現は使わずに、別の言い方に変えて下さい。”検索結果”が良いと思います。
回答文に「https://」や「http://」で始まるリンク箇所があればその部分はそのwebページを別ウィンドウで開くようにHTMLタグを追記してください。
回答文に「G:」で始まるGoogleドライブへのリンク箇所があればそのままにして変更しないでください。
## コンテクスト(開始) ##
{}
## コンテクスト(終了) ##
""").format(llm_context)
# Generate the response using OpenAI
print("Generating response using OpenAI GPT model")
response_gpt = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
]
)
# Extract the answer from the response
answer = response_gpt.choices[0].message.content + view_use_id #Varun edit20250220
return answer, system_prompt
except Exception as e:
print(f"Error in exec_rag: {e}")
raise
def continuous_genarate(question,session_id): #継続的な生成をする関数。question:質問(str),session_id:セッションID
llm_context = ''
dynamo_response = dynamodb_client.query(TableName=CONVO_HISTORY_TABLE_NAME,KeyConditionExpression= DN_P_KEY + '= :val',ExpressionAttributeValues={":val": {"S": session_id}},)
for idx,item in enumerate(dynamo_response['Items']):
llm_context = llm_context +'[時刻]'+item['exec_time']['S']+'----------\n'
llm_context = llm_context +'[利用者]\n'
if idx == 0 : #system_promptの前処理:1回目は会話履歴が無いので、そのまま利用。
system_prompt_val = item['system_prompt']['S']
else : #system_promptの前処理:前回以前の会話履歴は冗長かつ促進文も今回与える意味は無さそう。以下固定文を残す、とした。
system_prompt_val = 'あなたは質問に回答するチャットbotです。'
llm_context = llm_context +' <プロンプト>\n'+system_prompt_val+'\n'
llm_context = llm_context +' <質問>\n'+item['question']['S']+'\n'
llm_context = llm_context +'[生成AI]\n'+item['answer']['S']+'\n'
system_prompt = textwrap.dedent("""\
あなたは質問に回答するテクニカルセンターのチャットbotです。
以下の会話履歴は、あなた(生成AI)と利用者の最近の会話内容です。
これを参考にして質問に回答して下さい。
会話内容の中に存在するコンテクストに質問に対する答えが無い場合は、一般論として、ヒントとなる情報を回答して下さい。
その際は、憶測の情報である事を述べる言葉を必ず使って下さい。
わからない場合は正直に「わかりませんでした。CTCまでお問い合わせください。」と答えてください。
回答文に「https://」や「http://」で始まるリンク箇所があればその部分はそのwebページを別ウィンドウで開くようにHTMLタグを追記してください。
回答文に「G:」で始まるGoogleドライブへのリンク箇所があればそのままにして変更しないでください。
## 会話履歴(開始) ############################################################
{}
## 会話履歴(終了) ############################################################
""").format(llm_context)
response_gpt = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
],
#temperature=1
)
answer = response_gpt.choices[0].message.content
#answer ="継続会話部 trial" #for non openai debug
return answer,system_prompt
################### ユーザ評価 Handler ###################
# ◇◇◇ユーザ評価保存部◇◇◇
def handle_user_rev(event,context):
print("processing user review")
#body部のデコード
request_body = decode_body(event['body']) if event.get('body') else {}
print("user rev decoded_body",request_body)
session_id = ''
#パラメータの受理(clear,useful)
useful,clear = '0','0'
if event.get('queryStringParameters'):
useful,clear = event.get('queryStringParameters').get('useful'),event.get('queryStringParameters').get('clear')
elif event.get('body'):
useful,clear = request_body.get('useful'),request_body.get('clear')
print("Processing User review")
if 'cookies' in event:
cookie_header = event['headers']['cookie']
print(f"Processing User review Cookie header from headers: {cookie_header}")
C = cookies.SimpleCookie()
C.load(cookie_header)
cookie_dict = {k: v.value for k, v in C.items()}
if COOKIE_KEY not in cookie_dict:
session_id = str(uuid.uuid4())
print('During user rev IS_cookies:NO!! ')
else:
session_id = cookie_dict[COOKIE_KEY]
print('During user rev COOKIE_KEY:Exist!!',session_id)
#print(f"Received Feedback: Useful={useful}")
if session_id:
#fq = get_first_question(session_id) #v1code
fq, f_ans = get_first_question_and_answer(session_id)
print(f"First question from 会話履歴DB: {fq}")
print(f"First answer from 会話履歴DB: {f_ans}")
print(f"Feedback recorded for session: {session_id}")
record_reviews(session_id,fq,f_ans,useful)
set_cookie_header_1 = (
f"{COOKIE_KEY}={session_id}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
set_cookie_header_2 = (
f"clear={clear}; Secure; Domain=.lambda-url.ap-northeast-1.on.aws; SameSite=None; Path=/"
)
return {
"statusCode": 200,
"body": json.dumps({"message": "Feedback received", "session_id": session_id}),
"headers": {
"Content-Type": "application/json",
'Set-Cookie': f"{set_cookie_header_1},{set_cookie_header_2}"
}
}
def get_first_question_and_answer(session_id):
"""Fetches the first question and first answer for the session."""
response = dynamodb_client.query(
TableName=CONVO_HISTORY_TABLE_NAME,
KeyConditionExpression=f"{DN_P_KEY} = :val",
ExpressionAttributeValues={":val": {"S": session_id}},
ScanIndexForward=True,
Limit=1
)
item = response.get("Items", [{}])[0] # Default to empty dict if no items found
return item.get("question", {}).get("S", "N/A"), item.get("answer", {}).get("S", "N/A")
def record_reviews(session_id,fq,f_ans,useful): #評価記録保存関数
exec_time = str((datetime.utcnow() + timedelta(hours=9)).strftime("%Y-%m-%d %H:%M:%S"))
dynamodb_client.put_item(TableName=REVIEW_TABLE_NAME,Item={
DN_P_KEY: {"S": session_id},
"question": {"S": fq},
"helpful": {"N": str(useful)},
"exec_time": {"S": exec_time},
"answer": {"S": f_ans} #v2で追加
})
dynamo_response = dynamodb_client.query(TableName=REVIEW_TABLE_NAME,KeyConditionExpression= DN_P_KEY + '= :val',ExpressionAttributeValues={":val": {"S": session_id}},)
print("dynamoDB output:",dynamo_response)
return exec_time・handle_user_rev()関数(行:496~548)の主な処理は以下の通りです:
- クライアントから送信された評価(useful)を受け取れます。
- 最初の質問と回答は会話履歴DB(ctcrag_chat_history)に保存済みのため、そこから取得します。
- 取得した内容と評価を、record_reviews()で評価記録用DB(ctcrag_user_review)に保存します。
- 保存後、画面を初期状態に戻します。
・上記の補足情報として、record_reviews関数(行:562~574)では、取得した内容に基づいて以下の情報が評価記録用DB(ctcrag_user_review)に保存されます:
-exec_time:評価が登録された日時
-session_id:評価対象となる会話の識別子
-question:ユーザーが投稿した最初の質問
-answer:その質問に対するAIの回答
-helpful:ユーザー評価(1=GOOD、2=BAD)
試用してみましょう!
画面は出来たので早速テストをしてみます。
・チャット画面にログイン後、質問を入力して送信ボタンを押します。


・数秒後に回答が表示され、下に評価ボタン部も確認できました。


・正しい回答だったので「👍」を押してみます。


・確認モーダルで「はい」を押すと、ユーザー評価は送信されました!その後初期状態にリセットされました。
・最後に、評価情報が評価記録用のテーブルに保存されていることを確認しました。

「helpful=1」で情報1件は入っていますね!完成!
まとめ
今回、チャット画面の改善版を作成し、ユーザーからの評価(GOOD/BAD)を収集・保存することができました。
上記ではあまり触れませんでしたが、今回の実装を通じて、セッションIDをCookieに保持し、その値をDynamoDBにも保存することで、ユーザーの質問とその評価を正確に紐づけることができました。
また、サーバー側Lambdaでは、Google認証・チャット応答・評価記録などの処理をlambda_handler内で用途ごとに分離することで、構成が整理され、エラー対応や修正がしやすくなることを実感しました。
そして、次はこの画面で蓄積された情報を確認できるように、管理者用の確認画面を作成します。詳しい内容は、別の記事でご紹介したいと思います。
ಮುಂದಿನ ಲೇಖನದಲ್ಲಿ ನಿಮ್ಮನ್ನು ಭೇಟಿಯಾಗೋಣ
(次の記事でお会いしましょう)
