Spaces:
Runtime error
Runtime error
Commit
·
b98f566
1
Parent(s):
48922d0
st.experimental_rerun()을 st.rerun()으로 변경 및 UX 문구 일부 개선
Browse files
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 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
try:
|
314 |
-
|
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 |
-
|
319 |
-
st.markdown(f"
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|