import time import streamlit as st import google.generativeai as genai from streamlit_extras.colored_header import colored_header import markdown # markdown 라이브러리 임포트 확인 # --- Must be the first Streamlit command --- st.set_page_config(page_title="MBTI 관계 시뮬레이터", page_icon="🤝", layout="wide") # --- Configuration --- # Google Gemini API Key 설정 (Streamlit secrets 사용) try: # secrets에서 키 로드 시도 api_key = st.secrets.get("GEMINI_API_KEY") if not api_key: st.error("Streamlit secrets에 'GEMINI_API_KEY'를 설정해주세요.") st.info("Secrets 설정 방법: [Streamlit Docs](https://docs.streamlit.io/library/advanced-features/secrets-management)") st.stop() genai.configure(api_key=api_key) except Exception as e: st.error(f"API 키 설정 중 예상치 못한 오류 발생: {e}") st.stop() # 모델 설정 generation_config = { "temperature": 0.75, "top_p": 0.8, "top_k": 40, "max_output_tokens": 15000, } # 특정 모델 이름으로 설정 (사용자 요청 반영) # 참고: 'gemini-2.0-flash-thinking-exp-01-21'는 실험적 모델일 수 있습니다. # 안정적인 최신 모델을 원하시면 'gemini-1.5-flash-latest' 또는 'gemini-1.5-pro-latest' 사용을 고려하세요. target_model_name = "gemini-2.0-flash-thinking-exp-01-21" # 안정적인 모델로 변경 권장 (필요시 원래 모델명 사용) try: model = genai.GenerativeModel( model_name=target_model_name, generation_config=generation_config, # safety_settings 설정 추가 (콘텐츠 필터링 관련) safety_settings=[ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, ] ) # 모델 로딩 성공 시 간단한 정보 표시 (선택 사항) # st.sidebar.caption(f"Using model: {target_model_name}") except Exception as e: st.error(f"Gemini 모델 '{target_model_name}' 로딩 중 오류 발생: {e}") st.error("모델 이름을 다시 확인하거나, 사용 가능한 다른 모델(예: 'gemini-1.5-flash-latest')을 시도해보세요.") # 사용 가능한 모델 목록 확인 링크 (선택 사항) st.info("사용 가능한 모델 목록은 Google AI Studio 또는 Gemini API 문서를 참조하세요.") st.stop() # MBTI 유형 정보 (상세 설명 고도화) mbti_types = { "INTJ": "전략가 (Architect)", "INTP": "논리술사 (Logician)", "ENTJ": "통솔자 (Commander)", "ENTP": "변론가 (Debater)", "INFJ": "옹호자 (Advocate)", "INFP": "중재자 (Mediator)", "ENFJ": "선도자 (Protagonist)", "ENFP": "활동가 (Campaigner)", "ISTJ": "현실주의자 (Logistician)", "ISFJ": "수호자 (Defender)", "ESTJ": "경영자 (Executive)", "ESFJ": "관리자 (Consul)", "ISTP": "장인 (Virtuoso)", "ISFP": "모험가 (Adventurer)", "ESTP": "사업가 (Entrepreneur)", "ESFP": "연예인 (Entertainer)" } mbti_descriptions = { # 상세 설명 추가 (프롬프트에 활용) "INTJ": "상상력이 풍부하며 결단력이 있는 전략가입니다. 모든 일에 계획을 세우며, 지식을 갈망하고 논리적 사고를 중시합니다. 독립적이며 때로는 비판적으로 보일 수 있습니다.", "INTP": "끊임없이 새로운 지식에 목말라 하는 혁신가입니다. 분석적이고 객관적이며, 복잡한 문제를 해결하는 데 뛰어난 능력을 보입니다. 때로는 추상적인 개념에 몰두하는 경향이 있습니다.", "ENTJ": "대담하며 상상력이 풍부한 강력한 의지의 소유자로, 항상 길을 찾거나 만들어냅니다. 타고난 리더이며, 목표 지향적이고 효율성을 추구합니다. 때로는 다른 사람의 감정을 간과할 수 있습니다.", "ENTP": "지적 도전을 즐기는 똑똑하고 호기심 많은 사색가입니다. 새로운 아이디어를 탐구하고 논쟁하는 것을 좋아하며, 틀에 박힌 것을 싫어합니다. 때로는 일관성이 부족할 수 있습니다.", "INFJ": "조용하고 신비로우면서도 샘솟는 영감으로 지칠 줄 모르는 이상주의자입니다. 깊은 통찰력과 강한 직관력으로 사람들을 돕고자 하며, 의미 있는 관계를 추구합니다. 때로는 지나치게 완벽주의적일 수 있습니다.", "INFP": "상냥한 성격의 이타주의자로, 건강하고 밝은 사회 건설에 앞장서는 낭만형입니다. 깊은 감수성과 공감 능력을 지녔으며, 자신의 가치관에 따라 행동합니다. 때로는 현실 감각이 부족할 수 있습니다.", "ENFJ": "넘치는 카리스마와 영향력으로 청중을 압도하는 리더형입니다. 사람들에게 영감을 주고 긍정적인 변화를 이끌어내는 것을 목표로 하며, 타인의 성장을 돕는 데 열정적입니다. 때로는 타인의 인정을 지나치게 갈망할 수 있습니다.", "ENFP": "창의적이며 항상 웃을 거리를 찾아다니는 활발한 성격으로, 사람들과 자유롭게 어울리기를 좋아합니다. 열정적이고 사교적이며, 새로운 가능성을 탐색하는 것을 즐깁니다. 때로는 쉽게 싫증을 느낄 수 있습니다.", "ISTJ": "사실에 근거하여 사고하며 이성적이고 믿을 수 있는 현실주의자입니다. 책임감이 강하고 철저하며, 전통과 질서를 중시합니다. 때로는 변화에 저항적일 수 있습니다.", "ISFJ": "소중한 이들을 보호하는 데 심혈을 기울이는 헌신적이고 따뜻한 수호자입니다. 세심하고 충실하며, 타인의 감정에 민감하고 실질적인 도움을 주고자 합니다. 때로는 자신의 필요를 간과할 수 있습니다.", "ESTJ": "사물이나 사람을 관리하는 데 타의 추종을 불허하는 뛰어난 실력의 소유자입니다. 조직적이고 단호하며, 규칙과 절차를 중요하게 생각합니다. 때로는 지나치게 통제하려 할 수 있습니다.", "ESFJ": "타인을 향한 세심한 관심과 사교적인 성향으로 사람들 내에서 인기가 많으며, 타인을 돕는 데 열성적입니다. 협조적이고 동정심이 많으며, 조화로운 관계를 중요시합니다. 때로는 비판에 민감할 수 있습니다.", "ISTP": "대담하고 현실적인 성향으로 다양한 도구를 능숙하게 다루는 탐험형입니다. 논리적이고 실용적이며, 문제 해결 능력이 뛰어납니다. 위기 상황에서 침착함을 유지합니다. 때로는 감정 표현에 서툴 수 있습니다.", "ISFP": "항상 새로운 것을 찾아 시도하거나 도전할 준비가 되어 있는 융통성 있는 성격의 매력 넘치는 예술가입니다. 온화하고 겸손하며, 현재의 순간을 즐기고 미적 감각이 뛰어납니다. 때로는 장기적인 계획 수립에 어려움을 겪을 수 있습니다.", "ESTP": "명석한 두뇌와 에너지, 그리고 뛰어난 직관력으로 위험을 기회로 만드는 재치 있는 사업가입니다. 행동 지향적이고 사교적이며, 현실적인 문제 해결에 능숙합니다. 때로는 충동적일 수 있습니다.", "ESFP": "주위에 있으면 인생이 지루할 새가 없을 정도로 즉흥적이며 열정과 에너지가 넘치는 연예인형입니다. 사교적이고 낙천적이며, 사람들과 어울리는 것을 즐깁니다. 때로는 깊이 있는 관계 형성에 어려움을 느낄 수 있습니다." } # 관계 유형 (2인 관계와 다인 관계 분리) relationship_types_two = { "연인": "Romantic Couple", "부부": "Married Couple", "친구": "Friends", "가족 (형제자매, 부모자식 등)": "Family", "직장 동료": "Coworkers", "상사-부하": "Supervisor-Subordinate", "기타": "Others" } relationship_types_multiple = { "친구들": "Friends Group", "가족": "Family", "직장 팀": "Work Team", "프로젝트 팀": "Project Team", "스터디 그룹": "Study Group", "동호회/모임": "Club/Social Group", "기타": "Others" } # --- Functions --- def generate_relationship_scenario(people, relationship, situation): """Generates the relationship scenario using the Gemini API.""" # 참여자 정보 문자열 생성 함수 def create_type_info(person): # 입력값이 없을 경우 기본값 처리 name = person.get('name', '이름없음') gender = person.get('gender', '미지정') mbti_type = person.get('type', '미지정') type_description = mbti_descriptions.get(mbti_type, '알 수 없는 유형') # 상세 설명 사용 return f"{name} ({gender}, {mbti_type}): {type_description}" # 참여자 정보 목록 생성 (people 리스트가 비어있지 않을 때만 생성) if people: people_info = "\n".join([f"- {create_type_info(person)}" for person in people]) else: # people 리스트가 비어있는 예외적인 경우 처리 st.error("참여자 정보가 올바르게 전달되지 않았습니다. 사이드바 입력을 확인해주세요.") return "" # 오류 발생 시 빈 문자열 반환 # 관계 유형 영문명 가져오기 relationship_en = relationship_types_two.get(relationship, relationship_types_multiple.get(relationship, '')) # 시스템 프롬프트 (개선됨: 속마음/의도, 상호작용 역학, 오해 지점, 명확한 구조 강조, 테이블 형식 지양) system_prompt = f""" 당신은 MBTI 심층 분석가이자 관계 코칭 전문가입니다. 제공된 정보를 바탕으로 매우 상세하고 통찰력 있는 관계 시나리오를 생성해주세요. 각 MBTI 유형의 대표적인 특징뿐만 아니라, 개인 간의 미묘한 상호작용과 심리적 역학에 초점을 맞춰 분석해야 합니다. **[기본 정보]** * **참여자:** {people_info} * **관계 유형:** {relationship} ({relationship_en}) * **주어진 상황:** {situation} **[요청 사항]** **1. 참여자 MBTI 심층 분석:** * 각 참여자의 MBTI 유형에 대해 핵심 가치, 주요 동기, 의사소통 스타일, 스트레스 반응, 잠재적 강점 및 약점을 깊이 있게 설명해주세요. * 단순한 유형 설명을 넘어, 해당 유형이 주어진 **상황**과 **관계** 속에서 어떻게 발현될 가능성이 높은지 예측해주세요. **2. 관계 시나리오 상세 묘사:** * 주어진 상황을 바탕으로, 참여자들 간의 상호작용을 단계별 시나리오로 구체화해주세요. * **각 단계별로 다음 요소를 반드시 포함하여 상세하게 묘사해주세요:** * **구체적인 대화:** 실제 대화처럼 자연스럽게 작성해주세요. * **관찰 가능한 행동:** 표정, 몸짓, 말투 등 비언어적 표현을 포함해주세요. * **⭐ 중요: 각 인물의 '속마음' 또는 '숨겨진 의도':** 대화나 행동 이면에 있는 각 인물의 생각, 감정, 진짜 원하는 것, 혹은 우려하는 바를 괄호 안에 명확하게 서술해주세요. (예: OO (속마음: 사실은 불안하지만, 약하게 보이고 싶지 않아.)) * **MBTI 기반 해석:** 각 인물의 대화, 행동, 속마음이 그들의 MBTI 유형적 특성(예: 외향/내향, 감각/직관, 사고/감정, 판단/인식)과 어떻게 연결되는지 구체적으로 설명해주세요. 특히, **유형 간의 차이**가 상호작용에 어떤 영향을 미치는지 분석해주세요. **3. 상호작용 분석 및 잠재적 오해 지점:** * 시나리오 전반에 걸쳐 나타나는 참여자들 간의 **긍정적 상호작용(시너지)**과 **부정적 상호작용(갈등/오해 유발 지점)**을 명확히 식별하고 분석해주세요. * MBTI 유형 차이(예: T/F의 의사결정 방식 차이, J/P의 계획성 차이 등)로 인해 발생할 수 있는 **구체적인 오해의 순간들**을 지적하고, 왜 그런 오해가 발생하는지 설명해주세요. **4. 관계 개선을 위한 실질적 조언:** * **각 참여자에게 맞춤화된 조언**을 제공해주세요. 이 조언은 시나리오에서 드러난 **구체적인 상호작용, 속마음, 오해 지점**을 직접적으로 다루어야 합니다. * 서로를 더 잘 이해하고 **건강한 관계**를 구축하기 위해 각자가 **시도해볼 수 있는 구체적인 말과 행동**을 제안해주세요. (예: "{people[0]['name']}({people[0]['type']})님, {people[1]['name']}({people[1]['type']})님이 아이디어를 낼 때 즉시 분석하기보다, 먼저 '흥미로운 생각인데!'라고 반응하며 {people[1]['name']}님의 열정을 인정해주세요. 그 후에 함께 현실적인 부분을 논의하는 것이 좋습니다.") * 조언에는 반드시 **모든 참여자의 이름, 성별, MBTI 유형**이 명시되어야 합니다. **[출력 형식]** * 결과는 Markdown 형식을 사용하여 가독성을 높여주세요. * 위 요청사항의 번호(1, 2, 3, 4)에 맞춰 명확한 제목 (예: `## 1. 참여자 MBTI 심층 분석`)을 사용하여 내용을 구분해주세요. * 특히 시나리오(2번 항목)에서는 대화, 행동, 속마음, MBTI 해석을 명확히 구분하여 작성해주세요. * **표 형식의 출력은 지양하고, headings, lists, paragraphs 위주로 마크다운을 사용해주세요.** """ full_text = "" try: # 스트리밍 방식으로 응답 생성 및 표시 response = model.generate_content([system_prompt], stream=True) response_container = st.empty() # 응답 표시 영역 미리 확보 for chunk in response: # chunk.text가 None이 아닌지 확인 if chunk.text: full_text += chunk.text # Append chunk text to full_text # Markdown으로 실시간 업데이트 (unsafe_allow_html=False가 더 안전) response_container.markdown(full_text, unsafe_allow_html=False) # HTML 대신 마크다운 직접 렌더링 time.sleep(0.01) # 딜레이 약간 줄임 return full_text # 최종 전체 텍스트 반환 except Exception as e: st.error(f"시나리오 생성 중 오류 발생: {e}") # 오류 유형과 메시지를 함께 출력하여 디버깅에 도움 st.error(f"오류 상세 정보: {type(e).__name__} - {e}") # API 호출 관련 오류일 수 있으므로 추가 정보 제공 if "API key" in str(e): st.error("API 키가 유효하지 않거나 할당량이 초과되었을 수 있습니다. Streamlit secrets 설정을 확인하세요.") elif "model" in str(e).lower(): st.error(f"모델 '{target_model_name}'을 찾을 수 없거나 접근 권한이 없을 수 있습니다. 모델 이름을 확인하거나 다른 모델을 시도해보세요.") elif "safety" in str(e).lower() or "filtered" in str(e).lower(): st.error("콘텐츠 안전 설정에 의해 응답이 필터링되었습니다. 입력 내용을 수정하거나 안전 설정을 조정해보세요.") return "" # --- Streamlit UI --- # 헤더 색상 적용 colored_header( label="🤝 MBTI 관계 시뮬레이터 v2.1", # 버전 업데이트 description="참여자들의 MBTI, 관계, 상황을 입력하여 심층적인 관계 시나리오와 조언을 받아보세요.", color_name="blue-70" ) # UI/UX 개선 with st.sidebar: st.header("⚙️ 설정") # Instructions at the top of sidebar st.markdown("##### 1. 참여자 정보 입력") st.markdown("참여자의 이름, 성별, MBTI 유형을 선택하세요.") # 참여자 정보 입력 섹션 num_people = st.number_input("참여자 수", min_value=2, max_value=5, value=2, key="num_people", help="최소 2명, 최대 5명까지 설정할 수 있습니다.") people = [] # 리스트 초기화를 루프 바깥에서 수행 # 참여자 수에 맞춰 동적으로 입력 필드 생성 for i in range(num_people): # 첫 번째 참여자만 기본으로 확장되도록 수정 with st.expander(f"👤 참여자 {i+1}", expanded=(i == 0)): person = {} # 이름 예시 플레이스홀더 추가 default_names = ['철수', '영희', '민준', '서연', '지우'] person['name'] = st.text_input(f"이름/닉네임", key=f"name_{i}", placeholder=f"예: {default_names[i % len(default_names)]}") person['gender'] = st.radio("성별", ["남성", "여성", "기타"], key=f"gender_{i}", horizontal=True) # 기본 MBTI 선택 인덱스 계산 (오류 방지) default_mbti_index = i % len(mbti_types) person['type'] = st.selectbox(f"MBTI 유형", list(mbti_types.keys()), format_func=lambda x: f"{x} ({mbti_types[x]})", key=f"type_{i}", index=default_mbti_index) # --- ★★★ 수정된 부분 ★★★ --- people.append(person) # 입력받은 참여자 정보를 리스트에 추가 # --------------------------- st.divider() # Instructions for relationship and situation st.markdown("##### 2. 관계 및 상황 설정") st.markdown("관계 유형과 구체적인 상황을 설정하세요.") # 관계 및 상황 설정 섹션 if num_people == 2: relationship_options = relationship_types_two else: relationship_options = relationship_types_multiple relationship = st.selectbox("관계 유형", list(relationship_options.keys()), key="relationship") situation = st.text_area("구체적인 상황", key="situation", height=100, # 높이 유지 placeholder="예: 중요한 프로젝트 마감일을 앞두고 의견 충돌 발생") st.divider() # Generate button at the bottom, with clear instruction st.markdown("##### 3. 시나리오 생성") generate_button = st.button("🚀 시나리오 생성하기", use_container_width=True, type="primary") # Main area - Scenario output st.header("💡 생성된 시나리오") # Use st.container() for better layout control if needed, otherwise direct markdown is fine scenario_output_area = st.container() # scenario_output_area = st.empty() # Use empty() if you want to replace content completely on each run # Button click logic if generate_button: # Input validation 강화 valid_input = True # 이름 입력 확인 (공백만 입력된 경우도 방지) if not all(p.get('name', '').strip() for p in people): st.error("모든 참여자의 이름을 입력해주세요.") valid_input = False # 상황 입력 확인 (공백만 입력된 경우도 방지) if not situation.strip(): st.error("구체적인 상황을 입력해주세요.") valid_input = False # 관계 유형 선택 확인 (selectbox는 기본값이 있으므로 일반적으로 문제는 없으나 명시적으로 확인) if not relationship: st.error("관계 유형을 선택해주세요.") valid_input = False # people 리스트 자체 확인 (만약을 대비) if not people: st.error("참여자 정보가 없습니다. 페이지를 새로고침하거나 참여자 수를 다시 설정해보세요.") valid_input = False if valid_input: # 시나리오 생성 영역 초기화 (이전 결과 제거) scenario_output_area.empty() with scenario_output_area: # 컨테이너 내에서 스피너와 결과 표시 with st.spinner("🧠 MBTI 전문가가 시나리오 분석 및 생성 중... 잠시만 기다려주세요."): # people 리스트가 제대로 채워졌는지 확인 (디버깅용) # st.write("전달되는 참여자 정보:", people) full_text_result = generate_relationship_scenario(people, relationship, situation) if full_text_result: # 최종 결과를 Markdown으로 렌더링 (unsafe_allow_html=True 사용 시 주의) # html_output = markdown.markdown(full_text_result) # scenario_output_area.markdown(html_output, unsafe_allow_html=True) scenario_output_area.markdown(full_text_result, unsafe_allow_html=False) # 마크다운 직접 렌더링 권장 st.success("🎉 시나리오 생성 완료!") else: # generate_relationship_scenario 함수 내에서 이미 오류 메시지가 표시됨 st.warning("시나리오 생성에 실패했습니다. 오류 메시지를 확인해주세요.") else: st.warning("입력값을 다시 확인해주세요.") # Initial message when the app loads or before generation # Check if the button has been clicked at least once using session state if needed # For simplicity, just show if the button wasn't clicked in this run if not generate_button: scenario_output_area.info("왼쪽 사이드바에서 참여자 정보, 관계, 상황을 설정 후 '시나리오 생성하기' 버튼을 클릭하세요.")