morethanair commited on
Commit
b98f566
·
1 Parent(s): 48922d0

st.experimental_rerun()을 st.rerun()으로 변경 및 UX 문구 일부 개선

Browse files
Files changed (1) hide show
  1. app.py +143 -91
app.py CHANGED
@@ -251,9 +251,13 @@ def generate_khan_answer(query: str, search_results: List[Dict], client: OpenAI)
251
  logger.error(f"Error during OpenAI API call: {e}", exc_info=True)
252
  return "답변을 생성하는 중에 문제가 발생했습니다. OpenAI API 키 또는 서비스 상태를 확인해주세요."
253
 
254
- # --- Streamlit 앱 UI ---
255
  st.set_page_config(page_title="Khan 멘토 (PM 영상 기반)", layout="wide")
256
  st.title("✨ Khan 멘토에게 질문하기")
 
 
 
 
257
  st.markdown("PM 관련 영상 내용을 기반으로 Khan 멘토가 답변해 드립니다.")
258
 
259
  # --- API 키 확인 및 리소스 초기화 ---
@@ -262,95 +266,143 @@ pc = init_pinecone()
262
  model = load_embedding_model()
263
  index = get_pinecone_index(pc, INDEX_NAME)
264
 
265
- # --- 사용자 입력 ---
266
- query = st.text_input("멘토에게 질문할 내용을 입력하세요:", placeholder="예: 신입 PM이 가장 먼저 해야 할 일은 무엇인가요?")
267
-
268
- # --- 검색 답변 생성 실행 ---
269
- if st.button("Khan 멘토에게 질문하기"):
270
- # Always use top_k=3 for Pinecone search
271
- if query and index and model and openai_client:
272
- with st.spinner("관련 영상을 찾고 Khan 멘토가 답변을 준비하는 중..."):
273
- # 1. Pinecone 검색 (Always use top_k=3)
274
- pinecone_results = search(query, top_k=5, _index=index, _model=model)
275
-
276
- # 2. OpenAI 답변 생성
277
- khan_answer = generate_khan_answer(query, pinecone_results, openai_client)
278
-
279
- # 3. 결과 표시
280
- st.subheader("💡 Khan 멘토의 답변")
281
- st.markdown(khan_answer) # 생성된 답변 표시
282
-
283
- # 4. 참고 자료 (Pinecone 검색 결과) 표시
284
- if pinecone_results:
285
- with st.expander("답변에 참고한 영상 정보 보기"):
286
- displayed_urls = set() # Keep track of displayed video URLs
287
- # Display up to 3 *unique* results based on URL
288
- for i, r in enumerate(pinecone_results):
289
- url = r.get('URL', 'N/A')
290
-
291
- # Skip if this video URL has already been displayed
292
- if url in displayed_urls or url == 'N/A':
293
- continue
294
-
295
- # Add the URL to the set of displayed URLs
296
- displayed_urls.add(url)
297
-
298
- # --- Display unique video info ---
299
- st.markdown(f"--- **참고 자료 {len(displayed_urls)} (유사도: {r['점수']:.4f})** ---") # Use length of set for counter
300
- st.markdown(f"**제목:** {r.get('제목', 'N/A')}")
301
- st.markdown(f"**요약:** {r.get('요약', 'N/A')}")
302
-
303
- timestamp = r.get('타임스탬프', 'N/A')
304
- is_youtube = url and isinstance(url, str) and ('youtube.com' in url or 'youtu.be' in url)
305
- start_seconds = None # Initialize start_seconds
306
-
307
- # Try to calculate start_seconds if timestamp is valid
308
- if is_youtube and timestamp and timestamp != 'N/A':
309
- start_seconds = parse_timestamp_to_seconds(timestamp)
310
-
311
- # Display timestamped link (still useful for user)
312
- if is_youtube and start_seconds is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  try:
314
- # We still generate timestamped URL for the link text
315
- timestamped_link_url = add_timestamp_to_youtube_url(url, timestamp)
316
- st.markdown(f"**영상 링크 (타임스탬프 포함):** [{timestamped_link_url}]({timestamped_link_url})")
317
  except Exception as e:
318
- logger.error(f"Error creating timestamped URL for link: {e}")
319
- st.markdown(f"**영상 링크 (원본):** [{url}]({url})") # Fallback link
320
- elif url != "N/A" and isinstance(url, str) and url.startswith("http"):
321
- st.markdown(f"**URL:** [{url}]({url})")
322
- else:
323
- st.markdown(f"**URL:** {url}")
324
-
325
- # Use st.video with original URL and start_time parameter
326
- if is_youtube and url != "N/A":
327
- # Create columns to control width, place video in the first column (50% width)
328
- col1, col2 = st.columns(2)
329
- with col1:
330
- try:
331
- # Pass original URL and calculated start_seconds to st.video
332
- st.video(url, start_time=start_seconds or 0)
333
- except Exception as e:
334
- st.error(f"비디오({url}) 재생 중 오류 발생: {e}")
335
- # Fallback link uses the original URL here as start_time likely failed
336
- st.markdown(f"[YouTube에서 보기]({url})")
337
- elif url != "N/A": # Try st.video for other potential video URLs (no start_time)
338
- # Create columns for non-YouTube videos as well
339
- col1, col2 = st.columns(2)
340
- with col1:
341
- try:
342
- st.video(url)
343
- except Exception as e:
344
- logger.warning(f"st.video failed for non-YouTube URL {url}: {e}")
345
-
346
- # Remove the display of original timestamp and type
347
- # st.markdown(f"**타임스탬프 (원본):** {timestamp}")
348
- # st.markdown(f"**내용 타입:** {r.get('타입', 'N/A')}")
349
-
350
-
351
- elif not query:
352
- st.warning("질문 내용을 입력해주세요.")
353
- # API 키 등 다른 요소 부재 시 에러는 각 init 함수에서 처리됨
354
-
355
- st.markdown("---")
356
- st.caption("Powered by Pinecone, Sentence Transformers, and OpenAI")
 
251
  logger.error(f"Error during OpenAI API call: {e}", exc_info=True)
252
  return "답변을 생성하는 중에 문제가 발생했습니다. OpenAI API 키 또는 서비스 상태를 확인해주세요."
253
 
254
+ # --- Streamlit 앱 UI (리팩토링) ---
255
  st.set_page_config(page_title="Khan 멘토 (PM 영상 기반)", layout="wide")
256
  st.title("✨ Khan 멘토에게 질문하기")
257
+ st.markdown(
258
+ '<a href="https://forms.gle/SUqrGBT3dktSB7v26" target="_blank" style="display:inline-block; background:#f9e79f; color:#1a237e; font-weight:bold; padding:0.5em 1.2em; border-radius:8px; text-decoration:none; font-size:1.1em; margin-bottom:16px;">📝 서비스 사용성 설문조사 참여하기</a>',
259
+ unsafe_allow_html=True
260
+ )
261
  st.markdown("PM 관련 영상 내용을 기반으로 Khan 멘토가 답변해 드립니다.")
262
 
263
  # --- API 키 확인 및 리소스 초기화 ---
 
266
  model = load_embedding_model()
267
  index = get_pinecone_index(pc, INDEX_NAME)
268
 
269
+ # --- 상태 관리 ---
270
+ if 'user_state' not in st.session_state:
271
+ st.session_state['user_state'] = ''
272
+ if 'empathy_message' not in st.session_state:
273
+ st.session_state['empathy_message'] = ''
274
+ if 'example_questions' not in st.session_state:
275
+ st.session_state['example_questions'] = []
276
+ if 'selected_question' not in st.session_state:
277
+ st.session_state['selected_question'] = ''
278
+ if 'khan_answer' not in st.session_state:
279
+ st.session_state['khan_answer'] = ''
280
+ if 'step' not in st.session_state:
281
+ st.session_state['step'] = 1
282
+
283
+ # --- 1단계: 사용자 상태 입력 ---
284
+ if st.session_state['step'] == 1:
285
+ user_state = st.text_area("지금 어떤 상황이신가요? 고민/상황을 자유롭게 적어주세요.", value=st.session_state['user_state'])
286
+ if st.button("상태 말하기"):
287
+ st.session_state['user_state'] = user_state
288
+ # 2단계로 이동
289
+ st.session_state['step'] = 2
290
+ st.rerun()
291
+
292
+ # --- 2단계: 공감 메시지 + 예시 질문 생성 ---
293
+ if st.session_state['step'] == 2:
294
+ with st.spinner("상황에 공감하고, 좋은 질문을 생성하는 중..."):
295
+ # 1. 공감 메시지 생성
296
+ empathy_prompt = f"""
297
+ 너는 따뜻하고 공감 능력이 뛰어난 상담가야.
298
+ 아래 사용자의 상황을 듣고, 충분히 감정적으로 공감해주고, 용기를 북돋아주는 말��� 해줘.
299
+ 상황: "{st.session_state['user_state']}"
300
+ """
301
+ try:
302
+ empathy_response = openai_client.chat.completions.create(
303
+ model="gpt-4o",
304
+ messages=[{"role": "system", "content": empathy_prompt}],
305
+ temperature=0.7,
306
+ )
307
+ st.session_state['empathy_message'] = empathy_response.choices[0].message.content.strip()
308
+ except Exception as e:
309
+ st.session_state['empathy_message'] = f"공감 메시지 생성 중 오류: {e}"
310
+ # 2. 예시 질문 생성
311
+ example_prompt = f"""
312
+ 아래 상황에서 Khan 멘토에게 있는 구체적이고 실용적인 질문 3~4가지를 한국어로 만들어줘.\n각 질문은 한 문장으로, 실제로 도움이 될 만한 내용이어야 해.\n상황: "{st.session_state['user_state']}"
313
+ """
314
+ try:
315
+ example_response = openai_client.chat.completions.create(
316
+ model="gpt-4o",
317
+ messages=[{"role": "system", "content": example_prompt}],
318
+ temperature=0.5,
319
+ )
320
+ # 응답에서 질문만 추출 (숫자/기호/줄바꿈 등 정제)
321
+ import re
322
+ raw = example_response.choices[0].message.content.strip()
323
+ questions = re.findall(r'\d+\.\s*(.+)', raw)
324
+ if not questions:
325
+ # 숫자 없이 줄바꿈만 있을 경우
326
+ questions = [q.strip('-• ').strip() for q in raw.split('\n') if q.strip()]
327
+ st.session_state['example_questions'] = questions[:4]
328
+ except Exception as e:
329
+ st.session_state['example_questions'] = [f"예시 질문 생성 중 오류: {e}"]
330
+ # 3단계로 이동
331
+ st.session_state['step'] = 3
332
+ st.rerun()
333
+
334
+ # --- 3단계: 공감 메시지 + 예시 질문 버튼/직접입력 + Khan 답변 ---
335
+ if st.session_state['step'] == 3:
336
+ st.success(st.session_state['empathy_message'])
337
+ st.markdown("#### 이런 질문을 해볼 수 있어요!")
338
+ cols = st.columns(len(st.session_state['example_questions']))
339
+ for i, q in enumerate(st.session_state['example_questions']):
340
+ if cols[i].button(q):
341
+ st.session_state['selected_question'] = q
342
+ st.session_state['step'] = 4
343
+ st.rerun()
344
+ st.markdown("---")
345
+ user_q = st.text_input("직접 궁금한 점을 입력해도 좋아요!", value=st.session_state['selected_question'])
346
+ if st.button("Khan 멘토에게 질문하기"):
347
+ st.session_state['selected_question'] = user_q
348
+ st.session_state['step'] = 4
349
+ st.rerun()
350
+
351
+ # --- 4단계: Khan 멘토 답변 ---
352
+ if st.session_state['step'] == 4:
353
+ with st.spinner("Khan 멘토가 답변을 준비하는 중..."):
354
+ pinecone_results = search(st.session_state['selected_question'], top_k=5, _index=index, _model=model)
355
+ khan_answer = generate_khan_answer(st.session_state['selected_question'], pinecone_results, openai_client)
356
+ st.session_state['khan_answer'] = khan_answer
357
+ st.subheader("💡 Khan 멘토의 답변")
358
+ st.markdown(st.session_state['khan_answer'])
359
+ # 참고 영상 정보 표시
360
+ if pinecone_results:
361
+ with st.expander("답변에 참고한 영상 정보 보기"):
362
+ displayed_urls = set()
363
+ for i, r in enumerate(pinecone_results):
364
+ url = r.get('URL', 'N/A')
365
+ if url in displayed_urls or url == 'N/A':
366
+ continue
367
+ displayed_urls.add(url)
368
+ st.markdown(f"--- **참고 자료 {len(displayed_urls)} (유사도: {r['점수']:.4f})** ---")
369
+ st.markdown(f"**제목:** {r.get('제목', 'N/A')}")
370
+ st.markdown(f"**요약:** {r.get('요약', 'N/A')}")
371
+ timestamp = r.get('타임스탬프', 'N/A')
372
+ is_youtube = url and isinstance(url, str) and ('youtube.com' in url or 'youtu.be' in url)
373
+ start_seconds = None
374
+ if is_youtube and timestamp and timestamp != 'N/A':
375
+ start_seconds = parse_timestamp_to_seconds(timestamp)
376
+ if is_youtube and start_seconds is not None:
377
+ try:
378
+ timestamped_link_url = add_timestamp_to_youtube_url(url, timestamp)
379
+ st.markdown(f"**영상 링크 (타임스탬프 포함):** [{timestamped_link_url}]({timestamped_link_url})")
380
+ except Exception as e:
381
+ logger.error(f"Error creating timestamped URL for link: {e}")
382
+ st.markdown(f"**영상 링크 (원본):** [{url}]({url})")
383
+ elif url != "N/A" and isinstance(url, str) and url.startswith("http"):
384
+ st.markdown(f"**URL:** [{url}]({url})")
385
+ else:
386
+ st.markdown(f"**URL:** {url}")
387
+ if is_youtube and url != "N/A":
388
+ col1, col2 = st.columns(2)
389
+ with col1:
390
  try:
391
+ st.video(url, start_time=start_seconds or 0)
 
 
392
  except Exception as e:
393
+ st.error(f"비디오({url}) 재생 오류 발생: {e}")
394
+ st.markdown(f"[YouTube에서 보기]({url})")
395
+ elif url != "N/A":
396
+ col1, col2 = st.columns(2)
397
+ with col1:
398
+ try:
399
+ st.video(url)
400
+ except Exception as e:
401
+ logger.warning(f"st.video failed for non-YouTube URL {url}: {e}")
402
+ st.markdown("---")
403
+ st.caption("Powered by Pinecone, Sentence Transformers, and OpenAI")
404
+ # 다시 처음으로 돌아가기 버튼
405
+ if st.button("다시 질문 흐름 시작하기"):
406
+ for k in ['user_state','empathy_message','example_questions','selected_question','khan_answer','step']:
407
+ st.session_state[k] = '' if k != 'step' else 1
408
+ st.rerun()