From 084acd373189257569508f66ffba006077596be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=87=E1=85=A1=E1=86=A8=E1=84=80=E1=85=A7=E1=86=BC?= =?UTF-8?q?=E1=84=90=E1=85=A2?= Date: Wed, 1 Oct 2025 14:22:29 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=EB=A9=80=ED=8B=B0=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20Navigation?= =?UTF-8?q?=20API=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- interface/{ => app_pages}/graph_builder.py | 8 +- interface/app_pages/home.py | 26 +++++++ interface/{ => app_pages}/lang2sql.py | 51 ++++++------- interface/{ => core}/dialects.py | 0 interface/streamlit_app.py | 86 +++++----------------- 5 files changed, 75 insertions(+), 96 deletions(-) rename interface/{ => app_pages}/graph_builder.py (99%) create mode 100644 interface/app_pages/home.py rename interface/{ => app_pages}/lang2sql.py (95%) rename interface/{ => core}/dialects.py (100%) diff --git a/interface/graph_builder.py b/interface/app_pages/graph_builder.py similarity index 99% rename from interface/graph_builder.py rename to interface/app_pages/graph_builder.py index 6eb347e..916d290 100644 --- a/interface/graph_builder.py +++ b/interface/app_pages/graph_builder.py @@ -11,17 +11,17 @@ from typing import List import streamlit as st -from langgraph.graph import StateGraph, END +from langgraph.graph import END, StateGraph from llm_utils.graph_utils.base import ( - QueryMakerState, + CONTEXT_ENRICHMENT, GET_TABLE_INFO, PROFILE_EXTRACTION, - CONTEXT_ENRICHMENT, QUERY_MAKER, + QueryMakerState, + context_enrichment_node, get_table_info_node, profile_extraction_node, - context_enrichment_node, query_maker_node, ) diff --git a/interface/app_pages/home.py b/interface/app_pages/home.py new file mode 100644 index 0000000..c876e3d --- /dev/null +++ b/interface/app_pages/home.py @@ -0,0 +1,26 @@ +""" +홈 페이지 모듈. + +Lang2SQL 데이터 분석 도구의 소개와 사용 방법을 안내합니다. +""" + +import streamlit as st + +st.title("🏠 홈") + +st.markdown( + """ + ### Lang2SQL 데이터 분석 도구에 오신 것을 환영합니다 🎉 + + 이 도구는 자연어로 작성한 질문을 SQL 쿼리로 변환하고, + 데이터베이스를 조회하여 결과를 **표와 차트**로 시각화합니다. + + --- + #### 사용 방법 + 1. 왼쪽 메뉴에서 원하는 기능 페이지를 선택하세요. + 2. **🔍 Lang2SQL**: 자연어 → SQL 변환 및 결과 분석 + 3. **📊 그래프 빌더**: 데이터 시각화를 위한 차트 구성 + """ +) + +st.info("왼쪽 메뉴에서 기능 페이지를 선택해 시작하세요 🚀") diff --git a/interface/lang2sql.py b/interface/app_pages/lang2sql.py similarity index 95% rename from interface/lang2sql.py rename to interface/app_pages/lang2sql.py index 7eefb3c..799ecbf 100644 --- a/interface/lang2sql.py +++ b/interface/app_pages/lang2sql.py @@ -5,22 +5,18 @@ ClickHouse 데이터베이스에 실행한 결과를 출력합니다. """ -import re +from copy import deepcopy +import pandas as pd import streamlit as st from langchain_core.messages import AIMessage -from interface.dialects import PRESET_DIALECTS, DialectOption -from copy import deepcopy from db_utils import get_db_connector -from db_utils.base_connector import BaseConnector -from viz.display_chart import DisplayChart from engine.query_executor import execute_query as execute_query_common -from llm_utils.llm_response_parser import LLMResponseParser from infra.observability.token_usage import TokenUtils -from llm_utils.graph_utils.enriched_graph import builder as enriched_builder -from llm_utils.graph_utils.basic_graph import builder - +from interface.core.dialects import PRESET_DIALECTS, DialectOption +from llm_utils.llm_response_parser import LLMResponseParser +from viz.display_chart import DisplayChart TITLE = "Lang2SQL" DEFAULT_QUERY = "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리" @@ -37,6 +33,17 @@ } +def _get_graph_builder(use_enriched: bool): + """ + 순환 import를 피하기 위해 사용 시점에 그래프 빌더를 import한다. + """ + if use_enriched: + from llm_utils.graph_utils.enriched_graph import builder as _builder + else: + from llm_utils.graph_utils.basic_graph import builder as _builder + return _builder + + def execute_query( *, query: str, @@ -241,6 +248,7 @@ def _as_float(value): if show_table_section or show_chart_section: database = get_db_connector() + df = pd.DataFrame() try: sql_raw = ( res["generated_query"].content @@ -255,9 +263,9 @@ def _as_float(value): except Exception as e: st.markdown("---") st.error(f"쿼리 실행 중 오류 발생: {e}") - df = None + df = pd.DataFrame() - if df is not None and show_table_section: + if not df.empty and show_table_section: st.markdown("---") st.markdown("**쿼리 실행 결과:**") try: @@ -309,33 +317,26 @@ def _as_float(value): "graph" not in st.session_state or st.session_state.get("use_enriched") != use_enriched ): - # 그래프 선택 로직 - if use_enriched: - graph_builder = enriched_builder - graph_type = "확장된" - else: - graph_builder = builder - graph_type = "기본" + graph_builder = _get_graph_builder(use_enriched) + graph_type = "확장된" if use_enriched else "기본" st.session_state["graph"] = graph_builder.compile() st.session_state["use_enriched"] = use_enriched st.info(f"Lang2SQL이 성공적으로 시작되었습니다. ({graph_type} 워크플로우)") + # 새로고침 버튼 추가 if st.sidebar.button("Lang2SQL 새로고침"): - # 그래프 선택 로직 - if st.session_state.get("use_enriched"): - graph_builder = enriched_builder - graph_type = "확장된" - else: - graph_builder = builder - graph_type = "기본" + use_enriched_curr = st.session_state.get("use_enriched", False) + graph_builder = _get_graph_builder(use_enriched_curr) + graph_type = "확장된" if use_enriched_curr else "기본" st.session_state["graph"] = graph_builder.compile() st.sidebar.success( f"Lang2SQL이 성공적으로 새로고침되었습니다. ({graph_type} 워크플로우)" ) + user_query = st.text_area( "쿼리를 입력하세요:", value=DEFAULT_QUERY, diff --git a/interface/dialects.py b/interface/core/dialects.py similarity index 100% rename from interface/dialects.py rename to interface/core/dialects.py diff --git a/interface/streamlit_app.py b/interface/streamlit_app.py index 5ce5f86..308b867 100644 --- a/interface/streamlit_app.py +++ b/interface/streamlit_app.py @@ -1,75 +1,27 @@ """ -Streamlit 멀티 페이지 애플리케이션 설정 파일. +Streamlit 애플리케이션 메인 실행 모듈. -- PAGES 딕셔너리로 페이지 정보(page 경로 및 제목)를 관리합니다. -- PAGES를 기반으로 Streamlit 네비게이션 메뉴를 생성하고 실행합니다. +이 모듈은 Lang2SQL 데이터 분석 도구의 내비게이션을 정의하고, +각 페이지를 연결하여 사용자가 원하는 기능을 선택할 수 있도록 합니다. + +Example: + $ streamlit run interface/streamlit_app.py """ import streamlit as st -PAGES = { - "lang2sql": { - "page": "lang2sql.py", - "title": "Lang2SQL", - }, - "graph_builder": { - "page": "graph_builder.py", - "title": "Graph Builder", - }, -} - - -def validate_pages( - *, - pages_dict: dict, -) -> None: - """ - PAGES 딕셔너리의 구조와 값을 검증합니다. - - 검증 항목: - - 최상위 객체는 딕셔너리(dict)여야 합니다. - - 각 항목은 'page'와 'title' 키를 가진 딕셔너리여야 합니다. - - 'page'와 'title' 값은 비어 있지 않은 문자열이어야 합니다. - - 오류 발생: - - 조건을 만족하지 않으면 ValueError를 발생시킵니다. - - Args: - pages_dict (dict): 페이지 정보가 담긴 딕셔너리 - - Raises: - ValueError: 유효하지 않은 구조나 값을 가진 경우 - """ - - if not isinstance(pages_dict, dict): - raise ValueError("PAGES must be a dictionary.") - - for key, page_info in pages_dict.items(): - if not isinstance(page_info, dict): - raise ValueError( - f"Each page info must be a dictionary. Error at key: {key}" - ) - - if "page" not in page_info or "title" not in page_info: - raise ValueError( - f"Each page must have 'page' and 'title' fields. Error at key: {key}" - ) - - if not isinstance(page_info["page"], str) or not page_info["page"]: - raise ValueError(f"'page' must be a non-empty string. Error at key: {key}") - - if not isinstance(page_info["title"], str) or not page_info["title"]: - raise ValueError(f"'title' must be a non-empty string. Error at key: {key}") - - -validate_pages(pages_dict=PAGES) - -pages = [ - st.Page( - page=page_info["page"], - title=page_info["title"], - ) - for page_info in PAGES.values() +st.set_page_config( + page_title="Lang2SQL 데이터 분석 도구", + page_icon="🔎", + layout="wide", + initial_sidebar_state="expanded", +) + +PAGES = [ + st.Page("app_pages/home.py", title="🏠 홈"), + st.Page("app_pages/lang2sql.py", title="🔍 Lang2SQL"), + st.Page("app_pages/graph_builder.py", title="📊 그래프 빌더"), ] -st.navigation(pages).run() +pg = st.navigation(PAGES) +pg.run()