diff --git a/interface/app_pages/chatbot.py b/interface/app_pages/chatbot.py new file mode 100644 index 0000000..9879147 --- /dev/null +++ b/interface/app_pages/chatbot.py @@ -0,0 +1,159 @@ +""" +AI ChatBot 페이지 +LangGraph와 OpenAI를 활용한 대화형 인터페이스 +""" + +import os +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_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 키를 입력하거나, 사이드바에서 LLM 프로파일을 적용해주세요." + ) + st.stop() + + # 사용할 모델명 가져오기 (llm_selector에서 설정한 값) + model_name = ( + st.session_state.get("OPEN_AI_LLM_MODEL") + or os.getenv("OPEN_AI_LLM_MODEL") + 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, gms_server=gms_server + ) + else: + # 기존 인스턴스가 있는 경우, 모델이나 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, gms_server=gms_server + ) + + +# 세션 상태 초기화 실행 +initialize_session_state() + +# 페이지 제목 +st.title("🤖 AI ChatBot") + +st.markdown( + """ + LangGraph 기반 AI ChatBot과 대화를 나눌 수 있습니다. + - 데이터베이스 테이블 정보 검색 + - 용어집 조회 + - 쿼리 예제 조회 + - 대화를 통해 질문 구체화 + """ +) + +# 설정 로드 +config = load_config() + +# 사이드바 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() + 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 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..51bcab0 --- /dev/null +++ b/utils/llm/chatbot.py @@ -0,0 +1,214 @@ +""" +LangGraph 기반 ChatBot 모델 +OpenAI의 ChatGPT 모델을 사용하여 대화 기록을 유지하는 챗봇 구현 +""" + +from typing import Annotated, Sequence, TypedDict + +from langchain_core.messages import BaseMessage, SystemMessage +from langchain_openai import ChatOpenAI +from langgraph.checkpoint.memory import MemorySaver +from langgraph.graph import START, StateGraph +from langgraph.graph.message import add_messages +from langgraph.prebuilt import ToolNode + +from utils.llm.tools import ( + search_database_tables, + get_glossary_terms, + get_query_examples, +) + + +class ChatBotState(TypedDict): + """ + 챗봇 상태 - 사용자 질문을 SQL로 변환 가능한 구체적인 질문으로 만들어가는 과정 추적 + """ + + # 기본 메시지 (MessagesState와 동일) + messages: Annotated[Sequence[BaseMessage], add_messages] + + # datahub 서버 정보 + gms_server: str + + +class ChatBot: + """ + LangGraph를 사용한 대화형 챗봇 클래스 + OpenAI API를 통해 다양한 GPT 모델을 사용할 수 있으며, + MemorySaver를 통해 대화 기록을 관리합니다. + """ + + 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_glossary_terms, # 용어집 조회 도구 + get_query_examples, # 쿼리 예제 조회 도구 + ] + 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.0, # SQL 생성은 정확성이 중요하므로 0으로 설정 + 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 워크플로우 + """ + # ChatBotState를 사용하는 StateGraph 생성 + workflow = StateGraph(state_schema=ChatBotState) + + def call_model(state: ChatBotState): + """ + LLM 모델을 호출하는 노드 함수 + LLM이 응답을 생성하거나 tool 호출을 결정합니다. + + Args: + state: 현재 메시지 상태 + + Returns: + dict: LLM 응답이 포함된 상태 업데이트 + """ + # 질문 구체화 전문 어시스턴트 시스템 메시지 + sys_msg = SystemMessage( + content="""# 역할 +당신은 사용자의 모호한 질문을 명확하고 구체적인 질문으로 만드는 전문 AI 어시스턴트입니다. + +# 주요 임무 +- 사용자의 자연어 질문을 이해하고 의도를 정확히 파악합니다 +- 대화를 통해 날짜, 지표, 필터 조건 등 구체적인 정보를 수집합니다 +- 단계별로 사용자와 대화하며 명확하고 구체적인 질문으로 다듬어갑니다 + +# 작업 프로세스 +1. 사용자의 최초 질문에서 의도 파악 +2. 질문을 명확히 하기 위해 필요한 정보 식별 (날짜, 지표, 대상, 조건 등) +3. **도구를 적극 활용하여 데이터베이스 스키마, 테이블 정보, 용어집 등을 확인** +4. 부족한 정보를 자연스럽게 질문하여 수집 +5. 수집된 정보를 바탕으로 질문을 점진적으로 구체화 +6. 충분히 구체화되면 최종 질문 확정 + +# 도구 사용 가이드 +- **search_database_tables**: 사용자와의 대화를 데이터와 연관짓기 위해 관련 테이블을 적극적으로 확인할 수 있는 도구 +- **get_glossary_terms**: 사용자가 사용한 용어의 정확한 의미를 확인할 때 사용가능한 도구 +- **get_query_examples**: 조직내 저장된 쿼리 예제를 조회하여 참고할 수 있는 도구 +- 답변하기 전에 최대한 많은 도구를 적극 활용하여 정보를 수집하세요 +- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인하세요 + +# 예시 +- 모호한 질문: "KPI가 궁금해" +- 대화 후 구체화: "2025-01-02 날짜의 신규 유저가 발생시킨 매출이 궁금해" + +# 주의사항 +- 항상 친절하고 명확하게 대화합니다 +- 이전 대화 맥락을 고려하여 일관성 있게 응답합니다 +- 한 번에 너무 많은 것을 물어보지 않고 단계적으로 진행합니다 +- **중요: 사용자가 말한 내용이 충분히 구체화되지 않거나 의도가 명확히 파악되지 않을 경우, 추측하지 말고 모든 도구(get_glossary_terms, get_query_examples, search_database_tables)를 적극적으로 사용하여 맥락을 파악하세요** +- 도구를 통해 수집한 정보를 바탕으로 사용자에게 구체적인 방향성과 옵션을 제안하세요 +- 불확실한 정보가 있다면 추측하지 말고 도구를 사용하여 확인한 후 답변하세요 + +--- +다음은 사용자와의 대화입니다:""" + ) + # 시스템 메시지를 대화의 맨 앞에 추가 + messages = [sys_msg] + state["messages"] + response = self.llm.invoke(messages) + return {"messages": response} + + def route_model_output(state: ChatBotState): + """ + 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 응답을 포함한 결과 딕셔너리 + """ + 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): + """ + 사용 중인 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..f0dcb9d 100644 --- a/utils/llm/tools/__init__.py +++ b/utils/llm/tools/__init__.py @@ -4,8 +4,17 @@ set_gms_server, ) +from utils.llm.tools.chatbot_tool import ( + search_database_tables, + get_glossary_terms, + get_query_examples, +) + __all__ = [ "set_gms_server", "get_info_from_db", "get_metadata_from_db", + "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 new file mode 100644 index 0000000..9c496f0 --- /dev/null +++ b/utils/llm/tools/chatbot_tool.py @@ -0,0 +1,314 @@ +""" +LangGraph ChatBot에서 사용하는 도구(Tool) 함수들 +""" + +from langchain_core.tools import tool +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 + + +@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 쿼리를 생성하기 전에 스키마 정보가 필요할 때 + """ + from utils.llm.retrieval import search_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)}"} + + +@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)}", + }