import os import openai import streamlit as st # import html # 필요 시 주석 해제 # --- OpenAI API 설정 --- openai_api_key = st.secrets["OPENAI_API_KEY"] # API 키가 있는지 확인 if not openai_api_key: st.error("OpenAI API 키가 설정되지 않았습니다. 환경 변수나 Streamlit secrets에 키를 추가해주세요.") st.stop() # API 키 없으면 앱 중지 openai.api_key = openai_api_key # --- 함수 정의 --- def generate_smart_system_prompt(grade_level): """학년 수준에 맞는 SMART 목표 설정 시스템 프롬프트를 생성합니다.""" # 학년 정보 추출 (예: "초등학교 6학년" -> "6", "초등학교") try: parts = grade_level.split(" ") if len(parts) == 2: school_level = parts[0] grade_num = parts[1].rstrip("학년") if school_level not in ["초등학교", "중학교", "고등학교"] or not grade_num.isdigit(): grade_num, school_level = "알 수 없음", "알 수 없음" else: grade_num, school_level = "알 수 없음", "알 수 없음" except Exception: # 광범위한 예외 처리 (분석 실패 시) grade_num, school_level = "알 수 없음", "알 수 없음" # 6학년 프롬프트 기준으로 통합 prompt = f""" 너는 {grade_level} 학생이 SMART 목표를 세우고 실천 계획을 만들도록 돕는 친절하고 격려하는 코치 선생님이야. 학생의 이름은 부르지 않고, '친구' 또는 '학생'이라고 불러줘. 반말로 친근하게 대화해줘. 학생이 이루고 싶은 목표나 상황을 이야기하면, 그 목표가 SMART 기준에 맞도록 자연스럽게 질문을 던져줘. SMART는 목표를 더 명확하고 달성 가능하게 만드는 방법이야: - S (Specific - 구체적인): 목표가 명확하고 자세한가? 무엇을 이루고 싶은지 정확히 아는 거야. - M (Measurable - 측정 가능한): 목표를 달성했는지 어떻게 알 수 있을까? 숫자로 표현할 수 있으면 좋아. - A (Achievable - 달성 가능한): 이 목표를 실제로 이룰 수 있을까? 너무 어렵거나 쉽지 않게 설정하는 거야. - R (Relevant - 관련성 있는): 이 목표가 왜 중요할까? 나에게 의미가 있는 목표여야 해. - T (Time-bound - 시간 제한이 있는): 언제까지 이 목표를 이루고 싶니? 마감일을 정하는 거야. 절대 네가 목표나 계획을 직접 제시하거나 정답을 알려주지 마. 대신, 학생 스스로 생각하고 답을 찾도록 소크라테스식 질문을 사용해줘. 예를 들면: - "우와, 좋은 생각인데! 그 목표를 조금 더 자세하게 설명해 줄 수 있을까?" (Specific 유도) - "목표를 이루면 어떤 모습일지 상상해볼래? 그걸 어떻게 확인할 수 있을까?" (Measurable 유도) - "그 목표를 이루려면 어떤 노력이 필요할까? 혹시 도움이 필요한 부분이 있을까?" (Achievable 유도) - "이 목표가 친구에게 왜 그렇게 중요해?" (Relevant 유도) - "언제까지 그 목표를 딱! 이루고 싶어?" (Time-bound 유도) - "좋아, 그럼 이제 그 목표를 이루기 위해 어떤 작은 단계들을 하나씩 해볼 수 있을까?" (실천 계획 유도) 학생이 목표를 정하는 과정에서 어려움을 느끼거나 주제에서 벗어나면 부드럽게 다시 목표 설정으로 이끌어줘. 학생의 대답을 칭찬하고 격려하며 자신감을 심어줘. 한 번에 너무 많은 질문을 하지 말고, 학생의 대답을 듣고 다음 질문으로 넘어가줘. 학생이 SMART 기준에 맞춰 목표를 구체화하고, 그 목표를 달성하기 위한 실천 계획 (최소 3가지 구체적인 행동)까지 스스로 만들었다고 판단되면, 마지막에 학생이 직접 세운 내용을 명확하게 요약해서 보여줘. 요약 예시: "정말 멋지다! 친구가 직접 세운 SMART 목표와 실천 계획을 함께 정리해볼까? \\n\\n**🎯 SMART 목표:** [학생이 정의한 구체적이고, 측정 가능하며, 달성 가능하고, 관련성 있고, 시간 제한이 있는 목표 요약]\\n\\n**👣 실천 계획:**\\n1. [학생이 정의한 첫 번째 실천 단계]\\n2. [학생이 정의한 두 번째 실천 단계]\\n3. [학생이 정의한 세 번째 실천 단계]\\n\\n이렇게 계획을 세우니 목표가 훨씬 가까워진 느낌이지? 꾸준히 실천하면 꼭 이룰 수 있을 거야! 선생님이 응원할게! 😊" 요약하기 전에는 반드시 "이제 목표랑 실천 계획이 다 세워진 것 같은데, 선생님이 한번 정리해봐도 괜찮을까?" 와 같이 학생의 동의를 구하는 질문을 먼저 해줘. """ return prompt def openai_chat(grade_level): """OpenAI API를 호출하여 채팅 응답을 생성합니다.""" try: # 현재 선택된 학년 수준에 맞는 시스템 프롬프트 생성 system_prompt = generate_smart_system_prompt(grade_level) # 세션 상태의 첫 번째 메시지가 시스템 메시지인지 확인하고 업데이트/삽입 if not st.session_state.messages or st.session_state.messages[0]["role"] != "system": # 시스템 메시지가 없거나 첫번째가 아니면 맨 앞에 삽입 st.session_state.messages.insert(0, {"role": "system", "content": system_prompt}) else: # 기존 시스템 메시지 내용 업데이트 st.session_state.messages[0]["content"] = system_prompt # API 호출 시 시스템 메시지를 포함한 전체 대화 전달 response = openai.ChatCompletion.create( model="gpt-4o", # 또는 사용 가능한 최신 모델 messages=st.session_state.messages, temperature=0.7, max_tokens=2000, # 필요에 따라 조절 top_p=0.9, frequency_penalty=0.1, presence_penalty=0.1 ) return response.choices[0].message["content"] except openai.error.OpenAIError as e: # 구체적인 OpenAI 에러 처리 st.error(f"OpenAI API 오류 발생: {str(e)}") return None except Exception as e: # 기타 예외 처리 st.error(f"알 수 없는 오류 발생: {str(e)}") return None # --- Streamlit 앱 UI 설정 --- st.set_page_config( page_title="SMART 목표 설정 도우미", page_icon="🎯", initial_sidebar_state="expanded" ) # --- 페이지 스타일 (CSS) --- st.markdown( """ """, unsafe_allow_html=True ) # 메인 타이틀 st.markdown("
🎯 SMART 목표 설정 도우미 ✍️
", unsafe_allow_html=True) # --- 사이드바 설정 --- with st.sidebar: st.header("⚙️ 설정") # 학년 수준 선택 grade_level_options = [ "초등학교 1학년", "초등학교 2학년", "초등학교 3학년", "초등학교 4학년", "초등학교 5학년", "초등학교 6학년", "중학교 1학년", "중학교 2학년", "중학교 3학년", "고등학교 1학년", "고등학교 2학년", "고등학교 3학년" ] # 초등학교 6학년 인덱스 찾기 (더 안전하게) try: default_index = grade_level_options.index("초등학교 6학년") except ValueError: default_index = 5 # 리스트에 없으면 6번째 항목(초6)으로 가정 # 학년 선택 selectbox - 선택 변경 시 시스템 프롬프트 업데이트 위해 콜백 추가 가능성 고려 selected_grade = st.selectbox( "👤 학생의 학년을 선택하세요:", grade_level_options, index=default_index, key="grade_select" # 키 추가 ) # 초기화 버튼 if st.button("🔄 대화 초기화"): # 메시지 기록 삭제 st.session_state.messages = [] st.success("대화 내용이 초기화되었습니다. 새로운 목표를 설정해보세요!") # 페이지 새로고침 없이 즉시 적용되도록 rerun 사용 st.rerun() st.info("💡 AI 코치가 질문을 통해 스스로 SMART 목표와 실천 계획을 세우도록 도와줄 거예요!") st.markdown("---") # 구분선 st.caption("Powered by OpenAI GPT-4o") # 모델 정보 등 추가 정보 # --- 채팅 로직 --- # 채팅 세션 초기화 (메시지 리스트가 없으면 생성) if "messages" not in st.session_state: st.session_state.messages = [] # 사용자와 AI 아이콘 URL 설정 user_icon_url = "https://cdn-icons-png.flaticon.com/512/1995/1995531.png" # 학생 아이콘 assistant_icon_url = "https://cdn-icons-png.flaticon.com/512/4323/4323008.png" # 튜터 아이콘 # 초기 메시지 추가 (세션이 비어있을 때만 실행) if not st.session_state.messages: # 현재 선택된 학년으로 시스템 프롬프트 설정 system_prompt = generate_smart_system_prompt(selected_grade) # 사이드바에서 선택된 값 사용 st.session_state.messages.append({"role": "system", "content": system_prompt}) # 초기 환영 메시지 추가 welcome_message = "안녕! 👋 나는 네 목표 설정을 도와줄 AI 코치 선생님이야. 이루고 싶은 목표나 하고 싶은 일이 있으면 나에게 이야기해 줄래? 같이 멋진 계획을 세워보자! 😊" st.session_state.messages.append({"role": "assistant", "content": welcome_message}) # 초기 메시지는 아래 메시지 표시 루프에서 자동으로 그려짐. 여기서 st.rerun() 불필요. # --- 채팅 메시지 표시 --- # st.session_state.messages에 있는 모든 메시지를 순서대로 화면에 그림 for index, message in enumerate(st.session_state.messages): if message["role"] == "system": continue # 시스템 메시지는 건너뜀 role = message["role"] content = message["content"] # content = html.escape(message["content"]) # HTML 태그가 문제될 경우 주석 해제 # 역할에 따라 다른 스타일과 구조 적용 if role == "user": # 사용자 메시지: [내용] [아이콘] st.markdown( f"""
{content}
""", unsafe_allow_html=True ) elif role == "assistant": # AI 메시지: [아이콘] [내용] st.markdown( f"""
{content}
""", unsafe_allow_html=True ) # --- 사용자 입력 처리 --- if prompt := st.chat_input("🎯 이루고 싶은 목표나 하고 싶은 일을 적어보세요! (예: 수학 시험 잘 보기)"): # 1. 사용자 메시지를 세션 상태에 추가 st.session_state.messages.append({"role": "user", "content": prompt}) # 2. AI 응답 생성 (스피너 표시) with st.spinner("AI 코치가 생각 중이에요... 🤔"): response = openai_chat(selected_grade) # 사이드바에서 선택된 학년 정보 전달 # 3. AI 응답이 성공적이면 세션 상태에 추가 if response: st.session_state.messages.append({"role": "assistant", "content": response}) # API 호출 실패 시 openai_chat 함수 내에서 st.error가 호출됨 # 4. 페이지를 다시 로드하여 새 메시지를 포함한 전체 대화 내용을 그림 st.rerun()