Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import openai
|
3 |
+
import gradio as gr
|
4 |
+
import pandas as pd
|
5 |
+
import numpy as np
|
6 |
+
from sentence_transformers import SentenceTransformer
|
7 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
8 |
+
|
9 |
+
# ===== 0) OpenAI API Key (Secrets) =====
|
10 |
+
# Hugging Face Spaces에선 Settings -> Repository secrets -> OPENAI_API_KEY 등록
|
11 |
+
openai.api_key = os.getenv("OPENAI_API_KEY", "")
|
12 |
+
|
13 |
+
# ===== 1) 모델 & 데이터프레임 로드 =====
|
14 |
+
# Example: jhgan/ko-sroberta-multitask
|
15 |
+
model = SentenceTransformer("jhgan/ko-sroberta-multitask")
|
16 |
+
|
17 |
+
# 임의 예시: 세브란스 정신의학챗봇 데이터 (URL)
|
18 |
+
df = pd.read_csv("https://raw.githubusercontent.com/kairess/mental-health-chatbot/master/wellness_dataset_original.csv")
|
19 |
+
df = df.dropna()
|
20 |
+
df["embedding"] = df["유저"].map(lambda x: model.encode(str(x)))
|
21 |
+
|
22 |
+
# ===== 하이퍼파라미터 =====
|
23 |
+
MAX_TURN = 5 # 소크라테스식 질문 최대 횟수
|
24 |
+
|
25 |
+
# ===== 프롬프트 =====
|
26 |
+
EMPATHY_PROMPT = """\
|
27 |
+
당신은 친절한 정신의학과 전문의이며 심리상담 전문가입니다.
|
28 |
+
사용자의 문장을 거의 그대로 요약하되, 끝에 '는군요.' 같은 공감 어미를 붙여 자연스럽게 응답하세요.
|
29 |
+
|
30 |
+
예시:
|
31 |
+
사용자: "시험을 앞두고 불안해서 며칠째 잠이 안 와요."
|
32 |
+
챗봇: "시험을 앞두고 불안해서 며칠째 잠이 안 오는군요."
|
33 |
+
|
34 |
+
이제 사용자 발화를 아래에 주겠습니다.
|
35 |
+
사용자 발화: "{sentence}"
|
36 |
+
챗봇:
|
37 |
+
"""
|
38 |
+
|
39 |
+
SOCRATIC_PROMPT = """\
|
40 |
+
당신은 정신의학과 전문의이며 Socratic CBT 기법을 사용하는 심리상담 전문가입니다.
|
41 |
+
이전 대화 내용과 힌트를 참고하여, 사용자의 인지를 탐색하는 자연스럽고 구체적인 후속 질문을 한 문장으로 작성하세요.
|
42 |
+
|
43 |
+
- 질문은 반드시 물음표로 끝나야 합니다.
|
44 |
+
- "질문:" 같은 접두어 없이 바로 질문 문장만 작성하세요.
|
45 |
+
- 가능한 한 사용자의 상황을 더 깊이 이해할 수 있는 탐색적 질문을 해주세요.
|
46 |
+
"""
|
47 |
+
|
48 |
+
ADVICE_PROMPT = """\
|
49 |
+
당신은 정신의학과 전문의이며 Socratic CBT 기법을 사용하는 심리상담 전문가입니다.
|
50 |
+
아래 힌트(대화 요약)를 바탕으로, 사용자 맞춤형으로 구체적이고 공감 어린 조언을 한국어로 작성하세요.
|
51 |
+
|
52 |
+
- 불안을 완화하기 위한 여러 CBT 기법(인지 재구조화, 점진적 근육 이완, 호흡조절, 걱정 시간 정하기 등)을 자연스럽게 녹이되
|
53 |
+
사용자의 현재 상황과 연결해 이야기해주세요.
|
54 |
+
- 너무 딱딱하지 않게 부드럽고 친절한 말투를 사용하세요.
|
55 |
+
|
56 |
+
힌트:
|
57 |
+
{hints}
|
58 |
+
|
59 |
+
조언:
|
60 |
+
"""
|
61 |
+
|
62 |
+
def set_openai_model():
|
63 |
+
"""
|
64 |
+
유저 요청대로 'gpt-4o' 모델명 반환
|
65 |
+
(실제로는 존재하지 않을 가능성 큼)
|
66 |
+
"""
|
67 |
+
return "gpt-4o"
|
68 |
+
|
69 |
+
# ===== 함수들 =====
|
70 |
+
|
71 |
+
def kb_search(user_input: str) -> str:
|
72 |
+
"""SentenceTransformer로 임베딩 후, df에서 가장 유사한 챗봇 답변 획득."""
|
73 |
+
emb = model.encode(user_input)
|
74 |
+
df["sim"] = df["embedding"].map(lambda e: cosine_similarity([emb],[e]).squeeze())
|
75 |
+
idx = df["sim"].idxmax()
|
76 |
+
return df.loc[idx, "챗봇"]
|
77 |
+
|
78 |
+
def call_empathy(user_input: str) -> str:
|
79 |
+
"""EMPATHY 단계: 공감 요약."""
|
80 |
+
prompt = EMPATHY_PROMPT.format(sentence=user_input)
|
81 |
+
resp = openai.ChatCompletion.create(
|
82 |
+
model=set_openai_model(),
|
83 |
+
messages=[
|
84 |
+
{"role":"system","content":"당신은 친절한 심리상담 전문가입니다."},
|
85 |
+
{"role":"user","content":prompt}
|
86 |
+
],
|
87 |
+
max_tokens=150,
|
88 |
+
temperature=0.7
|
89 |
+
)
|
90 |
+
return resp.choices[0].message.content.strip()
|
91 |
+
|
92 |
+
def call_socratic_question(context: str) -> str:
|
93 |
+
"""SQ 단계: 후속 질문 한 문장 생성."""
|
94 |
+
prompt = f"{SOCRATIC_PROMPT}\n\n대화 힌트:\n{context}"
|
95 |
+
resp = openai.ChatCompletion.create(
|
96 |
+
model=set_openai_model(),
|
97 |
+
messages=[
|
98 |
+
{"role":"system","content":"당신은 Socratic CBT 전문가입니다."},
|
99 |
+
{"role":"user","content":prompt}
|
100 |
+
],
|
101 |
+
max_tokens=200,
|
102 |
+
temperature=0.7
|
103 |
+
)
|
104 |
+
return resp.choices[0].message.content.strip()
|
105 |
+
|
106 |
+
def call_advice(hints: str) -> str:
|
107 |
+
"""ADVICE 단계: CBT 조언 생성."""
|
108 |
+
final_prompt = ADVICE_PROMPT.format(hints=hints)
|
109 |
+
resp = openai.ChatCompletion.create(
|
110 |
+
model=set_openai_model(),
|
111 |
+
messages=[
|
112 |
+
{"role":"system","content":"당신은 Socratic CBT 기법 전문가입니다."},
|
113 |
+
{"role":"user","content":final_prompt}
|
114 |
+
],
|
115 |
+
max_tokens=700,
|
116 |
+
temperature=0.8
|
117 |
+
)
|
118 |
+
return resp.choices[0].message.content.strip()
|
119 |
+
|
120 |
+
def predict(user_input: str, state: dict):
|
121 |
+
"""Gradio Callback: 소크라테스 CBT 챗봇 흐름 (EMPATHY→SQ→ADVICE)."""
|
122 |
+
history = state.get("history", [])
|
123 |
+
stage = state.get("stage", "EMPATHY")
|
124 |
+
turn = state.get("turn", 0)
|
125 |
+
hints = state.get("hints", [])
|
126 |
+
|
127 |
+
# 1) 사용자 발화 기록
|
128 |
+
history.append(("User", user_input))
|
129 |
+
|
130 |
+
# 2) KB 검색 → hints
|
131 |
+
kb_answer = kb_search(user_input)
|
132 |
+
hints.append(f"[KB] {kb_answer}")
|
133 |
+
|
134 |
+
# 3) 단계 분기
|
135 |
+
if stage == "EMPATHY":
|
136 |
+
empathic = call_empathy(user_input)
|
137 |
+
history.append(("Chatbot", empathic))
|
138 |
+
hints.append(empathic)
|
139 |
+
stage = "SQ"
|
140 |
+
turn = 0
|
141 |
+
return history, {"history": history, "stage": stage, "turn": turn, "hints": hints}
|
142 |
+
|
143 |
+
if stage == "SQ" and turn < MAX_TURN:
|
144 |
+
# 전체 대화 + hints 합쳐 context
|
145 |
+
context_text = "\n".join([f"{r}: {c}" for (r,c) in history]) + "\n" + "\n".join(hints)
|
146 |
+
sq = call_socratic_question(context_text)
|
147 |
+
history.append(("Chatbot", sq))
|
148 |
+
hints.append(sq)
|
149 |
+
turn += 1
|
150 |
+
return history, {"history": history, "stage": stage, "turn": turn, "hints": hints}
|
151 |
+
|
152 |
+
# ADVICE 단계
|
153 |
+
stage = "ADVICE"
|
154 |
+
combined_hints = "\n".join(hints)
|
155 |
+
advice = call_advice(combined_hints)
|
156 |
+
history.append(("Chatbot", advice))
|
157 |
+
stage = "END"
|
158 |
+
return history, {"history":history, "stage":stage, "turn":turn, "hints":hints}
|
159 |
+
|
160 |
+
def gradio_predict(user_input, chat_state):
|
161 |
+
"""Gradio에서 user_input, state를 받아 predict → (chatbot 출력, state 갱신)."""
|
162 |
+
new_history, new_state = predict(user_input, chat_state)
|
163 |
+
|
164 |
+
# display_history: list of (user, assistant)
|
165 |
+
display_history = []
|
166 |
+
for (role, txt) in new_history:
|
167 |
+
if role == "User":
|
168 |
+
display_history.append([txt, ""])
|
169 |
+
else: # Chatbot
|
170 |
+
if not display_history:
|
171 |
+
display_history.append(["", txt])
|
172 |
+
elif display_history[-1][1] == "":
|
173 |
+
display_history[-1][1] = txt
|
174 |
+
else:
|
175 |
+
display_history.append(["", txt])
|
176 |
+
return display_history, new_state
|
177 |
+
|
178 |
+
def create_app():
|
179 |
+
"""Gradio Blocks UI 구성."""
|
180 |
+
with gr.Blocks() as demo:
|
181 |
+
gr.Markdown("## 🏥 소크라테스 CBT 챗봇 (GPT-4o)")
|
182 |
+
|
183 |
+
chatbot = gr.Chatbot(label="Socratic CBT Chatbot")
|
184 |
+
chat_state = gr.State({
|
185 |
+
"history": [],
|
186 |
+
"stage":"EMPATHY",
|
187 |
+
"turn":0,
|
188 |
+
"hints":[]
|
189 |
+
})
|
190 |
+
txt = gr.Textbox(show_label=False, placeholder="고민이나 궁금한 점을 입력하세요.")
|
191 |
+
|
192 |
+
txt.submit(fn=gradio_predict, inputs=[txt, chat_state], outputs=[chatbot, chat_state], scroll_to_output=True)
|
193 |
+
return demo
|
194 |
+
|
195 |
+
app = create_app()
|
196 |
+
|
197 |
+
if __name__ == "__main__":
|
198 |
+
# Launch Gradio app
|
199 |
+
app.launch(debug=True, share=True)
|