morethanair commited on
Commit
e313748
·
1 Parent(s): c29181d

상사 MBTI 보고문 프롬프트 개선 및 gpt-4o-mini 모델 적용

Browse files
Files changed (1) hide show
  1. app.py +211 -168
app.py CHANGED
@@ -278,181 +278,224 @@ A: 단기적으로는 맞습니다. 성과 없이 퇴사하면 이력서에 남
278
  logger.error(f"Error during OpenAI API call: {e}", exc_info=True)
279
  return "답변을 생성하는 중에 문제가 발생했습니다. OpenAI API 키 또는 서비스 상태를 확인해주세요."
280
 
281
- # --- Streamlit 앱 UI (리팩토링) ---
282
  st.set_page_config(page_title="Khan 멘토 (PM 영상 기반)", layout="wide")
283
- st.markdown(
284
- """
285
- <a href="https://vo.la/eAKBGo" target="_blank">
286
- <img src="https://morethanair.com/wp-content/uploads/2025/04/KakaoTalk_Photo_2025-04-20-23-47-37.png" style="width:70%; border-radius: 10px;" />
287
- </a>
288
- """,
289
- unsafe_allow_html=True
290
- )
291
- st.title("✨ Khan 멘토에게 질문하기")
292
- st.markdown(
293
- '<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>',
294
- unsafe_allow_html=True
295
  )
296
- st.markdown("PM 관련 영상 내용을 기반으로 Khan 멘토가 답변해 드립니다.")
297
 
298
- # --- API 키 확인 및 리소스 초기화 ---
299
  openai_client = init_openai_client()
300
- pc = init_pinecone()
301
- model = load_embedding_model()
302
- index = get_pinecone_index(pc, INDEX_NAME)
303
-
304
- # --- 상태 관리 ---
305
- if 'user_state' not in st.session_state:
306
- st.session_state['user_state'] = ''
307
- if 'empathy_message' not in st.session_state:
308
- st.session_state['empathy_message'] = ''
309
- if 'example_questions' not in st.session_state:
310
- st.session_state['example_questions'] = []
311
- if 'selected_question' not in st.session_state:
312
- st.session_state['selected_question'] = ''
313
- if 'khan_answer' not in st.session_state:
314
- st.session_state['khan_answer'] = ''
315
- if 'step' not in st.session_state:
316
- st.session_state['step'] = 1
317
-
318
- # --- 1단계: 사용자 상태 입력 ---
319
- if st.session_state['step'] == 1:
320
- user_state = st.text_area("지금 어떤 상황이신가요? 고민을 자유롭게 적어주세요.", value=st.session_state['user_state'])
321
- if st.button("나의 이야기 들려주기 ✍️"):
322
- st.session_state['user_state'] = user_state
323
- # 2단계로 이동
324
- st.session_state['step'] = 2
325
- st.rerun()
326
-
327
- # --- 2단계: 공감 메시지 + 예시 질문 생성 ---
328
- if st.session_state['step'] == 2:
329
- with st.spinner("어떤 상황인지 고민해볼께요..."):
330
- # 1. 공감 메시지 생성
331
- empathy_prompt = f"""
332
- 너는 따뜻하고 공감 능력이 뛰어난 상담가야.
333
- 아래 사용자의 상황을 듣고, 충분히 감정적으로 공감해주고, 용기를 북돋아주는 말을 해줘.
334
- 부를 때는 '당신은', '당신이'처럼 친근한 말투로 부르고, 물음표는 사용하지 않아.
335
- 상황: "{st.session_state['user_state']}"
336
- """
337
- try:
338
- empathy_response = openai_client.chat.completions.create(
339
- model="gpt-4o",
340
- messages=[{"role": "system", "content": empathy_prompt}],
341
- temperature=0.7,
342
- )
343
- st.session_state['empathy_message'] = empathy_response.choices[0].message.content.strip()
344
- except Exception as e:
345
- st.session_state['empathy_message'] = f"공감 메시지 생성 중 오류: {e}"
346
- # 2. 예시 질문 생성
347
- example_prompt = f"""
348
- 아래 상황에서 Khan 멘토에게 할 수 있는 구체적이고 실용적인 질문 3~4가지를 한국어로 만들어줘.\n각 질문은 한 문장으로, 실제로 도움이 될 만한 내용이어야 해.\n상황: "{st.session_state['user_state']}"
349
- """
350
- try:
351
- example_response = openai_client.chat.completions.create(
352
- model="gpt-4o",
353
- messages=[{"role": "system", "content": example_prompt}],
354
- temperature=0.5,
355
- )
356
- # 응답에서 질문만 추출 (숫자/기호/줄바꿈 등 정제)
357
- import re
358
- raw = example_response.choices[0].message.content.strip()
359
- questions = re.findall(r'\d+\.\s*(.+)', raw)
360
- if not questions:
361
- # 숫자 없이 줄바꿈만 있을 경우
362
- questions = [q.strip('-• ').strip() for q in raw.split('\n') if q.strip()]
363
- st.session_state['example_questions'] = questions[:4]
364
- except Exception as e:
365
- st.session_state['example_questions'] = [f"예시 질문 생성 중 오류: {e}"]
366
- # 3단계로 이동
367
- st.session_state['step'] = 3
368
- st.rerun()
369
-
370
- # --- 3단계: 공감 메시지 + 예시 질문 버튼/직접입력 + Khan 답변 ---
371
- if st.session_state['step'] == 3:
372
- st.success(st.session_state['empathy_message'])
373
- st.markdown("#### 이런 질문을 해볼 수 있어요!")
374
- cols = st.columns(len(st.session_state['example_questions']))
375
- for i, q in enumerate(st.session_state['example_questions']):
376
- if cols[i].button(q):
377
- st.session_state['selected_question'] = q
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  st.session_state['step'] = 4
379
  st.rerun()
380
- st.markdown("---")
381
- user_q = st.text_input("직접 궁금한 점을 입력해도 좋아요!", value=st.session_state['selected_question'])
382
- if st.button("Khan 멘토에게 질문하기"):
383
- st.session_state['selected_question'] = user_q
384
- st.session_state['step'] = 4
385
- st.rerun()
386
-
387
- # --- 4단계: Khan 멘토 답변 ---
388
- if st.session_state['step'] == 4:
389
- with st.spinner("Khan 멘토가 답변을 준비하는 중..."):
390
- pinecone_results = search(st.session_state['selected_question'], top_k=5, _index=index, _model=model)
391
- khan_answer = generate_khan_answer(st.session_state['selected_question'], pinecone_results, openai_client)
392
- st.session_state['khan_answer'] = khan_answer
393
- st.subheader("💡 Khan 멘토의 답변")
394
- st.markdown(st.session_state['khan_answer'])
395
- # 참고 영상 정보 표시
396
- if pinecone_results:
397
- with st.expander("답변에 참고한 영상 정보 보기"):
398
- displayed_urls = set()
399
- for i, r in enumerate(pinecone_results):
400
- url = r.get('URL', 'N/A')
401
- if url in displayed_urls or url == 'N/A':
402
- continue
403
- displayed_urls.add(url)
404
- st.markdown(f"--- **참고 자료 {len(displayed_urls)} (유사도: {r['점수']:.4f})** ---")
405
- st.markdown(f"**제목:** {r.get('제목', 'N/A')}")
406
- st.markdown(f"**요약:** {r.get('요약', 'N/A')}")
407
- timestamp = r.get('타임스탬프', 'N/A')
408
- is_youtube = url and isinstance(url, str) and ('youtube.com' in url or 'youtu.be' in url)
409
- start_seconds = None
410
- if is_youtube and timestamp and timestamp != 'N/A':
411
- start_seconds = parse_timestamp_to_seconds(timestamp)
412
- if is_youtube and start_seconds is not None:
413
- try:
414
- timestamped_link_url = add_timestamp_to_youtube_url(url, timestamp)
415
- st.markdown(f"**영상 링크 (타임스탬프 포함):** [{timestamped_link_url}]({timestamped_link_url})")
416
- except Exception as e:
417
- logger.error(f"Error creating timestamped URL for link: {e}")
418
- st.markdown(f"**영상 링크 (원본):** [{url}]({url})")
419
- elif url != "N/A" and isinstance(url, str) and url.startswith("http"):
420
- st.markdown(f"**URL:** [{url}]({url})")
421
- else:
422
- st.markdown(f"**URL:** {url}")
423
- if is_youtube and url != "N/A":
424
- col1, col2 = st.columns(2)
425
- with col1:
426
- try:
427
- st.video(url, start_time=start_seconds or 0)
428
- except Exception as e:
429
- st.error(f"비디오({url}) 재생 중 오류 발생: {e}")
430
- st.markdown(f"[YouTube에서 보기]({url})")
431
- elif url != "N/A":
432
- col1, col2 = st.columns(2)
433
- with col1:
434
  try:
435
- st.video(url)
 
436
  except Exception as e:
437
- logger.warning(f"st.video failed for non-YouTube URL {url}: {e}")
438
- st.markdown("---")
439
- # --- 예시 질문 버튼/직접입력 다시 노출 (후속 질문) ---
440
- st.markdown("#### 추가로 궁금한 점이 있으신가요? 아래 예시 질문을 클릭하거나 직접 입력해보세요!")
441
- cols = st.columns(len(st.session_state['example_questions']))
442
- for i, q in enumerate(st.session_state['example_questions']):
443
- if cols[i].button(q, key=f"followup_{i}"):
444
- st.session_state['selected_question'] = q
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  st.session_state['step'] = 4
446
  st.rerun()
447
- user_q = st.text_input("직접 궁금한 점을 입력해도 좋아요! (후속 질문)", value="", key="followup_input")
448
- if st.button("Khan 멘토에게 추가 질문하기", key="followup_btn"):
449
- st.session_state['selected_question'] = user_q
450
- st.session_state['step'] = 4
451
- st.rerun()
452
- st.markdown("---")
453
- st.caption("Powered by Pinecone, Sentence Transformers, and OpenAI")
454
- # 다시 처음으로 돌아가기 버튼
455
- if st.button("처음으로 돌아가기"):
456
- for k in ['user_state','empathy_message','example_questions','selected_question','khan_answer','step']:
457
- st.session_state[k] = '' if k != 'step' else 1
458
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  logger.error(f"Error during OpenAI API call: {e}", exc_info=True)
279
  return "답변을 생성하는 중에 문제가 발생했습니다. OpenAI API 키 또는 서비스 상태를 확인해주세요."
280
 
281
+ # --- Streamlit 앱 UI (사이드바 메뉴 추가) ---
282
  st.set_page_config(page_title="Khan 멘토 (PM 영상 기반)", layout="wide")
283
+
284
+ # --- 사이드바 메뉴 ---
285
+ menu = st.sidebar.radio(
286
+ "기능 선택",
287
+ ("Khan 멘토에게 상담하기", "상사에게 잘보이기")
 
 
 
 
 
 
 
288
  )
 
289
 
 
290
  openai_client = init_openai_client()
291
+
292
+ if menu == "Khan 멘토에게 상담하기":
293
+ st.title("✨ Khan 멘토에게 질문하기")
294
+ st.markdown(
295
+ '<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>',
296
+ unsafe_allow_html=True
297
+ )
298
+ st.markdown("PM 관련 영상 내용을 기반으로 Khan 멘토가 답변해 드립니다.")
299
+
300
+ # --- API 확인 및 리소스 초기화 ---
301
+ pc = init_pinecone()
302
+ model = load_embedding_model()
303
+ index = get_pinecone_index(pc, INDEX_NAME)
304
+
305
+ # --- 상태 관리 ---
306
+ if 'user_state' not in st.session_state:
307
+ st.session_state['user_state'] = ''
308
+ if 'empathy_message' not in st.session_state:
309
+ st.session_state['empathy_message'] = ''
310
+ if 'example_questions' not in st.session_state:
311
+ st.session_state['example_questions'] = []
312
+ if 'selected_question' not in st.session_state:
313
+ st.session_state['selected_question'] = ''
314
+ if 'khan_answer' not in st.session_state:
315
+ st.session_state['khan_answer'] = ''
316
+ if 'step' not in st.session_state:
317
+ st.session_state['step'] = 1
318
+
319
+ # --- 1단계: 사용자 상태 입력 ---
320
+ if st.session_state['step'] == 1:
321
+ user_state = st.text_area("지금 어떤 상황이신가요? 고민을 자유롭게 적어주세요.", value=st.session_state['user_state'])
322
+ if st.button("나의 이야기 들려주기 ✍️"):
323
+ st.session_state['user_state'] = user_state
324
+ # 2단계로 이동
325
+ st.session_state['step'] = 2
326
+ st.rerun()
327
+
328
+ # --- 2단계: 공감 메시지 + 예시 질문 생성 ---
329
+ if st.session_state['step'] == 2:
330
+ with st.spinner("어떤 상황인지 고민해볼께요..."):
331
+ # 1. 공감 메시지 생성
332
+ empathy_prompt = f"""
333
+ 너는 따뜻하고 공감 능력이 뛰어난 상담가야.
334
+ 아래 사용자의 상황을 듣고, 충분히 감정적으로 공감해주고, 용기를 북돋아주는 말을 해줘.
335
+ 부를 때는 '당신은', '당신이'처럼 친근한 말투로 부르고, 물음표는 사용하지 않아.
336
+ 상황: "{st.session_state['user_state']}"
337
+ """
338
+ try:
339
+ empathy_response = openai_client.chat.completions.create(
340
+ model="gpt-4o",
341
+ messages=[{"role": "system", "content": empathy_prompt}],
342
+ temperature=0.7,
343
+ )
344
+ st.session_state['empathy_message'] = empathy_response.choices[0].message.content.strip()
345
+ except Exception as e:
346
+ st.session_state['empathy_message'] = f"공감 메시지 생성 중 오류: {e}"
347
+ # 2. 예시 질문 생성
348
+ example_prompt = f"""
349
+ 아래 상황에서 Khan 멘토에게 할 수 있는 구체적이고 실용적인 질문 3~4가지를 한국어로 만들어줘.\n각 질문은 한 문장으로, 실제로 도움이 될 만한 내용이어야 해.\n상황: "{st.session_state['user_state']}"
350
+ """
351
+ try:
352
+ example_response = openai_client.chat.completions.create(
353
+ model="gpt-4o",
354
+ messages=[{"role": "system", "content": example_prompt}],
355
+ temperature=0.5,
356
+ )
357
+ # 응답에서 질문만 추출 (숫자/기호/줄바꿈 등 정제)
358
+ import re
359
+ raw = example_response.choices[0].message.content.strip()
360
+ questions = re.findall(r'\d+\.\s*(.+)', raw)
361
+ if not questions:
362
+ # 숫자 없이 줄바꿈만 있을 경우
363
+ questions = [q.strip('-• ').strip() for q in raw.split('\n') if q.strip()]
364
+ st.session_state['example_questions'] = questions[:4]
365
+ except Exception as e:
366
+ st.session_state['example_questions'] = [f"예시 질문 생성 중 오류: {e}"]
367
+ # 3단계로 이동
368
+ st.session_state['step'] = 3
369
+ st.rerun()
370
+
371
+ # --- 3단계: 공감 메시지 + 예시 질문 버튼/직접입력 + Khan 답변 ---
372
+ if st.session_state['step'] == 3:
373
+ st.success(st.session_state['empathy_message'])
374
+ st.markdown("#### 이런 질문을 해볼 수 있어요!")
375
+ cols = st.columns(len(st.session_state['example_questions']))
376
+ for i, q in enumerate(st.session_state['example_questions']):
377
+ if cols[i].button(q):
378
+ st.session_state['selected_question'] = q
379
+ st.session_state['step'] = 4
380
+ st.rerun()
381
+ st.markdown("---")
382
+ user_q = st.text_input("직접 궁금한 점을 입력해도 좋아요!", value=st.session_state['selected_question'])
383
+ if st.button("Khan 멘토에게 질문하기"):
384
+ st.session_state['selected_question'] = user_q
385
  st.session_state['step'] = 4
386
  st.rerun()
387
+
388
+ # --- 4단계: Khan 멘토 답변 ---
389
+ if st.session_state['step'] == 4:
390
+ with st.spinner("Khan 멘토가 답변을 준비하는 중..."):
391
+ pinecone_results = search(st.session_state['selected_question'], top_k=5, _index=index, _model=model)
392
+ khan_answer = generate_khan_answer(st.session_state['selected_question'], pinecone_results, openai_client)
393
+ st.session_state['khan_answer'] = khan_answer
394
+ st.subheader("💡 Khan 멘토의 답변")
395
+ st.markdown(st.session_state['khan_answer'])
396
+ # 참고 영상 정보 표시
397
+ if pinecone_results:
398
+ with st.expander("답변에 참고한 영상 정보 보기"):
399
+ displayed_urls = set()
400
+ for i, r in enumerate(pinecone_results):
401
+ url = r.get('URL', 'N/A')
402
+ if url in displayed_urls or url == 'N/A':
403
+ continue
404
+ displayed_urls.add(url)
405
+ st.markdown(f"--- **참고 자료 {len(displayed_urls)} (유사도: {r['점수']:.4f})** ---")
406
+ st.markdown(f"**제목:** {r.get('제목', 'N/A')}")
407
+ st.markdown(f"**요약:** {r.get('요약', 'N/A')}")
408
+ timestamp = r.get('타임스탬프', 'N/A')
409
+ is_youtube = url and isinstance(url, str) and ('youtube.com' in url or 'youtu.be' in url)
410
+ start_seconds = None
411
+ if is_youtube and timestamp and timestamp != 'N/A':
412
+ start_seconds = parse_timestamp_to_seconds(timestamp)
413
+ if is_youtube and start_seconds is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  try:
415
+ timestamped_link_url = add_timestamp_to_youtube_url(url, timestamp)
416
+ st.markdown(f"**영상 링크 (타임스탬프 포함):** [{timestamped_link_url}]({timestamped_link_url})")
417
  except Exception as e:
418
+ logger.error(f"Error creating timestamped URL for link: {e}")
419
+ st.markdown(f"**영상 링크 (원본):** [{url}]({url})")
420
+ elif url != "N/A" and isinstance(url, str) and url.startswith("http"):
421
+ st.markdown(f"**URL:** [{url}]({url})")
422
+ else:
423
+ st.markdown(f"**URL:** {url}")
424
+ if is_youtube and url != "N/A":
425
+ col1, col2 = st.columns(2)
426
+ with col1:
427
+ try:
428
+ st.video(url, start_time=start_seconds or 0)
429
+ except Exception as e:
430
+ st.error(f"비디오({url}) 재생 중 오류 발생: {e}")
431
+ st.markdown(f"[YouTube에서 보기]({url})")
432
+ elif url != "N/A":
433
+ col1, col2 = st.columns(2)
434
+ with col1:
435
+ try:
436
+ st.video(url)
437
+ except Exception as e:
438
+ logger.warning(f"st.video failed for non-YouTube URL {url}: {e}")
439
+ st.markdown("---")
440
+ # --- 예시 질문 버튼/직접입력 다시 노출 (후속 질문) ---
441
+ st.markdown("#### 추가로 궁금한 점이 있으신가요? 아래 예시 질문을 클릭하거나 직접 입력해보세요!")
442
+ cols = st.columns(len(st.session_state['example_questions']))
443
+ for i, q in enumerate(st.session_state['example_questions']):
444
+ if cols[i].button(q, key=f"followup_{i}"):
445
+ st.session_state['selected_question'] = q
446
+ st.session_state['step'] = 4
447
+ st.rerun()
448
+ user_q = st.text_input("직접 궁금한 점을 입력해도 좋아요! (후속 질문)", value="", key="followup_input")
449
+ if st.button("Khan 멘토에게 추가 질문하기", key="followup_btn"):
450
+ st.session_state['selected_question'] = user_q
451
  st.session_state['step'] = 4
452
  st.rerun()
453
+ st.markdown("---")
454
+ st.caption("Powered by Pinecone, Sentence Transformers, and OpenAI")
455
+ # 다시 처음으로 돌아가기 버튼
456
+ if st.button("처음으로 돌아가기"):
457
+ for k in ['user_state','empathy_message','example_questions','selected_question','khan_answer','step']:
458
+ st.session_state[k] = '' if k != 'step' else 1
459
+ st.rerun()
460
+ else:
461
+ st.title("👔 상사에게 잘보이기: 맞춤 보고문 만들기")
462
+ st.markdown("상사의 MBTI 성향에 맞게 보고문을 자동으로 다듬어드립니다.")
463
+ mbti_types = [
464
+ "ISTJ", "ISFJ", "INFJ", "INTJ",
465
+ "ISTP", "ISFP", "INFP", "INTP",
466
+ "ESTP", "ESFP", "ENFP", "ENTP",
467
+ "ESTJ", "ESFJ", "ENFJ", "ENTJ"
468
+ ]
469
+ mbti = st.selectbox("상사의 MBTI를 선택하세요", mbti_types)
470
+ user_report = st.text_area("상사에게 보고할 내용을 입력하세요 (300자 이내)", max_chars=300)
471
+ if st.button("MBTI 맞춤 보고문 생성"):
472
+ if not user_report.strip():
473
+ st.warning("보고문을 입력해 주세요.")
474
+ else:
475
+ with st.spinner("상사의 성향에 맞게 보고문을 다듬는 중..."):
476
+ prompt = f"""
477
+ 상사의 MBTI가 {mbti}일 때, 아래 보고문을 그 성향에 맞게 수정해줘.\n 이 유형의 상사가 중요하게 생각하는 것이 보고서에 빠져있다면 어떤 부분을 보완해야 하는지 상세히 설명해 줘\n그리고 왜 그렇게 수정했는지 이유도 설명해줘.\n보고문: "{user_report}"
478
+
479
+ 아래 형식으로 답변 해.
480
+ 수정된 보고문: ...
481
+ 이유: ...
482
+ """
483
+ try:
484
+ response = openai_client.chat.completions.create(
485
+ model="gpt-4o-mini",
486
+ messages=[{"role": "system", "content": prompt}],
487
+ temperature=0.5,
488
+ )
489
+ answer = response.choices[0].message.content.strip()
490
+ # 간단한 파싱: "수정된 보고문:" ~ "이유:" 분리
491
+ import re
492
+ mod_match = re.search(r"수정된 보고문[:\n]*([\s\S]+?)이유[:\n]", answer)
493
+ reason_match = re.search(r"이유[:\n]*([\s\S]+)", answer)
494
+ if mod_match:
495
+ st.markdown(f"**수정된 보고문**\n\n{mod_match.group(1).strip()}")
496
+ else:
497
+ st.markdown(f"**수정된 보고문**\n\n{answer}")
498
+ if reason_match:
499
+ st.markdown(f"**이유 설명**\n\n{reason_match.group(1).strip()}")
500
+ except Exception as e:
501
+ st.error(f"GPT 호출 중 오류: {e}")