Skip to content

Commit 0f73a5a

Browse files
committed
챗봇 기능 및 테스트 페이지 추가
1 parent 34ce19f commit 0f73a5a

File tree

5 files changed

+365
-0
lines changed

5 files changed

+365
-0
lines changed

interface/app_pages/chatbot.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""
2+
AI ChatBot 페이지
3+
LangGraph와 OpenAI를 활용한 대화형 인터페이스
4+
"""
5+
6+
import os
7+
import streamlit as st
8+
9+
from utils.llm.chatbot import ChatBot
10+
11+
12+
def initialize_session_state():
13+
"""세션 상태 초기화 함수
14+
15+
Streamlit의 session_state를 사용하여 앱의 상태를 유지합니다.
16+
"""
17+
# 채팅 세션 시작 여부 플래그
18+
if "chatbot_started" not in st.session_state:
19+
st.session_state.chatbot_started = False
20+
# 채팅 메시지 기록 저장
21+
if "chatbot_messages" not in st.session_state:
22+
st.session_state.chatbot_messages = []
23+
24+
# OpenAI API 키 확인
25+
openai_api_key = st.session_state.get("OPEN_AI_KEY") or os.getenv("OPEN_AI_KEY")
26+
27+
if not openai_api_key:
28+
st.error(
29+
"⚠️ OpenAI API 키가 설정되지 않았습니다. 설정 > LLM에서 OpenAI API 키를 입력해주세요."
30+
)
31+
st.stop()
32+
33+
# ChatBot 인스턴스 생성 (OpenAI API 키 사용)
34+
if "chatbot_instance" not in st.session_state:
35+
st.session_state.chatbot_instance = ChatBot(openai_api_key)
36+
37+
38+
# 세션 상태 초기화 실행
39+
initialize_session_state()
40+
41+
# 페이지 제목
42+
st.title("🤖 AI ChatBot")
43+
44+
st.markdown(
45+
"""
46+
LangGraph 기반 AI ChatBot과 대화를 나눌 수 있습니다.
47+
- 날씨 정보 조회
48+
- 유명한 오픈소스 프로젝트 정보
49+
- 일반적인 질문과 대화
50+
"""
51+
)
52+
53+
# 사이드바 UI 구성
54+
with st.sidebar:
55+
st.markdown("### 🤖 ChatBot 설정")
56+
st.divider()
57+
58+
# LLM 모델 선택 드롭다운
59+
selected_model = st.selectbox(
60+
"LLM 모델",
61+
options=list(ChatBot.AVAILABLE_MODELS.keys()),
62+
format_func=lambda x: ChatBot.AVAILABLE_MODELS[x],
63+
key="chatbot_model_select",
64+
)
65+
66+
# 선택된 모델이 변경되면 ChatBot 업데이트
67+
if selected_model != st.session_state.chatbot_instance.model_name:
68+
st.session_state.chatbot_instance.update_model(selected_model)
69+
st.sidebar.success(
70+
f"모델이 '{ChatBot.AVAILABLE_MODELS[selected_model]}'로 변경되었습니다."
71+
)
72+
73+
st.divider()
74+
75+
# 채팅 세션 ID 입력 (대화 기록을 구분하는 용도)
76+
thread_id = st.text_input(
77+
"세션 ID",
78+
value="default",
79+
key="chatbot_thread_id",
80+
help="대화 기록을 구분하는 고유 ID입니다.",
81+
)
82+
83+
# 채팅 세션 시작/종료 버튼
84+
if not st.session_state.chatbot_started:
85+
# 세션이 시작되지 않았을 때: 시작 버튼 표시
86+
if st.button("대화 시작", use_container_width=True, type="primary"):
87+
st.session_state.chatbot_started = True
88+
st.session_state.chatbot_messages = []
89+
st.rerun()
90+
else:
91+
# 세션이 시작되었을 때: 종료 버튼 표시
92+
if st.button("대화 종료", use_container_width=True):
93+
st.session_state.chatbot_started = False
94+
st.rerun()
95+
96+
st.divider()
97+
98+
# 세션 히스토리를 JSON 형식으로 표시 (접힌 상태)
99+
with st.expander("대화 기록 (JSON)", expanded=False):
100+
st.json(st.session_state.chatbot_messages)
101+
102+
# 채팅 세션이 시작된 경우에만 채팅 인터페이스 표시
103+
if st.session_state.chatbot_started:
104+
# 첫 메시지가 없으면 환영 메시지 추가
105+
if not st.session_state.chatbot_messages:
106+
hello_message = "안녕하세요! 무엇을 도와드릴까요? 🤖"
107+
st.session_state.chatbot_messages = [
108+
{"role": "assistant", "content": hello_message}
109+
]
110+
111+
# 저장된 모든 메시지를 순서대로 표시
112+
for message in st.session_state.chatbot_messages:
113+
with st.chat_message(message["role"]):
114+
st.markdown(message["content"])
115+
116+
# 사용자 입력 처리
117+
if prompt := st.chat_input("메시지를 입력하세요"):
118+
# 사용자 메시지를 기록에 추가
119+
st.session_state.chatbot_messages.append({"role": "user", "content": prompt})
120+
with st.chat_message("user"):
121+
st.markdown(prompt)
122+
123+
# AI 응답 생성 및 표시
124+
with st.chat_message("assistant"):
125+
try:
126+
# ChatBot을 통해 응답 생성
127+
response = st.session_state.chatbot_instance.chat(prompt, thread_id)
128+
129+
# 응답 내용 추출
130+
response_content = response["messages"][-1].content
131+
132+
# 스트리밍 방식으로 응답 표시 (타이핑 효과)
133+
response_str = ""
134+
response_container = st.empty()
135+
for token in response_content:
136+
response_str += token
137+
response_container.markdown(response_str)
138+
139+
# AI 응답을 기록에 추가
140+
st.session_state.chatbot_messages.append(
141+
{"role": "assistant", "content": response_content}
142+
)
143+
except Exception as e:
144+
error_msg = f"오류가 발생했습니다: {str(e)}"
145+
st.error(error_msg)
146+
st.session_state.chatbot_messages.append(
147+
{"role": "assistant", "content": error_msg}
148+
)
149+
else:
150+
st.info("👈 왼쪽 사이드바에서 '대화 시작' 버튼을 눌러 ChatBot과 대화를 시작하세요!")

interface/pages_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
- 홈 페이지
99
- Lang2SQL 페이지
1010
- 그래프 빌더 페이지
11+
- ChatBot 페이지
12+
- 설정 페이지
1113
"""
1214

1315
import streamlit as st
@@ -16,5 +18,6 @@
1618
st.Page("app_pages/home.py", title="🏠 홈"),
1719
st.Page("app_pages/lang2sql.py", title="🔍 Lang2SQL"),
1820
st.Page("app_pages/graph_builder.py", title="📊 그래프 빌더"),
21+
st.Page("app_pages/chatbot.py", title="🤖 ChatBot"),
1922
st.Page("app_pages/settings.py", title="⚙️ 설정"),
2023
]

utils/llm/chatbot.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""
2+
LangGraph 기반 ChatBot 모델
3+
OpenAI의 ChatGPT 모델을 사용하여 대화 기록을 유지하는 챗봇 구현
4+
"""
5+
6+
from langchain_openai import ChatOpenAI
7+
from langgraph.checkpoint.memory import MemorySaver
8+
from langgraph.graph import START, MessagesState, StateGraph
9+
from langgraph.prebuilt import ToolNode
10+
11+
from utils.llm.tools import get_weather, get_famous_opensource
12+
13+
14+
class ChatBot:
15+
"""
16+
LangGraph를 사용한 대화형 챗봇 클래스
17+
OpenAI API를 통해 다양한 GPT 모델을 사용할 수 있으며,
18+
MemorySaver를 통해 대화 기록을 관리합니다.
19+
"""
20+
21+
# 사용 가능한 OpenAI 모델 목록 (키: 모델ID, 값: 표시명)
22+
AVAILABLE_MODELS = {
23+
"gpt-4o": "GPT-4o",
24+
"gpt-4o-mini": "GPT-4o Mini",
25+
"gpt-4-turbo": "GPT-4 Turbo",
26+
"gpt-3.5-turbo": "GPT-3.5 Turbo",
27+
}
28+
29+
def __init__(self, openai_api_key: str, model_name: str = "gpt-4o-mini"):
30+
"""
31+
ChatBot 인스턴스 초기화
32+
33+
Args:
34+
openai_api_key: OpenAI API 키
35+
model_name: 사용할 모델명 (기본값: gpt-4o-mini)
36+
"""
37+
self.openai_api_key = openai_api_key
38+
self.model_name = model_name
39+
self.tools = [get_weather, get_famous_opensource] # 사용 가능한 tool 목록
40+
self.llm = self._setup_llm() # LLM 인스턴스 설정
41+
self.app = self._setup_workflow() # LangGraph 워크플로우 설정
42+
43+
def _setup_llm(self):
44+
"""
45+
OpenAI ChatGPT LLM 인스턴스 생성
46+
Tool을 바인딩하여 LLM이 필요시 tool을 호출할 수 있도록 설정합니다.
47+
48+
Returns:
49+
ChatOpenAI: Tool이 바인딩된 LLM 인스턴스
50+
"""
51+
llm = ChatOpenAI(
52+
temperature=0.1, # 응답의 일관성을 위해 낮은 temperature 설정
53+
openai_api_key=self.openai_api_key,
54+
model_name=self.model_name,
55+
)
56+
# Tool을 LLM에 바인딩하여 함수 호출 기능 활성화
57+
return llm.bind_tools(self.tools)
58+
59+
def _setup_workflow(self):
60+
"""
61+
LangGraph 워크플로우 설정
62+
대화 기록을 관리하고 LLM과 통신하는 그래프 구조를 생성합니다.
63+
Tool 호출 기능을 포함하여 LLM이 필요시 도구를 사용할 수 있도록 합니다.
64+
65+
Returns:
66+
CompiledGraph: 컴파일된 LangGraph 워크플로우
67+
"""
68+
# MessagesState를 사용하는 StateGraph 생성
69+
workflow = StateGraph(state_schema=MessagesState)
70+
71+
def call_model(state: MessagesState):
72+
"""
73+
LLM 모델을 호출하는 노드 함수
74+
LLM이 응답을 생성하거나 tool 호출을 결정합니다.
75+
76+
Args:
77+
state: 현재 메시지 상태
78+
79+
Returns:
80+
dict: LLM 응답이 포함된 상태 업데이트
81+
"""
82+
# sys_msg = SystemMessage(content="You are a helpful assistant ")
83+
response = self.llm.invoke(state["messages"])
84+
return {"messages": response}
85+
86+
def route_model_output(state: MessagesState):
87+
"""
88+
LLM 출력에 따라 다음 노드를 결정하는 라우팅 함수
89+
Tool 호출이 필요한 경우 'tools' 노드로, 아니면 대화를 종료합니다.
90+
91+
Args:
92+
state: 현재 메시지 상태
93+
94+
Returns:
95+
str: 다음에 실행할 노드 이름 ('tools' 또는 '__end__')
96+
"""
97+
messages = state["messages"]
98+
last_message = messages[-1]
99+
# LLM이 tool을 호출하려고 하는 경우 (tool_calls가 있는 경우)
100+
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
101+
return "tools"
102+
# Tool 호출이 없으면 대화 종료
103+
return "__end__"
104+
105+
# 워크플로우 구조 정의
106+
workflow.add_edge(START, "model") # 시작 -> model 노드
107+
workflow.add_node("model", call_model) # LLM 호출 노드
108+
workflow.add_node("tools", ToolNode(self.tools)) # Tool 실행 노드
109+
110+
# model 노드 이후 조건부 라우팅
111+
workflow.add_conditional_edges("model", route_model_output)
112+
# Tool 실행 후 다시 model로 돌아가서 최종 응답 생성
113+
workflow.add_edge("tools", "model")
114+
115+
# MemorySaver를 사용하여 대화 기록 저장 기능 추가
116+
return workflow.compile(checkpointer=MemorySaver())
117+
118+
def chat(self, message: str, thread_id: str):
119+
"""
120+
사용자 메시지에 대한 응답 생성
121+
122+
Args:
123+
message: 사용자 입력 메시지
124+
thread_id: 대화 세션을 구분하는 고유 ID
125+
126+
Returns:
127+
dict: LLM 응답을 포함한 결과 딕셔너리
128+
"""
129+
return self.app.invoke(
130+
{"messages": [{"role": "user", "content": message}]},
131+
{"configurable": {"thread_id": thread_id}}, # thread_id로 대화 기록 관리
132+
)
133+
134+
def update_model(self, model_name: str):
135+
"""
136+
사용 중인 LLM 모델 변경
137+
모델 변경 시 LLM 인스턴스와 워크플로우를 재설정합니다.
138+
139+
Args:
140+
model_name: 변경할 모델명
141+
"""
142+
self.model_name = model_name
143+
self.llm = self._setup_llm() # 새 모델로 LLM 재설정
144+
self.app = self._setup_workflow() # 워크플로우 재생성

utils/llm/tools/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
set_gms_server,
55
)
66

7+
from utils.llm.tools.test import get_weather, get_famous_opensource
8+
79
__all__ = [
810
"set_gms_server",
911
"get_info_from_db",
1012
"get_metadata_from_db",
13+
"get_weather",
14+
"get_famous_opensource",
1115
]

utils/llm/tools/test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
LangGraph ChatBot에서 사용하는 도구(Tool) 함수들
3+
"""
4+
5+
from langchain_core.tools import tool
6+
7+
8+
@tool
9+
def get_weather(city: str) -> str:
10+
"""
11+
특정 도시의 현재 날씨 정보를 조회합니다.
12+
13+
이 함수는 도시 이름을 입력받아 해당 도시의 날씨 정보를 반환합니다.
14+
사용자가 날씨, 기상, weather 등의 키워드와 함께 도시 이름을 언급하면 이 도구를 사용하세요.
15+
16+
Args:
17+
city (str): 날씨를 확인하고 싶은 도시의 이름입니다.
18+
예: "Seoul", "New York", "Tokyo", "서울", "부산" 등
19+
영문과 한글 도시명을 모두 지원합니다.
20+
21+
Returns:
22+
str: 해당 도시의 날씨 정보를 담은 문자열입니다.
23+
현재는 항상 맑은 날씨를 반환합니다.
24+
25+
Examples:
26+
>>> get_weather("Seoul")
27+
'Seoul is sunny'
28+
29+
>>> get_weather("서울")
30+
'서울 is sunny'
31+
32+
Note:
33+
이 도구는 다음과 같은 경우에 사용하세요:
34+
- "서울 날씨 어때?"
35+
- "What's the weather in New York?"
36+
- "도쿄의 날씨를 알려줘"
37+
- "부산 날씨 확인해줘"
38+
"""
39+
return f"{city} is sunny"
40+
41+
42+
@tool
43+
def get_famous_opensource() -> str:
44+
"""
45+
가장 유명한 오픈소스 프로젝트를 조회합니다.
46+
47+
이 함수는 현재 가장 유명한 오픈소스 프로젝트의 이름을 반환합니다.
48+
사용자가 유명한 오픈소스, 인기있는 오픈소스, 최고의 오픈소스 등을 물어보면 이 도구를 사용하세요.
49+
50+
Returns:
51+
str: 가장 유명한 오픈소스 프로젝트 이름
52+
53+
Examples:
54+
>>> get_famous_opensource()
55+
'Lang2SQL'
56+
57+
Note:
58+
이 도구는 다음과 같은 경우에 사용하세요:
59+
- "제일 유명한 오픈소스가 뭐야?"
60+
- "가장 인기있는 오픈소스는?"
61+
- "최고의 오픈소스 프로젝트 알려줘"
62+
- "유명한 오픈소스 추천해줘"
63+
"""
64+
return "Lang2SQL"

0 commit comments

Comments
 (0)