lhss0520 commited on
Commit
49a97fa
ยท
verified ยท
1 Parent(s): ce8ed6d

Upload src/app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/app.py +468 -273
src/app.py CHANGED
@@ -1,32 +1,39 @@
1
  import streamlit as st
2
  from pathlib import Path
3
-
 
 
 
 
 
 
4
  from langchain_community.document_loaders import PyMuPDFLoader
5
  from langchain_text_splitters import RecursiveCharacterTextSplitter
6
  from langchain_openai import OpenAIEmbeddings, ChatOpenAI
7
  from langchain_community.vectorstores import FAISS
8
  from langchain_core.prompts import ChatPromptTemplate
9
  from langchain_core.output_parsers import StrOutputParser
10
-
11
  st.set_page_config(
12
  page_title="AIVLE ํ•™์Šต๋„์šฐ๋ฏธ",
13
  page_icon="๐Ÿค–",
14
  layout="wide"
15
  )
16
-
 
 
17
  BASE_DIR = Path(__file__).resolve().parent
18
  PDF_PATH = BASE_DIR / "AIVLE_School_๋ฐฑ์„œ_ํ†ตํ•ฉ๋ณธ_์ตœ์ข….pdf"
19
-
20
- # =========================
21
- # CSS
22
- # =========================
23
  st.markdown("""
24
  <style>
25
  .main .block-container {
26
  padding-top: 2rem;
27
  max-width: 1100px;
28
  }
29
-
30
  .hero-box {
31
  background: #d9f3f2;
32
  border-radius: 22px;
@@ -36,46 +43,35 @@ st.markdown("""
36
  justify-content: space-between;
37
  align-items: center;
38
  }
39
-
40
  .hero-title {
41
  font-size: 30px;
42
  font-weight: 800;
43
  color: #111827;
44
  margin-bottom: 10px;
45
  }
46
-
47
  .hero-sub {
48
  font-size: 15px;
49
  color: #6b7280;
50
  }
51
-
52
  .robot {
53
  font-size: 72px;
54
  }
55
-
56
  .section-title {
57
  font-size: 17px;
58
  font-weight: 800;
59
  margin-bottom: 12px;
60
  }
61
-
62
- .question-card {
63
- background: white;
64
- border: 1px solid #e5e7eb;
65
- border-radius: 14px;
66
- padding: 16px 18px;
67
- font-size: 15px;
68
- font-weight: 600;
69
- color: #374151;
70
- box-shadow: 0 2px 8px rgba(0,0,0,0.03);
71
- }
72
-
73
  .chat-wrap {
74
  border-top: 1px solid #e5e7eb;
75
  margin-top: 28px;
76
  padding-top: 26px;
77
  }
78
-
79
  .user-bubble {
80
  background: #d9f3f2;
81
  padding: 14px 18px;
@@ -86,7 +82,7 @@ st.markdown("""
86
  margin-bottom: 8px;
87
  font-weight: 500;
88
  }
89
-
90
  .assistant-card {
91
  background: white;
92
  border: 1px solid #e5e7eb;
@@ -96,50 +92,59 @@ st.markdown("""
96
  max-width: 780px;
97
  box-shadow: 0 2px 10px rgba(0,0,0,0.04);
98
  }
99
-
100
  .assistant-name {
101
  font-size: 14px;
102
  color: #6b7280;
103
  font-weight: 700;
104
  margin-bottom: 8px;
105
  }
106
-
107
- .time-text {
108
- color: #9ca3af;
109
- font-size: 12px;
110
- text-align: right;
111
- }
112
-
113
- .input-box {
114
- border: 1px solid #e5e7eb;
115
- border-radius: 18px;
116
- padding: 16px;
117
- margin-top: 30px;
118
- background: white;
119
- }
120
-
121
- .side-history {
122
- background: #f9fafb;
123
- border-radius: 12px;
124
- padding: 10px 12px;
125
- margin-bottom: 8px;
126
- font-size: 14px;
127
- }
128
  </style>
129
  """, unsafe_allow_html=True)
130
-
131
  # =========================
132
  # ์„ธ์…˜ ์ƒํƒœ
133
  # =========================
134
- if "messages" not in st.session_state:
135
- st.session_state.messages = []
136
-
137
- if "chat_history_titles" not in st.session_state:
138
- st.session_state.chat_history_titles = []
139
-
 
 
 
 
 
 
 
140
  if "show_faq" not in st.session_state:
141
  st.session_state.show_faq = False
142
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # =========================
144
  # RAG
145
  # =========================
@@ -148,106 +153,275 @@ def build_rag_chain():
148
  if not PDF_PATH.exists():
149
  st.error(f"PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {PDF_PATH}")
150
  st.stop()
151
-
152
  loader = PyMuPDFLoader(str(PDF_PATH))
153
  docs = loader.load()
154
-
155
  splitter = RecursiveCharacterTextSplitter(
156
  chunk_size=1200,
157
  chunk_overlap=200,
158
  separators=["\n\n", "\n", ".", " ", ""]
159
  )
160
  chunks = splitter.split_documents(docs)
161
-
162
  embedding = OpenAIEmbeddings(model="text-embedding-3-small")
163
  vectorstore = FAISS.from_documents(chunks, embedding)
164
- retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
165
-
166
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
167
-
168
  prompt = ChatPromptTemplate.from_template("""
169
  ๋‹น์‹ ์€ AIVLE School ๋ฐฑ์„œ PDF ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€ํ•˜๋Š” ํ•™์Šต๋„์šฐ๋ฏธ ์ฑ—๋ด‡์ž…๋‹ˆ๋‹ค.
170
-
171
  ๊ทœ์น™:
172
  1. ๋ฐ˜๋“œ์‹œ [๋ฌธ์„œ ๋‚ด์šฉ]์— ๊ทผ๊ฑฐํ•ด์„œ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
173
  2. ๋ฌธ์„œ์—์„œ ํ™•์ธ๋˜์ง€ ์•Š๋Š” ๋‚ด์šฉ์€ "๋ฐฑ์„œ์—์„œ ํ™•์ธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."๋ผ๊ณ  ๋‹ตํ•˜์„ธ์š”.
174
  3. ๋‹ต๋ณ€์€ ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
175
-
176
  [๋ฌธ์„œ ๋‚ด์šฉ]
177
  {context}
178
-
179
  [์งˆ๋ฌธ]
180
  {question}
181
  """)
182
-
183
  def format_docs(docs):
184
  return "\n\n".join(doc.page_content for doc in docs)
185
-
186
  chain = prompt | llm | StrOutputParser()
187
-
188
  return retriever, chain, format_docs
189
-
190
  retriever, rag_chain, format_docs = build_rag_chain()
191
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  # =========================
193
  # ์‚ฌ์ด๋“œ๋ฐ”
194
  # =========================
195
  with st.sidebar:
 
 
 
 
 
 
196
  st.markdown("## AIVLE")
197
  st.markdown("### ํ•™์Šต๋„์šฐ๋ฏธ ์ฑ—๋ด‡")
198
  st.caption("์—์ด๋ธ”์Šค์ฟจ์˜ ํ•™์Šต ์—ฌ์ •์„ ํ•จ๊ป˜ํ•˜๋Š” AI ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค.")
199
-
200
  if st.button("๏ผ‹ ์ƒˆ ๋Œ€ํ™”", use_container_width=True):
201
- st.session_state.messages = []
 
 
 
 
 
202
  st.rerun()
203
-
204
  st.divider()
205
-
206
  st.button("๐Ÿ  ํ™ˆ", use_container_width=True)
 
207
  if st.button("โ” FAQ", use_container_width=True):
208
  st.session_state.show_faq = True
209
-
 
210
  st.divider()
211
-
212
  st.markdown("### ํŒŒ์ผ ๊ด€๋ฆฌ")
213
- st.file_uploader("ํŒŒ์ผ ์—…๋กœ๋“œ", type=["pdf", "txt", "csv"])
214
- st.button("๐Ÿ“ ์—…๋กœ๋“œ ๋ชฉ๋ก", use_container_width=True)
215
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  st.divider()
217
-
218
  st.markdown("### ๋Œ€ํ™” ๊ธฐ๋ก")
219
- if st.session_state.chat_history_titles:
220
- for title in reversed(st.session_state.chat_history_titles[-5:]):
221
- st.markdown(f'<div class="side-history">{title}</div>', unsafe_allow_html=True)
222
- else:
223
- st.caption("์•„์ง ๋Œ€ํ™” ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค.")
224
-
225
- # ์—ฌ๊ธฐ๋ถ€ํ„ฐ ๋กœ๊ทธ์ธ ์ฝ”๋“œ๋„ ์‚ฌ์ด๋“œ๋ฐ” ์•ˆ
226
- if "logged_in" not in st.session_state:
227
- st.session_state.logged_in = False
228
-
229
- if "user_name" not in st.session_state:
230
- st.session_state.user_name = ""
231
-
232
- if "show_login" not in st.session_state:
233
- st.session_state.show_login = False
234
-
235
  st.divider()
236
-
237
  if not st.session_state.logged_in:
238
  st.markdown("### ๐Ÿ‘ค ๊ณ„์ •")
239
-
240
  if st.button("๋กœ๊ทธ์ธ", use_container_width=True):
241
  st.session_state.show_login = not st.session_state.show_login
242
-
243
  if st.session_state.show_login:
244
  with st.form("sidebar_login_form", clear_on_submit=False):
245
  login_name = st.text_input("์ด๋ฆ„", key="login_name_input")
246
  login_id = st.text_input("์•„์ด๋””", key="login_id_input")
247
  login_pw = st.text_input("๋น„๋ฐ€๋ฒˆํ˜ธ", type="password", key="login_pw_input")
248
-
249
  login_btn = st.form_submit_button("๋กœ๊ทธ์ธ", use_container_width=True)
250
-
251
  if login_btn:
252
  if login_name.strip() and login_id.strip() and login_pw.strip():
253
  st.session_state.logged_in = True
@@ -256,219 +430,194 @@ with st.sidebar:
256
  st.rerun()
257
  else:
258
  st.warning("์ด๋ฆ„, ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•˜์„ธ์š”.")
259
-
260
  else:
261
  st.markdown(f"๐Ÿ‘ค **{st.session_state.user_name}**")
262
- st.caption("์—์ด๋ธ”์Šค์ฟจ 7๊ธฐ")
263
-
264
  if st.button("๋กœ๊ทธ์•„์›ƒ", use_container_width=True):
265
  st.session_state.logged_in = False
266
  st.session_state.user_name = ""
267
  st.session_state.show_login = False
268
  st.rerun()
269
-
270
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  # =========================
272
  # FAQ ํ™”๋ฉด
273
  # =========================
274
  if st.session_state.show_faq:
275
-
276
- st.markdown("""
277
- <style>
278
- .faq-box-wrap {
279
- border: 2px solid #8b5cf6;
280
- border-radius: 12px;
281
- background: white;
282
- padding: 24px;
283
- margin-bottom: 24px;
284
- }
285
-
286
- .faq-title {
287
- font-size: 26px;
288
- font-weight: 800;
289
- }
290
-
291
- .faq-sub {
292
- color: #6b7280;
293
- font-size: 15px;
294
- margin-top: 4px;
295
- }
296
-
297
- .faq-category {
298
- background: #e0f7f5;
299
- color: #0f766e;
300
- padding: 12px;
301
- border-radius: 12px;
302
- font-weight: 700;
303
- margin-bottom: 10px;
304
- }
305
-
306
- .faq-category-normal {
307
- padding: 12px;
308
- font-weight: 700;
309
- color: #374151;
310
- margin-bottom: 10px;
311
- }
312
- </style>
313
- """, unsafe_allow_html=True)
314
-
315
- st.markdown('<div class="faq-box-wrap">', unsafe_allow_html=True)
316
-
317
- col_title, col_close = st.columns([10, 1])
318
-
319
- with col_title:
320
- st.markdown(
321
- '<div class="faq-title">โ” FAQ</div>',
322
- unsafe_allow_html=True
323
- )
324
-
325
- st.markdown(
326
- '<div class="faq-sub">์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ์„ ํ™•์ธํ•ด๋ณด์„ธ์š”.</div>',
327
- unsafe_allow_html=True
328
- )
329
-
330
- with col_close:
331
- if st.button("โœ•", key="close_faq"):
332
- st.session_state.show_faq = False
333
- st.rerun()
334
-
335
  left, right = st.columns([1, 3])
336
-
337
  with left:
338
- st.markdown("**์นดํ…Œ๊ณ ๋ฆฌ**")
339
-
340
- st.markdown(
341
- '<div class="faq-category">์ „์ฒด</div>',
342
- unsafe_allow_html=True
343
- )
344
-
345
- st.markdown(
346
- '<div class="faq-category-normal">ํ•™์Šต ๊ณผ์ •</div>',
347
- unsafe_allow_html=True
348
- )
349
-
350
- st.markdown(
351
- '<div class="faq-category-normal">๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ</div>',
352
- unsafe_allow_html=True
353
- )
354
-
355
- st.markdown(
356
- '<div class="faq-category-normal">AI/๋ฐ์ดํ„ฐ</div>',
357
- unsafe_allow_html=True
358
- )
359
-
360
- st.markdown(
361
- '<div class="faq-category-normal">์‹œ์Šคํ…œ/์ ‘์†</div>',
362
- unsafe_allow_html=True
363
- )
364
-
365
- st.markdown(
366
- '<div class="faq-category-normal">๊ธฐํƒ€</div>',
367
- unsafe_allow_html=True
368
- )
369
-
370
  with right:
371
- st.markdown("**์ „์ฒด**")
372
-
373
- with st.expander(
374
- "๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?",
375
- expanded=True
376
- ):
377
- st.write(
378
- "๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์ฃผ์ œ ์„ ์ •, ๊ธฐํš, ๊ตฌํ˜„, ๋ฐœํ‘œ, ํ”ผ๋“œ๋ฐฑ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค."
379
- )
380
-
381
- with st.expander(
382
- "๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ์„ฑ๋Šฅ์€ ์–ด๋–ป๊ฒŒ ํ‰๊ฐ€ํ•˜๋‚˜์š”?",
383
- expanded=True
384
- ):
385
- st.write(
386
- "Accuracy, Precision, Recall, F1 Score ๋“ฑ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."
387
- )
388
-
389
- st.markdown('</div>', unsafe_allow_html=True)
390
-
391
-
392
-
393
  # =========================
394
  # ๋ฉ”์ธ ํ™”๋ฉด ํ—ค๋”
395
  # =========================
396
- st.markdown("""
397
- <div class="hero-box">
398
- <div>
399
- <div class="hero-title">์•ˆ๋…•ํ•˜์„ธ์š”! ๐Ÿ˜Š<br>๋ฌด์—‡์„ ๋„์™€๋“œ๋ฆด๊นŒ์š”?</div>
400
- <div class="hero-sub">ํ•™์Šต, ํ”„๋กœ์ ํŠธ, AI, ๋ฐ์ดํ„ฐ ๋ถ„์„ ๋“ฑ ๋ฌด์—‡์ด๋“  ๋ฌผ์–ด๋ณด์„ธ์š”!</div>
401
- </div>
402
- <div class="robot">๐Ÿค–</div>
403
- </div>
404
- """, unsafe_allow_html=True)
405
-
406
  # =========================
407
  # ์ถ”์ฒœ ์งˆ๋ฌธ
408
  # =========================
409
  st.markdown('<div class="section-title">์ถ”์ฒœ ์งˆ๋ฌธ</div>', unsafe_allow_html=True)
410
-
 
 
411
  q1, q2 = st.columns(2)
412
-
413
  with q1:
414
- if st.button("๐Ÿ“Œ ๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?", use_container_width=True):
415
- st.session_state.pending_question = "๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?"
416
- st.rerun()
417
-
418
- if st.button("๐Ÿ Python์—์„œ ๋ฆฌ์ŠคํŠธ ์ปดํ”„๋ฆฌํ—จ์…˜์ด ๋ญ์˜ˆ์š”?", use_container_width=True):
419
- st.session_state.pending_question = "Python์—์„œ ๋ฆฌ์ŠคํŠธ ์ปดํ”„๋ฆฌํ—จ์…˜์ด ๋ญ์˜ˆ์š”?"
420
- st.rerun()
421
-
 
 
422
  with q2:
423
- if st.button("๐Ÿ‘ฅ ํŒ€ ํ”„๋กœ์ ํŠธ ํ˜‘์—… ํŒ์„ ์•Œ๋ ค์ฃผ์„ธ์š”.", use_container_width=True):
424
- st.session_state.pending_question = "ํŒ€ ํ”„๋กœ์ ํŠธ ํ˜‘์—… ํŒ์„ ์•Œ๋ ค์ฃผ์„ธ์š”."
425
- st.rerun()
426
-
427
- if st.button("๐Ÿ“Š ๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ์„ฑ๋Šฅ์€ ์–ด๋–ป๊ฒŒ ํ‰๊ฐ€ํ•˜๋‚˜์š”?", use_container_width=True):
428
- st.session_state.pending_question = "๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ์„ฑ๋Šฅ์€ ์–ด๋–ป๊ฒŒ ํ‰๊ฐ€ํ•˜๋‚˜์š”?"
429
- st.rerun()
430
-
 
 
431
  # =========================
432
  # ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
433
  # =========================
434
  def answer_question(question):
435
- st.session_state.messages.append({"role": "user", "content": question})
436
-
437
- title = question[:22] + "..." if len(question) > 22 else question
438
- if title not in st.session_state.chat_history_titles:
439
- st.session_state.chat_history_titles.append(title)
440
-
 
 
 
 
441
  docs = retriever.invoke(question)
442
- context = format_docs(docs)
443
-
 
 
 
 
 
 
 
 
 
 
 
444
  answer = rag_chain.invoke({
445
  "context": context,
446
  "question": question
447
  })
448
-
449
- st.session_state.messages.append({"role": "assistant", "content": answer})
450
-
 
 
451
  # ์ถ”์ฒœ ์งˆ๋ฌธ ํด๋ฆญ ์ฒ˜๋ฆฌ
452
  if "pending_question" in st.session_state:
453
  question = st.session_state.pending_question
454
  del st.session_state.pending_question
455
  answer_question(question)
456
  st.rerun()
457
-
458
  # =========================
459
  # ์ฑ„ํŒ… ์˜์—ญ
460
  # =========================
 
 
 
461
  st.markdown('<div class="chat-wrap">', unsafe_allow_html=True)
462
-
463
- if not st.session_state.messages:
464
  st.markdown("""
465
- <div class="assistant-name">๐Ÿค– AIVLE ๋„์šฐ๋ฏธ</div>
466
- <div class="assistant-card">
467
- ์•ˆ๋…•ํ•˜์„ธ์š”! AIVLE ๋ฐฑ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•™์Šต๊ณผ ํ”„๋กœ์ ํŠธ ๊ด€๋ จ ์งˆ๋ฌธ์— ๋‹ต๋ณ€๋“œ๋ฆด๊ฒŒ์š”.
 
 
468
  </div>
469
  """, unsafe_allow_html=True)
470
-
471
- for msg in st.session_state.messages:
472
  if msg["role"] == "user":
473
  st.markdown(
474
  f'<div class="user-bubble">{msg["content"]}</div>',
@@ -482,14 +631,60 @@ for msg in st.session_state.messages:
482
  """,
483
  unsafe_allow_html=True
484
  )
485
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  st.markdown('</div>', unsafe_allow_html=True)
487
-
488
  # =========================
489
  # ์ž…๋ ฅ ์˜์—ญ
490
  # =========================
491
- user_input = st.chat_input("๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...")
492
-
 
 
 
 
 
 
 
 
 
 
493
  if user_input:
494
  answer_question(user_input)
495
  st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  from pathlib import Path
3
+ import streamlit.components.v1 as components
4
+ import html
5
+ from langchain_community.document_loaders import TextLoader, CSVLoader
6
+
7
+ from openai import OpenAI
8
+ import uuid
9
+ import tempfile
10
  from langchain_community.document_loaders import PyMuPDFLoader
11
  from langchain_text_splitters import RecursiveCharacterTextSplitter
12
  from langchain_openai import OpenAIEmbeddings, ChatOpenAI
13
  from langchain_community.vectorstores import FAISS
14
  from langchain_core.prompts import ChatPromptTemplate
15
  from langchain_core.output_parsers import StrOutputParser
16
+
17
  st.set_page_config(
18
  page_title="AIVLE ํ•™์Šต๋„์šฐ๋ฏธ",
19
  page_icon="๐Ÿค–",
20
  layout="wide"
21
  )
22
+
23
+ # PDF_PATH = Path("Docs/AIVLE_School_๋ฐฑ์„œ_ํ†ตํ•ฉ๋ณธ_์ตœ์ข….pdf")
24
+
25
  BASE_DIR = Path(__file__).resolve().parent
26
  PDF_PATH = BASE_DIR / "AIVLE_School_๋ฐฑ์„œ_ํ†ตํ•ฉ๋ณธ_์ตœ์ข….pdf"
27
+ client = OpenAI()
28
+
29
+
 
30
  st.markdown("""
31
  <style>
32
  .main .block-container {
33
  padding-top: 2rem;
34
  max-width: 1100px;
35
  }
36
+
37
  .hero-box {
38
  background: #d9f3f2;
39
  border-radius: 22px;
 
43
  justify-content: space-between;
44
  align-items: center;
45
  }
46
+
47
  .hero-title {
48
  font-size: 30px;
49
  font-weight: 800;
50
  color: #111827;
51
  margin-bottom: 10px;
52
  }
53
+
54
  .hero-sub {
55
  font-size: 15px;
56
  color: #6b7280;
57
  }
58
+
59
  .robot {
60
  font-size: 72px;
61
  }
62
+
63
  .section-title {
64
  font-size: 17px;
65
  font-weight: 800;
66
  margin-bottom: 12px;
67
  }
68
+
 
 
 
 
 
 
 
 
 
 
 
69
  .chat-wrap {
70
  border-top: 1px solid #e5e7eb;
71
  margin-top: 28px;
72
  padding-top: 26px;
73
  }
74
+
75
  .user-bubble {
76
  background: #d9f3f2;
77
  padding: 14px 18px;
 
82
  margin-bottom: 8px;
83
  font-weight: 500;
84
  }
85
+
86
  .assistant-card {
87
  background: white;
88
  border: 1px solid #e5e7eb;
 
92
  max-width: 780px;
93
  box-shadow: 0 2px 10px rgba(0,0,0,0.04);
94
  }
95
+
96
  .assistant-name {
97
  font-size: 14px;
98
  color: #6b7280;
99
  font-weight: 700;
100
  margin-bottom: 8px;
101
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  </style>
103
  """, unsafe_allow_html=True)
104
+
105
  # =========================
106
  # ์„ธ์…˜ ์ƒํƒœ
107
  # =========================
108
+ if "conversations" not in st.session_state:
109
+ first_id = str(uuid.uuid4())
110
+ st.session_state.conversations = {
111
+ first_id: {
112
+ "title": "์ƒˆ ๋Œ€ํ™”",
113
+ "messages": []
114
+ }
115
+ }
116
+ st.session_state.current_chat_id = first_id
117
+
118
+ if "current_chat_id" not in st.session_state:
119
+ st.session_state.current_chat_id = list(st.session_state.conversations.keys())[0]
120
+
121
  if "show_faq" not in st.session_state:
122
  st.session_state.show_faq = False
123
+
124
+ if "faq_category" not in st.session_state:
125
+ st.session_state.faq_category = "์ „์ฒด"
126
+
127
+ if "logged_in" not in st.session_state:
128
+ st.session_state.logged_in = False
129
+
130
+ if "user_name" not in st.session_state:
131
+ st.session_state.user_name = ""
132
+
133
+ if "show_login" not in st.session_state:
134
+ st.session_state.show_login = False
135
+ if "recommended_questions" not in st.session_state:
136
+ st.session_state.recommended_questions = [
137
+ "๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?",
138
+ "ํ”„๋กœ์ ํŠธ ์ฃผ์ œ๋Š” ์–ด๋–ป๊ฒŒ ์ •ํ•˜๋‚˜์š”?",
139
+ "ํŒ€ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฌด์—‡์„ ๊ธฐ๋กํ•ด์•ผ ํ•˜๋‚˜์š”?",
140
+ "๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ์„ฑ๋Šฅ์€ ์–ด๋–ป๊ฒŒ ํ‰๊ฐ€ํ•˜๋‚˜์š”?"
141
+ ]
142
+ if "show_help" not in st.session_state:
143
+ st.session_state.show_help = False
144
+
145
+ if "uploaded_retriever" not in st.session_state:
146
+ st.session_state.uploaded_retriever = None
147
+
148
  # =========================
149
  # RAG
150
  # =========================
 
153
  if not PDF_PATH.exists():
154
  st.error(f"PDF ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: {PDF_PATH}")
155
  st.stop()
156
+
157
  loader = PyMuPDFLoader(str(PDF_PATH))
158
  docs = loader.load()
159
+
160
  splitter = RecursiveCharacterTextSplitter(
161
  chunk_size=1200,
162
  chunk_overlap=200,
163
  separators=["\n\n", "\n", ".", " ", ""]
164
  )
165
  chunks = splitter.split_documents(docs)
166
+
167
  embedding = OpenAIEmbeddings(model="text-embedding-3-small")
168
  vectorstore = FAISS.from_documents(chunks, embedding)
169
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
170
+
171
  llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
172
+
173
  prompt = ChatPromptTemplate.from_template("""
174
  ๋‹น์‹ ์€ AIVLE School ๋ฐฑ์„œ PDF ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ต๋ณ€ํ•˜๋Š” ํ•™์Šต๋„์šฐ๋ฏธ ์ฑ—๋ด‡์ž…๋‹ˆ๋‹ค.
175
+
176
  ๊ทœ์น™:
177
  1. ๋ฐ˜๋“œ์‹œ [๋ฌธ์„œ ๋‚ด์šฉ]์— ๊ทผ๊ฑฐํ•ด์„œ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
178
  2. ๋ฌธ์„œ์—์„œ ํ™•์ธ๋˜์ง€ ์•Š๋Š” ๋‚ด์šฉ์€ "๋ฐฑ์„œ์—์„œ ํ™•์ธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."๋ผ๊ณ  ๋‹ตํ•˜์„ธ์š”.
179
  3. ๋‹ต๋ณ€์€ ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
180
+
181
  [๋ฌธ์„œ ๋‚ด์šฉ]
182
  {context}
183
+
184
  [์งˆ๋ฌธ]
185
  {question}
186
  """)
187
+
188
  def format_docs(docs):
189
  return "\n\n".join(doc.page_content for doc in docs)
190
+
191
  chain = prompt | llm | StrOutputParser()
192
+
193
  return retriever, chain, format_docs
194
+
195
  retriever, rag_chain, format_docs = build_rag_chain()
196
+
197
+ def build_uploaded_retriever(uploaded_file):
198
+
199
+ suffix = Path(uploaded_file.name).suffix
200
+
201
+ with tempfile.NamedTemporaryFile(
202
+ delete=False,
203
+ suffix=suffix
204
+ ) as tmp:
205
+
206
+ tmp.write(uploaded_file.getvalue())
207
+ tmp_path = tmp.name
208
+
209
+ if suffix == ".pdf":
210
+ loader = PyMuPDFLoader(tmp_path)
211
+
212
+ elif suffix == ".txt":
213
+ loader = TextLoader(
214
+ tmp_path,
215
+ encoding="utf-8"
216
+ )
217
+
218
+ elif suffix == ".csv":
219
+ loader = CSVLoader(tmp_path)
220
+
221
+ else:
222
+ return None
223
+
224
+ docs = loader.load()
225
+
226
+ splitter = RecursiveCharacterTextSplitter(
227
+ chunk_size=1000,
228
+ chunk_overlap=150
229
+ )
230
+
231
+ chunks = splitter.split_documents(docs)
232
+
233
+ embedding = OpenAIEmbeddings(
234
+ model="text-embedding-3-small"
235
+ )
236
+
237
+ vectorstore = FAISS.from_documents(
238
+ chunks,
239
+ embedding
240
+ )
241
+
242
+ return vectorstore.as_retriever(
243
+ search_kwargs={"k": 3}
244
+ )
245
+
246
+
247
+
248
+
249
+ def generate_recommend_questions(user_question):
250
+ docs = retriever.invoke(user_question)
251
+ context = format_docs(docs)
252
+
253
+ recommend_prompt = ChatPromptTemplate.from_template("""
254
+ ๋‹น์‹ ์€ AIVLE School ๋ฐฑ์„œ ๋‚ด์šฉ์„ ๊ธฐ๋ฐ˜์œผ๋กœ '์ถ”์ฒœ ์งˆ๋ฌธ'๋งŒ ๋งŒ๋“œ๋Š” ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค.
255
+
256
+ ๊ทœ์น™:
257
+ 1. ๋‹ต๋ณ€์„ ์ ˆ๋Œ€ ์ž‘์„ฑํ•˜์ง€ ๋งˆ์„ธ์š”.
258
+ 2. ์„ค๋ช…๋„ ์ ˆ๋Œ€ ์ž‘์„ฑํ•˜์ง€ ๋งˆ์„ธ์š”.
259
+ 3. ์ถ”์ฒœ ์งˆ๋ฌธ๋งŒ 4๊ฐœ ์ž‘์„ฑํ•˜์„ธ์š”.
260
+ 4. ๊ฐ ์ค„์€ ๋ฐ˜๋“œ์‹œ ๋ฌผ์Œํ‘œ(?)๋กœ ๋๋‚˜๋Š” ์งˆ๋ฌธ ๋ฌธ์žฅ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
261
+ 5. ๋ฒˆํ˜ธ, ๋ถˆ๋ฆฟ, ๋”ฐ์˜ดํ‘œ, ์ ‘๋‘์–ด๋ฅผ ์“ฐ์ง€ ๋งˆ์„ธ์š”.
262
+ 6. ๋ฐฑ์„œ์— ๊ทผ๊ฑฐํ•œ ์งˆ๋ฌธ๋งŒ ๋งŒ๋“œ์„ธ์š”.
263
+ 7. ์‚ฌ์šฉ์ž์˜ ์งˆ๋ฌธ๊ณผ ์ง์ ‘ ๊ด€๋ จ๋œ ์งˆ๋ฌธ๋งŒ ์ƒ์„ฑํ•˜์„ธ์š”.
264
+
265
+ [๋ฐฑ์„œ ๋‚ด์šฉ]
266
+ {context}
267
+
268
+ [์‚ฌ์šฉ์ž ์งˆ๋ฌธ]
269
+ {question}
270
+ """)
271
+
272
+ recommend_chain = (
273
+ recommend_prompt
274
+ | ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
275
+ | StrOutputParser()
276
+ )
277
+
278
+ result = recommend_chain.invoke({
279
+ "context": context,
280
+ "question": user_question
281
+ })
282
+
283
+ questions = []
284
+
285
+ for line in result.split("\n"):
286
+ line = line.strip()
287
+
288
+ if not line:
289
+ continue
290
+
291
+ if not line.endswith("?"):
292
+ continue
293
+
294
+ line = line.lstrip("-โ€ข0123456789. ").strip()
295
+
296
+ questions.append(line)
297
+
298
+ return questions[:4]
299
+
300
+
301
+ def copy_button(text, key):
302
+ safe_text = html.escape(text).replace("\n", "\\n").replace("'", "\\'")
303
+
304
+ components.html(
305
+ f"""
306
+ <button onclick="navigator.clipboard.writeText('{safe_text}')"
307
+ style="
308
+ margin-top:8px;
309
+ padding:6px 12px;
310
+ border-radius:8px;
311
+ border:1px solid #d1d5db;
312
+ background:white;
313
+ cursor:pointer;
314
+ font-size:13px;
315
+ ">
316
+ ๐Ÿ“‹ ๋ณต์‚ฌ
317
+ </button>
318
+ """,
319
+ height=40
320
+ )
321
+
322
+ def generate_tts(text, filename):
323
+ speech_file = f"temp/{filename}.mp3"
324
+
325
+ Path("temp").mkdir(exist_ok=True)
326
+
327
+ response = client.audio.speech.create(
328
+ model="gpt-4o-mini-tts",
329
+ voice="nova",
330
+ input=text
331
+ )
332
+
333
+ response.stream_to_file(speech_file)
334
+
335
+ return speech_file
336
+
337
+ def transcribe_audio(audio_file):
338
+ transcript = client.audio.transcriptions.create(
339
+ model="whisper-1",
340
+ file=audio_file
341
+ )
342
+
343
+ return transcript.text
344
+
345
  # =========================
346
  # ์‚ฌ์ด๋“œ๋ฐ”
347
  # =========================
348
  with st.sidebar:
349
+
350
+ st.image(
351
+ "images/์—์ด๋ธ”ํ•™์Šต๋„์šฐ๋ฏธ ๋กœ๊ณ .png",
352
+ width=270
353
+ )
354
+
355
  st.markdown("## AIVLE")
356
  st.markdown("### ํ•™์Šต๋„์šฐ๋ฏธ ์ฑ—๋ด‡")
357
  st.caption("์—์ด๋ธ”์Šค์ฟจ์˜ ํ•™์Šต ์—ฌ์ •์„ ํ•จ๊ป˜ํ•˜๋Š” AI ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค.")
358
+
359
  if st.button("๏ผ‹ ์ƒˆ ๋Œ€ํ™”", use_container_width=True):
360
+ new_id = str(uuid.uuid4())
361
+ st.session_state.conversations[new_id] = {
362
+ "title": "์ƒˆ ๋Œ€ํ™”",
363
+ "messages": []
364
+ }
365
+ st.session_state.current_chat_id = new_id
366
  st.rerun()
367
+
368
  st.divider()
369
+
370
  st.button("๐Ÿ  ํ™ˆ", use_container_width=True)
371
+
372
  if st.button("โ” FAQ", use_container_width=True):
373
  st.session_state.show_faq = True
374
+ st.rerun()
375
+
376
  st.divider()
377
+
378
  st.markdown("### ํŒŒ์ผ ๊ด€๋ฆฌ")
379
+
380
+ uploaded_file = st.file_uploader(
381
+ "ํŒŒ์ผ ์—…๋กœ๋“œ",
382
+ type=["pdf", "txt", "csv"]
383
+ )
384
+
385
+ if uploaded_file is not None:
386
+
387
+ st.session_state.uploaded_retriever = (
388
+ build_uploaded_retriever(uploaded_file)
389
+ )
390
+
391
+ st.success(
392
+ "์—…๋กœ๋“œํ•œ ํŒŒ์ผ์„ ์ฑ—๋ด‡ ์ฐธ๊ณ  ์ž๋ฃŒ๋กœ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค."
393
+ )
394
+
395
+ st.button(
396
+ "๐Ÿ“ ์—…๋กœ๋“œ ๋ชฉ๋ก",
397
+ use_container_width=True
398
+ )
399
+
400
  st.divider()
401
+
402
  st.markdown("### ๋Œ€ํ™” ๊ธฐ๋ก")
403
+ for chat_id, chat in reversed(list(st.session_state.conversations.items())):
404
+ title = chat["title"]
405
+ if st.button(f"๐Ÿ’ฌ {title}", key=f"chat_{chat_id}", use_container_width=True):
406
+ st.session_state.current_chat_id = chat_id
407
+ st.session_state.show_faq = False
408
+ st.rerun()
409
+
 
 
 
 
 
 
 
 
 
410
  st.divider()
411
+
412
  if not st.session_state.logged_in:
413
  st.markdown("### ๐Ÿ‘ค ๊ณ„์ •")
414
+
415
  if st.button("๋กœ๊ทธ์ธ", use_container_width=True):
416
  st.session_state.show_login = not st.session_state.show_login
417
+
418
  if st.session_state.show_login:
419
  with st.form("sidebar_login_form", clear_on_submit=False):
420
  login_name = st.text_input("์ด๋ฆ„", key="login_name_input")
421
  login_id = st.text_input("์•„์ด๋””", key="login_id_input")
422
  login_pw = st.text_input("๋น„๋ฐ€๋ฒˆํ˜ธ", type="password", key="login_pw_input")
 
423
  login_btn = st.form_submit_button("๋กœ๊ทธ์ธ", use_container_width=True)
424
+
425
  if login_btn:
426
  if login_name.strip() and login_id.strip() and login_pw.strip():
427
  st.session_state.logged_in = True
 
430
  st.rerun()
431
  else:
432
  st.warning("์ด๋ฆ„, ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋‘ ์ž…๋ ฅํ•˜์„ธ์š”.")
 
433
  else:
434
  st.markdown(f"๐Ÿ‘ค **{st.session_state.user_name}**")
435
+ st.caption("์—์ด๋ธ”์Šค์ฟจ 9๊ธฐ")
436
+
437
  if st.button("๋กœ๊ทธ์•„์›ƒ", use_container_width=True):
438
  st.session_state.logged_in = False
439
  st.session_state.user_name = ""
440
  st.session_state.show_login = False
441
  st.rerun()
442
+
443
+ st.divider()
444
+
445
+ st.markdown("### ๋„์›€์ด ํ•„์š”ํ•˜์‹ ๊ฐ€์š”?")
446
+
447
+ if st.button("๐Ÿ’ฌ ๋ฌธ์˜ํ•˜๊ธฐ", use_container_width=True):
448
+ st.session_state.show_help = True
449
+
450
+
451
+
452
+
453
+ # =========================
454
+ # FAQ ๋ฐ์ดํ„ฐ
455
+ # =========================
456
+ faq_data = {
457
+ "์ „์ฒด": [
458
+ ("๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?", "๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์ฃผ์ œ ์„ ์ •, ๊ธฐํš, ๊ตฌํ˜„, ๋ฐœํ‘œ, ํ”ผ๋“œ๋ฐฑ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค."),
459
+ ("๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ ํŒ€์€ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑ๋˜๋‚˜์š”?", "ํŒ€์€ ๊ณผ์ • ์šด์˜ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค."),
460
+ ("ํ”„๋กœ์ ํŠธ ์ฃผ์ œ๋Š” ์–ด๋–ป๊ฒŒ ์ •ํ•˜๋‚˜์š”?", "๋ฌธ์ œ ์ •์˜, ๋ฐ์ดํ„ฐ ํ™œ์šฉ ๊ฐ€๋Šฅ์„ฑ, ๊ตฌํ˜„ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•ด ์ •ํ•ฉ๋‹ˆ๋‹ค."),
461
+ ("๋จธ์‹ ๋Ÿฌ๋‹ ๋ชจ๋ธ ์„ฑ๋Šฅ์€ ์–ด๋–ป๊ฒŒ ํ‰๊ฐ€ํ•˜๋‚˜์š”?", "Accuracy, Precision, Recall, F1 Score ๋“ฑ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค."),
462
+ ("๋ฐ์ดํ„ฐ ์ „์ฒ˜๋ฆฌ๋Š” ์™œ ์ค‘์š”ํ•œ๊ฐ€์š”?", "๋ฐ์ดํ„ฐ ํ’ˆ์งˆ์ด ๋ถ„์„ ๊ฒฐ๊ณผ์™€ ๋ชจ๋ธ ์„ฑ๋Šฅ์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ์ฃผ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.")
463
+ ],
464
+ "ํ•™์Šต ๊ณผ์ •": [
465
+ ("ํ•™์Šต์€ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์ง„ํ–‰๋˜๋‚˜์š”?", "์ด๋ก  ํ•™์Šต, ์‹ค์Šต, ํ”„๋กœ์ ํŠธ, ํ”ผ๋“œ๋ฐฑ ์ค‘์‹ฌ์œผ๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค."),
466
+ ("๋ณต์Šต์€ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ์ข‹๋‚˜์š”?", "์‹ค์Šต ์ฝ”๋“œ ์žฌ์ž‘์„ฑ, ๊ฐœ๋… ์š”์•ฝ, ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ๊ณผ์ •์„ ๋ฐ˜๋ณตํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.")
467
+ ],
468
+ "ํ”„๋กœ์ ํŠธ": [
469
+ ("๋ฏธ๋‹ˆ ํ”„๋กœ์ ํŠธ๋Š” ์–ด๋–ป๊ฒŒ ์ง„ํ–‰๋˜๋‚˜์š”?", "์ฃผ์ œ ์„ ์ •, ๊ธฐํš, ๊ตฌํ˜„, ๋ฐœํ‘œ, ํ”ผ๋“œ๋ฐฑ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค."),
470
+ ("ํ”„๋กœ์ ํŠธ ์ฃผ์ œ๋Š” ์–ด๋–ป๊ฒŒ ์ •ํ•˜๋‚˜์š”?", "๋ฌธ์ œ ์ •์˜์™€ ๊ตฌํ˜„ ๊ฐ€๋Šฅ์„ฑ์„ ์ค‘์‹ฌ์œผ๋กœ ์ •ํ•ฉ๋‹ˆ๋‹ค."),
471
+ ("ํŒ€ ํ”„๋กœ์ ํŠธ์—์„œ ๋ฌด์—‡์„ ๊ธฐ๋กํ•ด์•ผ ํ•˜๋‚˜์š”?", "๋ฌธ์ œ ์ •์˜, ๋ฐ์ดํ„ฐ ๊ทผ๊ฑฐ, ๋ณธ์ธ ์—ญํ• , ์˜์‚ฌ๊ฒฐ์ • ๊ณผ์ •, ์‚ฐ์ถœ๋ฌผ, ํ•œ๊ณ„์™€ ๊ฐœ์„  ๋ฐฉํ–ฅ์„ ๊พธ์ค€ํžˆ ๊ธฐ๋กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
472
+ ],
473
+ "KDT": [
474
+ ("๊ต์œก ์ˆ˜๋ฃŒ ๊ธฐ์ค€์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?", "์ด ํ›ˆ๋ จ์ผ์ˆ˜์˜ ์ผ์ • ๋น„์œจ ์ด์ƒ ์ถœ์„ํ•ด์•ผ ์ˆ˜๋ฃŒ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉฐ, ํ›ˆ๋ จ ์ˆ˜์ค€ ์œ ์ง€๋ฅผ ์œ„ํ•ด 100% ์ฐธ์—ฌ๊ฐ€ ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค."),
475
+ ("์ค‘๋„ ํฌ๊ธฐ ์‹œ ๋‚ด์ผ๋ฐฐ์›€์นด๋“œ ์ฐจ๊ฐ์•ก์ด ์žˆ๋‚˜์š”?", "๊ตญ๋ฏผ๋‚ด์ผ๋ฐฐ์›€์นด๋“œ ํ›ˆ๋ จ๊ณผ์ • ์ค‘๋„ํฌ๊ธฐ ์‹œ ํšŒ์ฐจ์— ๋”ฐ๋ผ ์ฐจ๊ฐ ๊ธฐ์ค€์ด ์ ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„ธ๋ถ€ ๊ธˆ์•ก๊ณผ ๊ธฐ์ค€์€ ๊ณ ์šฉ์„ผํ„ฐ ์•ˆ๋‚ด๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."),
476
+ ("ํ›ˆ๋ จ์žฅ๋ ค๊ธˆ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?", "์žฅ๊ธฐ๊ฐ„ ๊ตญ๋น„ ๊ณผ์ •์„ ์ˆ˜๊ฐ•ํ•˜๋Š” ๊ต์œก์ƒ์˜ ๊ฒฝ์ œ์  ๋ถ€๋‹ด์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ๊ตํ†ต๋น„์™€ ์‹๋น„ ๋ช…๋ชฉ์œผ๋กœ ์ œ๊ณต๋˜๋Š” ์ง€์›๊ธˆ์ž…๋‹ˆ๋‹ค."),
477
+ ("ํ›ˆ๋ จ์žฅ๋ ค๊ธˆ ์ง€๊ธ‰ ๊ธฐ์ค€์€ ์–ด๋–ป๊ฒŒ ๋˜๋‚˜์š”?", "๋‹จ์œ„๊ธฐ๊ฐ„ ์ถœ์„๋ฅ , ๊ณ ์šฉ ์ƒํƒœ, ์†Œ๋“ ์กฐ๊ฑด ๋“ฑ ์ง€๊ธ‰ ๊ธฐ์ค€์„ ์ถฉ์กฑํ•ด์•ผ ์ˆ˜๋ นํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ์ธ๋ณ„ ์ƒํ™ฉ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.")
478
+ ],
479
+ "์‹œ์Šคํ…œ/์ ‘์†": [
480
+ ("์ ‘์† ์˜ค๋ฅ˜๊ฐ€ ๋‚˜๋ฉด ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?", "์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ, ๊ณ„์ • ์ •๋ณด, ๋ธŒ๋ผ์šฐ์ € ์บ์‹œ๋ฅผ ๋จผ์ € ํ™•์ธํ•ฉ๋‹ˆ๋‹ค."),
481
+ ("ํŒŒ์ผ ์—…๋กœ๋“œ๊ฐ€ ์•ˆ ๋˜๋ฉด ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์š”?", "ํŒŒ์ผ ํ˜•์‹๊ณผ ์šฉ๋Ÿ‰ ์ œํ•œ์„ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.")
482
+ ],
483
+ "๊ธฐํƒ€": [
484
+ ("๋ฌธ์˜๋Š” ์–ด๋””๋กœ ํ•˜๋‚˜์š”?", "์šด์˜์ง„ ๋˜๋Š” ์ง€์ •๋œ ๋ฌธ์˜ ์ฑ„๋„์„ ํ†ตํ•ด ๋ฌธ์˜ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.")
485
+ ]
486
+ }
487
+
488
  # =========================
489
  # FAQ ํ™”๋ฉด
490
  # =========================
491
  if st.session_state.show_faq:
492
+ st.markdown("## โ” FAQ")
493
+ st.caption("์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ์„ ํ™•์ธํ•ด๋ณด์„ธ์š”.")
494
+
495
+ if st.button("โœ• FAQ ๋‹ซ๊ธฐ"):
496
+ st.session_state.show_faq = False
497
+ st.rerun()
498
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  left, right = st.columns([1, 3])
500
+
501
  with left:
502
+ st.markdown("### ์นดํ…Œ๊ณ ๋ฆฌ")
503
+ for category in faq_data.keys():
504
+ if st.button(category, key=f"faq_{category}", use_container_width=True):
505
+ st.session_state.faq_category = category
506
+ st.rerun()
507
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  with right:
509
+ selected = st.session_state.faq_category
510
+ st.markdown(f"### {selected}")
511
+
512
+ for question, answer in faq_data[selected]:
513
+ with st.expander(question):
514
+ st.write(answer)
515
+
516
+ st.stop()
517
+
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  # =========================
519
  # ๋ฉ”์ธ ํ™”๋ฉด ํ—ค๋”
520
  # =========================
521
+
522
+ st.image(
523
+ "images/์ด๋ฏธ์ง€.png",
524
+ use_container_width=True
525
+ )
526
+
 
 
 
 
527
  # =========================
528
  # ์ถ”์ฒœ ์งˆ๋ฌธ
529
  # =========================
530
  st.markdown('<div class="section-title">์ถ”์ฒœ ์งˆ๋ฌธ</div>', unsafe_allow_html=True)
531
+
532
+ recommended = st.session_state.recommended_questions
533
+
534
  q1, q2 = st.columns(2)
535
+
536
  with q1:
537
+ for idx in [0, 1]:
538
+ if idx < len(recommended):
539
+ if st.button(
540
+ recommended[idx],
541
+ key=f"rec_{idx}",
542
+ use_container_width=True
543
+ ):
544
+ st.session_state.pending_question = recommended[idx]
545
+ st.rerun()
546
+
547
  with q2:
548
+ for idx in [2, 3]:
549
+ if idx < len(recommended):
550
+ if st.button(
551
+ recommended[idx],
552
+ key=f"rec_{idx}",
553
+ use_container_width=True
554
+ ):
555
+ st.session_state.pending_question = recommended[idx]
556
+ st.rerun()
557
+
558
  # =========================
559
  # ์งˆ๋ฌธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
560
  # =========================
561
  def answer_question(question):
562
+ chat_id = st.session_state.current_chat_id
563
+ chat = st.session_state.conversations[chat_id]
564
+
565
+ chat["messages"].append({"role": "user", "content": question})
566
+
567
+ if chat["title"] == "์ƒˆ ๋Œ€ํ™”":
568
+ chat["title"] = question[:18] + "..." if len(question) > 18 else question
569
+
570
+
571
+
572
  docs = retriever.invoke(question)
573
+
574
+ all_docs = list(docs)
575
+
576
+ if st.session_state.uploaded_retriever is not None:
577
+
578
+ uploaded_docs = (
579
+ st.session_state.uploaded_retriever.invoke(question)
580
+ )
581
+
582
+ all_docs.extend(uploaded_docs)
583
+
584
+ context = format_docs(all_docs)
585
+
586
  answer = rag_chain.invoke({
587
  "context": context,
588
  "question": question
589
  })
590
+
591
+ chat["messages"].append({"role": "assistant", "content": answer})
592
+
593
+ st.session_state.recommended_questions = generate_recommend_questions(question)
594
+
595
  # ์ถ”์ฒœ ์งˆ๋ฌธ ํด๋ฆญ ์ฒ˜๋ฆฌ
596
  if "pending_question" in st.session_state:
597
  question = st.session_state.pending_question
598
  del st.session_state.pending_question
599
  answer_question(question)
600
  st.rerun()
601
+
602
  # =========================
603
  # ์ฑ„ํŒ… ์˜์—ญ
604
  # =========================
605
+ current_chat = st.session_state.conversations[st.session_state.current_chat_id]
606
+ messages = current_chat["messages"]
607
+
608
  st.markdown('<div class="chat-wrap">', unsafe_allow_html=True)
609
+
610
+ if not messages:
611
  st.markdown("""
612
+ <div style="min-height: 560px;">
613
+ <div class="assistant-name">๐Ÿค– AIVLE ๋„์šฐ๋ฏธ</div>
614
+ <div class="assistant-card">
615
+ ์•ˆ๋…•ํ•˜์„ธ์š”! AIVLE ๋ฐฑ์„œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•™์Šต๊ณผ ํ”„๋กœ์ ํŠธ ๊ด€๋ จ ์งˆ๋ฌธ์— ๋‹ต๋ณ€๋“œ๋ฆด๊ฒŒ์š”.
616
+ </div>
617
  </div>
618
  """, unsafe_allow_html=True)
619
+
620
+ for idx, msg in enumerate(messages):
621
  if msg["role"] == "user":
622
  st.markdown(
623
  f'<div class="user-bubble">{msg["content"]}</div>',
 
631
  """,
632
  unsafe_allow_html=True
633
  )
634
+
635
+
636
+
637
+ copy_button(msg["content"], key=f"copy_{idx}")
638
+
639
+
640
+ if st.button("๐Ÿ”Š ์Œ์„ฑ์œผ๋กœ ๋“ฃ๊ธฐ", key=f"tts_{idx}"):
641
+
642
+ audio_path = generate_tts(
643
+ msg["content"],
644
+ f"audio_{idx}"
645
+ )
646
+
647
+ st.audio(audio_path)
648
+
649
+
650
+
651
  st.markdown('</div>', unsafe_allow_html=True)
652
+
653
  # =========================
654
  # ์ž…๋ ฅ ์˜์—ญ
655
  # =========================
656
+ col1, col2 = st.columns([5, 1])
657
+
658
+ with col1:
659
+ user_input = st.chat_input("๋ฉ”์‹œ์ง€๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...")
660
+
661
+ with col2:
662
+ audio_file = st.audio_input(
663
+ "๐ŸŽค ์Œ์„ฑ ์งˆ๋ฌธ",
664
+ label_visibility="collapsed"
665
+ )
666
+
667
+ # ํ…์ŠคํŠธ ์งˆ๋ฌธ
668
  if user_input:
669
  answer_question(user_input)
670
  st.rerun()
671
+
672
+ # ์Œ์„ฑ ์งˆ๋ฌธ
673
+ if audio_file is not None:
674
+
675
+ with st.spinner("์Œ์„ฑ์„ ๋ถ„์„ํ•˜๋Š” ์ค‘์ž…๋‹ˆ๋‹ค..."):
676
+
677
+ question = transcribe_audio(audio_file)
678
+
679
+ current_chat = st.session_state.conversations[
680
+ st.session_state.current_chat_id
681
+ ]
682
+
683
+ current_chat["messages"].append({
684
+ "role": "user",
685
+ "content": f"๐ŸŽค {question}"
686
+ })
687
+
688
+ answer_question(question)
689
+
690
+ st.rerun()