Skip to content

Commit b31de27

Browse files
committed
LLM 및 Embeddings 선택 기능 추가: Lang2SQL 페이지와 설정 페이지에 LLM 및 Embeddings 선택 컴포넌트 통합
1 parent af2d7b6 commit b31de27

File tree

6 files changed

+365
-1
lines changed

6 files changed

+365
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
import streamlit as st
3+
4+
from interface.core.config import update_embedding_settings
5+
from interface.app_pages.settings_sections.llm_section import LLM_PROVIDERS
6+
7+
8+
def render_sidebar_embedding_selector() -> None:
9+
st.sidebar.markdown("### Embeddings 선택")
10+
11+
default_emb = (
12+
(
13+
st.session_state.get("EMBEDDING_PROVIDER")
14+
or os.getenv("EMBEDDING_PROVIDER")
15+
or "openai"
16+
)
17+
).lower()
18+
try:
19+
default_idx = LLM_PROVIDERS.index(default_emb)
20+
except ValueError:
21+
default_idx = 0
22+
23+
selected = st.sidebar.selectbox(
24+
"Embeddings 공급자",
25+
options=LLM_PROVIDERS,
26+
index=default_idx,
27+
key="sidebar_embedding_provider",
28+
)
29+
30+
if selected != default_emb:
31+
try:
32+
update_embedding_settings(provider=selected, values={})
33+
st.sidebar.success(f"Embeddings 공급자가 '{selected}'로 변경되었습니다.")
34+
except Exception as e:
35+
st.sidebar.error(f"Embeddings 공급자 변경 실패: {e}")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
import streamlit as st
3+
4+
from interface.core.config import update_llm_settings
5+
from interface.app_pages.settings_sections.llm_section import LLM_PROVIDERS
6+
7+
8+
def render_sidebar_llm_selector() -> None:
9+
st.sidebar.markdown("### LLM 선택")
10+
11+
default_llm = (
12+
(st.session_state.get("LLM_PROVIDER") or os.getenv("LLM_PROVIDER") or "openai")
13+
).lower()
14+
try:
15+
default_idx = LLM_PROVIDERS.index(default_llm)
16+
except ValueError:
17+
default_idx = 0
18+
19+
selected = st.sidebar.selectbox(
20+
"LLM 공급자",
21+
options=LLM_PROVIDERS,
22+
index=default_idx,
23+
key="sidebar_llm_provider",
24+
)
25+
26+
if selected != default_llm:
27+
try:
28+
update_llm_settings(provider=selected, values={})
29+
st.sidebar.success(f"LLM 공급자가 '{selected}'로 변경되었습니다.")
30+
except Exception as e:
31+
st.sidebar.error(f"LLM 공급자 변경 실패: {e}")

interface/app_pages/lang2sql.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
from interface.app_pages.components.data_source_selector import (
2525
render_sidebar_data_source_selector,
2626
)
27+
from interface.app_pages.components.llm_selector import render_sidebar_llm_selector
28+
from interface.app_pages.components.embedding_selector import (
29+
render_sidebar_embedding_selector,
30+
)
2731

2832
TITLE = "Lang2SQL"
2933
DEFAULT_QUERY = "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리"
@@ -44,6 +48,8 @@
4448
config = load_config()
4549

4650
render_sidebar_data_source_selector(config)
51+
render_sidebar_llm_selector()
52+
render_sidebar_embedding_selector()
4753

4854
st.sidebar.markdown("### 워크플로우 선택")
4955
use_enriched = st.sidebar.checkbox(
@@ -63,6 +69,8 @@
6369
f"Lang2SQL이 성공적으로 새로고침되었습니다. ({GRAPH_TYPE} 워크플로우)"
6470
)
6571

72+
## moved to component: render_sidebar_llm_selector()
73+
6674
user_query = st.text_area("쿼리를 입력하세요:", value=DEFAULT_QUERY)
6775

6876
if "dialects" not in st.session_state:

interface/app_pages/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from interface.app_pages.settings_sections.data_source_section import (
99
render_data_source_section,
1010
)
11+
from interface.app_pages.settings_sections.llm_section import render_llm_section
1112

1213

1314
st.title("⚙️ 설정")
@@ -20,7 +21,7 @@
2021
render_data_source_section(config)
2122

2223
with tabs[1]:
23-
st.info("LLM 설정은 곧 제공됩니다.")
24+
render_llm_section(config)
2425

2526
with tabs[2]:
2627
st.info("DB 연결 설정은 곧 제공됩니다.")
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import os
2+
import streamlit as st
3+
4+
from interface.core.config import (
5+
update_llm_settings,
6+
update_embedding_settings,
7+
Config,
8+
load_config,
9+
)
10+
11+
12+
LLM_PROVIDERS = [
13+
"openai",
14+
"azure",
15+
"bedrock",
16+
"gemini",
17+
"ollama",
18+
"huggingface",
19+
]
20+
21+
22+
def _llm_fields(provider: str) -> list[tuple[str, str, bool]]:
23+
"""Return list of (label, env_key, is_secret) for LLM provider."""
24+
p = provider.lower()
25+
if p == "openai":
26+
return [
27+
("Model", "OPEN_AI_LLM_MODEL", False),
28+
("API Key", "OPEN_AI_KEY", True),
29+
]
30+
if p == "azure":
31+
return [
32+
("Endpoint", "AZURE_OPENAI_LLM_ENDPOINT", False),
33+
("Deployment(Model)", "AZURE_OPENAI_LLM_MODEL", False),
34+
("API Version", "AZURE_OPENAI_LLM_API_VERSION", False),
35+
("API Key", "AZURE_OPENAI_LLM_KEY", True),
36+
]
37+
if p == "bedrock":
38+
return [
39+
("Model", "AWS_BEDROCK_LLM_MODEL", False),
40+
("Access Key ID", "AWS_BEDROCK_LLM_ACCESS_KEY_ID", True),
41+
("Secret Access Key", "AWS_BEDROCK_LLM_SECRET_ACCESS_KEY", True),
42+
("Region", "AWS_BEDROCK_LLM_REGION", False),
43+
]
44+
if p == "gemini":
45+
return [
46+
("Model", "GEMINI_LLM_MODEL", False),
47+
# ChatGoogleGenerativeAI uses GOOGLE_API_KEY at process level, but factory currently reads only model
48+
]
49+
if p == "ollama":
50+
return [
51+
("Model", "OLLAMA_LLM_MODEL", False),
52+
("Base URL", "OLLAMA_LLM_BASE_URL", False),
53+
]
54+
if p == "huggingface":
55+
return [
56+
("Endpoint URL", "HUGGING_FACE_LLM_ENDPOINT", False),
57+
("Repo ID", "HUGGING_FACE_LLM_REPO_ID", False),
58+
("Model", "HUGGING_FACE_LLM_MODEL", False),
59+
("API Token", "HUGGING_FACE_LLM_API_TOKEN", True),
60+
]
61+
return []
62+
63+
64+
def _embedding_fields(provider: str) -> list[tuple[str, str, bool]]:
65+
p = provider.lower()
66+
if p == "openai":
67+
return [
68+
("Model", "OPEN_AI_EMBEDDING_MODEL", False),
69+
("API Key", "OPEN_AI_KEY", True),
70+
]
71+
if p == "azure":
72+
return [
73+
("Endpoint", "AZURE_OPENAI_EMBEDDING_ENDPOINT", False),
74+
("Deployment(Model)", "AZURE_OPENAI_EMBEDDING_MODEL", False),
75+
("API Version", "AZURE_OPENAI_EMBEDDING_API_VERSION", False),
76+
("API Key", "AZURE_OPENAI_EMBEDDING_KEY", True),
77+
]
78+
if p == "bedrock":
79+
return [
80+
("Model", "AWS_BEDROCK_EMBEDDING_MODEL", False),
81+
("Access Key ID", "AWS_BEDROCK_EMBEDDING_ACCESS_KEY_ID", True),
82+
("Secret Access Key", "AWS_BEDROCK_EMBEDDING_SECRET_ACCESS_KEY", True),
83+
("Region", "AWS_BEDROCK_EMBEDDING_REGION", False),
84+
]
85+
if p == "gemini":
86+
return [
87+
("Model", "GEMINI_EMBEDDING_MODEL", False),
88+
("API Key", "GEMINI_EMBEDDING_KEY", True),
89+
]
90+
if p == "ollama":
91+
return [
92+
("Model", "OLLAMA_EMBEDDING_MODEL", False),
93+
("Base URL", "OLLAMA_EMBEDDING_BASE_URL", False),
94+
]
95+
if p == "huggingface":
96+
return [
97+
("Model", "HUGGING_FACE_EMBEDDING_MODEL", False),
98+
("Repo ID", "HUGGING_FACE_EMBEDDING_REPO_ID", False),
99+
("API Token", "HUGGING_FACE_EMBEDDING_API_TOKEN", True),
100+
]
101+
return []
102+
103+
104+
def render_llm_section(config: Config | None = None) -> None:
105+
st.subheader("LLM 설정")
106+
107+
if config is None:
108+
try:
109+
config = load_config()
110+
except Exception:
111+
config = None # UI 일관성을 위한 옵셔널 처리
112+
113+
llm_col, emb_col = st.columns(2)
114+
115+
with llm_col:
116+
st.markdown("**Chat LLM**")
117+
default_llm_provider = (
118+
(
119+
st.session_state.get("LLM_PROVIDER")
120+
or os.getenv("LLM_PROVIDER")
121+
or "openai"
122+
)
123+
).lower()
124+
try:
125+
default_llm_index = LLM_PROVIDERS.index(default_llm_provider)
126+
except ValueError:
127+
default_llm_index = 0
128+
provider = st.selectbox(
129+
"공급자",
130+
options=LLM_PROVIDERS,
131+
index=default_llm_index,
132+
key="llm_provider",
133+
)
134+
fields = _llm_fields(provider)
135+
values: dict[str, str | None] = {}
136+
for label, env_key, is_secret in fields:
137+
prefill = st.session_state.get(env_key) or os.getenv(env_key) or ""
138+
if is_secret:
139+
values[env_key] = st.text_input(
140+
label, value=prefill, type="password", key=f"llm_{env_key}"
141+
)
142+
else:
143+
values[env_key] = st.text_input(
144+
label, value=prefill, key=f"llm_{env_key}"
145+
)
146+
147+
# 메시지 영역: 버튼 컬럼 밖(섹션 폭)으로 배치하여 좁은 폭에 눌려 깨지는 문제 방지
148+
llm_msg = st.empty()
149+
150+
save_cols = st.columns([1, 1, 2])
151+
with save_cols[0]:
152+
if st.button("저장", key="llm_save"):
153+
try:
154+
update_llm_settings(provider=provider, values=values)
155+
llm_msg.success("LLM 설정이 저장되었습니다.")
156+
except Exception as e:
157+
llm_msg.error(f"저장 실패: {e}")
158+
with save_cols[1]:
159+
if st.button("검증", key="llm_validate"):
160+
# 가벼운 검증: 필수 키 존재 여부만 확인
161+
try:
162+
update_llm_settings(provider=provider, values=values)
163+
llm_msg.success(
164+
"형식 검증 완료. 실제 호출은 실행 경로에서 재검증됩니다."
165+
)
166+
except Exception as e:
167+
llm_msg.error(f"검증 실패: {e}")
168+
169+
with emb_col:
170+
st.markdown("**Embeddings**")
171+
default_emb_provider = (
172+
(
173+
st.session_state.get("EMBEDDING_PROVIDER")
174+
or os.getenv("EMBEDDING_PROVIDER")
175+
or "openai"
176+
)
177+
).lower()
178+
try:
179+
default_emb_index = LLM_PROVIDERS.index(default_emb_provider)
180+
except ValueError:
181+
default_emb_index = 0
182+
e_provider = st.selectbox(
183+
"공급자",
184+
options=LLM_PROVIDERS,
185+
index=default_emb_index,
186+
key="embedding_provider",
187+
)
188+
e_fields = _embedding_fields(e_provider)
189+
e_values: dict[str, str | None] = {}
190+
for label, env_key, is_secret in e_fields:
191+
prefill = st.session_state.get(env_key) or os.getenv(env_key) or ""
192+
if is_secret:
193+
e_values[env_key] = st.text_input(
194+
label, value=prefill, type="password", key=f"emb_{env_key}"
195+
)
196+
else:
197+
e_values[env_key] = st.text_input(
198+
label, value=prefill, key=f"emb_{env_key}"
199+
)
200+
201+
# 메시지 영역: 버튼 컬럼 밖(섹션 폭)
202+
emb_msg = st.empty()
203+
204+
e_cols = st.columns([1, 1, 2])
205+
with e_cols[0]:
206+
if st.button("저장", key="emb_save"):
207+
try:
208+
update_embedding_settings(provider=e_provider, values=e_values)
209+
emb_msg.success("Embeddings 설정이 저장되었습니다.")
210+
except Exception as e:
211+
emb_msg.error(f"저장 실패: {e}")
212+
with e_cols[1]:
213+
if st.button("검증", key="emb_validate"):
214+
try:
215+
update_embedding_settings(provider=e_provider, values=e_values)
216+
emb_msg.success("형식 검증 완료.")
217+
except Exception as e:
218+
emb_msg.error(f"검증 실패: {e}")

0 commit comments

Comments
 (0)