ms / app.py
ll7098ll's picture
Update app.py
86cdc58 verified
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("왼쪽 사이드바에서 참여자 정보, 관계, 상황을 설정 후 '시나리오 생성하기' 버튼을 클릭하세요.")