File size: 15,025 Bytes
1d355ad 2ab8dd5 e90cb1f 680a728 d1b4f6b 680a728 4ab59f5 ff9b005 680a728 ff9b005 2ab8dd5 d1b4f6b 680a728 ff9b005 f29cb4f 13ca125 4ab59f5 13ca125 f29cb4f 13ca125 f29cb4f 13ca125 f29cb4f 13ca125 ff9b005 9a41ff4 680a728 ff9b005 680a728 ff9b005 f29cb4f ff9b005 4ab59f5 f29cb4f 680a728 f29cb4f 680a728 4ab59f5 2ab8dd5 680a728 beb811e f29cb4f 680a728 5ff42d5 ff9b005 680a728 5ff42d5 680a728 2ab8dd5 ff9b005 2ab8dd5 680a728 ff9b005 2ab8dd5 680a728 2ab8dd5 680a728 2ab8dd5 ff9b005 680a728 2ab8dd5 680a728 2ab8dd5 680a728 2ab8dd5 ff9b005 680a728 2ab8dd5 680a728 2ab8dd5 680a728 ff9b005 680a728 2ab8dd5 680a728 2ab8dd5 680a728 ff9b005 2ab8dd5 680a728 2ab8dd5 680a728 2ab8dd5 680a728 ff9b005 680a728 ff9b005 2ab8dd5 ff9b005 680a728 2ab8dd5 ff9b005 2ab8dd5 680a728 2ab8dd5 5ff42d5 2ab8dd5 ff9b005 2ab8dd5 ff9b005 2ab8dd5 ff9b005 680a728 9a41ff4 f29cb4f 9a41ff4 f29cb4f ff9b005 f29cb4f ff9b005 f29cb4f 680a728 f29cb4f ff9b005 680a728 13ca125 ff9b005 2ab8dd5 ff9b005 f29cb4f ff9b005 2ab8dd5 ff9b005 680a728 ff9b005 680a728 ff9b005 680a728 ff9b005 f29cb4f 13ca125 f29cb4f 680a728 f29cb4f ff9b005 680a728 f29cb4f 680a728 f29cb4f 06c54b2 ff9b005 680a728 ff9b005 2ab8dd5 06c54b2 680a728 f29cb4f ff9b005 680a728 ff9b005 680a728 ff9b005 680a728 f29cb4f 680a728 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 | 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(
"""
<style>
/* 전체 배경색 설정 */
.stApp {
background-color: #f0f8ff; /* 부드러운 하늘색 배경 */
}
/* 타이틀 스타일 */
.main-title {
font-size: 2.5rem;
color: #4682b4; /* 차분한 파란색 */
font-weight: 700;
text-align: center;
margin-bottom: 20px;
}
/* 채팅 메시지 컨테이너 */
.chat-message {
border-radius: 15px;
padding: 12px 15px; /* 패딩 약간 조정 */
margin: 10px 0;
display: flex;
align-items: flex-start; /* 아이콘과 텍스트 상단 정렬 */
flex-wrap: nowrap;
word-break: break-word;
max-width: 85%; /* 메시지 최대 너비 */
box-shadow: 0 2px 4px rgba(0,0,0,0.05); /* 약간의 그림자 효과 */
}
/* 사용자 메시지 스타일 */
.chat-message-user {
background-color: #e0f7fa; /* 밝은 청록색 */
color: #00796b; /* 어두운 청록색 */
margin-left: auto; /* 오른쪽 정렬 */
flex-direction: row-reverse; /* 내용과 아이콘 순서 변경 */
}
/* AI 메시지 스타일 */
.chat-message-assistant {
background-color: #fff0f5; /* 라벤더 블러시 */
color: #c71585; /* 미디엄 바이올렛 레드 */
margin-right: auto; /* 왼쪽 정렬 */
flex-direction: row; /* 기본 순서 (아이콘 먼저) */
}
/* 아바타(아이콘) 스타일 */
.chat-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px; /* AI 메시지 아이콘 오른쪽 여백 */
flex-shrink: 0; /* 아이콘 크기 고정 */
}
/* 사용자 아바타 스타일 */
.chat-avatar-user {
margin-left: 10px; /* 사용자 메시지 아이콘 왼쪽 여백 */
margin-right: 0;
}
/* 채팅 내용 스타일 */
.chat-content {
flex-grow: 1; /* 텍스트 영역이 남은 공간 차지 */
/* 텍스트 선택 가능하게 (기본값) */
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
}
/* 사용자 입력 창 스타일 */
.stTextInput input {
border-radius: 15px;
border: 2px solid #add8e6; /* 밝은 파란색 */
padding: 10px 15px;
}
/* 버튼 스타일 */
.stButton button {
background-color: #4682b4; /* 차분한 파란색 */
color: #fff;
border-radius: 15px;
padding: 10px 20px;
border: none;
transition: background-color 0.2s ease; /* 부드러운 색상 변경 효과 */
}
.stButton button:hover {
background-color: #5a9bd3; /* 호버 시 약간 밝게 */
}
.stButton button:active {
background-color: #3e74a0; /* 클릭 시 약간 어둡게 */
}
</style>
""",
unsafe_allow_html=True
)
# 메인 타이틀
st.markdown("<div class='main-title'>🎯 SMART 목표 설정 도우미 ✍️</div>", 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"""
<div class='chat-message chat-message-user' key='user_msg_{index}'>
<div class="chat-content">{content}</div>
<img src='{user_icon_url}' class='chat-avatar chat-avatar-user'>
</div>
""",
unsafe_allow_html=True
)
elif role == "assistant":
# AI 메시지: [아이콘] [내용]
st.markdown(
f"""
<div class='chat-message chat-message-assistant' key='assistant_msg_{index}'>
<img src='{assistant_icon_url}' class='chat-avatar'>
<div class="chat-content">{content}</div>
</div>
""",
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() |