Spaces:
Runtime error
Runtime error
Commit
·
e313748
1
Parent(s):
c29181d
상사 MBTI 보고문 프롬프트 개선 및 gpt-4o-mini 모델 적용
Browse files
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 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
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 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
if '
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
if
|
320 |
-
|
321 |
-
if
|
322 |
-
st.session_state['
|
323 |
-
|
324 |
-
st.session_state['
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
st.session_state['
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
#
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
st.session_state['
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
st.session_state['step'] = 4
|
379 |
st.rerun()
|
380 |
-
|
381 |
-
|
382 |
-
if st.
|
383 |
-
st.
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
pinecone_results
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
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 |
-
|
|
|
436 |
except Exception as e:
|
437 |
-
logger.
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
445 |
st.session_state['step'] = 4
|
446 |
st.rerun()
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
st.
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}")
|