diff --git a/cli/__init__.py b/cli/__init__.py index 6f9ee8a..52b7c87 100644 --- a/cli/__init__.py +++ b/cli/__init__.py @@ -1,29 +1,24 @@ -""" -Lang2SQL CLI 프로그램입니다. +"""Lang2SQL CLI 프로그램입니다. 이 프로그램은 Datahub GMS 서버 URL을 설정하고, 필요 시 Streamlit 인터페이스를 실행합니다. 명령어 예시: lang2sql --datahub_server http://localhost:8080 --run-streamlit """ -import logging -import os -import subprocess - import click -import dotenv +from cli.commands.quary import query_command +from cli.commands.run_streamlit import run_streamlit_cli_command +from cli.core.environment import initialize_environment +from cli.core.streamlit_runner import run_streamlit_command +from cli.utils.logger import configure_logging from infra.monitoring.check_server import CheckServer from llm_utils.tools import set_gms_server from version import __version__ -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s [%(levelname)s] %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", -) -logger = logging.getLogger(__name__) +logger = configure_logging() +# pylint: disable=redefined-outer-name,broad-exception-caught @click.group() @click.version_option(version=__version__) @click.pass_context @@ -79,7 +74,6 @@ "기본값: FAISS는 './dev/table_info_db', pgvector는 환경변수 사용" ), ) -# pylint: disable=redefined-outer-name def cli( ctx: click.Context, datahub_server: str, @@ -90,69 +84,24 @@ def cli( vectordb_type: str = "faiss", vectordb_location: str = None, ) -> None: - """ - Datahub GMS 서버 URL을 설정하고, Streamlit 애플리케이션을 실행할 수 있는 CLI 명령 그룹입니다. - - 이 함수는 다음 역할을 수행합니다: - - 전달받은 'datahub_server' URL을 바탕으로 GMS 서버 연결을 설정합니다. - - 설정 과정 중 오류가 발생하면 오류 메시지를 출력하고 프로그램을 종료합니다. - - '--run-streamlit' 옵션이 활성화된 경우, 지정된 포트에서 Streamlit 웹 앱을 즉시 실행합니다. - - '--env-file-path' 옵션이 지정된 경우, 해당 .env 파일에서 환경 변수를 로드합니다. - - '--prompt-dir-path' 옵션이 지정된 경우, 해당 디렉토리에서 프롬프트 템플릿을 로드합니다. - - 매개변수: - ctx (click.Context): 명령어 실행 컨텍스트 객체입니다. - datahub_server (str): 설정할 Datahub GMS 서버의 URL입니다. - run_streamlit (bool): Streamlit 앱을 실행할지 여부를 나타내는 플래그입니다. - port (int): Streamlit 서버가 바인딩될 포트 번호입니다. - env_file_path (str, optional): 환경 변수를 로드할 .env 파일 경로입니다. - prompt_dir_path (str, optional): 프롬프트 템플릿을 로드할 디렉토리 경로입니다. + """Lang2SQL CLI 엔트리포인트. - 주의: - 'set_gms_server' 함수에서 ValueError가 발생할 경우, 프로그램은 비정상 종료(exit code 1)합니다. + - 환경 변수 및 VectorDB 설정 초기화 + - GMS 서버 연결 및 헬스체크 + - 필요 시 Streamlit 애플리케이션 실행 """ - # 환경 변수 파일 로드 - if env_file_path: - try: - if not dotenv.load_dotenv(env_file_path, override=True): - click.secho(f"환경 변수 파일 로드 실패: {env_file_path}", fg="yellow") - else: - click.secho(f"환경 변수 파일 로드 성공: {env_file_path}", fg="green") - except Exception as e: - click.secho(f"환경 변수 로드 중 오류 발생: {str(e)}", fg="red") - ctx.exit(1) - else: - dotenv.load_dotenv(override=True) - - # 프롬프트 디렉토리를 환경 변수로 설정 - if prompt_dir_path: - try: - os.environ["PROMPT_TEMPLATES_DIR"] = prompt_dir_path - click.secho( - f"프롬프트 디렉토리 환경변수 설정됨: {prompt_dir_path}", fg="green" - ) - except Exception as e: - click.secho(f"프롬프트 디렉토리 환경변수 설정 실패: {str(e)}", fg="red") - ctx.exit(1) - - # VectorDB 타입을 환경 변수로 설정 try: - os.environ["VECTORDB_TYPE"] = vectordb_type - click.secho(f"VectorDB 타입 설정됨: {vectordb_type}", fg="green") - except Exception as e: - click.secho(f"VectorDB 타입 설정 실패: {str(e)}", fg="red") + initialize_environment( + env_file_path=env_file_path, + prompt_dir_path=prompt_dir_path, + vectordb_type=vectordb_type, + vectordb_location=vectordb_location, + ) + except Exception: + logger.error("Initialization failed.", exc_info=True) ctx.exit(1) - # VectorDB 경로를 환경 변수로 설정 - if vectordb_location: - try: - os.environ["VECTORDB_LOCATION"] = vectordb_location - click.secho(f"VectorDB 경로 설정됨: {vectordb_location}", fg="green") - except Exception as e: - click.secho(f"VectorDB 경로 설정 실패: {str(e)}", fg="red") - ctx.exit(1) - logger.info( "Initialization started: GMS server = %s, run_streamlit = %s, port = %d", datahub_server, @@ -171,181 +120,5 @@ def cli( run_streamlit_command(port) -def run_streamlit_command(port: int) -> None: - """ - 지정된 포트에서 Streamlit 애플리케이션을 실행하는 함수입니다. - - 이 함수는 subprocess를 통해 'streamlit run' 명령어를 실행하여 - 'interface/streamlit_app.py' 파일을 웹 서버 형태로 구동합니다. - 사용자가 지정한 포트 번호를 Streamlit 서버의 포트로 설정합니다. - - 매개변수: - port (int): Streamlit 서버가 바인딩될 포트 번호입니다. - - 주의: - - Streamlit이 시스템에 설치되어 있어야 정상 동작합니다. - - subprocess 호출 실패 시 예외가 발생할 수 있습니다. - """ - - logger.info("Starting Streamlit application on port %d...", port) - - try: - subprocess.run( - [ - "streamlit", - "run", - "interface/streamlit_app.py", - "--server.address=0.0.0.0", - "--server.port", - str(port), - ], - check=True, - ) - logger.info("Streamlit application started successfully.") - except subprocess.CalledProcessError as e: - logger.error("Failed to start Streamlit application: %s", e) - raise - - -@cli.command(name="run-streamlit") -@click.option( - "-p", - "--port", - type=int, - default=8501, - help=( - "Streamlit 애플리케이션이 바인딩될 포트 번호를 지정합니다. " - "기본 포트는 8501이며, 필요 시 포트 충돌을 피하거나 " - "여러 인스턴스를 동시에 실행할 때 다른 포트 번호를 설정할 수 있습니다." - ), -) -def run_streamlit_cli_command(port: int) -> None: - """ - CLI 명령어를 통해 Streamlit 애플리케이션을 실행하는 함수입니다. - - 이 명령은 'interface/streamlit_app.py' 파일을 Streamlit 서버로 구동하며, - 사용자가 지정한 포트 번호를 바인딩하여 웹 인터페이스를 제공합니다. - - 매개변수: - port (int): Streamlit 서버가 사용할 포트 번호입니다. 기본값은 8501입니다. - - 주의: - - Streamlit이 시스템에 설치되어 있어야 정상적으로 실행됩니다. - - Streamlit 실행에 실패할 경우 subprocess 호출에서 예외가 발생할 수 있습니다. - """ - - logger.info("Executing 'run-streamlit' command on port %d...", port) - run_streamlit_command(port) - - -@cli.command(name="query") -@click.argument("question", type=str) -@click.option( - "--database-env", - default="clickhouse", - help="사용할 데이터베이스 환경 (기본값: clickhouse)", -) -@click.option( - "--retriever-name", - default="기본", - help="테이블 검색기 이름 (기본값: 기본)", -) -@click.option( - "--top-n", - type=int, - default=5, - help="검색된 상위 테이블 수 제한 (기본값: 5)", -) -@click.option( - "--device", - default="cpu", - help="LLM 실행에 사용할 디바이스 (기본값: cpu)", -) -@click.option( - "--use-enriched-graph", - is_flag=True, - help="확장된 그래프(프로파일 추출 + 컨텍스트 보강) 사용 여부", -) -@click.option( - "--vectordb-type", - type=click.Choice(["faiss", "pgvector"]), - default="faiss", - help="사용할 벡터 데이터베이스 타입 (기본값: faiss)", -) -@click.option( - "--vectordb-location", - help=( - "VectorDB 위치 설정\n" - "- FAISS: 디렉토리 경로 (예: ./my_vectordb)\n" - "- pgvector: 연결 문자열 (예: postgresql://user:pass@host:port/db)\n" - "기본값: FAISS는 './dev/table_info_db', pgvector는 환경변수 사용" - ), -) -def query_command( - question: str, - database_env: str, - retriever_name: str, - top_n: int, - device: str, - use_enriched_graph: bool, - vectordb_type: str = "faiss", - vectordb_location: str = None, -) -> None: - """ - 자연어 질문을 SQL 쿼리로 변환하여 출력하는 명령어입니다. - - 이 명령은 사용자가 입력한 자연어 질문을 받아서 SQL 쿼리로 변환하고, - 생성된 SQL 쿼리만을 표준 출력으로 출력합니다. - - 매개변수: - question (str): SQL로 변환할 자연어 질문 - database_env (str): 사용할 데이터베이스 환경 - retriever_name (str): 테이블 검색기 이름 - top_n (int): 검색된 상위 테이블 수 제한 - device (str): LLM 실행에 사용할 디바이스 - use_enriched_graph (bool): 확장된 그래프 사용 여부 - - 예시: - lang2sql query "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리" - lang2sql query "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리" --use-enriched-graph - lang2sql query "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리" --vectordb-type pgvector - """ - - try: - from engine.query_executor import execute_query, extract_sql_from_result - - # VectorDB 타입을 환경 변수로 설정 - os.environ["VECTORDB_TYPE"] = vectordb_type - - # VectorDB 위치를 환경 변수로 설정 - if vectordb_location: - os.environ["VECTORDB_LOCATION"] = vectordb_location - - # 공용 함수를 사용하여 쿼리 실행 - res = execute_query( - query=question, - database_env=database_env, - retriever_name=retriever_name, - top_n=top_n, - device=device, - use_enriched_graph=use_enriched_graph, - ) - - # SQL 추출 및 출력 - sql = extract_sql_from_result(res) - if sql: - print(sql) - else: - # SQL 추출 실패 시 원본 쿼리 텍스트 출력 - generated_query = res.get("generated_query") - if generated_query: - query_text = ( - generated_query.content - if hasattr(generated_query, "content") - else str(generated_query) - ) - print(query_text) - - except Exception as e: - logger.error("쿼리 처리 중 오류 발생: %s", e) - raise +cli.add_command(run_streamlit_cli_command) +cli.add_command(query_command) diff --git a/cli/commands/quary.py b/cli/commands/quary.py new file mode 100644 index 0000000..0db9b9c --- /dev/null +++ b/cli/commands/quary.py @@ -0,0 +1,113 @@ +"""자연어 질문을 SQL 쿼리로 변환하는 CLI 명령어 정의 모듈. + +이 모듈은 사용자가 입력한 자연어 질문을 SQL 쿼리로 변환하여 출력하는 +`query` CLI 명령어를 제공합니다. +""" + +import os + +import click + +from cli.utils.logger import configure_logging + +logger = configure_logging() + + +@click.command(name="query") +@click.argument("question", type=str) +@click.option( + "--database-env", + default="clickhouse", + help="사용할 데이터베이스 환경 (기본값: clickhouse)", +) +@click.option( + "--retriever-name", + default="기본", + help="테이블 검색기 이름 (기본값: 기본)", +) +@click.option( + "--top-n", + type=int, + default=5, + help="검색된 상위 테이블 수 제한 (기본값: 5)", +) +@click.option( + "--device", + default="cpu", + help="LLM 실행에 사용할 디바이스 (기본값: cpu)", +) +@click.option( + "--use-enriched-graph", + is_flag=True, + help="확장된 그래프(프로파일 추출 + 컨텍스트 보강) 사용 여부", +) +@click.option( + "--vectordb-type", + type=click.Choice(["faiss", "pgvector"]), + default="faiss", + help="사용할 벡터 데이터베이스 타입 (기본값: faiss)", +) +@click.option( + "--vectordb-location", + help=( + "VectorDB 위치 설정\n" + "- FAISS: 디렉토리 경로 (예: ./my_vectordb)\n" + "- pgvector: 연결 문자열 (예: postgresql://user:pass@host:port/db)\n" + "기본값: FAISS는 './dev/table_info_db', pgvector는 환경변수 사용" + ), +) +def query_command( + question: str, + database_env: str, + retriever_name: str, + top_n: int, + device: str, + use_enriched_graph: bool, + vectordb_type: str = "faiss", + vectordb_location: str = None, +) -> None: + """자연어 질문을 SQL 쿼리로 변환하여 출력합니다. + + Args: + question (str): SQL로 변환할 자연어 질문 + database_env (str): 사용할 데이터베이스 환경 + retriever_name (str): 테이블 검색기 이름 + top_n (int): 검색된 상위 테이블 수 제한 + device (str): LLM 실행 디바이스 + use_enriched_graph (bool): 확장된 그래프 사용 여부 + vectordb_type (str): 벡터 데이터베이스 타입 ("faiss" 또는 "pgvector") + vectordb_location (Optional[str]): 벡터DB 경로 또는 연결 URL + """ + try: + from engine.query_executor import execute_query, extract_sql_from_result + + os.environ["VECTORDB_TYPE"] = vectordb_type + + if vectordb_location: + os.environ["VECTORDB_LOCATION"] = vectordb_location + + res = execute_query( + query=question, + database_env=database_env, + retriever_name=retriever_name, + top_n=top_n, + device=device, + use_enriched_graph=use_enriched_graph, + ) + + sql = extract_sql_from_result(res) + if sql: + print(sql) + else: + generated_query = res.get("generated_query") + if generated_query: + query_text = ( + generated_query.content + if hasattr(generated_query, "content") + else str(generated_query) + ) + print(query_text) + + except Exception as e: + logger.error("쿼리 처리 중 오류 발생: %s", e) + raise diff --git a/cli/commands/run_streamlit.py b/cli/commands/run_streamlit.py new file mode 100644 index 0000000..21aff02 --- /dev/null +++ b/cli/commands/run_streamlit.py @@ -0,0 +1,29 @@ +"""Streamlit 실행 CLI 명령어 모듈.""" + +import click + +from cli.core.streamlit_runner import run_streamlit_command +from cli.utils.logger import configure_logging + +logger = configure_logging() + + +@click.command(name="run-streamlit") +@click.option( + "-p", + "--port", + type=int, + default=8501, + help=( + "Streamlit 애플리케이션이 바인딩될 포트 번호를 지정합니다. " + "기본 포트는 8501이며, 필요 시 다른 포트를 설정할 수 있습니다." + ), +) +def run_streamlit_cli_command(port: int) -> None: + """CLI 명령어로 Streamlit 애플리케이션을 실행합니다. + + Args: + port (int): Streamlit 서버가 바인딩될 포트 번호. 기본값은 8501. + """ + logger.info("Executing 'run-streamlit' command on port %d...", port) + run_streamlit_command(port) diff --git a/cli/core/environment.py b/cli/core/environment.py new file mode 100644 index 0000000..5d4317f --- /dev/null +++ b/cli/core/environment.py @@ -0,0 +1,28 @@ +"""환경 변수 및 VectorDB 초기화 모듈.""" + +from typing import Optional + +from cli.utils.env_loader import load_env, set_prompt_dir, set_vectordb + + +def initialize_environment( + *, + env_file_path: Optional[str], + prompt_dir_path: Optional[str], + vectordb_type: str, + vectordb_location: Optional[str], +) -> None: + """환경 변수와 VectorDB 설정을 초기화합니다. + + Args: + env_file_path (Optional[str]): 로드할 .env 파일 경로. None이면 기본값 사용. + prompt_dir_path (Optional[str]): 프롬프트 템플릿 디렉토리 경로. None이면 설정하지 않음. + vectordb_type (str): VectorDB 타입 ("faiss" 또는 "pgvector"). + vectordb_location (Optional[str]): VectorDB 위치. None이면 기본값 사용. + + Raises: + Exception: 초기화 과정에서 오류가 발생한 경우. + """ + load_env(env_file_path=env_file_path) + set_prompt_dir(prompt_dir_path=prompt_dir_path) + set_vectordb(vectordb_type=vectordb_type, vectordb_location=vectordb_location) diff --git a/cli/core/streamlit_runner.py b/cli/core/streamlit_runner.py new file mode 100644 index 0000000..8437bf3 --- /dev/null +++ b/cli/core/streamlit_runner.py @@ -0,0 +1,36 @@ +"""Streamlit 실행 유틸리티 모듈.""" + +import subprocess + +from cli.utils.logger import configure_logging + +logger = configure_logging() + + +def run_streamlit_command(port: int) -> None: + """지정된 포트에서 Streamlit 애플리케이션을 실행합니다. + + Args: + port (int): 바인딩할 포트 번호. + + Raises: + subprocess.CalledProcessError: 실행 실패 시 발생. + """ + logger.info("Starting Streamlit application on port %d...", port) + + try: + subprocess.run( + [ + "streamlit", + "run", + "interface/streamlit_app.py", + "--server.address=0.0.0.0", + "--server.port", + str(port), + ], + check=True, + ) + logger.info("Streamlit application started successfully.") + except subprocess.CalledProcessError as e: + logger.error("Failed to start Streamlit application: %s", e) + raise diff --git a/cli/utils/env_loader.py b/cli/utils/env_loader.py new file mode 100644 index 0000000..768fdce --- /dev/null +++ b/cli/utils/env_loader.py @@ -0,0 +1,103 @@ +"""환경 변수 유틸리티 모듈. + +.env 파일 로드, 프롬프트 디렉토리 설정, +VectorDB 타입 및 위치 설정을 제공합니다. +""" + +import os +from pathlib import Path +from typing import Optional + +import click +import dotenv + + +def load_env( + *, + env_file_path: Optional[str] = None, +) -> None: + """환경 변수 파일(.env)을 로드합니다. + + Args: + env_file_path (Optional[str]): .env 파일 경로. None이면 기본 경로 사용. + """ + try: + if env_file_path: + loaded = dotenv.load_dotenv(env_file_path, override=True) + if loaded: + click.secho(f".env 파일 로드 성공: {env_file_path}", fg="green") + else: + click.secho(f".env 파일을 찾을 수 없음: {env_file_path}", fg="yellow") + else: + dotenv.load_dotenv(override=True) + click.secho("기본 .env 파일 로드 시도", fg="blue") + except Exception as e: + click.secho(f".env 파일 로드 중 오류 발생: {e}", fg="red") + raise + + +def set_prompt_dir( + *, + prompt_dir_path: Optional[str], +) -> None: + """프롬프트 템플릿 디렉토리 경로를 설정합니다. + + Args: + prompt_dir_path (Optional[str]): 디렉토리 경로. None이면 설정하지 않음. + + Raises: + ValueError: 경로가 유효하지 않을 경우. + """ + if not prompt_dir_path: + click.secho( + "프롬프트 디렉토리 경로가 지정되지 않아 설정을 건너뜁니다.", fg="yellow" + ) + return + + path_obj = Path(prompt_dir_path) + if not path_obj.exists() or not path_obj.is_dir(): + click.secho(f"유효하지 않은 디렉토리 경로: {prompt_dir_path}", fg="red") + raise ValueError(f"Invalid prompt directory path: {prompt_dir_path}") + + os.environ["PROMPT_TEMPLATES_DIR"] = str(path_obj.resolve()) + click.secho(f"프롬프트 디렉토리 환경변수 설정됨: {path_obj.resolve()}", fg="green") + + +def set_vectordb( + *, + vectordb_type: str, + vectordb_location: Optional[str] = None, +) -> None: + """VectorDB 타입과 위치를 설정합니다. + + Args: + vectordb_type (str): VectorDB 타입 ("faiss" 또는 "pgvector"). + vectordb_location (Optional[str]): 경로 또는 연결 URL. + + Raises: + ValueError: 잘못된 타입이나 경로/URL일 경우. + """ + + if vectordb_type not in ("faiss", "pgvector"): + raise ValueError(f"지원하지 않는 VectorDB 타입: {vectordb_type}") + + os.environ["VECTORDB_TYPE"] = vectordb_type + click.secho(f"VectorDB 타입 설정됨: {vectordb_type}", fg="green") + + if vectordb_location: + if vectordb_type == "faiss": + path = Path(vectordb_location) + if not path.exists() or not path.is_dir(): + raise ValueError( + f"유효하지 않은 FAISS 디렉토리 경로: {vectordb_location}" + ) + elif vectordb_type == "pgvector": + if not vectordb_location.startswith("postgresql://"): + raise ValueError( + f"pgvector URL은 'postgresql://'로 시작해야 합니다: {vectordb_location}" + ) + + os.environ["VECTORDB_LOCATION"] = vectordb_location + click.secho(f"VectorDB 경로 설정됨: {vectordb_location}", fg="green") + else: + click.secho("VectorDB 경로가 지정되지 않아 기본값을 사용합니다.", fg="yellow") diff --git a/cli/utils/logger.py b/cli/utils/logger.py new file mode 100644 index 0000000..cfebc9b --- /dev/null +++ b/cli/utils/logger.py @@ -0,0 +1,20 @@ +"""CLI 전용 로깅 유틸리티 모듈.""" + +import logging + + +def configure_logging(level: int = logging.INFO) -> logging.Logger: + """로깅을 설정하고 기본 로거를 반환합니다. + + Args: + level (int, optional): 로깅 레벨. 기본값은 logging.INFO. + + Returns: + logging.Logger: 설정된 로거 인스턴스. + """ + logging.basicConfig( + level=level, + format="%(asctime)s [%(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + return logging.getLogger("cli")