소스는 이전 파이썬 코드를 참조
state 를 세션으로 잘 버무려 보았다.
주석은 chatgpt 에세 정중하게 요청하였다.
import re
from typing import TypedDict, Optional
from pydantic import BaseModel
from flask import Flask, render_template, request, jsonify
from langchain_core.tools import Tool as LangGraphTool
from langgraph.graph import StateGraph, END
# Flask 애플리케이션 초기화
app = Flask(__name__)
# ✅ 에이전트 상태 정의 (입력, 출력, 대화 내역, 현재 단계)
class AgentState(TypedDict):
input: str
output: Optional[str]
chat_history: list
step: Optional[str] # 인증 프로세스 단계 (이름 → 전화번호 → 주민번호)
# ✅ 도구 입력 스키마 정의 (pydantic 사용)
class AuthInput(BaseModel):
auth: Optional[str] = None
class AuthNameInput(BaseModel):
name: Optional[str] = None
class AuthPhoneInput(BaseModel):
phone: Optional[str] = None
class AuthRrnInput(BaseModel):
rrn: Optional[str] = None
# ✅ 인증 단계별 응답 함수 정의
def set_auth(keyword: Optional[str] = None):
return "본인 인증을 위해 이름을 입력해주세요."
def set_auth_name(name: Optional[str] = None):
return f"{name}님, 본인 인증을 위해 전화번호를 입력해주세요."
def set_auth_phone(phone: Optional[str] = None):
# 전화번호 형식 유효성 검사
if not phone or not re.match(r"^010[-]?\d{4}[-]?\d{4}$", phone):
return {"result": False, "message": "유효한 전화번호 형식이 아닙니다. 예: 01012345678"}
return {"result": True, "message": f"{phone}로 전화번호 인증이 완료되었습니다. 주민등록번호를 입력해주세요."}
def set_auth_rrn(rrn: Optional[str] = None):
# 주민등록번호 형식 유효성 검사
if not rrn or not re.match(r"^\d{6}-?\d{7}$", rrn):
return {"result": False, "message": "⚠️ 주민등록번호 형식이 올바르지 않습니다. 예: 900101-1234567"}
return {"result": True, "message": "✅ 본인 인증이 완료되었습니다. 감사합니다!"}
# ✅ LangGraph 도구 래핑 (함수들을 LangGraph 도구로 등록)
auth_tool = LangGraphTool.from_function(
name="set_auth",
description="본인인증 명령어를 입력받는 도구입니다.",
func=set_auth,
args_schema=AuthInput,
)
auth_name_tool = LangGraphTool.from_function(
name="set_auth_name",
description="이름을 받아서 저장하는 도구입니다.",
func=set_auth_name,
args_schema=AuthNameInput,
)
auth_phone_tool = LangGraphTool.from_function(
name="set_auth_phone",
description="전화번호를 받아서 저장하는 도구입니다.",
func=set_auth_phone,
args_schema=AuthPhoneInput,
)
auth_rrn_tool = LangGraphTool.from_function(
name="set_auth_rrn",
description="주민등록번호를 받아서 저장하는 도구입니다.",
func=set_auth_rrn,
args_schema=AuthRrnInput,
)
# ✅ 각 도구 실행 노드 정의 (LangGraph용 함수)
def auth_exec_tool(state: AgentState):
output = auth_tool.invoke({"auth": state["input"]})
return {
"input": "",
"output": output,
"chat_history": state["chat_history"] + [("user", state["input"]), ("ai", output)],
"step": "auth_name", # 다음 단계로 이동
}
def auth_name_exec_tool(state: AgentState):
output = auth_name_tool.invoke({"name": state["input"]})
return {
"input": "",
"output": output,
"chat_history": state["chat_history"] + [("user", state["input"]), ("ai", output)],
"step": "auth_phone", # 다음 단계로 이동
}
def auth_phone_exec_tool(state: AgentState):
result = auth_phone_tool.invoke({"phone": state["input"]})
return {
"input": "",
"output": result["message"],
"chat_history": state["chat_history"] + [("user", state["input"]), ("ai", result["message"])],
"step": "auth_rrn" if result["result"] else "auth_phone", # 실패 시 반복
}
def auth_rrn_exec_tool(state: AgentState):
result = auth_rrn_tool.invoke({"rrn": state["input"]})
return {
"input": "",
"output": result["message"],
"chat_history": state["chat_history"] + [("user", state["input"]), ("ai", result["message"])],
"step": None if result["result"] else "auth_rrn" # 완료되면 종료
}
# ✅ 현재 상태에 따라 다음 실행할 노드 결정
def route_auth_exec_tool(state: AgentState):
if state.get("step") == "auth_name":
return "auth_name_exec_tool"
elif state.get("step") == "auth_phone":
return "auth_phone_exec_tool"
elif state.get("step") == "auth_rrn":
return "auth_rrn_exec_tool"
elif state["input"] == "본인인증":
return "auth_exec_tool"
return "end" # 기본 종료
# ✅ 라우터 노드: 상태를 그대로 전달
def router(state: AgentState) -> AgentState:
return state
# ✅ LangGraph 상태 그래프 빌더 구성
builder = StateGraph(AgentState)
# 노드 등록
builder.add_node("router", router)
builder.add_node("auth_exec_tool", auth_exec_tool)
builder.add_node("auth_name_exec_tool", auth_name_exec_tool)
builder.add_node("auth_phone_exec_tool", auth_phone_exec_tool)
builder.add_node("auth_rrn_exec_tool", auth_rrn_exec_tool)
builder.set_entry_point("router") # 시작점 설정
# 조건부 분기 설정
builder.add_conditional_edges("router", route_auth_exec_tool, {
"auth_exec_tool": "auth_exec_tool",
"auth_name_exec_tool": "auth_name_exec_tool",
"auth_phone_exec_tool": "auth_phone_exec_tool",
"auth_rrn_exec_tool": "auth_rrn_exec_tool",
"end": END,
})
# 각 노드 실행 후 종료 처리
builder.add_edge("auth_exec_tool", END)
builder.add_edge("auth_name_exec_tool", END)
builder.add_edge("auth_phone_exec_tool", END)
builder.add_edge("auth_rrn_exec_tool", END)
# 그래프 컴파일
graph = builder.compile()
# ✅ 사용자별 세션 저장소 (메모리 기반)
sessions = {}
# ✅ 기본 페이지 렌더링
@app.route("/")
def index():
return render_template("index.html")
# ✅ 챗 인터페이스 처리 라우트
@app.route("/chat", methods=["POST"])
def chat():
user_input = request.json.get("message")
session_id = request.json.get("session_id", "default_session") # 기본 세션 ID 사용
# 세션 초기화 (처음 요청 시)
if session_id not in sessions:
sessions[session_id] = {
"chat_history": [],
"step": None,
}
current_session = sessions[session_id]
# 현재 상태 구성
state = {
"input": user_input,
"output": None,
"chat_history": current_session["chat_history"],
"step": current_session["step"],
}
try:
# LangGraph 실행
result = graph.invoke(state)
# 세션 업데이트
current_session["chat_history"] = result["chat_history"]
current_session["step"] = result.get("step")
response_message = result["output"]
except Exception as e:
response_message = f"❗ 오류 발생: {str(e)}"
return jsonify({"response": response_message, "session_id": session_id})
# ✅ 서버 실행 (디버그 모드)
if __name__ == "__main__":
app.run(debug=True)
localStorage 또는 sessionStorage 를 사용하여 세션관리
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LangGraph Chatbot</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="chat-container">
<div class="chat-display" id="chat-display">
<div class="message ai-message">🤖 AI: 안녕하세요! 무엇을 도와드릴까요?</div>
</div>
<div class="chat-input">
<input type="text" id="user-input" placeholder="메시지를 입력하세요...">
<button id="send-button">보내기</button>
</div>
</div>
<script>
const chatDisplay = document.getElementById('chat-display');
const userInput = document.getElementById('user-input');
const sendButton = document.getElementById('send-button');
let sessionId = localStorage.getItem('sessionId'); // Get or create a session ID
if (!sessionId) {
sessionId = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
localStorage.setItem('sessionId', sessionId);
}
function appendMessage(sender, message) {
const messageElement = document.createElement('div');
messageElement.classList.add('message');
messageElement.classList.add(sender + '-message');
messageElement.innerHTML = `${sender === 'user' ? '🧑💻 You' : '🤖 AI'}: ${message}`;
chatDisplay.appendChild(messageElement);
chatDisplay.scrollTop = chatDisplay.scrollHeight; // Auto-scroll to bottom
}
async function sendMessage() {
const message = userInput.value.trim();
if (message === '') return;
appendMessage('user', message);
userInput.value = '';
try {
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message, session_id: sessionId }),
});
const data = await response.json();
appendMessage('ai', data.response);
// Update session_id if it changes (though for this app, it's consistent)
sessionId = data.session_id;
localStorage.setItem('sessionId', sessionId);
} catch (error) {
console.error('Error sending message:', error);
appendMessage('ai', '메시지를 보내는 중 오류가 발생했습니다.');
}
}
sendButton.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.chat-container {
width: 100%;
max-width: 600px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
height: 80vh;
}
.chat-display {
flex-grow: 1;
padding: 20px;
overflow-y: auto;
border-bottom: 1px solid #eee;
}
.message {
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 18px;
max-width: 70%;
word-wrap: break-word;
}
.user-message {
background-color: #dcf8c6;
align-self: flex-end;
margin-left: auto;
text-align: right;
}
.ai-message {
background-color: #e0e0e0;
align-self: flex-start;
margin-right: auto;
text-align: left;
}
.chat-input {
display: flex;
padding: 15px;
border-top: 1px solid #eee;
}
.chat-input input {
flex-grow: 1;
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 20px;
margin-right: 10px;
font-size: 16px;
}
.chat-input button {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 20px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s;
}
.chat-input button:hover {
background-color: #45a049;
}