Skip to content

Commit d1746f2

Browse files
authored
Merge branch 'master' into feature/79-보여주는-결과의-확장-필요성-추가-개발
2 parents a15d3e0 + 73be0fd commit d1746f2

File tree

13 files changed

+507
-110
lines changed

13 files changed

+507
-110
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ dist/
88
.venv/
99
test_lhm/
1010
.cursorignore
11-
.vscode
11+
.vscode
12+
table_info_db
13+
ko_reranker_local

Dockerfile

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Python 3.10 slim 이미지 기반
2+
FROM python:3.12-slim
3+
4+
# 시스템 라이브러리 설치
5+
RUN apt-get update && apt-get install -y \
6+
build-essential \
7+
curl \
8+
software-properties-common \
9+
git \
10+
libpq-dev \
11+
&& rm -rf /var/lib/apt/lists/*
12+
13+
# 작업 디렉토리 설정
14+
WORKDIR /app
15+
16+
# 의존성 파일 복사 및 설치
17+
COPY requirements.txt .
18+
RUN pip install --no-cache-dir -r requirements.txt
19+
20+
# 전체 서비스 코드 복사
21+
COPY . .
22+
23+
# Python 환경 설정
24+
ENV PYTHONPATH=/app
25+
ENV PYTHONUNBUFFERED=1
26+
27+
# Streamlit 포트 노출
28+
EXPOSE 8501
29+
30+
# Streamlit 실행 명령
31+
CMD ["python", "-c", "from llm_utils.tools import set_gms_server; import os; set_gms_server(os.getenv('DATAHUB_SERVER', 'http://localhost:8080'))"]
32+
CMD ["streamlit", "run", "./interface/streamlit_app.py", "--server.port=8501"]

cli/__init__.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
Datahub GMS 서버 URL을 설정하고, 필요 시 Streamlit 인터페이스를 실행하는 CLI 프로그램입니다.
33
"""
44

5+
import logging
56
import subprocess
67

78
import click
89

10+
from llm_utils.check_server import CheckServer
911
from llm_utils.tools import set_gms_server
1012

13+
logging.basicConfig(
14+
level=logging.INFO,
15+
format="%(asctime)s [%(levelname)s] %(message)s",
16+
datefmt="%Y-%m-%d %H:%M:%S",
17+
)
18+
logger = logging.getLogger(__name__)
19+
1120

1221
@click.group()
1322
@click.version_option(version="0.1.4")
@@ -64,11 +73,20 @@ def cli(
6473
'set_gms_server' 함수에서 ValueError가 발생할 경우, 프로그램은 비정상 종료(exit code 1)합니다.
6574
"""
6675

67-
try:
76+
logger.info(
77+
"Initialization started: GMS server = %s, run_streamlit = %s, port = %d",
78+
datahub_server,
79+
run_streamlit,
80+
port,
81+
)
82+
83+
if CheckServer.is_gms_server_healthy(url=datahub_server):
6884
set_gms_server(datahub_server)
69-
except ValueError as e:
70-
click.secho(f"GMS 서버 URL 설정 실패: {str(e)}", fg="red")
85+
logger.info("GMS server URL successfully set: %s", datahub_server)
86+
else:
87+
logger.error("GMS server health check failed. URL: %s", datahub_server)
7188
ctx.exit(1)
89+
7290
if run_streamlit:
7391
run_streamlit_command(port)
7492

@@ -89,6 +107,8 @@ def run_streamlit_command(port: int) -> None:
89107
- subprocess 호출 실패 시 예외가 발생할 수 있습니다.
90108
"""
91109

110+
logger.info("Starting Streamlit application on port %d...", port)
111+
92112
try:
93113
subprocess.run(
94114
[
@@ -100,8 +120,9 @@ def run_streamlit_command(port: int) -> None:
100120
],
101121
check=True,
102122
)
123+
logger.info("Streamlit application started successfully.")
103124
except subprocess.CalledProcessError as e:
104-
click.echo(f"Streamlit 실행 실패: {e}")
125+
logger.error("Failed to start Streamlit application: %s", e)
105126
raise
106127

107128

@@ -132,4 +153,5 @@ def run_streamlit_cli_command(port: int) -> None:
132153
- Streamlit 실행에 실패할 경우 subprocess 호출에서 예외가 발생할 수 있습니다.
133154
"""
134155

156+
logger.info("Executing 'run-streamlit' command on port %d...", port)
135157
run_streamlit_command(port)

data_utils/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-
# data_utils 패키지 초기화 파일

docker-compose.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
services:
2+
streamlit:
3+
build: .
4+
ports:
5+
- "8501:8501"
6+
volumes:
7+
- .:/app
8+
env_file:
9+
- .env
10+
environment:
11+
- DATABASE_URL=postgresql://postgres:password@db:5432/streamlit_db
12+
depends_on:
13+
- db
14+
15+
db:
16+
image: pgvector/pgvector:pg17
17+
container_name: pgvector-db
18+
environment:
19+
POSTGRES_USER: postgres
20+
POSTGRES_PASSWORD: password
21+
POSTGRES_DB: streamlit_db
22+
ports:
23+
- "5432:5432"
24+
volumes:
25+
- pgdata:/var/lib/postgresql/data
26+
- ./postgres/schema.sql:/docker-entrypoint-initdb.d/schema.sql
27+
volumes:
28+
pgdata:

interface/lang2sql.py

Lines changed: 92 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import streamlit as st
99
from langchain.chains.sql_database.prompt import SQL_PROMPTS
10-
from langchain_core.messages import HumanMessage
10+
from langchain_core.messages import AIMessage, HumanMessage
1111

1212
from llm_utils.connect_db import ConnectDB
1313
from llm_utils.graph import builder
1414
from llm_utils.display_chart import DisplayChart
15+
from llm_utils.llm_response_parser import LLMResponseParser
16+
1517

1618
DEFAULT_QUERY = "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리"
1719
SIDEBAR_OPTIONS = {
@@ -52,18 +54,27 @@ def execute_query(
5254
device: str = "cpu",
5355
) -> dict:
5456
"""
55-
Lang2SQL 그래프를 실행하여 자연어 쿼리를 SQL 쿼리로 변환하고 결과를 반환합니다.
57+
자연어 쿼리를 SQL로 변환하고 실행 결과를 반환하는 Lang2SQL 그래프 인터페이스 함수입니다.
58+
59+
이 함수는 Lang2SQL 파이프라인(graph)을 세션 상태에서 가져오거나 새로 컴파일한 뒤,
60+
사용자의 자연어 질문을 SQL 쿼리로 변환하고 관련 메타데이터와 함께 결과를 반환합니다.
61+
내부적으로 LangChain의 `graph.invoke` 메서드를 호출합니다.
5662
5763
Args:
58-
query (str): 자연어로 작성된 사용자 쿼리.
59-
database_env (str): 사용할 데이터베이스 환경 설정 이름.
60-
retriever_name (str): 사용할 검색기 이름.
61-
top_n (int): 검색할 테이블 정보의 개수.
64+
query (str): 사용자가 입력한 자연어 기반 질문.
65+
database_env (str): 사용할 데이터베이스 환경 이름 또는 키 (예: "dev", "prod").
66+
retriever_name (str, optional): 테이블 검색기 이름. 기본값은 "기본".
67+
top_n (int, optional): 검색된 상위 테이블 수 제한. 기본값은 5.
68+
device (str, optional): LLM 실행에 사용할 디바이스 ("cpu" 또는 "cuda"). 기본값은 "cpu".
6269
6370
Returns:
64-
dict: 변환된 SQL 쿼리 및 관련 메타데이터를 포함하는 결과 딕셔너리.
71+
dict: 다음 정보를 포함한 Lang2SQL 실행 결과 딕셔너리:
72+
- "generated_query": 생성된 SQL 쿼리 (`AIMessage`)
73+
- "messages": 전체 LLM 응답 메시지 목록
74+
- "refined_input": AI가 재구성한 입력 질문
75+
- "searched_tables": 참조된 테이블 목록 등 추가 정보
6576
"""
66-
# 세션 상태에서 그래프 가져오기
77+
6778
graph = st.session_state.get("graph")
6879
if graph is None:
6980
graph = builder.compile()
@@ -103,34 +114,83 @@ def display_result(
103114
- 참조된 테이블 목록
104115
- 쿼리 실행 결과 테이블
105116
"""
106-
total_tokens = summarize_total_tokens(res["messages"])
107-
108-
if st.session_state.get("show_total_token_usage", True):
109-
st.write("총 토큰 사용량:", total_tokens)
110-
if st.session_state.get("show_sql", True):
111-
st.write("결과:", "\n\n```sql\n" + res["generated_query"].content + "\n```")
112-
if st.session_state.get("show_result_description", True):
113-
st.write("결과 설명:\n\n", res["messages"][-1].content)
114-
if st.session_state.get("show_question_reinterpreted_by_ai", True):
115-
st.write("AI가 재해석한 사용자 질문:\n", res["refined_input"].content)
116-
if st.session_state.get("show_referenced_tables", True):
117-
st.write("참고한 테이블 목록:", res["searched_tables"])
118-
if st.session_state.get("show_table", True):
119-
st.write("쿼리 실행 결과")
120-
sql = res["generated_query"].content.split("```")[1][
121-
3:
122-
] # 쿼리 앞쪽의 "sql " 제거
123-
df = database.run_sql(sql)
124-
st.dataframe(df.head(10) if len(df) > 10 else df)
125-
if st.session_state.get("show_chart", True):
126-
st.write("쿼리 결과 시각화")
117+
118+
def should_show(_key: str) -> bool:
119+
st.markdown("---")
120+
return st.session_state.get(_key, True)
121+
122+
if should_show("show_total_token_usage"):
123+
total_tokens = summarize_total_tokens(res["messages"])
124+
st.write("**총 토큰 사용량:**", total_tokens)
125+
126+
if should_show("show_sql"):
127+
generated_query = res.get("generated_query")
128+
query_text = (
129+
generated_query.content
130+
if isinstance(generated_query, AIMessage)
131+
else str(generated_query)
132+
)
133+
134+
try:
135+
sql = LLMResponseParser.extract_sql(query_text)
136+
st.markdown("**생성된 SQL 쿼리:**")
137+
st.code(sql, language="sql")
138+
except ValueError:
139+
st.warning("SQL 블록을 추출할 수 없습니다.")
140+
st.text(query_text)
141+
142+
interpretation = LLMResponseParser.extract_interpretation(query_text)
143+
if interpretation:
144+
st.markdown("**결과 해석:**")
145+
st.code(interpretation)
146+
147+
if should_show("show_result_description"):
148+
st.markdown("**결과 설명:**")
149+
result_message = res["messages"][-1].content
150+
151+
try:
152+
sql = LLMResponseParser.extract_sql(result_message)
153+
st.code(sql, language="sql")
154+
except ValueError:
155+
st.warning("SQL 블록을 추출할 수 없습니다.")
156+
st.text(result_message)
157+
158+
interpretation = LLMResponseParser.extract_interpretation(result_message)
159+
if interpretation:
160+
st.code(interpretation, language="plaintext")
161+
162+
if should_show("show_question_reinterpreted_by_ai"):
163+
st.markdown("**AI가 재해석한 사용자 질문:**")
164+
st.code(res["refined_input"].content)
165+
166+
if should_show("show_referenced_tables"):
167+
st.markdown("**참고한 테이블 목록:**")
168+
st.write(res.get("searched_tables", []))
169+
170+
if should_show("show_table"):
171+
try:
172+
sql_raw = (
173+
res["generated_query"].content
174+
if isinstance(res["generated_query"], AIMessage)
175+
else str(res["generated_query"])
176+
)
177+
sql = LLMResponseParser.extract_sql(sql_raw)
178+
df = database.run_sql(sql)
179+
st.dataframe(df.head(10) if len(df) > 10 else df)
180+
except Exception as e:
181+
st.error(f"쿼리 실행 중 오류 발생: {e}")
182+
if should_show("show_chart") and df is not None:
183+
st.markdown("**쿼리 결과 시각화:**")
127184
display_code = DisplayChart(
128185
question=res["refined_input"].content,
129186
sql=sql,
130-
df_metadata=f"Running df.dtypes gives:\n {df.dtypes}",
187+
df_metadata=f"Running df.dtypes gives:\n{df.dtypes}"
188+
)
189+
# plotly_code 변수도 따로 보관할 필요 없이 바로 그려도 됩니다
190+
fig = display_code.get_plotly_figure(
191+
plotly_code=display_code.generate_plotly_code(),
192+
df=df
131193
)
132-
plotly_code = display_code.generate_plotly_code()
133-
fig = display_code.get_plotly_figure(plotly_code=plotly_code, df=df)
134194
st.plotly_chart(fig)
135195

136196

llm_utils/check_server.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
서버 상태 확인 및 연결 관련 기능을 제공하는 유틸리티 클래스입니다.
3+
4+
이 모듈은 HTTP 기반의 서버에 대해 다음과 같은 기능을 제공합니다:
5+
- `/health` 엔드포인트를 통한 서버 헬스 체크
6+
- 향후 서버 연결 또는 상태 점검과 관련된 기능 추가 예정
7+
8+
각 기능은 요청 실패, 타임아웃, 연결 오류 등의 다양한 예외 상황을 포괄적으로 처리하며,
9+
로깅을 통해 상세한 실패 원인을 기록하고 결과를 boolean 또는 적절한 형태로 반환합니다.
10+
"""
11+
12+
import logging
13+
from urllib.parse import urljoin
14+
15+
import requests
16+
17+
logging.basicConfig(
18+
level=logging.INFO,
19+
format="%(asctime)s [%(levelname)s] %(message)s",
20+
datefmt="%Y-%m-%d %H:%M:%S",
21+
)
22+
logger = logging.getLogger(__name__)
23+
24+
25+
class CheckServer:
26+
"""
27+
서버의 상태를 확인하거나 연결을 테스트하는 유틸리티 메서드를 제공하는 클래스입니다.
28+
29+
현재는 GMS 서버의 `/health` 엔드포인트에 대한 헬스 체크 기능을 포함하고 있으며,
30+
향후에는 다양한 서버 연결 확인 및 상태 점검 기능이 추가될 수 있도록 확장 가능한 구조로 설계되었습니다.
31+
모든 기능은 네트워크 오류 및 서버 응답 상태에 따라 예외를 로깅하며, 호출자가 결과를 판단할 수 있도록 boolean 값을 반환합니다.
32+
"""
33+
34+
@staticmethod
35+
def is_gms_server_healthy(*, url: str) -> bool:
36+
"""
37+
지정된 GMS 서버의 `/health` 엔드포인트에 요청을 보내 상태를 확인합니다.
38+
39+
서버가 HTTP 200 응답을 반환하면 True를 반환하며,
40+
요청 실패, 타임아웃, 연결 오류 등의 예외 발생 시 False를 반환하고,
41+
로깅을 통해 상세한 에러 정보를 출력합니다.
42+
43+
Args:
44+
url (str): 헬스 체크를 수행할 GMS 서버의 기본 URL (예: "http://localhost:8080")
45+
46+
Returns:
47+
bool: 서버가 정상적으로 응답하면 True, 예외 발생 시 False
48+
"""
49+
50+
health_url = urljoin(url, "/health")
51+
52+
try:
53+
response = requests.get(
54+
health_url,
55+
timeout=3,
56+
)
57+
response.raise_for_status()
58+
logger.info("GMS server is healthy: %s", url)
59+
return True
60+
except (
61+
requests.exceptions.ConnectTimeout,
62+
requests.exceptions.ReadTimeout,
63+
) as e:
64+
logger.error(
65+
"Timeout while connecting to GMS server: %s | %s", health_url, e
66+
)
67+
except requests.exceptions.ConnectionError as e:
68+
logger.error("Failed to connect to GMS server: %s | %s", health_url, e)
69+
except requests.exceptions.HTTPError as e:
70+
logger.error("GMS server returned HTTP error: %s | %s", health_url, e)
71+
except requests.exceptions.RequestException as e:
72+
logger.exception("Unexpected request error to GMS server: %s", health_url)
73+
74+
return False

0 commit comments

Comments
 (0)