[AI] LangGraph + mcp + flask 구현해보자.

늘 공부하는 괴짜·2025년 5월 28일
0

AI : LangGraph

목록 보기
3/3

1. python 코드

소스는 이전 파이썬 코드를 참조

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)

2. html 페이지

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>

3. css

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;
}

4. 페이지 접속

profile
인공지능이라는 옷을 입었습니다. 뭔가 멋지면서도 잘 맞습니다.

0개의 댓글