앞에서 미처 구현하지 못한, 비교적 작은 기능들 몇 가지를 추가한 기록들이다.
사용자가 서비스에 접속하면 "어떤 이야기를 나누고 싶나요?"라는 웰컴 메시지가 바로 표시되도록 기획하였다.

이때 DDB에 해당 메시지를 저장해 두어서 이것이 실제 대화의 첫 메시지가 되어, 과거 대화를 불러오면 처음에 이 웰컴 메시지가 포함되어 있도록 하고자 했다.
하지만 페이지 로드 시마다 세션을 만들고 연결한 후 웰컴 메시지를 표시하는 작업이 과도한 연결과 DDB I/O를 일으킬 수있다는 점, 또 페이지 로딩을 지연시킬 수 있는 점이 우려되었다. 따라서 다음과 같이 해결하였다:
1. 실제 대화가 이루어지기 전까지는 별도의 세션 연결을 하지 않는다. 그때까 웰컴 메시지 버블을 단순 element로만 삽입한다(A).
2. 세션에 성공적으로 연결될 경우 해당 요소를 제거하고, 만약 새 세션을 시작한 경우 해당 메시지를 실제 DDB 메모리에 삽입한다.
3. DDB 로드가 완료될 경우 (A)를 제거하고 DDB에 있는 세션 내역을 불러온다. 실제로는 웰컴 메시지가 바꿔치기되는 셈이지만 사용자에게는 해당 변화가 보이지 않는다.
displayWelcomeMessage 함수는 채팅 영역이 초기화되는 경우(page initialize, 세션 삭제 등) 실행된다.
function displayWelcomeMessage() {
const chatBox = document.getElementById('chatBox');
chatBox.innerHTML = '<div class="message ai-message temporary-welcome"><div class="message-content">어떤 이야기를 하고 싶나요?</div></div>';
}
한편 새 대화 시작 시에도, 세션 연결 전까지 동일한 말풍선을 생성하고, 세션에 연결되면 이를 제거한다.
async function startNewChat() {
(중략)
document.getElementById('chatBox').innerHTML =
'<div class="message ai-message temporary-welcome"><div class="message-content">어떤 이야기를 하고 싶나요?</div></div>';
(중략)
// (세션 연결 성공 후)
if (result.welcomeMessage) {
const tempWelcome = document.querySelector('.temporary-welcome');
if (tempWelcome) {
tempWelcome.remove();
}
appendMessage('ai', result.welcomeMessage);
} // DDB에 저장된 실제 웰컴 메시지 호출
(이하 생략)
tarotchat_newsession
js에서 정의된 welcomeMessage는 다음과 같이 lambda handler에서 리턴되어 Bedrock에 전달된다.
welcome_message = "어떤 이야기를 하고 싶나요?"
return {
'statusCode': 200,
'body': json.dumps({
'sessionId': session_id,
'createdAt': created_at,
'sessionName': session_name,
'welcomeMessage': welcome_message
}),
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
또 후술한 create_new_session 부분에서 해당 메시지를 table에 넣을 수 있도록 하였다.
def create_new_session(user_id):
(중간 생략)
item = {
'UserId': user_id,
'SessionId': session_id,
'CreatedAt': current_time,
'LastUpdatedAt': current_time,
'SessionName': session_name,
'History': json.dumps([{
"type": "ai",
"content": welcome_message
}])
}
table.put_item(Item=item)
return ...
사용자가 개별 대화를 삭제할 수 있도록 한다. 사용자 입장에서의 흐름은 크게 아래와 같다:
1. 사이드바에서 세션 목록에 마우스오버 시 삭제 아이콘이 나타난다.

2. 해당 버튼 클릭 시 삭제 여부를 묻는 모달이 나타난다.

3. 삭제 모달 내에서 삭제 확인 버튼을 누를 경우 모달이 삭제된다.
일단 삭제 모달을 띄우는 showDeleteModal 함수부터 살펴보자. 해당 모달이 표시되면 현재 session ID를 sessionToDelete로 지정한다.
function showDeleteModal(sessionId) {
sessionToDelete = sessionId;
const modal = document.getElementById('deleteSessionModal');
modal.style.display = 'block';
}
한편 모달의 삭제 확인 버튼에 event listener를 연결, 삭제 버튼 시 동작하는 deleteSession 함수를 생성하였다.
function initializeEventListeners() {
(중략)
const deleteModal = document.getElementById('deleteSessionModal');
if (deleteModal) {
const closeBtn = deleteModal.querySelector('.close');
const confirmBtn = document.getElementById('confirmDelete');
const cancelBtn = document.getElementById('cancelDelete');
if (closeBtn) closeBtn.onclick = closeDeleteModal;
if (confirmBtn) confirmBtn.onclick = deleteSession;
if (cancelBtn) cancelBtn.onclick = closeDeleteModal;
세션 삭제 후 바로 세션 목록을 다시 불러오는 로직을 구현하였지만 예상보다 시간이 오래 걸리는 관계로 약간의 트릭을 썼다. 웹 페이지에서 해당 세션 element를 먼저 삭제, 사용자에게는 세션이 바로 삭제되는 것처럼 보이게 하고 그 후에 백엔드에서 세션 목록을 갱신하는 것.
async function deleteSession() {
if (!sessionToDelete) return; // 삭제할 세션이 없다면 리턴값 없이 종료
try {
const sessionElement = document.querySelector(`.session-item[data-session-id="${sessionToDelete}"]`);
if (sessionElement) {
sessionElement.remove();
}
if (currentSessionId === sessionToDelete) {
currentSessionId = null;
document.getElementById('chatBox').innerHTML = '';
displayWelcomeMessage();
}
await ensureValidToken();
const idToken = localStorage.getItem('auth_token');
const response = await fetch(`${config.restEndpoint}/sessions/${sessionToDelete}?userId=${userId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${idToken}`,
'Content-Type': 'application/json'
},
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to delete session');
}
fetchSessions(userId).catch(error => {
console.error('Error refreshing sessions:', error);
});
} catch (error) {
console.error('Error deleting session:', error);
await fetchSessions(userId);
alert('세션 삭제 중 오류가 발생했습니다. 다시 시도해 주세요.');
} finally {
closeDeleteModal();
sessionToDelete = null;
}
}
개별 대화를 삭제하는 Lambda 함수를 추가하였다. 해당 함수에 부여된 IAM role은 나머지 함수들과 동일하므로 생략하겠다.
tarotchat_deletesession
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('tarotchat_ddb')
def lambda_handler(event, context):
먼저 session id, user id를 찾고 이를 테이블에서 삭제한다.
try:
session_id = event.get('pathParameters', {}).get('sessionId')
user_id = event.get('queryStringParameters', {}).get('userId')
#해당 아이템을 테이블에서 삭제
response = table.delete_item(
Key={
'UserId': user_id,
'SessionId': session_id
}
)
삭제 성공 시 200을 반환, 실패 시 500을 반환한다.
return {
'statusCode': 200,
'body': json.dumps('Session deleted successfully'),
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
except Exception as e:
return {
'statusCode': 500,
'body': json.dumps({'error': str(e)}),
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true'
}
}
REST API에 {sessionid}/DELTE 메소드를 추가하고 위에서 생성한 함수를 연결하였다.

기존 tarotchat_sendmessage함수에서는, 새 세션을 만들면 세션을 만든 시간을 그대로 세션명으로 사용하였다.
body = json.loads(event['body'])
user_id = body['userId']
session_id = str(uuid.uuid4())
current_time = datetime.now().isoformat()
item = {
'UserId': user_id,
'SessionId': session_id,
'CreatedAt': current_time,
'LastUpdatedAt': current_time,
'SessionName': f"Session {current_time}" # 현재 시간을 이용한 세션 이름 생성
}
table.put_item(Item=item)
# 리턴값 생략
이를 다음과 같이 개선하였다.
tarotchat_newsession
먼저 '새 대화'라는 이름의 세션이 생성되도록 create_new_session 함수를 추가하였다.
def create_new_session(user_id):
session_id = str(uuid.uuid4())
current_time = datetime.now().isoformat()
session_name = "새 대화"
item = {
'UserId': user_id,
'SessionId': session_id,
'CreatedAt': current_time,
'LastUpdatedAt': current_time,
'SessionName': session_name,
'History': json.dumps([{
"type": "ai",
"content": welcome_message
}])
}
table.put_item(Item=item)
return session_id, current_time, session_name
이어서 Lambda handler에서 해당 함수를 사용하는 구조이다.
def lambda_handler(event, context):
try:
body = json.loads(event['body'])
user_id = body.get('userId')
session_id, created_at, session_name = create_new_session(user_id)
# 리턴값 생략
except:
# 리턴값 생략
tarotchat_sendmessage
먼저 세션명을 생성하여 리턴하는 함수를 만들었다. 세션명 생성에는 Bedrock API를 사용하였고, 여기에는 경량화된 모델이 적합하다고 생각해서 Claude 3 Haiku FM을 사용하였다.
def generate_session_name(user_message):
try:
response = bedrock_runtime.invoke_model(
modelId='anthropic.claude-3-haiku-20240307-v1:0',
body=json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1000,
"temperature": 0.1,
"messages": [
{
"role": "user",
"content": f"사용자의 다음 고민을 요약해서 20자 이내의 대화 세션 이름을 생성해 줘. 따옴표를 쓰지 말아 줘: {user_message}"
}
]
})
)
# Bedrock의 response 파싱한 후 리턴
response_body = json.loads(response['body'].read())
session_name = response_body['content'][0]['text'].strip()
return session_name
except Exception as e: # 에러 발생 시 예외 처리
return "새 대화"
이어서 새 세션명을 갖고 DDB의 해당 아이템을 업데이트하는 함수와,
def update_session_name(user_id, session_id, new_name):
response = table.update_item(
Key={'UserId': user_id, 'SessionId': session_id},
UpdateExpression="SET SessionName = :name",
ExpressionAttributeValues={':name': new_name}
)
이를 APIGW를 통해 프론트로 보내는 함수를 만들었다.
def send_session_name_update(connection_id, new_name):
gateway_client.post_to_connection(
ConnectionId=connection_id,
Data=json.dumps({"type": "session_name_update", "name": new_name}).encode('utf-8')
)
그리고 lambda handler 안에서 조건문을 통해 해당 함수들이 트리거되도록 하였다.
히스토리에 존재하는 메시지의 길이가 1인 경우 (에이전트의 웰컴 메시지만 DDB에 존재하는 경우) 동작하도록 하였으며 간혹 빈 메시지가 오전송되었을 때 불필요하게 동작하는 경우가 존재해서, 사용자의 메시지가 공백이 아닌 경우를 조건으로 덧붙였다.
여담이지만 이 경우 claude는 '~~은 어떨까요?' 라며 어떻게든 세션 이름을 제안해주려 애를 썼다.
def lambda_handler(event, context):
(중략)
if len(existing_messages) == 1 and user_message != "":
new_session_name = generate_session_name(user_message)
update_session_name(user_id, session_id, new_session_name)
send_session_name_update(connection_id, new_session_name)
빈 세션을 생성한 이후 더 이상 대화를 하지 않고 다른 대화로 넘어가는 경우, 생성된 빈 대화세션은 자동으로 삭제되도록 하였다.
disconnectcurrentsession 함수에서 대화 내용을 확인하고 조건에 부합하면 삭제하는 로직을 추가하였다.
async function disconnectCurrentSession() {
if (socket) {
socket.close();
확인을 위해 백엔드에서부터 세션의 대화 내역을 가져온다.
try {
const response = await fetch(`${config.restEndpoint}/sessions/${currentSessionId}?userId=${userId}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch session data');
}
const messages = await response.json();
이렇게 불러온 메시지가 빈 대화인지 (세션을 연결하면서 삽입된 웰컴 메시지 하나만 있는지) 확인하고,
if (Array.isArray(messages) &&
messages.length === 1 &&
messages[0].type === 'ai' &&
messages[0].content === "어떤 이야기를 하고 싶나요?") {
해당 조건이 참일 경우 세션 삭제를 수행한다.
일단 UI에서 먼저 해당 요소 제거하고, 그 후에 백그라운드 작업으로 API를 호출하여 실제 세션 삭제를 수행한다.
const sessionToRemove = document.querySelector(`.session-item[data-session-id="${currentSessionId}"]`);
if (sessionToRemove) {
sessionToRemove.remove();
}
console.log('Empty session deleted.');
fetch(`${config.restEndpoint}/sessions/${currentSessionId}?userId=${userId}`, {
method: 'DELETE',
credentials: 'include'
}).catch(error => {
에러 발생 시에는 세션을 다시 불러와서 실제 세션 목록과 차이가 발생하는 것을 막는다.
console.error('빈 대화 삭제 실패:', error);
fetchSessions(userId);
});
}
} catch (error) {
console.error('세션 연결해제 실패:', error);
}
}
}
다만 현재 앱에서는 빈 세션을 생성한 후에 바로 앱을 종료할 경우 이를 삭제할 방법이 없는데,
이는 추후 백엔드에서 정기적으로 빈 대화를 삭제하는 형태로 보완하고자 한다.
지금까지는 '새 대화 시작'버튼을 통해서만 세션이 시작되었는데, 메인 화면에서 대화를 입력할 경우에도 바로 세션 생성이 가능하도록 세션 생성과 연결을 동시 수행하는 함수를 만들었다.
async function createAndConnectNewSession(initialMessage) {
try {
우선 REST API의 POST 메소드를 통해 새 세션을 생성한다. 연결된 함수는 tarotchat_newsession이다.
const response = await fetch(`${config.restEndpoint}/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ userId: userId })
});
const result = await response.json();
연결에 성공하면 chatbox에 남아 있는 웰컴 메시지를 제거하고 세션 새로고침을 수행한다.
if (response.ok) {
currentSessionId = result.sessionId;
document.getElementById('chatBox').innerHTML = '';
fetchSessions(userId);
if (result.welcomeMessage) {
appendMessage('ai', result.welcomeMessage);
}
이제 웹소켓 연결을 수행하여 대화를 나눌 수 있도록 한다.
await connectWebSocket();
if (initialMessage) {
sendMessageToCurrentSession(initialMessage);
}
} else {
세션 생성에 실패할 경우 실패 메시지를 추가해 사용자에게 알리도록 하였다.
console.error('Error creating new session:', result.error);
appendMessage('ai', '세션 생성에 실패했어요. 증상이 계속되면 제작자에게 문의해주세요.')
}
} catch (error) {
console.error('Error creating new session:', error);
appendMessage('ai', '세션 생성에 실패했어요. 증상이 계속되면 제작자에게 문의해주세요.')
}
}