From 0f73a5aadea5d49484a67229c304fa17f3a99338 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Sun, 19 Oct 2025 19:27:09 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=EC=B1=97=EB=B4=87=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface/app_pages/chatbot.py | 150 +++++++++++++++++++++++++++++++++ interface/pages_config.py | 3 + utils/llm/chatbot.py | 144 +++++++++++++++++++++++++++++++ utils/llm/tools/__init__.py | 4 + utils/llm/tools/test.py | 64 ++++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 interface/app_pages/chatbot.py create mode 100644 utils/llm/chatbot.py create mode 100644 utils/llm/tools/test.py diff --git a/interface/app_pages/chatbot.py b/interface/app_pages/chatbot.py new file mode 100644 index 0000000..357707c --- /dev/null +++ b/interface/app_pages/chatbot.py @@ -0,0 +1,150 @@ +""" +AI ChatBot 페이지 +LangGraph와 OpenAI를 활용한 대화형 인터페이스 +""" + +import os +import streamlit as st + +from utils.llm.chatbot import ChatBot + + +def initialize_session_state(): + """세션 상태 초기화 함수 + + Streamlit의 session_state를 사용하여 앱의 상태를 유지합니다. + """ + # 채팅 세션 시작 여부 플래그 + if "chatbot_started" not in st.session_state: + st.session_state.chatbot_started = False + # 채팅 메시지 기록 저장 + if "chatbot_messages" not in st.session_state: + st.session_state.chatbot_messages = [] + + # OpenAI API 키 확인 + openai_api_key = st.session_state.get("OPEN_AI_KEY") or os.getenv("OPEN_AI_KEY") + + if not openai_api_key: + st.error( + "⚠️ OpenAI API 키가 설정되지 않았습니다. 설정 > LLM에서 OpenAI API 키를 입력해주세요." + ) + st.stop() + + # ChatBot 인스턴스 생성 (OpenAI API 키 사용) + if "chatbot_instance" not in st.session_state: + st.session_state.chatbot_instance = ChatBot(openai_api_key) + + +# 세션 상태 초기화 실행 +initialize_session_state() + +# 페이지 제목 +st.title("🤖 AI ChatBot") + +st.markdown( + """ + LangGraph 기반 AI ChatBot과 대화를 나눌 수 있습니다. + - 날씨 정보 조회 + - 유명한 오픈소스 프로젝트 정보 + - 일반적인 질문과 대화 + """ +) + +# 사이드바 UI 구성 +with st.sidebar: + st.markdown("### 🤖 ChatBot 설정") + st.divider() + + # LLM 모델 선택 드롭다운 + selected_model = st.selectbox( + "LLM 모델", + options=list(ChatBot.AVAILABLE_MODELS.keys()), + format_func=lambda x: ChatBot.AVAILABLE_MODELS[x], + key="chatbot_model_select", + ) + + # 선택된 모델이 변경되면 ChatBot 업데이트 + if selected_model != st.session_state.chatbot_instance.model_name: + st.session_state.chatbot_instance.update_model(selected_model) + st.sidebar.success( + f"모델이 '{ChatBot.AVAILABLE_MODELS[selected_model]}'로 변경되었습니다." + ) + + st.divider() + + # 채팅 세션 ID 입력 (대화 기록을 구분하는 용도) + thread_id = st.text_input( + "세션 ID", + value="default", + key="chatbot_thread_id", + help="대화 기록을 구분하는 고유 ID입니다.", + ) + + # 채팅 세션 시작/종료 버튼 + if not st.session_state.chatbot_started: + # 세션이 시작되지 않았을 때: 시작 버튼 표시 + if st.button("대화 시작", use_container_width=True, type="primary"): + st.session_state.chatbot_started = True + st.session_state.chatbot_messages = [] + st.rerun() + else: + # 세션이 시작되었을 때: 종료 버튼 표시 + if st.button("대화 종료", use_container_width=True): + st.session_state.chatbot_started = False + st.rerun() + + st.divider() + + # 세션 히스토리를 JSON 형식으로 표시 (접힌 상태) + with st.expander("대화 기록 (JSON)", expanded=False): + st.json(st.session_state.chatbot_messages) + +# 채팅 세션이 시작된 경우에만 채팅 인터페이스 표시 +if st.session_state.chatbot_started: + # 첫 메시지가 없으면 환영 메시지 추가 + if not st.session_state.chatbot_messages: + hello_message = "안녕하세요! 무엇을 도와드릴까요? 🤖" + st.session_state.chatbot_messages = [ + {"role": "assistant", "content": hello_message} + ] + + # 저장된 모든 메시지를 순서대로 표시 + for message in st.session_state.chatbot_messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + + # 사용자 입력 처리 + if prompt := st.chat_input("메시지를 입력하세요"): + # 사용자 메시지를 기록에 추가 + st.session_state.chatbot_messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # AI 응답 생성 및 표시 + with st.chat_message("assistant"): + try: + # ChatBot을 통해 응답 생성 + response = st.session_state.chatbot_instance.chat(prompt, thread_id) + + # 응답 내용 추출 + response_content = response["messages"][-1].content + + # 스트리밍 방식으로 응답 표시 (타이핑 효과) + response_str = "" + response_container = st.empty() + for token in response_content: + response_str += token + response_container.markdown(response_str) + + # AI 응답을 기록에 추가 + st.session_state.chatbot_messages.append( + {"role": "assistant", "content": response_content} + ) + except Exception as e: + error_msg = f"오류가 발생했습니다: {str(e)}" + st.error(error_msg) + st.session_state.chatbot_messages.append( + {"role": "assistant", "content": error_msg} + ) +else: + st.info("👈 왼쪽 사이드바에서 '대화 시작' 버튼을 눌러 ChatBot과 대화를 시작하세요!") diff --git a/interface/pages_config.py b/interface/pages_config.py index 0c78353..8963f03 100644 --- a/interface/pages_config.py +++ b/interface/pages_config.py @@ -8,6 +8,8 @@ - 홈 페이지 - Lang2SQL 페이지 - 그래프 빌더 페이지 + - ChatBot 페이지 + - 설정 페이지 """ import streamlit as st @@ -16,5 +18,6 @@ st.Page("app_pages/home.py", title="🏠 홈"), st.Page("app_pages/lang2sql.py", title="🔍 Lang2SQL"), st.Page("app_pages/graph_builder.py", title="📊 그래프 빌더"), + st.Page("app_pages/chatbot.py", title="🤖 ChatBot"), st.Page("app_pages/settings.py", title="⚙️ 설정"), ] diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py new file mode 100644 index 0000000..8bf7e90 --- /dev/null +++ b/utils/llm/chatbot.py @@ -0,0 +1,144 @@ +""" +LangGraph 기반 ChatBot 모델 +OpenAI의 ChatGPT 모델을 사용하여 대화 기록을 유지하는 챗봇 구현 +""" + +from langchain_openai import ChatOpenAI +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import START, MessagesState, StateGraph +from langgraph.prebuilt import ToolNode + +from utils.llm.tools import get_weather, get_famous_opensource + + +class ChatBot: + """ + LangGraph를 사용한 대화형 챗봇 클래스 + OpenAI API를 통해 다양한 GPT 모델을 사용할 수 있으며, + MemorySaver를 통해 대화 기록을 관리합니다. + """ + + # 사용 가능한 OpenAI 모델 목록 (키: 모델ID, 값: 표시명) + AVAILABLE_MODELS = { + "gpt-4o": "GPT-4o", + "gpt-4o-mini": "GPT-4o Mini", + "gpt-4-turbo": "GPT-4 Turbo", + "gpt-3.5-turbo": "GPT-3.5 Turbo", + } + + def __init__(self, openai_api_key: str, model_name: str = "gpt-4o-mini"): + """ + ChatBot 인스턴스 초기화 + + Args: + openai_api_key: OpenAI API 키 + model_name: 사용할 모델명 (기본값: gpt-4o-mini) + """ + self.openai_api_key = openai_api_key + self.model_name = model_name + self.tools = [get_weather, get_famous_opensource] # 사용 가능한 tool 목록 + self.llm = self._setup_llm() # LLM 인스턴스 설정 + self.app = self._setup_workflow() # LangGraph 워크플로우 설정 + + def _setup_llm(self): + """ + OpenAI ChatGPT LLM 인스턴스 생성 + Tool을 바인딩하여 LLM이 필요시 tool을 호출할 수 있도록 설정합니다. + + Returns: + ChatOpenAI: Tool이 바인딩된 LLM 인스턴스 + """ + llm = ChatOpenAI( + temperature=0.1, # 응답의 일관성을 위해 낮은 temperature 설정 + openai_api_key=self.openai_api_key, + model_name=self.model_name, + ) + # Tool을 LLM에 바인딩하여 함수 호출 기능 활성화 + return llm.bind_tools(self.tools) + + def _setup_workflow(self): + """ + LangGraph 워크플로우 설정 + 대화 기록을 관리하고 LLM과 통신하는 그래프 구조를 생성합니다. + Tool 호출 기능을 포함하여 LLM이 필요시 도구를 사용할 수 있도록 합니다. + + Returns: + CompiledGraph: 컴파일된 LangGraph 워크플로우 + """ + # MessagesState를 사용하는 StateGraph 생성 + workflow = StateGraph(state_schema=MessagesState) + + def call_model(state: MessagesState): + """ + LLM 모델을 호출하는 노드 함수 + LLM이 응답을 생성하거나 tool 호출을 결정합니다. + + Args: + state: 현재 메시지 상태 + + Returns: + dict: LLM 응답이 포함된 상태 업데이트 + """ + # sys_msg = SystemMessage(content="You are a helpful assistant ") + response = self.llm.invoke(state["messages"]) + return {"messages": response} + + def route_model_output(state: MessagesState): + """ + LLM 출력에 따라 다음 노드를 결정하는 라우팅 함수 + Tool 호출이 필요한 경우 'tools' 노드로, 아니면 대화를 종료합니다. + + Args: + state: 현재 메시지 상태 + + Returns: + str: 다음에 실행할 노드 이름 ('tools' 또는 '__end__') + """ + messages = state["messages"] + last_message = messages[-1] + # LLM이 tool을 호출하려고 하는 경우 (tool_calls가 있는 경우) + if hasattr(last_message, "tool_calls") and last_message.tool_calls: + return "tools" + # Tool 호출이 없으면 대화 종료 + return "__end__" + + # 워크플로우 구조 정의 + workflow.add_edge(START, "model") # 시작 -> model 노드 + workflow.add_node("model", call_model) # LLM 호출 노드 + workflow.add_node("tools", ToolNode(self.tools)) # Tool 실행 노드 + + # model 노드 이후 조건부 라우팅 + workflow.add_conditional_edges("model", route_model_output) + # Tool 실행 후 다시 model로 돌아가서 최종 응답 생성 + workflow.add_edge("tools", "model") + + # MemorySaver를 사용하여 대화 기록 저장 기능 추가 + return workflow.compile(checkpointer=MemorySaver()) + + def chat(self, message: str, thread_id: str): + """ + 사용자 메시지에 대한 응답 생성 + + Args: + message: 사용자 입력 메시지 + thread_id: 대화 세션을 구분하는 고유 ID + + Returns: + dict: LLM 응답을 포함한 결과 딕셔너리 + """ + return self.app.invoke( + {"messages": [{"role": "user", "content": message}]}, + {"configurable": {"thread_id": thread_id}}, # thread_id로 대화 기록 관리 + ) + + def update_model(self, model_name: str): + """ + 사용 중인 LLM 모델 변경 + 모델 변경 시 LLM 인스턴스와 워크플로우를 재설정합니다. + + Args: + model_name: 변경할 모델명 + """ + self.model_name = model_name + self.llm = self._setup_llm() # 새 모델로 LLM 재설정 + self.app = self._setup_workflow() # 워크플로우 재생성 diff --git a/utils/llm/tools/__init__.py b/utils/llm/tools/__init__.py index d7ab34a..e9ebee9 100644 --- a/utils/llm/tools/__init__.py +++ b/utils/llm/tools/__init__.py @@ -4,8 +4,12 @@ set_gms_server, ) +from utils.llm.tools.test import get_weather, get_famous_opensource + __all__ = [ "set_gms_server", "get_info_from_db", "get_metadata_from_db", + "get_weather", + "get_famous_opensource", ] diff --git a/utils/llm/tools/test.py b/utils/llm/tools/test.py new file mode 100644 index 0000000..b764b31 --- /dev/null +++ b/utils/llm/tools/test.py @@ -0,0 +1,64 @@ +""" +LangGraph ChatBot에서 사용하는 도구(Tool) 함수들 +""" + +from langchain_core.tools import tool + + +@tool +def get_weather(city: str) -> str: + """ + 특정 도시의 현재 날씨 정보를 조회합니다. + + 이 함수는 도시 이름을 입력받아 해당 도시의 날씨 정보를 반환합니다. + 사용자가 날씨, 기상, weather 등의 키워드와 함께 도시 이름을 언급하면 이 도구를 사용하세요. + + Args: + city (str): 날씨를 확인하고 싶은 도시의 이름입니다. + 예: "Seoul", "New York", "Tokyo", "서울", "부산" 등 + 영문과 한글 도시명을 모두 지원합니다. + + Returns: + str: 해당 도시의 날씨 정보를 담은 문자열입니다. + 현재는 항상 맑은 날씨를 반환합니다. + + Examples: + >>> get_weather("Seoul") + 'Seoul is sunny' + + >>> get_weather("서울") + '서울 is sunny' + + Note: + 이 도구는 다음과 같은 경우에 사용하세요: + - "서울 날씨 어때?" + - "What's the weather in New York?" + - "도쿄의 날씨를 알려줘" + - "부산 날씨 확인해줘" + """ + return f"{city} is sunny" + + +@tool +def get_famous_opensource() -> str: + """ + 가장 유명한 오픈소스 프로젝트를 조회합니다. + + 이 함수는 현재 가장 유명한 오픈소스 프로젝트의 이름을 반환합니다. + 사용자가 유명한 오픈소스, 인기있는 오픈소스, 최고의 오픈소스 등을 물어보면 이 도구를 사용하세요. + + Returns: + str: 가장 유명한 오픈소스 프로젝트 이름 + + Examples: + >>> get_famous_opensource() + 'Lang2SQL' + + Note: + 이 도구는 다음과 같은 경우에 사용하세요: + - "제일 유명한 오픈소스가 뭐야?" + - "가장 인기있는 오픈소스는?" + - "최고의 오픈소스 프로젝트 알려줘" + - "유명한 오픈소스 추천해줘" + """ + return "Lang2SQL" From 87feb63f2fccefda963493f476ad53bcf8d7f701 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Sun, 19 Oct 2025 20:15:54 +0900 Subject: [PATCH 2/7] =?UTF-8?q?ChatBot=20=EC=84=B8=EC=85=98=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20?= =?UTF-8?q?=EA=B5=AC=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface/app_pages/chatbot.py | 207 +++++++++--------- .../app_pages/sidebar_components/__init__.py | 2 + .../chatbot_session_controller.py | 63 ++++++ 3 files changed, 170 insertions(+), 102 deletions(-) create mode 100644 interface/app_pages/sidebar_components/chatbot_session_controller.py diff --git a/interface/app_pages/chatbot.py b/interface/app_pages/chatbot.py index 357707c..fcdc6f7 100644 --- a/interface/app_pages/chatbot.py +++ b/interface/app_pages/chatbot.py @@ -7,32 +7,68 @@ import streamlit as st from utils.llm.chatbot import ChatBot +from interface.app_pages.sidebar_components import ( + render_sidebar_data_source_selector, + render_sidebar_llm_selector, + render_sidebar_embedding_selector, + render_sidebar_db_selector, + render_sidebar_chatbot_session_controller, +) +from interface.core.config import load_config def initialize_session_state(): """세션 상태 초기화 함수 Streamlit의 session_state를 사용하여 앱의 상태를 유지합니다. + LLM 설정을 sidebar의 llm_selector에서 선택한 값으로부터 가져옵니다. """ - # 채팅 세션 시작 여부 플래그 - if "chatbot_started" not in st.session_state: - st.session_state.chatbot_started = False - # 채팅 메시지 기록 저장 + # 채팅 메시지 기록 저장 (자동으로 시작) if "chatbot_messages" not in st.session_state: st.session_state.chatbot_messages = [] + # LLM 공급자 확인 (현재 ChatBot은 OpenAI만 지원) + llm_provider = ( + st.session_state.get("LLM_PROVIDER") or os.getenv("LLM_PROVIDER") or "openai" + ).lower() + + if llm_provider != "openai": + st.error( + f"⚠️ ChatBot은 현재 OpenAI만 지원합니다. 설정 > LLM에서 OpenAI 프로파일을 선택하거나 LLM_PROVIDER를 'openai'로 설정해주세요." + ) + st.stop() + # OpenAI API 키 확인 openai_api_key = st.session_state.get("OPEN_AI_KEY") or os.getenv("OPEN_AI_KEY") if not openai_api_key: st.error( - "⚠️ OpenAI API 키가 설정되지 않았습니다. 설정 > LLM에서 OpenAI API 키를 입력해주세요." + "⚠️ OpenAI API 키가 설정되지 않았습니다. 설정 > LLM에서 OpenAI API 키를 입력하거나, 사이드바에서 LLM 프로파일을 적용해주세요." ) st.stop() - # ChatBot 인스턴스 생성 (OpenAI API 키 사용) + # 사용할 모델명 가져오기 (llm_selector에서 설정한 값) + model_name = ( + st.session_state.get("OPEN_AI_LLM_MODEL") + or os.getenv("OPEN_AI_LLM_MODEL") + or "gpt-4o-mini" + ) + + # ChatBot 인스턴스 생성 또는 모델 업데이트 if "chatbot_instance" not in st.session_state: - st.session_state.chatbot_instance = ChatBot(openai_api_key) + st.session_state.chatbot_instance = ChatBot( + openai_api_key, model_name=model_name + ) + else: + # 기존 인스턴스가 있는 경우, 모델이나 API 키가 변경되었는지 확인 + existing_bot = st.session_state.chatbot_instance + if ( + existing_bot.model_name != model_name + or existing_bot.openai_api_key != openai_api_key + ): + st.session_state.chatbot_instance = ChatBot( + openai_api_key, model_name=model_name + ) # 세션 상태 초기화 실행 @@ -50,101 +86,68 @@ def initialize_session_state(): """ ) -# 사이드바 UI 구성 -with st.sidebar: - st.markdown("### 🤖 ChatBot 설정") - st.divider() - - # LLM 모델 선택 드롭다운 - selected_model = st.selectbox( - "LLM 모델", - options=list(ChatBot.AVAILABLE_MODELS.keys()), - format_func=lambda x: ChatBot.AVAILABLE_MODELS[x], - key="chatbot_model_select", - ) +# 설정 로드 +config = load_config() - # 선택된 모델이 변경되면 ChatBot 업데이트 - if selected_model != st.session_state.chatbot_instance.model_name: - st.session_state.chatbot_instance.update_model(selected_model) - st.sidebar.success( - f"모델이 '{ChatBot.AVAILABLE_MODELS[selected_model]}'로 변경되었습니다." - ) +# 사이드바 UI 구성 (lang2sql.py와 동일한 구조) +render_sidebar_data_source_selector(config) +st.sidebar.divider() +render_sidebar_llm_selector() +st.sidebar.divider() +render_sidebar_embedding_selector() +st.sidebar.divider() +render_sidebar_db_selector() +st.sidebar.divider() +# ChatBot 전용 설정 +with st.sidebar: + st.markdown("### 🤖 ChatBot 설정") st.divider() - - # 채팅 세션 ID 입력 (대화 기록을 구분하는 용도) - thread_id = st.text_input( - "세션 ID", - value="default", - key="chatbot_thread_id", - help="대화 기록을 구분하는 고유 ID입니다.", - ) - - # 채팅 세션 시작/종료 버튼 - if not st.session_state.chatbot_started: - # 세션이 시작되지 않았을 때: 시작 버튼 표시 - if st.button("대화 시작", use_container_width=True, type="primary"): - st.session_state.chatbot_started = True - st.session_state.chatbot_messages = [] - st.rerun() - else: - # 세션이 시작되었을 때: 종료 버튼 표시 - if st.button("대화 종료", use_container_width=True): - st.session_state.chatbot_started = False - st.rerun() - - st.divider() - - # 세션 히스토리를 JSON 형식으로 표시 (접힌 상태) - with st.expander("대화 기록 (JSON)", expanded=False): - st.json(st.session_state.chatbot_messages) - -# 채팅 세션이 시작된 경우에만 채팅 인터페이스 표시 -if st.session_state.chatbot_started: - # 첫 메시지가 없으면 환영 메시지 추가 - if not st.session_state.chatbot_messages: - hello_message = "안녕하세요! 무엇을 도와드릴까요? 🤖" - st.session_state.chatbot_messages = [ - {"role": "assistant", "content": hello_message} - ] - - # 저장된 모든 메시지를 순서대로 표시 - for message in st.session_state.chatbot_messages: - with st.chat_message(message["role"]): - st.markdown(message["content"]) - - # 사용자 입력 처리 - if prompt := st.chat_input("메시지를 입력하세요"): - # 사용자 메시지를 기록에 추가 - st.session_state.chatbot_messages.append({"role": "user", "content": prompt}) - with st.chat_message("user"): - st.markdown(prompt) - - # AI 응답 생성 및 표시 - with st.chat_message("assistant"): - try: - # ChatBot을 통해 응답 생성 - response = st.session_state.chatbot_instance.chat(prompt, thread_id) - - # 응답 내용 추출 - response_content = response["messages"][-1].content - - # 스트리밍 방식으로 응답 표시 (타이핑 효과) - response_str = "" - response_container = st.empty() - for token in response_content: - response_str += token - response_container.markdown(response_str) - - # AI 응답을 기록에 추가 - st.session_state.chatbot_messages.append( - {"role": "assistant", "content": response_content} - ) - except Exception as e: - error_msg = f"오류가 발생했습니다: {str(e)}" - st.error(error_msg) - st.session_state.chatbot_messages.append( - {"role": "assistant", "content": error_msg} - ) -else: - st.info("👈 왼쪽 사이드바에서 '대화 시작' 버튼을 눌러 ChatBot과 대화를 시작하세요!") + thread_id = render_sidebar_chatbot_session_controller() + + +# 첫 메시지가 없으면 환영 메시지 추가 +if not st.session_state.chatbot_messages: + hello_message = "안녕하세요! 무엇을 도와드릴까요? 🤖" + st.session_state.chatbot_messages = [ + {"role": "assistant", "content": hello_message} + ] + +# 저장된 모든 메시지를 순서대로 표시 +for message in st.session_state.chatbot_messages: + with st.chat_message(message["role"]): + st.markdown(message["content"]) + +# 사용자 입력 처리 +if prompt := st.chat_input("메시지를 입력하세요"): + # 사용자 메시지를 기록에 추가 + st.session_state.chatbot_messages.append({"role": "user", "content": prompt}) + with st.chat_message("user"): + st.markdown(prompt) + + # AI 응답 생성 및 표시 + with st.chat_message("assistant"): + try: + # ChatBot을 통해 응답 생성 + response = st.session_state.chatbot_instance.chat(prompt, thread_id) + + # 응답 내용 추출 + response_content = response["messages"][-1].content + + # 모델 정보 표시 + model_name = st.session_state.chatbot_instance.model_name + st.caption(f"🤖 모델: {model_name}") + + # 응답 표시 + st.markdown(response_content) + + # AI 응답을 기록에 추가 + st.session_state.chatbot_messages.append( + {"role": "assistant", "content": response_content} + ) + except Exception as e: + error_msg = f"오류가 발생했습니다: {str(e)}" + st.error(error_msg) + st.session_state.chatbot_messages.append( + {"role": "assistant", "content": error_msg} + ) diff --git a/interface/app_pages/sidebar_components/__init__.py b/interface/app_pages/sidebar_components/__init__.py index 36a371b..47026dc 100644 --- a/interface/app_pages/sidebar_components/__init__.py +++ b/interface/app_pages/sidebar_components/__init__.py @@ -2,10 +2,12 @@ from .llm_selector import render_sidebar_llm_selector from .embedding_selector import render_sidebar_embedding_selector from .db_selector import render_sidebar_db_selector +from .chatbot_session_controller import render_sidebar_chatbot_session_controller __all__ = [ "render_sidebar_data_source_selector", "render_sidebar_llm_selector", "render_sidebar_embedding_selector", "render_sidebar_db_selector", + "render_sidebar_chatbot_session_controller", ] diff --git a/interface/app_pages/sidebar_components/chatbot_session_controller.py b/interface/app_pages/sidebar_components/chatbot_session_controller.py new file mode 100644 index 0000000..f4e774a --- /dev/null +++ b/interface/app_pages/sidebar_components/chatbot_session_controller.py @@ -0,0 +1,63 @@ +"""ChatBot 세션 제어를 위한 사이드바 컴포넌트""" + +import streamlit as st +import uuid + + +def render_sidebar_chatbot_session_controller() -> str: + """ChatBot 세션 관리 및 대화 기록 표시 (사이드바 전용) + + Returns: + str: 현재 thread_id + """ + # 세션 ID 자동 생성 (처음 방문 시에만) + if "chatbot_thread_id" not in st.session_state: + st.session_state.chatbot_thread_id = str(uuid.uuid4())[:8] # 8자리 짧은 ID + + thread_id = st.session_state.chatbot_thread_id + + # 세션 관리 섹션 + st.markdown("### 📋 세션 관리") + + # 세션 정보 표시 + st.markdown(f"**현재 세션:** `{thread_id}`") + st.caption("대화 기록을 구분하는 고유 ID입니다.") + + # 새 세션 시작 버튼 + if st.button( + "🔄 새 세션 시작", + use_container_width=True, + help="새로운 대화 세션을 시작합니다.", + ): + st.session_state.chatbot_thread_id = str(uuid.uuid4())[:8] + st.session_state.chatbot_messages = [] + st.rerun() + + # 대화 기록 섹션 + if st.session_state.get("chatbot_messages"): + st.divider() + st.markdown("### 💬 대화 기록") + + # 메시지 개수 표시 + message_count = len(st.session_state.chatbot_messages) + st.caption(f"총 {message_count}개의 메시지") + + # 대화 기록 표시 (접힌 상태) + with st.expander("📄 전체 기록 보기 (JSON)", expanded=False): + st.json(st.session_state.chatbot_messages) + + # 최근 메시지 미리보기 + if message_count > 0: + with st.expander("👀 최근 메시지 미리보기", expanded=False): + recent_messages = st.session_state.chatbot_messages[-3:] # 최근 3개 + for msg in recent_messages: + role_icon = "👤" if msg["role"] == "user" else "🤖" + role_text = "사용자" if msg["role"] == "user" else "AI" + content_preview = ( + msg["content"][:50] + "..." + if len(msg["content"]) > 50 + else msg["content"] + ) + st.caption(f"{role_icon} {role_text}: {content_preview}") + + return thread_id From 7d8a1080b12b63d47938e88542e50442266f7596 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Sun, 19 Oct 2025 20:57:06 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=EC=B1=97=EB=B4=87=20tool=20=EB=A1=9C=20sea?= =?UTF-8?q?rch=5Fdatabase=5Ftables=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/llm/chatbot.py | 45 ++++++++++-- utils/llm/tools/__init__.py | 7 +- utils/llm/tools/chatbot_tool.py | 120 ++++++++++++++++++++++++++++++++ utils/llm/tools/test.py | 64 ----------------- 4 files changed, 166 insertions(+), 70 deletions(-) create mode 100644 utils/llm/tools/chatbot_tool.py delete mode 100644 utils/llm/tools/test.py diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py index 8bf7e90..b24b444 100644 --- a/utils/llm/chatbot.py +++ b/utils/llm/chatbot.py @@ -4,11 +4,16 @@ """ from langchain_openai import ChatOpenAI +from langchain_core.messages import SystemMessage from langgraph.checkpoint.memory import MemorySaver from langgraph.graph import START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode -from utils.llm.tools import get_weather, get_famous_opensource +from utils.llm.tools import ( + get_weather, + get_famous_opensource, + search_database_tables, +) class ChatBot: @@ -36,7 +41,12 @@ def __init__(self, openai_api_key: str, model_name: str = "gpt-4o-mini"): """ self.openai_api_key = openai_api_key self.model_name = model_name - self.tools = [get_weather, get_famous_opensource] # 사용 가능한 tool 목록 + # SQL 생성을 위한 데이터베이스 메타데이터 조회 도구 + self.tools = [ + search_database_tables, # 데이터베이스 테이블 정보 검색 + get_weather, # 테스트용 도구 (추후 제거 가능) + get_famous_opensource, # 테스트용 도구 (추후 제거 가능) + ] self.llm = self._setup_llm() # LLM 인스턴스 설정 self.app = self._setup_workflow() # LangGraph 워크플로우 설정 @@ -49,7 +59,7 @@ def _setup_llm(self): ChatOpenAI: Tool이 바인딩된 LLM 인스턴스 """ llm = ChatOpenAI( - temperature=0.1, # 응답의 일관성을 위해 낮은 temperature 설정 + temperature=0.0, # SQL 생성은 정확성이 중요하므로 0으로 설정 openai_api_key=self.openai_api_key, model_name=self.model_name, ) @@ -79,8 +89,33 @@ def call_model(state: MessagesState): Returns: dict: LLM 응답이 포함된 상태 업데이트 """ - # sys_msg = SystemMessage(content="You are a helpful assistant ") - response = self.llm.invoke(state["messages"]) + # SQL 생성 전문 어시스턴트 시스템 메시지 + sys_msg = SystemMessage( + content="""# 역할 +당신은 사용자가 SQL 쿼리를 생성하도록 돕는 전문 AI 어시스턴트입니다. + +# 주요 임무 +- 사용자의 자연어 질문을 이해하고 SQL 쿼리 생성에 필요한 정보를 파악합니다 +- 필요한 경우 데이터베이스 스키마나 메타데이터를 확인하기 위해 도구를 활용합니다 +- 단계별로 사용자와 대화하며 명확한 SQL 쿼리를 만들어갑니다 +- 생성된 SQL에 대해 이해하기 쉽게 설명합니다 + +# 작업 프로세스 +1. 사용자의 의도를 명확히 파악 +2. 필요한 테이블/컬럼 정보 확인 (도구 사용) +3. 사용자의 질문을 바탕으로 정보를 추출할 수 있는 명확한 질문으로 변환합니다 + +# 주의사항 +- 항상 친절하고 명확하게 대화합니다 +- 이전 대화 맥락을 고려하여 일관성 있게 응답합니다 +- 사용자가 SQL을 이해할 수 있도록 단계별로 설명합니다 + +--- +다음은 사용자와의 대화입니다:""" + ) + # 시스템 메시지를 대화의 맨 앞에 추가 + messages = [sys_msg] + state["messages"] + response = self.llm.invoke(messages) return {"messages": response} def route_model_output(state: MessagesState): diff --git a/utils/llm/tools/__init__.py b/utils/llm/tools/__init__.py index e9ebee9..7575d32 100644 --- a/utils/llm/tools/__init__.py +++ b/utils/llm/tools/__init__.py @@ -4,7 +4,11 @@ set_gms_server, ) -from utils.llm.tools.test import get_weather, get_famous_opensource +from utils.llm.tools.chatbot_tool import ( + get_weather, + get_famous_opensource, + search_database_tables, +) __all__ = [ "set_gms_server", @@ -12,4 +16,5 @@ "get_metadata_from_db", "get_weather", "get_famous_opensource", + "search_database_tables", ] diff --git a/utils/llm/tools/chatbot_tool.py b/utils/llm/tools/chatbot_tool.py new file mode 100644 index 0000000..d12bba0 --- /dev/null +++ b/utils/llm/tools/chatbot_tool.py @@ -0,0 +1,120 @@ +""" +LangGraph ChatBot에서 사용하는 도구(Tool) 함수들 +""" + +from langchain_core.tools import tool +from utils.llm.retrieval import search_tables + + +@tool +def get_weather(city: str) -> str: + """ + 특정 도시의 현재 날씨 정보를 조회합니다. + + 이 함수는 도시 이름을 입력받아 해당 도시의 날씨 정보를 반환합니다. + 사용자가 날씨, 기상, weather 등의 키워드와 함께 도시 이름을 언급하면 이 도구를 사용하세요. + + Args: + city (str): 날씨를 확인하고 싶은 도시의 이름입니다. + 예: "Seoul", "New York", "Tokyo", "서울", "부산" 등 + 영문과 한글 도시명을 모두 지원합니다. + + Returns: + str: 해당 도시의 날씨 정보를 담은 문자열입니다. + 현재는 항상 맑은 날씨를 반환합니다. + + Examples: + >>> get_weather("Seoul") + 'Seoul is sunny' + + >>> get_weather("서울") + '서울 is sunny' + + Note: + 이 도구는 다음과 같은 경우에 사용하세요: + - "서울 날씨 어때?" + - "What's the weather in New York?" + - "도쿄의 날씨를 알려줘" + - "부산 날씨 확인해줘" + """ + return f"{city} is sunny" + + +@tool +def get_famous_opensource() -> str: + """ + 가장 유명한 오픈소스 프로젝트를 조회합니다. + + 이 함수는 현재 가장 유명한 오픈소스 프로젝트의 이름을 반환합니다. + 사용자가 유명한 오픈소스, 인기있는 오픈소스, 최고의 오픈소스 등을 물어보면 이 도구를 사용하세요. + + Returns: + str: 가장 유명한 오픈소스 프로젝트 이름 + + Examples: + >>> get_famous_opensource() + 'Lang2SQL' + + Note: + 이 도구는 다음과 같은 경우에 사용하세요: + - "제일 유명한 오픈소스가 뭐야?" + - "가장 인기있는 오픈소스는?" + - "최고의 오픈소스 프로젝트 알려줘" + - "유명한 오픈소스 추천해줘" + """ + return "Lang2SQL" + + +@tool +def search_database_tables( + query: str, retriever_name: str = "기본", top_n: int = 5, device: str = "cpu" +) -> dict: + """ + 사용자의 자연어 쿼리를 기반으로 관련된 데이터베이스 테이블 정보를 검색합니다. + + 이 함수는 SQL 쿼리 생성을 위해 필요한 테이블과 컬럼 정보를 찾아줍니다. + 사용자가 어떤 테이블을 사용해야 할지, 어떤 컬럼이 있는지 물어보거나, + SQL 쿼리를 만들기 위한 스키마 정보가 필요할 때 이 도구를 사용하세요. + + Args: + query (str): 검색하려는 자연어 질문입니다. + 예: "고객 정보를 조회하려면?", "주문 관련 테이블" + retriever_name (str, optional): 검색기 유형입니다. + "기본" 또는 "Reranker" 중 선택. 기본값은 "기본" + top_n (int, optional): 검색할 테이블 개수입니다. 기본값은 5개 + device (str, optional): 모델 실행 장치입니다. "cpu" 또는 "cuda". 기본값은 "cpu" + + Returns: + dict: 테이블 정보가 담긴 딕셔너리입니다. + 각 테이블은 키로 저장되며, 값으로 테이블 설명과 컬럼 정보를 포함합니다. + + 예시 형태: + { + "customers": { + "table_description": "고객 정보 테이블", + "customer_id": "고객 고유 ID", + "name": "고객 이름", + "email": "고객 이메일" + }, + "orders": { + "table_description": "주문 정보 테이블", + "order_id": "주문 ID", + "customer_id": "고객 ID (외래키)" + } + } + + Examples: + >>> search_database_tables("고객 정보가 필요해") + {'customers': {'table_description': '고객 정보 테이블', ...}} + + Note: + 이 도구는 다음과 같은 경우에 사용하세요: + - "어떤 테이블을 사용해야 해?" + - "고객 관련 테이블 정보를 알려줘" + - "주문 데이터는 어디에 있어?" + - "사용 가능한 컬럼을 보여줘" + - SQL 쿼리를 생성하기 전에 스키마 정보가 필요할 때 + """ + return search_tables( + query=query, retriever_name=retriever_name, top_n=top_n, device=device + ) diff --git a/utils/llm/tools/test.py b/utils/llm/tools/test.py deleted file mode 100644 index b764b31..0000000 --- a/utils/llm/tools/test.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -LangGraph ChatBot에서 사용하는 도구(Tool) 함수들 -""" - -from langchain_core.tools import tool - - -@tool -def get_weather(city: str) -> str: - """ - 특정 도시의 현재 날씨 정보를 조회합니다. - - 이 함수는 도시 이름을 입력받아 해당 도시의 날씨 정보를 반환합니다. - 사용자가 날씨, 기상, weather 등의 키워드와 함께 도시 이름을 언급하면 이 도구를 사용하세요. - - Args: - city (str): 날씨를 확인하고 싶은 도시의 이름입니다. - 예: "Seoul", "New York", "Tokyo", "서울", "부산" 등 - 영문과 한글 도시명을 모두 지원합니다. - - Returns: - str: 해당 도시의 날씨 정보를 담은 문자열입니다. - 현재는 항상 맑은 날씨를 반환합니다. - - Examples: - >>> get_weather("Seoul") - 'Seoul is sunny' - - >>> get_weather("서울") - '서울 is sunny' - - Note: - 이 도구는 다음과 같은 경우에 사용하세요: - - "서울 날씨 어때?" - - "What's the weather in New York?" - - "도쿄의 날씨를 알려줘" - - "부산 날씨 확인해줘" - """ - return f"{city} is sunny" - - -@tool -def get_famous_opensource() -> str: - """ - 가장 유명한 오픈소스 프로젝트를 조회합니다. - - 이 함수는 현재 가장 유명한 오픈소스 프로젝트의 이름을 반환합니다. - 사용자가 유명한 오픈소스, 인기있는 오픈소스, 최고의 오픈소스 등을 물어보면 이 도구를 사용하세요. - - Returns: - str: 가장 유명한 오픈소스 프로젝트 이름 - - Examples: - >>> get_famous_opensource() - 'Lang2SQL' - - Note: - 이 도구는 다음과 같은 경우에 사용하세요: - - "제일 유명한 오픈소스가 뭐야?" - - "가장 인기있는 오픈소스는?" - - "최고의 오픈소스 프로젝트 알려줘" - - "유명한 오픈소스 추천해줘" - """ - return "Lang2SQL" From 1f439a4d1cb29ec11a9bc4ed7fa11413a9aa3b69 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Sun, 19 Oct 2025 22:09:59 +0900 Subject: [PATCH 4/7] =?UTF-8?q?get=5Fglossary=5Fterms=20tool=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface/app_pages/chatbot.py | 11 ++- utils/llm/chatbot.py | 83 +++++++++++++++----- utils/llm/tools/__init__.py | 2 + utils/llm/tools/chatbot_tool.py | 135 ++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 24 deletions(-) diff --git a/interface/app_pages/chatbot.py b/interface/app_pages/chatbot.py index fcdc6f7..e7a8ef4 100644 --- a/interface/app_pages/chatbot.py +++ b/interface/app_pages/chatbot.py @@ -54,20 +54,25 @@ def initialize_session_state(): or "gpt-4o-mini" ) + # DataHub 서버 URL 가져오기 (config에서 로드) + config = load_config() + gms_server = config.datahub_server + # ChatBot 인스턴스 생성 또는 모델 업데이트 if "chatbot_instance" not in st.session_state: st.session_state.chatbot_instance = ChatBot( - openai_api_key, model_name=model_name + openai_api_key, model_name=model_name, gms_server=gms_server ) else: - # 기존 인스턴스가 있는 경우, 모델이나 API 키가 변경되었는지 확인 + # 기존 인스턴스가 있는 경우, 모델이나 API 키, gms_server가 변경되었는지 확인 existing_bot = st.session_state.chatbot_instance if ( existing_bot.model_name != model_name or existing_bot.openai_api_key != openai_api_key + or existing_bot.gms_server != gms_server ): st.session_state.chatbot_instance = ChatBot( - openai_api_key, model_name=model_name + openai_api_key, model_name=model_name, gms_server=gms_server ) diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py index b24b444..641fc6c 100644 --- a/utils/llm/chatbot.py +++ b/utils/llm/chatbot.py @@ -3,19 +3,35 @@ OpenAI의 ChatGPT 모델을 사용하여 대화 기록을 유지하는 챗봇 구현 """ +from typing import Annotated, Sequence, TypedDict + +from langchain_core.messages import BaseMessage, SystemMessage from langchain_openai import ChatOpenAI -from langchain_core.messages import SystemMessage from langgraph.checkpoint.memory import MemorySaver -from langgraph.graph import START, MessagesState, StateGraph +from langgraph.graph import START, StateGraph +from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode from utils.llm.tools import ( get_weather, get_famous_opensource, search_database_tables, + get_glossary_terms, ) +class ChatBotState(TypedDict): + """ + 챗봇 상태 - 사용자 질문을 SQL로 변환 가능한 구체적인 질문으로 만들어가는 과정 추적 + """ + + # 기본 메시지 (MessagesState와 동일) + messages: Annotated[Sequence[BaseMessage], add_messages] + + # datahub 서버 정보 + gms_server: str + + class ChatBot: """ LangGraph를 사용한 대화형 챗봇 클래스 @@ -31,21 +47,29 @@ class ChatBot: "gpt-3.5-turbo": "GPT-3.5 Turbo", } - def __init__(self, openai_api_key: str, model_name: str = "gpt-4o-mini"): + def __init__( + self, + openai_api_key: str, + model_name: str = "gpt-4o-mini", + gms_server: str = "http://localhost:8080", + ): """ ChatBot 인스턴스 초기화 Args: openai_api_key: OpenAI API 키 model_name: 사용할 모델명 (기본값: gpt-4o-mini) + gms_server: DataHub GMS 서버 URL (기본값: http://localhost:8080) """ self.openai_api_key = openai_api_key self.model_name = model_name + self.gms_server = gms_server # SQL 생성을 위한 데이터베이스 메타데이터 조회 도구 self.tools = [ search_database_tables, # 데이터베이스 테이블 정보 검색 get_weather, # 테스트용 도구 (추후 제거 가능) get_famous_opensource, # 테스트용 도구 (추후 제거 가능) + get_glossary_terms, # 용어집 조회 도구 ] self.llm = self._setup_llm() # LLM 인스턴스 설정 self.app = self._setup_workflow() # LangGraph 워크플로우 설정 @@ -75,10 +99,10 @@ def _setup_workflow(self): Returns: CompiledGraph: 컴파일된 LangGraph 워크플로우 """ - # MessagesState를 사용하는 StateGraph 생성 - workflow = StateGraph(state_schema=MessagesState) + # ChatBotState를 사용하는 StateGraph 생성 + workflow = StateGraph(state_schema=ChatBotState) - def call_model(state: MessagesState): + def call_model(state: ChatBotState): """ LLM 모델을 호출하는 노드 함수 LLM이 응답을 생성하거나 tool 호출을 결정합니다. @@ -89,26 +113,38 @@ def call_model(state: MessagesState): Returns: dict: LLM 응답이 포함된 상태 업데이트 """ - # SQL 생성 전문 어시스턴트 시스템 메시지 + # 질문 구체화 전문 어시스턴트 시스템 메시지 sys_msg = SystemMessage( content="""# 역할 -당신은 사용자가 SQL 쿼리를 생성하도록 돕는 전문 AI 어시스턴트입니다. +당신은 사용자의 모호한 질문을 명확하고 구체적인 질문으로 만드는 전문 AI 어시스턴트입니다. # 주요 임무 -- 사용자의 자연어 질문을 이해하고 SQL 쿼리 생성에 필요한 정보를 파악합니다 -- 필요한 경우 데이터베이스 스키마나 메타데이터를 확인하기 위해 도구를 활용합니다 -- 단계별로 사용자와 대화하며 명확한 SQL 쿼리를 만들어갑니다 -- 생성된 SQL에 대해 이해하기 쉽게 설명합니다 +- 사용자의 자연어 질문을 이해하고 의도를 정확히 파악합니다 +- 대화를 통해 날짜, 지표, 필터 조건 등 구체적인 정보를 수집합니다 +- 단계별로 사용자와 대화하며 명확하고 구체적인 질문으로 다듬어갑니다 # 작업 프로세스 -1. 사용자의 의도를 명확히 파악 -2. 필요한 테이블/컬럼 정보 확인 (도구 사용) -3. 사용자의 질문을 바탕으로 정보를 추출할 수 있는 명확한 질문으로 변환합니다 +1. 사용자의 최초 질문에서 의도 파악 +2. 질문을 명확히 하기 위해 필요한 정보 식별 (날짜, 지표, 대상, 조건 등) +3. **도구를 적극 활용하여 데이터베이스 스키마, 테이블 정보, 용어집 등을 확인** +4. 부족한 정보를 자연스럽게 질문하여 수집 +5. 수집된 정보를 바탕으로 질문을 점진적으로 구체화 +6. 충분히 구체화되면 최종 질문 확정 + +# 도구 사용 가이드 +- **search_database_tables**: 사용자와의 대화를 데이터와 연관짓기 위해 관련 테이블을 적극적으로 확인 +- **get_glossary_terms**: 사용자가 사용한 용어의 정확한 의미를 확인할 때 사용 +- 도구를 사용하면 더 정확하고 구체적인 질문을 만들 수 있습니다 +- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인하세요 + +# 예시 +- 모호한 질문: "KPI가 궁금해" +- 대화 후 구체화: "2025-01-02 날짜의 신규 유저가 발생시킨 매출이 궁금해" # 주의사항 - 항상 친절하고 명확하게 대화합니다 - 이전 대화 맥락을 고려하여 일관성 있게 응답합니다 -- 사용자가 SQL을 이해할 수 있도록 단계별로 설명합니다 +- 한 번에 너무 많은 것을 물어보지 않고 단계적으로 진행합니다 --- 다음은 사용자와의 대화입니다:""" @@ -118,7 +154,7 @@ def call_model(state: MessagesState): response = self.llm.invoke(messages) return {"messages": response} - def route_model_output(state: MessagesState): + def route_model_output(state: ChatBotState): """ LLM 출력에 따라 다음 노드를 결정하는 라우팅 함수 Tool 호출이 필요한 경우 'tools' 노드로, 아니면 대화를 종료합니다. @@ -161,10 +197,15 @@ def chat(self, message: str, thread_id: str): Returns: dict: LLM 응답을 포함한 결과 딕셔너리 """ - return self.app.invoke( - {"messages": [{"role": "user", "content": message}]}, - {"configurable": {"thread_id": thread_id}}, # thread_id로 대화 기록 관리 - ) + config = {"configurable": {"thread_id": thread_id}} + + # 상태 준비 + input_state = { + "messages": [{"role": "user", "content": message}], + "gms_server": self.gms_server, # DataHub 서버 URL을 상태에 포함 + } + + return self.app.invoke(input_state, config) def update_model(self, model_name: str): """ diff --git a/utils/llm/tools/__init__.py b/utils/llm/tools/__init__.py index 7575d32..8720b1a 100644 --- a/utils/llm/tools/__init__.py +++ b/utils/llm/tools/__init__.py @@ -8,6 +8,7 @@ get_weather, get_famous_opensource, search_database_tables, + get_glossary_terms, ) __all__ = [ @@ -17,4 +18,5 @@ "get_weather", "get_famous_opensource", "search_database_tables", + "get_glossary_terms", ] diff --git a/utils/llm/tools/chatbot_tool.py b/utils/llm/tools/chatbot_tool.py index d12bba0..b75be7e 100644 --- a/utils/llm/tools/chatbot_tool.py +++ b/utils/llm/tools/chatbot_tool.py @@ -4,6 +4,8 @@ from langchain_core.tools import tool from utils.llm.retrieval import search_tables +from utils.data.datahub_services.base_client import DataHubBaseClient +from utils.data.datahub_services.glossary_service import GlossaryService @tool @@ -118,3 +120,136 @@ def search_database_tables( return search_tables( query=query, retriever_name=retriever_name, top_n=top_n, device=device ) + + +def _simplify_glossary_data(glossary_data): + """ + 용어집 데이터를 name, description, children만 포함하는 간단한 형태로 변환 + + Args: + glossary_data: 처리된 용어집 데이터 + + Returns: + list: 간소화된 용어집 데이터 (name, description, children만 포함) + """ + if "error" in glossary_data: + return glossary_data + + result = [] + + for node in glossary_data.get("nodes", []): + simplified_node = { + "name": node.get("name"), + "description": node.get("description"), + } + + # children 정보가 있으면 추가 + if "details" in node and "children" in node["details"]: + children = [] + for child in node["details"]["children"]: + child_info = { + "name": child.get("name"), + "description": child.get("description"), + } + children.append(child_info) + + if children: + simplified_node["children"] = children + + result.append(simplified_node) + + return result + + +@tool +def get_glossary_terms(gms_server: str = "http://35.222.65.99:8080") -> list: + """ + DataHub에서 용어집(Glossary) 정보를 조회합니다. + + 이 함수는 DataHub 서버에 연결하여 전체 용어집 데이터를 가져옵니다. + 용어집은 비즈니스 용어, 도메인 지식, 데이터 정의 등을 표준화하여 관리하는 곳입니다. + + **중요**: 사용자의 질문이나 대화에서 다음과 같은 상황이 발생하면 반드시 이 도구를 사용하세요: + 1. 이해되지 않거나 모호한 단어가 나왔을 때 + 2. 특정 조직이나 도메인에서 고유하게 사용되는 전문 용어가 나왔을 때 + 3. 일반적이지 않은 약어나 줄임말이 나왔을 때 + 4. 조직 내부에서만 통용되는 용어가 나왔을 때 + 5. 표준 정의가 필요한 비즈니스 용어가 나왔을 때 + + Args: + gms_server (str, optional): DataHub GMS 서버 URL입니다. + 기본값은 "http://35.222.65.99:8080" + + Returns: + list: 간소화된 용어집 데이터 리스트입니다. + 각 항목은 name, description, children(선택적) 필드를 포함합니다. + + 예시 형태: + [ + { + "name": "가짜연구소", + "description": "스터디 단체 가짜연구소를 의미하며...", + "children": [ + { + "name": "빌더", + "description": "가짜연구소 스터디 리더를 지칭..." + } + ] + }, + { + "name": "PII", + "description": "개인 식별 정보...", + "children": [ + { + "name": "identifier", + "description": "개인식별정보중 github 아이디..." + } + ] + } + ] + + Examples: + >>> get_glossary_terms() + [{'name': '가짜연구소', 'description': '...', 'children': [...]}] + + Note: + 이 도구는 다음과 같은 경우에 **반드시** 사용하세요: + + [명시적 요청] + - "용어집을 보여줘" + - "비즈니스 용어가 뭐가 있어?" + - "데이터 사전 정보를 알려줘" + - "정의된 용어들을 보여줘" + + [이해되지 않는 단어 감지 - 매우 중요!] + - 일반적이지 않은 약어나 전문 용어가 대화에 등장할 때 + - 표준 정의가 없는 도메인 특화 용어가 나올 때 + - 질문의 맥락에서 모호하거나 불명확한 용어가 있을 때 + + [조직/도메인 특화 상황] + - 특정 조직에서만 사용하는 내부 용어가 나올 때 + - 업계/도메인 전문 용어가 필요할 때 + - 데이터나 테이블 관련 비즈니스 컨텍스트를 이해하기 위해 + + **핵심**: 응답하기 전에 사용자의 질문에 조직 특화 용어나 모호한 단어가 + 있는지 확인하고, 있다면 먼저 이 도구를 호출하여 정확한 정의를 파악하세요. + """ + try: + # DataHub 클라이언트 초기화 + client = DataHubBaseClient(gms_server=gms_server) + + # GlossaryService 초기화 + glossary_service = GlossaryService(client) + + # 전체 용어집 데이터 가져오기 + glossary_data = glossary_service.get_glossary_data() + + # 간소화된 데이터 반환 + simplified_data = _simplify_glossary_data(glossary_data) + + return simplified_data + + except ValueError as e: + return {"error": True, "message": f"DataHub 서버 연결 실패: {str(e)}"} + except Exception as e: + return {"error": True, "message": f"용어집 조회 중 오류 발생: {str(e)}"} From 96861bf216272bf418a7ad5b9e0f8411cb01b204 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Sun, 19 Oct 2025 23:37:33 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=EC=B1=97=EB=B4=87=20=EB=8F=84=EA=B5=AC?= =?UTF-8?q?=EC=97=90=EC=84=9C=20get=5Fweather=20=EB=B0=8F=20get=5Ffamous?= =?UTF-8?q?=5Fopensource=20=EC=A0=9C=EA=B1=B0,=20get=5Fquery=5Fexamples=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/llm/chatbot.py | 16 +-- utils/llm/tools/__init__.py | 6 +- utils/llm/tools/chatbot_tool.py | 176 +++++++++++++++++++++----------- 3 files changed, 128 insertions(+), 70 deletions(-) diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py index 641fc6c..27bea40 100644 --- a/utils/llm/chatbot.py +++ b/utils/llm/chatbot.py @@ -13,10 +13,9 @@ from langgraph.prebuilt import ToolNode from utils.llm.tools import ( - get_weather, - get_famous_opensource, search_database_tables, get_glossary_terms, + get_query_examples, ) @@ -67,9 +66,8 @@ def __init__( # SQL 생성을 위한 데이터베이스 메타데이터 조회 도구 self.tools = [ search_database_tables, # 데이터베이스 테이블 정보 검색 - get_weather, # 테스트용 도구 (추후 제거 가능) - get_famous_opensource, # 테스트용 도구 (추후 제거 가능) get_glossary_terms, # 용어집 조회 도구 + get_query_examples, # 쿼리 예제 조회 도구 ] self.llm = self._setup_llm() # LLM 인스턴스 설정 self.app = self._setup_workflow() # LangGraph 워크플로우 설정 @@ -132,9 +130,10 @@ def call_model(state: ChatBotState): 6. 충분히 구체화되면 최종 질문 확정 # 도구 사용 가이드 -- **search_database_tables**: 사용자와의 대화를 데이터와 연관짓기 위해 관련 테이블을 적극적으로 확인 -- **get_glossary_terms**: 사용자가 사용한 용어의 정확한 의미를 확인할 때 사용 -- 도구를 사용하면 더 정확하고 구체적인 질문을 만들 수 있습니다 +- **search_database_tables**: 사용자와의 대화를 데이터와 연관짓기 위해 관련 테이블을 적극적으로 확인할 수 있는 도구 +- **get_glossary_terms**: 사용자가 사용한 용어의 정확한 의미를 확인할 때 사용가능한 도구 +- **get_query_examples**: 조직내 저장된 쿼리 예제를 조회하여 참고할 수 있는 도구 +- 답변하기 전에 최대한 많은 도구를 적극 활용하여 정보를 수집하세요 - 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인하세요 # 예시 @@ -145,6 +144,9 @@ def call_model(state: ChatBotState): - 항상 친절하고 명확하게 대화합니다 - 이전 대화 맥락을 고려하여 일관성 있게 응답합니다 - 한 번에 너무 많은 것을 물어보지 않고 단계적으로 진행합니다 +- **중요: 사용자가 말한 내용이 충분히 구체화되지 않거나 의도가 명확히 파악되지 않을 경우, 추측하지 말고 모든 도구(get_glossary_terms, get_query_examples, search_database_tables)를 적극적으로 사용하여 맥락을 파악하세요** +- 도구를 통해 수집한 정보를 바탕으로 사용자에게 구체적인 방향성과 옵션을 제안하세요 +- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인한 후 답변하세요 --- 다음은 사용자와의 대화입니다:""" diff --git a/utils/llm/tools/__init__.py b/utils/llm/tools/__init__.py index 8720b1a..f0dcb9d 100644 --- a/utils/llm/tools/__init__.py +++ b/utils/llm/tools/__init__.py @@ -5,18 +5,16 @@ ) from utils.llm.tools.chatbot_tool import ( - get_weather, - get_famous_opensource, search_database_tables, get_glossary_terms, + get_query_examples, ) __all__ = [ "set_gms_server", "get_info_from_db", "get_metadata_from_db", - "get_weather", - "get_famous_opensource", "search_database_tables", "get_glossary_terms", + "get_query_examples", ] diff --git a/utils/llm/tools/chatbot_tool.py b/utils/llm/tools/chatbot_tool.py index b75be7e..da6e0c9 100644 --- a/utils/llm/tools/chatbot_tool.py +++ b/utils/llm/tools/chatbot_tool.py @@ -6,65 +6,7 @@ from utils.llm.retrieval import search_tables from utils.data.datahub_services.base_client import DataHubBaseClient from utils.data.datahub_services.glossary_service import GlossaryService - - -@tool -def get_weather(city: str) -> str: - """ - 특정 도시의 현재 날씨 정보를 조회합니다. - - 이 함수는 도시 이름을 입력받아 해당 도시의 날씨 정보를 반환합니다. - 사용자가 날씨, 기상, weather 등의 키워드와 함께 도시 이름을 언급하면 이 도구를 사용하세요. - - Args: - city (str): 날씨를 확인하고 싶은 도시의 이름입니다. - 예: "Seoul", "New York", "Tokyo", "서울", "부산" 등 - 영문과 한글 도시명을 모두 지원합니다. - - Returns: - str: 해당 도시의 날씨 정보를 담은 문자열입니다. - 현재는 항상 맑은 날씨를 반환합니다. - - Examples: - >>> get_weather("Seoul") - 'Seoul is sunny' - - >>> get_weather("서울") - '서울 is sunny' - - Note: - 이 도구는 다음과 같은 경우에 사용하세요: - - "서울 날씨 어때?" - - "What's the weather in New York?" - - "도쿄의 날씨를 알려줘" - - "부산 날씨 확인해줘" - """ - return f"{city} is sunny" - - -@tool -def get_famous_opensource() -> str: - """ - 가장 유명한 오픈소스 프로젝트를 조회합니다. - - 이 함수는 현재 가장 유명한 오픈소스 프로젝트의 이름을 반환합니다. - 사용자가 유명한 오픈소스, 인기있는 오픈소스, 최고의 오픈소스 등을 물어보면 이 도구를 사용하세요. - - Returns: - str: 가장 유명한 오픈소스 프로젝트 이름 - - Examples: - >>> get_famous_opensource() - 'Lang2SQL' - - Note: - 이 도구는 다음과 같은 경우에 사용하세요: - - "제일 유명한 오픈소스가 뭐야?" - - "가장 인기있는 오픈소스는?" - - "최고의 오픈소스 프로젝트 알려줘" - - "유명한 오픈소스 추천해줘" - """ - return "Lang2SQL" +from utils.data.datahub_services.query_service import QueryService @tool @@ -253,3 +195,119 @@ def get_glossary_terms(gms_server: str = "http://35.222.65.99:8080") -> list: return {"error": True, "message": f"DataHub 서버 연결 실패: {str(e)}"} except Exception as e: return {"error": True, "message": f"용어집 조회 중 오류 발생: {str(e)}"} + + +@tool +def get_query_examples( + gms_server: str = "http://35.222.65.99:8080", + start: int = 0, + count: int = 10, + query: str = "*", +) -> list: + """ + DataHub에서 저장된 쿼리 예제들을 조회합니다. + + 이 함수는 DataHub 서버에 연결하여 저장된 SQL 쿼리 목록을 가져옵니다. + 조직에서 실제로 사용되고 검증된 쿼리 패턴을 참고하여 더 정확한 SQL을 생성할 수 있습니다. + + **중요**: 사용자의 질문이나 대화에서 다음과 같은 상황이 발생하면 반드시 이 도구를 사용하세요: + 1. 일반적인 SQL 패턴으로 해결하기 어려운 복잡한 쿼리 요청일 때 + 2. 조직 특화된 비즈니스 로직이나 데이터 처리 방식이 필요할 때 + 3. 특정 도메인의 표준 쿼리 패턴이나 관례를 따라야 할 때 + 4. 여러 테이블 간의 복잡한 JOIN이나 집계가 필요할 때 + 5. 사용자가 과거 실행했던 쿼리와 유사한 작업을 요청할 때 + 6. 조직 내에서 검증된 쿼리 작성 방식을 확인해야 할 때 + + Args: + gms_server (str, optional): DataHub GMS 서버 URL입니다. + 기본값은 "http://35.222.65.99:8080" + start (int, optional): 조회 시작 위치입니다. 기본값은 0 + count (int, optional): 조회할 쿼리 개수입니다. 기본값은 10 + query (str, optional): 검색 쿼리입니다. 기본값은 "*" (모든 쿼리) + + Returns: + list: 쿼리 정보 리스트입니다. + 각 항목은 name, description, statement 필드를 포함합니다. + + 예시 형태: + [ + { + "name": "고객별 주문 수 조회", + "description": "각 고객별 주문 건수를 집계하는 쿼리", + "statement": "SELECT customer_id, COUNT(*) as order_count FROM orders GROUP BY customer_id" + }, + { + "name": "월별 매출 현황", + "description": "월별 총 매출을 계산하는 쿼리", + "statement": "SELECT DATE_TRUNC('month', order_date) as month, SUM(amount) FROM orders GROUP BY month" + } + ] + + Examples: + >>> get_query_examples() + [{'name': '고객별 주문 수 조회', 'description': '...', 'statement': 'SELECT ...'}] + + >>> get_query_examples(count=5) + # 5개의 쿼리 예제만 조회 + + Note: + 이 도구는 다음과 같은 경우에 **반드시** 사용하세요: + + [명시적 요청] + - "쿼리 예제를 보여줘" + - "저장된 쿼리들을 알려줘" + - "과거 쿼리 내역을 보고 싶어" + - "SQL 예제가 있어?" + + [도메인/조직 특화 패턴 감지 - 매우 중요!] + - 조직 특화된 데이터 처리 방식이나 계산 로직이 필요할 때 + - 특정 도메인의 관례적인 쿼리 패턴을 따라야 할 때 + - 데이터 품질 규칙이나 비즈니스 룰이 반영된 쿼리가 필요할 때 + - 조직 내에서 표준화된 쿼리 작성 방식을 확인해야 할 때 + + [쿼리 작성 참고] + - "이런 유형의 쿼리는 어떻게 작성해?" + - "비슷한 쿼리 있어?" + - "다른 사람들은 어떻게 쿼리를 작성했어?" + - "참고할만한 쿼리가 있을까?" + - "이 테이블들을 어떻게 조인해야 해?" + + **핵심**: SQL 쿼리를 생성하기 전에 사용자의 요청이 복잡하거나, + 조직 특화된 비즈니스 로직이 필요하거나, 일반적인 패턴으로 커버하기 + 어렵다고 판단되면, 먼저 이 도구를 호출하여 조직에서 검증된 + 쿼리 예제를 참고하세요. 이는 더 정확하고 조직의 표준을 따르는 + SQL을 생성하는 데 큰 도움이 됩니다. + """ + try: + # DataHub 클라이언트 초기화 + client = DataHubBaseClient(gms_server=gms_server) + + # QueryService 초기화 + query_service = QueryService(client) + + # 쿼리 데이터 가져오기 + result = query_service.get_query_data(start=start, count=count, query=query) + + # 오류 체크 + if "error" in result and result["error"]: + return {"error": True, "message": result.get("message")} + + # name, description, statement만 추출하여 리스트 생성 + simplified_queries = [] + for query_item in result.get("queries", []): + simplified_query = { + "name": query_item.get("name"), + "description": query_item.get("description", ""), + "statement": query_item.get("statement", ""), + } + simplified_queries.append(simplified_query) + + return simplified_queries + + except ValueError as e: + return {"error": True, "message": f"DataHub 서버 연결 실패: {str(e)}"} + except Exception as e: + return { + "error": True, + "message": f"쿼리 예제 조회 중 오류 발생: {str(e)}", + } From 62bb267e22abedaba32a66f29c42481e0c304fd8 Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Mon, 20 Oct 2025 08:03:24 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=EC=A7=80=EC=97=B0=20import=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chatbot_tool 에서 search_tables를 import 하면 순환참조가 발생해 수정 --- utils/llm/tools/chatbot_tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/llm/tools/chatbot_tool.py b/utils/llm/tools/chatbot_tool.py index da6e0c9..9c496f0 100644 --- a/utils/llm/tools/chatbot_tool.py +++ b/utils/llm/tools/chatbot_tool.py @@ -3,7 +3,6 @@ """ from langchain_core.tools import tool -from utils.llm.retrieval import search_tables from utils.data.datahub_services.base_client import DataHubBaseClient from utils.data.datahub_services.glossary_service import GlossaryService from utils.data.datahub_services.query_service import QueryService @@ -59,6 +58,8 @@ def search_database_tables( - "사용 가능한 컬럼을 보여줘" - SQL 쿼리를 생성하기 전에 스키마 정보가 필요할 때 """ + from utils.llm.retrieval import search_tables + return search_tables( query=query, retriever_name=retriever_name, top_n=top_n, device=device ) From d22bf3c3b05f42391260252779a0b67d266f660b Mon Sep 17 00:00:00 2001 From: ehddnr301 Date: Mon, 20 Oct 2025 08:06:25 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore=20-=20=EB=AC=B8=EA=B5=AC=EC=88=98?= =?UTF-8?q?=EC=A0=95,=20=EB=B6=88=ED=95=84=EC=9A=94=EA=B0=92=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface/app_pages/chatbot.py | 7 ++++--- utils/llm/chatbot.py | 8 -------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/interface/app_pages/chatbot.py b/interface/app_pages/chatbot.py index e7a8ef4..9879147 100644 --- a/interface/app_pages/chatbot.py +++ b/interface/app_pages/chatbot.py @@ -85,9 +85,10 @@ def initialize_session_state(): st.markdown( """ LangGraph 기반 AI ChatBot과 대화를 나눌 수 있습니다. - - 날씨 정보 조회 - - 유명한 오픈소스 프로젝트 정보 - - 일반적인 질문과 대화 + - 데이터베이스 테이블 정보 검색 + - 용어집 조회 + - 쿼리 예제 조회 + - 대화를 통해 질문 구체화 """ ) diff --git a/utils/llm/chatbot.py b/utils/llm/chatbot.py index 27bea40..51bcab0 100644 --- a/utils/llm/chatbot.py +++ b/utils/llm/chatbot.py @@ -38,14 +38,6 @@ class ChatBot: MemorySaver를 통해 대화 기록을 관리합니다. """ - # 사용 가능한 OpenAI 모델 목록 (키: 모델ID, 값: 표시명) - AVAILABLE_MODELS = { - "gpt-4o": "GPT-4o", - "gpt-4o-mini": "GPT-4o Mini", - "gpt-4-turbo": "GPT-4 Turbo", - "gpt-3.5-turbo": "GPT-3.5 Turbo", - } - def __init__( self, openai_api_key: str,