Shresthh03 commited on
Commit
04c7f03
·
verified ·
1 Parent(s): 81d8a75

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +259 -190
app.py CHANGED
@@ -1,46 +1,52 @@
1
  import os
2
  import json
3
- import re
4
  import random
5
  import datetime
 
6
  from flask import Flask, request, jsonify, send_from_directory
7
 
8
- # Try to import huggingface pipeline; if unavailable we'll fallback to a keyword detector
9
  try:
10
  from transformers import pipeline
11
  HF_AVAILABLE = True
12
  except Exception:
13
  HF_AVAILABLE = False
14
 
15
- app = Flask(__name__)
 
 
 
 
 
 
 
 
16
 
17
- # ---------- Config ----------
18
- MEMORY_FILE = "chat_memory.json"
19
- MEMORY_KEEP_DAYS = 15
20
 
21
- # Safe helplines map (country-code fallback handled via ipapi)
 
 
22
  HELPLINES = {
23
  "IN": "🇮🇳 India: AASRA Helpline 91-9820466726",
24
  "US": "🇺🇸 USA: Call or text 988 (Suicide & Crisis Lifeline)",
25
- "GB": "🇬🇧 UK: Samaritans at 116 123",
26
- "CA": "🇨🇦 Canada: Talk Suicide Canada at 1-833-456-4566",
27
  "AU": "🇦🇺 Australia: Lifeline 13 11 14",
28
- "DEFAULT": "If you are in crisis, please contact your local emergency number or visit https://findahelpline.com"
29
  }
 
30
 
31
- CRISIS_KEYWORDS = ["suicide","kill myself","end my life","want to die","hurt myself","cant go on","can't go on","i want to die"]
32
-
33
- # ---------- Try to load HF emotion model ----------
34
  emotion_model = None
35
  if HF_AVAILABLE:
36
  try:
37
- # this may be large; if it fails we'll catch
38
  emotion_model = pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base", top_k=5)
39
  except Exception:
40
  emotion_model = None
41
 
42
- # ---------- Memory helpers ----------
43
- def read_memory():
44
  if os.path.exists(MEMORY_FILE):
45
  try:
46
  with open(MEMORY_FILE, "r") as f:
@@ -49,44 +55,41 @@ def read_memory():
49
  data = {}
50
  else:
51
  data = {}
52
- # prune old entries
53
- cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=MEMORY_KEEP_DAYS)
54
- keep = {}
55
  for k,v in data.items():
56
  try:
57
  t = datetime.datetime.fromisoformat(v.get("last_seen"))
58
  if t >= cutoff:
59
- keep[k] = v
60
  except Exception:
61
- # keep if malformed time
62
- keep[k] = v
63
- return keep
64
 
65
- def write_memory(mem):
66
  with open(MEMORY_FILE, "w") as f:
67
  json.dump(mem, f, indent=2)
68
 
69
- memory = read_memory()
70
 
71
- # ---------- Utilities ----------
72
- def extract_name(text:str):
73
- # look for patterns: i'm X, i am X, my name is X
74
  text = text.strip()
75
  patterns = [
76
- r"^(?:i am|i'm|im|i’m)\s+([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)", # "I'm John", favor capitalization
77
- r"my name is\s+([A-Z][a-z]+(?:\s[A-Z][a-z]+)?)",
78
- r"^([A-Z][a-z]{1,20})$" # single capitalized word
79
  ]
80
  for p in patterns:
81
  m = re.search(p, text, flags=re.IGNORECASE)
82
  if m:
83
  name = m.group(1).strip()
84
- # normalize capitalization
85
  return " ".join([w.capitalize() for w in name.split()])
86
  return None
87
 
88
- def extract_age(text:str):
89
- # extract first integer that looks like an age between 8 and 120
90
  nums = re.findall(r"\b([1-9][0-9]?)\b", text)
91
  for n in nums:
92
  val = int(n)
@@ -94,225 +97,291 @@ def extract_age(text:str):
94
  return val
95
  return None
96
 
97
- def detect_crisis(text:str):
98
- low = text.lower()
99
- for kw in CRISIS_KEYWORDS:
100
- if kw in low:
101
- return True
102
- return False
103
 
104
- def get_country_from_ip(ip):
105
- # Best-effort; if it fails return None
106
  try:
107
  import requests
108
- r = requests.get(f"https://ipapi.co/{ip}/json/", timeout=3)
 
 
109
  if r.status_code == 200:
110
  data = r.json()
111
- return data.get("country_code")
 
112
  except Exception:
113
  pass
114
- return None
115
 
116
- def emotion_from_text(text):
117
- # Primary: use HF if available
118
  if emotion_model:
119
  try:
120
  out = emotion_model(text)
121
- # out is list of dicts; take top label
122
- if isinstance(out, list) and len(out)>0:
123
- # out may be list of lists: ensure flatten
124
- if isinstance(out[0], list):
125
- best = out[0][0]["label"]
126
  else:
127
- best = out[0]["label"]
128
- return best.lower()
129
  except Exception:
130
  pass
131
- # Fallback keyword mapping
132
  low = text.lower()
133
- if any(w in low for w in ["happy","glad","great","good","joy","joyful","thank"]):
134
  return "joy"
135
  if any(w in low for w in ["sad","down","depressed","unhappy","lonely","cry"]):
136
  return "sadness"
137
- if any(w in low for w in ["angry","mad","furious","annoyed","irritat"]):
138
  return "anger"
139
- if any(w in low for w in ["scared","afraid","anxious","panic","fear"]):
140
  return "fear"
141
  if any(w in low for w in ["love","loving","cherish","fond"]):
142
  return "love"
143
  return "neutral"
144
 
145
- # Varied empathetic templates to avoid repetition
146
- EMP_OPENERS = [
147
- "That sounds really hard — thank you for telling me.",
148
- "I can hear how much that means to you.",
149
- "Wow that must have taken a lot to share. I’m here.",
150
- "You’ve taken a brave step by saying that. I’m listening.",
151
- "I’m with you — that sounds heavy and important.",
152
- "I appreciate your honesty; I’m right here to listen.",
153
- "That must have felt deeply upsetting. Tell me more if you want."
154
  ]
155
- EMP_FOLLOWUPS = [
156
- "Would you like to share more about what’s been going on?",
157
- "How has that been affecting your day-to-day?",
158
- "What helps you feel even a little bit better when that happens?",
159
- "Would you prefer a calming exercise or some practical steps?",
160
- "Do you want help breaking this down into smaller parts?"
161
  ]
162
- MOTIVATIONAL = [
163
- "Even small steps are meaningful — you dont have to do it all at once.",
164
- "Rest is allowed. Healing isn’t a straight line.",
165
- "You’ve come this far; that shows strength.",
166
- "A single breath can sometimes soften what’s heavy."
167
  ]
168
 
169
- # ---------- Main chat route ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  @app.route("/chat", methods=["POST"])
171
  def chat():
172
  global memory
173
- payload = request.get_json() or {}
174
- session = payload.get("session") or request.remote_addr or "default"
175
- message = (payload.get("message") or "").strip()
176
- # flag used by frontend to ask the server for initial greeting only once
177
- init_flag = payload.get("init", False)
178
 
179
- # ensure memory slot for session
180
  slot = memory.get(session, {})
181
- now_iso = datetime.datetime.utcnow().isoformat()
182
 
183
- # On init: if no name known, return friendly opener (but only if init true)
184
  if init_flag:
185
  if not slot.get("name"):
186
- return jsonify({"reply":"Hey there! What’s your name?", "emotion":"calm"})
 
 
 
187
  else:
188
- # If name present, maybe check recent mood
189
  last_mood = slot.get("last_mood")
190
  last_seen = slot.get("last_seen")
191
- if last_mood in ("sadness","anger","fear") and last_seen:
192
- try:
193
- last_t = datetime.datetime.fromisoformat(last_seen)
194
- if (datetime.datetime.utcnow() - last_t).days <= MEMORY_KEEP_DAYS:
195
- return jsonify({"reply":f"Hey {slot.get('name')}, you seemed a bit down last time how are you feeling now?", "emotion":"warm"})
196
- except Exception:
197
- pass
198
- return jsonify({"reply":f"Hey {slot.get('name')} — Im here. Whats on your mind?", "emotion":"calm"})
199
-
200
- # If no text provided, ask user to say something
201
  if not message:
202
- return jsonify({"reply":"I’m listening when you’re ready, share anything that’s on your mind.", "emotion":"neutral"})
203
-
204
- # If session has awaiting flag (expecting name or age), handle it
205
- awaiting = slot.get("awaiting") # could be "name" or "age" or None
206
 
207
- # Try to extract name if message looks like a name
 
208
  if not slot.get("name") and not awaiting:
209
- # If message contains natural name phrases, extract
210
- extracted = extract_name(message)
211
- if extracted:
212
- slot["name"] = extracted
213
- slot["last_seen"] = now_iso
214
- slot["last_mood"] = emotion_from_text(message)
215
  memory[session] = slot
216
- write_memory(memory)
217
- # ask age next
218
- slot["awaiting"]="age"
219
- memory[session]=slot
220
- write_memory(memory)
221
- return jsonify({"reply":f"Lovely to meet you, {extracted}! How old are you?", "emotion":"curious"})
222
  else:
223
- # If not extractable, ask name explicitly and set awaiting flag
224
- slot["awaiting"]="name"
225
- slot["last_seen"] = now_iso
226
- memory[session]=slot
227
- write_memory(memory)
228
- return jsonify({"reply":"Hey there! What’s your name?", "emotion":"calm"})
229
 
230
- # If we are expecting name
231
  if awaiting == "name":
232
- # accept entire message as name (sanitize)
233
  name_guess = extract_name(message) or message.split()[0].capitalize()
234
  slot["name"] = name_guess
235
  slot.pop("awaiting", None)
236
- slot["last_seen"]=now_iso
 
237
  memory[session]=slot
238
- write_memory(memory)
239
- # ask age
240
- slot["awaiting"]="age"
241
- memory[session]=slot
242
- write_memory(memory)
243
  return jsonify({"reply":f"Nice to meet you, {name_guess}! How old are you?", "emotion":"curious"})
244
 
245
- # If we are expecting age
246
  if awaiting == "age":
247
- age_val = extract_age(message)
248
- if age_val:
249
- slot["age"]=age_val
250
  slot.pop("awaiting", None)
251
- slot["last_seen"]=now_iso
252
  memory[session]=slot
253
- write_memory(memory)
254
- return jsonify({"reply":f"Thanks — got that. {slot.get('name')}, how are you feeling today?", "emotion":"curious"})
255
  else:
256
- # If message didn't include a clear age, ask again
257
- return jsonify({"reply":"Could you tell me your age as a number (for example: 20)?", "emotion":"neutral"})
258
-
259
- # At this point, name and age are present (or already set). Proceed to normal chat.
260
 
261
- # If message indicates crisis, handle immediately
262
  if detect_crisis(message):
263
- # try to detect country from IP
264
- ip = request.remote_addr or ""
265
- country_code = get_country_from_ip(ip) or "DEFAULT"
266
- helpline = HELPLINES.get(country_code, HELPLINES["DEFAULT"])
267
- reply = (f"I’m really concerned about how you’re feeling. You are not alone. "
268
- f"Please consider reaching out to emergency services or a helpline nearby: {helpline}")
269
- # don't overwrite memory last_mood to crisis; set a flag
270
- slot["last_seen"]=now_iso
271
- slot["last_mood"]="crisis"
272
- memory[session]=slot
273
- write_memory(memory)
274
  return jsonify({"reply":reply, "emotion":"crisis"})
275
 
276
- # Normal chat: compute emotion
277
- emotion = emotion_from_text(message)
278
-
279
- # generate supportive response (use varied templates and avoid repetition)
280
- base = ""
281
- # Introspective responses for heavy emotions
282
- if emotion in ("sadness","fear","anger"):
283
- base = random.choice(EMP_OPENERS) + " " + random.choice(EMP_FOLLOWUPS)
284
- elif emotion in ("joy","love"):
285
- base = f"That’s lovely — I’m really glad to hear that, {slot.get('name','friend')}!"
286
- else:
287
- # neutral / other
288
- if any(word in message.lower() for word in ("help","guidance","motivate","motivation","inspire")):
289
- base = random.choice(MOTIVATIONAL)
290
- else:
291
- base = random.choice(EMP_OPENERS)
292
-
293
- # occasionally add a short motivational bit when appropriate
294
- if emotion in ("sadness","fear") and random.random() < 0.5:
295
- base = base + " " + random.choice(MOTIVATIONAL)
296
-
297
- # update memory
298
  slot["last_mood"] = emotion
299
- slot["last_seen"] = now_iso
300
- if not slot.get("name"):
301
- slot["name"] = "friend"
 
 
 
302
  memory[session] = slot
303
- write_memory(memory)
304
 
305
- # Enrich to avoid very short repetitive replies
306
- # If the base is very short or repeated wordy, diversify
307
- reply = base
308
 
309
- return jsonify({"reply": reply, "emotion": emotion})
310
-
311
- # Serve frontend
312
- @app.route("/")
313
- def index():
314
- return send_from_directory(".", "index.html")
 
 
 
315
 
316
  if __name__ == "__main__":
317
- # On Spaces, they auto-run; but for standalone testing:
318
- app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
1
  import os
2
  import json
 
3
  import random
4
  import datetime
5
+ import re
6
  from flask import Flask, request, jsonify, send_from_directory
7
 
8
+ # Optional: Hugging Face emotion model if available (fallback to keyword heuristics)
9
  try:
10
  from transformers import pipeline
11
  HF_AVAILABLE = True
12
  except Exception:
13
  HF_AVAILABLE = False
14
 
15
+ # OpenAI for generating supportive replies
16
+ try:
17
+ import openai
18
+ OPENAI_AVAILABLE = True
19
+ openai.api_key = os.environ.get("OPENAI_API_KEY")
20
+ if not openai.api_key:
21
+ OPENAI_AVAILABLE = False
22
+ except Exception:
23
+ OPENAI_AVAILABLE = False
24
 
25
+ app = Flask(__name__, static_folder=".", static_url_path="/")
 
 
26
 
27
+ # ----- CONFIG -----
28
+ MEMORY_FILE = "session_memory.json"
29
+ MEMORY_DAYS = 15
30
  HELPLINES = {
31
  "IN": "🇮🇳 India: AASRA Helpline 91-9820466726",
32
  "US": "🇺🇸 USA: Call or text 988 (Suicide & Crisis Lifeline)",
33
+ "GB": "🇬🇧 UK: Samaritans 116 123",
34
+ "CA": "🇨🇦 Canada: Talk Suicide Canada 1-833-456-4566",
35
  "AU": "🇦🇺 Australia: Lifeline 13 11 14",
36
+ "DEFAULT": "If you are in crisis, please contact your local emergency number or use https://findahelpline.com"
37
  }
38
+ CRISIS_TERMS = ["suicide","kill myself","end my life","i want to die","hurt myself","can't go on","cant go on","i don't want to live"]
39
 
40
+ # ----- Try load HF model (optional) -----
 
 
41
  emotion_model = None
42
  if HF_AVAILABLE:
43
  try:
 
44
  emotion_model = pipeline("text-classification", model="j-hartmann/emotion-english-distilroberta-base", top_k=5)
45
  except Exception:
46
  emotion_model = None
47
 
48
+ # ----- Memory helpers -----
49
+ def load_memory():
50
  if os.path.exists(MEMORY_FILE):
51
  try:
52
  with open(MEMORY_FILE, "r") as f:
 
55
  data = {}
56
  else:
57
  data = {}
58
+ # prune by date
59
+ cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=MEMORY_DAYS)
60
+ out = {}
61
  for k,v in data.items():
62
  try:
63
  t = datetime.datetime.fromisoformat(v.get("last_seen"))
64
  if t >= cutoff:
65
+ out[k] = v
66
  except Exception:
67
+ out[k] = v
68
+ return out
 
69
 
70
+ def save_memory(mem):
71
  with open(MEMORY_FILE, "w") as f:
72
  json.dump(mem, f, indent=2)
73
 
74
+ memory = load_memory()
75
 
76
+ # ----- small NLP helpers -----
77
+ def extract_name(text):
78
+ # patterns: "I'm X", "I am X", "My name is X" or single capitalized word
79
  text = text.strip()
80
  patterns = [
81
+ r"^(?:i am|i'm|im|i’m)\s+([A-Za-z][A-Za-z '-]{1,40})",
82
+ r"my name is\s+([A-Za-z][A-Za-z '-]{1,40})",
83
+ r"^([A-Z][a-z]{1,30})$"
84
  ]
85
  for p in patterns:
86
  m = re.search(p, text, flags=re.IGNORECASE)
87
  if m:
88
  name = m.group(1).strip()
 
89
  return " ".join([w.capitalize() for w in name.split()])
90
  return None
91
 
92
+ def extract_age(text):
 
93
  nums = re.findall(r"\b([1-9][0-9]?)\b", text)
94
  for n in nums:
95
  val = int(n)
 
97
  return val
98
  return None
99
 
100
+ def detect_crisis(text):
101
+ t = text.lower()
102
+ return any(term in t for term in CRISIS_TERMS)
 
 
 
103
 
104
+ def get_helpline_by_ip(remote_addr):
105
+ # best effort: call ipapi.co
106
  try:
107
  import requests
108
+ ip = remote_addr if remote_addr and ":" not in remote_addr else ""
109
+ url = "https://ipapi.co/json/" if not ip else f"https://ipapi.co/{ip}/json/"
110
+ r = requests.get(url, timeout=2)
111
  if r.status_code == 200:
112
  data = r.json()
113
+ code = data.get("country_code", "").upper()
114
+ return HELPLINES.get(code, HELPLINES["DEFAULT"])
115
  except Exception:
116
  pass
117
+ return HELPLINES["DEFAULT"]
118
 
119
+ def classify_emotion(text):
120
+ # try HF model, fallback to keywords
121
  if emotion_model:
122
  try:
123
  out = emotion_model(text)
124
+ if isinstance(out, list):
125
+ # pipeline may return list of lists or list of dicts
126
+ first = out[0]
127
+ if isinstance(first, list):
128
+ label = first[0]["label"]
129
  else:
130
+ label = first["label"]
131
+ return label.lower()
132
  except Exception:
133
  pass
134
+ # keyword fallback
135
  low = text.lower()
136
+ if any(w in low for w in ["happy","glad","joy","great","good","awesome"]):
137
  return "joy"
138
  if any(w in low for w in ["sad","down","depressed","unhappy","lonely","cry"]):
139
  return "sadness"
140
+ if any(w in low for w in ["angry","mad","furious","hate","annoy"]):
141
  return "anger"
142
+ if any(w in low for w in ["scared","afraid","anxious","panic","worried"]):
143
  return "fear"
144
  if any(w in low for w in ["love","loving","cherish","fond"]):
145
  return "love"
146
  return "neutral"
147
 
148
+ # ----- Avoid repetitiveness helpers -----
149
+ VARIED_ACKS = [
150
+ "That sounds really heavy — thank you for trusting me with that.",
151
+ "I can hear how much that matters to you.",
152
+ "You’ve shared something important. I’m here with you.",
153
+ "That must have taken courage to say. I’m listening."
 
 
 
154
  ]
155
+ VARIED_FOLLOWUPS = [
156
+ "Would you like to tell me more about what’s been happening?",
157
+ "How has that been affecting your day-to-day life?",
158
+ "What usually helps you when you feel this way?",
159
+ "Would you like a calming exercise or practical next steps?"
 
160
  ]
161
+ MOTIVATIONAL_SNIPPETS = [
162
+ "Even small steps count. You don't have to do it all at once.",
163
+ "It's okay to rest and take care of yourself.",
164
+ "You’ve handled so much already — that shows strength.",
165
+ "Breath by breath, you are moving forward."
166
  ]
167
 
168
+ # ----- OpenAI helper: build system prompt for mixed-mode personalities (Option C) -----
169
+ def build_system_prompt(personality_choice, past_memory):
170
+ # personality_choice is an id like 'calm_male', etc.
171
+ # past_memory: dict with name, age, last_mood
172
+ base = (
173
+ "You are 'Serenity', a warm, compassionate emotional-support assistant. "
174
+ "Be supportive, curiosity-led, and avoid giving medical or legal advice. "
175
+ "Do not use the same short phrase repeatedly. Vary vocabulary and sentence structure. "
176
+ "Always be empathetic, concise when user is distressed, more chatty when user is okay. "
177
+ "When user shows crisis language, immediately offer helpline info and encourage contacting emergency services."
178
+ )
179
+
180
+ # personalities mapping
181
+ persona_map = {
182
+ "calm_male": "Use a calm male-tone voice: steady, grounding, gentle. Slightly formal but warm.",
183
+ "deep_male": "Use a deep baritone male-tone: slow, resonant, reassuring.",
184
+ "soothing_male": "Use a soothing male counselor-tone: mellow and kind.",
185
+ "gentle_female": "Use a gentle female-tone: tender, nurturing, caring.",
186
+ "feminine_female": "Use a feminine bright-tone: warm and encouraging.",
187
+ "deep_female": "Use a deeper female-tone: soulful and empathetic.",
188
+ "soothing_female": "Use a clear soothing female-tone: calm and steady.",
189
+ "neutral": "Use a neutral friendly-tone: balanced, soft, non-gendered."
190
+ }
191
+ persona = persona_map.get(personality_choice, persona_map["neutral"])
192
+
193
+ memory_note = ""
194
+ if past_memory:
195
+ nm = past_memory.get("name")
196
+ last = past_memory.get("last_mood")
197
+ if nm:
198
+ memory_note += f" The user is called {nm}."
199
+ if last:
200
+ memory_note += f" The user's recent mood was: {last}."
201
+
202
+ # Add few-shot style guidance for followups and variety
203
+ examples = (
204
+ "\nExamples of empathetic flow (do not repeat exact wording):\n"
205
+ "- User: 'I've been so down lately.' -> Assistant: 'I can hear how heavy that feels. Would you like to share what triggered it?' \n"
206
+ "- User: 'I can't sleep, I'm anxious' -> Assistant: 'That must be exhausting. Try breathing with me for a minute — would you like that?' \n"
207
+ "- User: 'How are you?' -> Assistant: Provide a short warm, slightly human reply and then re-focus on the user: e.g. 'I'm here and ready to listen — how are you feeling today?'\n"
208
+ )
209
+
210
+ return "\n".join([base, persona, memory_note, examples])
211
+
212
+ # ----- Generate assistant reply via OpenAI (preferred) or friendly fallback -----
213
+ def generate_assistant_reply(user_message, personality, session_memory):
214
+ name = session_memory.get("name", "friend")
215
+ age = session_memory.get("age")
216
+ last_mood = session_memory.get("last_mood")
217
+
218
+ # If OpenAI not configured, produce a careful local fallback using templates
219
+ if not OPENAI_AVAILABLE:
220
+ # we still use classification to vary replies
221
+ emo = classify_emotion(user_message)
222
+ if emo in ("sadness","fear","anger"):
223
+ reply = random.choice(VARIED_ACKS) + " " + random.choice(VARIED_FOLLOWUPS)
224
+ elif emo in ("joy","love"):
225
+ reply = f"That's wonderful, {name}! So glad to hear that. What made it bright for you?"
226
+ else:
227
+ if any(w in user_message.lower() for w in ("help","guidance","motivate","motivation","advice")):
228
+ reply = random.choice(MOTIVATIONAL_SNIPPETS)
229
+ else:
230
+ reply = random.choice(VARIED_ACKS)
231
+ return reply, emo
232
+
233
+ # Build system + user prompt for OpenAI
234
+ system_prompt = build_system_prompt(personality or "neutral", session_memory)
235
+ messages = [
236
+ {"role":"system", "content": system_prompt},
237
+ {"role":"user", "content": f"User (named {name if name else 'unknown'}) says: {user_message}"}
238
+ ]
239
+
240
+ try:
241
+ resp = openai.ChatCompletion.create(
242
+ model = os.environ.get("OPENAI_MODEL","gpt-4o-mini"),
243
+ messages = messages,
244
+ temperature = 0.8,
245
+ max_tokens = 512,
246
+ n = 1
247
+ )
248
+ text = resp.choices[0].message.content.strip()
249
+ # compute emotion via classifier
250
+ emo = classify_emotion(user_message)
251
+ # small safety: prevent repetitive single-line "I understand"
252
+ normalized = text.strip().lower()
253
+ if normalized in ("i understand", "i see", "okay", "i'm sorry to hear that"):
254
+ text = random.choice(VARIED_ACKS)
255
+ return text, emo
256
+ except Exception:
257
+ # fallback if OpenAI errors
258
+ emo = classify_emotion(user_message)
259
+ if emo in ("sadness","fear","anger"):
260
+ reply = random.choice(VARIED_ACKS) + " " + random.choice(VARIED_FOLLOWUPS)
261
+ elif emo in ("joy","love"):
262
+ reply = f"That's wonderful, {name}! Tell me more about that good thing."
263
+ else:
264
+ reply = random.choice(VARIED_ACKS)
265
+ return reply, emo
266
+
267
+ # ----- Routes -----
268
+ @app.route("/")
269
+ def index():
270
+ return send_from_directory(".", "index.html")
271
+
272
  @app.route("/chat", methods=["POST"])
273
  def chat():
274
  global memory
275
+ data = request.get_json() or {}
276
+ session = data.get("session") or request.remote_addr or "default"
277
+ message = (data.get("message") or "").strip()
278
+ personality = data.get("personality") or data.get("voice_profile") or "neutral"
279
+ init_flag = data.get("init", False)
280
 
281
+ # ensure session slot
282
  slot = memory.get(session, {})
283
+ now = datetime.datetime.utcnow().isoformat()
284
 
285
+ # If init flag, return initial greeting or follow-up if we have memory
286
  if init_flag:
287
  if not slot.get("name"):
288
+ slot.setdefault("last_seen", now)
289
+ memory[session] = slot
290
+ save_memory(memory)
291
+ return jsonify({"reply":"Hey there — I’m Serenity. What’s your name?", "emotion":"calm"})
292
  else:
 
293
  last_mood = slot.get("last_mood")
294
  last_seen = slot.get("last_seen")
295
+ try:
296
+ if last_mood in ("sadness","fear","anger") and last_seen:
297
+ t = datetime.datetime.fromisoformat(last_seen)
298
+ if (datetime.datetime.utcnow() - t).days <= MEMORY_DAYS:
299
+ return jsonify({"reply":f"Hey {slot.get('name')}, I remember you were having a tough time last time. How are you today?", "emotion":"warm"})
300
+ except Exception:
301
+ pass
302
+ return jsonify({"reply":f"Welcome back {slot.get('name')} — I'm here for you. What's on your mind?", "emotion":"calm"})
303
+
304
+ # If message empty
305
  if not message:
306
+ return jsonify({"reply":"I’m listening whenever you’re ready tell me what’s on your mind.", "emotion":"neutral"})
 
 
 
307
 
308
+ # If awaiting name or age from previous flow
309
+ awaiting = slot.get("awaiting")
310
  if not slot.get("name") and not awaiting:
311
+ # Try to extract name
312
+ name_extracted = extract_name(message)
313
+ if name_extracted:
314
+ slot["name"] = name_extracted
315
+ slot["awaiting"] = "age"
316
+ slot["last_seen"] = now
317
  memory[session] = slot
318
+ save_memory(memory)
319
+ return jsonify({"reply":f"Lovely to meet you, {name_extracted}! How old are you?", "emotion":"curious"})
 
 
 
 
320
  else:
321
+ slot["awaiting"] = "name"
322
+ slot["last_seen"] = now
323
+ memory[session] = slot
324
+ save_memory(memory)
325
+ return jsonify({"reply":"Hey — what should I call you? What's your name?", "emotion":"calm"})
 
326
 
 
327
  if awaiting == "name":
 
328
  name_guess = extract_name(message) or message.split()[0].capitalize()
329
  slot["name"] = name_guess
330
  slot.pop("awaiting", None)
331
+ slot["last_seen"] = now
332
+ slot["awaiting"] = "age"
333
  memory[session]=slot
334
+ save_memory(memory)
 
 
 
 
335
  return jsonify({"reply":f"Nice to meet you, {name_guess}! How old are you?", "emotion":"curious"})
336
 
 
337
  if awaiting == "age":
338
+ age_guess = extract_age(message)
339
+ if age_guess:
340
+ slot["age"] = age_guess
341
  slot.pop("awaiting", None)
342
+ slot["last_seen"] = now
343
  memory[session]=slot
344
+ save_memory(memory)
345
+ return jsonify({"reply":f"Thanks — got it. {slot.get('name')}, how have you been feeling lately?", "emotion":"curious"})
346
  else:
347
+ return jsonify({"reply":"Could you tell me your age as a number (for example: 24)?", "emotion":"neutral"})
 
 
 
348
 
349
+ # Crisis detection
350
  if detect_crisis(message):
351
+ helpline = get_helpline_by_ip(request.remote_addr)
352
+ slot["last_mood"] = "crisis"
353
+ slot["last_seen"] = now
354
+ memory[session] = slot
355
+ save_memory(memory)
356
+ reply = (f"I’m really concerned about how you're feeling right now. You are not alone. "
357
+ f"Please consider contacting emergency services or a helpline: {helpline}")
 
 
 
 
358
  return jsonify({"reply":reply, "emotion":"crisis"})
359
 
360
+ # Generate reply via OpenAI or fallback templates
361
+ reply_text, emotion = generate_assistant_reply(message, personality, slot)
362
+ # store memory
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  slot["last_mood"] = emotion
364
+ slot["last_seen"] = now
365
+ # keep a short history
366
+ hist = slot.get("history", [])
367
+ hist.append({"time": now, "in": message, "out": reply_text, "emotion": emotion})
368
+ # keep only last 20 messages
369
+ slot["history"] = hist[-20:]
370
  memory[session] = slot
371
+ save_memory(memory)
372
 
373
+ return jsonify({"reply": reply_text, "emotion": emotion})
 
 
374
 
375
+ # optional endpoint to clear a session (new chat)
376
+ @app.route("/reset_session", methods=["POST"])
377
+ def reset_session():
378
+ data = request.get_json() or {}
379
+ session = data.get("session")
380
+ if session and session in memory:
381
+ memory.pop(session, None)
382
+ save_memory(memory)
383
+ return jsonify({"ok": True})
384
 
385
  if __name__ == "__main__":
386
+ port = int(os.environ.get("PORT", 7860))
387
+ app.run(host="0.0.0.0", port=port)