Spaces:
Sleeping
Sleeping
| import os | |
| import json | |
| import re | |
| import random | |
| import datetime | |
| from flask import Flask, request, jsonify, send_from_directory | |
| # Try optional packages | |
| try: | |
| from transformers import pipeline | |
| HF_AVAILABLE = True | |
| except Exception: | |
| HF_AVAILABLE = False | |
| try: | |
| import requests | |
| REQ_AVAILABLE = True | |
| except Exception: | |
| REQ_AVAILABLE = False | |
| # Optional OpenAI usage for richer replies | |
| try: | |
| import openai | |
| OPENAI_AVAILABLE = bool(os.environ.get("OPENAI_API_KEY")) | |
| if OPENAI_AVAILABLE: | |
| openai.api_key = os.environ.get("OPENAI_API_KEY") | |
| except Exception: | |
| OPENAI_AVAILABLE = False | |
| app = Flask(__name__, static_folder=".", static_url_path="/") | |
| # ---------- Config ---------- | |
| MEMORY_FILE = "session_memory.json" | |
| MEMORY_RETENTION_DAYS = 15 | |
| 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", "i dont want to live" | |
| ] | |
| HELPLINES = { | |
| "IN": "🇮🇳 India: AASRA Helpline 91-9820466726", | |
| "US": "🇺🇸 USA: Call or text 988 (Suicide & Crisis Lifeline)", | |
| "GB": "🇬🇧 UK: Samaritans 116 123", | |
| "CA": "🇨🇦 Canada: Talk Suicide Canada 1-833-456-4566", | |
| "AU": "🇦🇺 Australia: Lifeline 13 11 14", | |
| "DEFAULT": "If you are in crisis, please contact your local emergency number or visit https://findahelpline.com" | |
| } | |
| # ---------- Optional HF emotion model (heavy) ---------- | |
| emotion_model = None | |
| if HF_AVAILABLE: | |
| try: | |
| emotion_model = pipeline("text-classification", | |
| model="j-hartmann/emotion-english-distilroberta-base", | |
| top_k=5) | |
| except Exception: | |
| emotion_model = None | |
| # ---------- Memory helpers ---------- | |
| def load_memory(): | |
| if os.path.exists(MEMORY_FILE): | |
| try: | |
| with open(MEMORY_FILE, "r") as f: | |
| data = json.load(f) | |
| except Exception: | |
| data = {} | |
| else: | |
| data = {} | |
| # prune old | |
| cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=MEMORY_RETENTION_DAYS) | |
| keep = {} | |
| for k, v in data.items(): | |
| try: | |
| t = datetime.datetime.fromisoformat(v.get("last_seen")) | |
| if t >= cutoff: | |
| keep[k] = v | |
| except Exception: | |
| keep[k] = v | |
| return keep | |
| def save_memory(mem): | |
| with open(MEMORY_FILE, "w") as f: | |
| json.dump(mem, f, indent=2) | |
| memory = load_memory() | |
| # ---------- small NLP helpers ---------- | |
| name_patterns = [ | |
| r"^(?:i am|i'm|im|i’m)\s+([A-Za-z][A-Za-z '-]{1,40})", | |
| r"my name is\s+([A-Za-z][A-Za-z '-]{1,40})", | |
| r"^([A-Z][a-z]{1,30})$" | |
| ] | |
| def extract_name(text): | |
| text = text.strip() | |
| for p in name_patterns: | |
| m = re.search(p, text, flags=re.IGNORECASE) | |
| if m: | |
| name = m.group(1).strip() | |
| return " ".join([w.capitalize() for w in name.split()]) | |
| return None | |
| def extract_age(text): | |
| nums = re.findall(r"\b([1-9][0-9]?)\b", text) | |
| for n in nums: | |
| v = int(n) | |
| if 8 <= v <= 120: | |
| return v | |
| return None | |
| def is_crisis(text): | |
| low = text.lower() | |
| return any(term in low for term in CRISIS_TERMS) | |
| def helpline_for_request(remote_addr): | |
| # best-effort country lookup via ipapi | |
| try: | |
| if REQ_AVAILABLE: | |
| ip = remote_addr if remote_addr and ":" not in remote_addr else "" | |
| url = "https://ipapi.co/json/" if not ip else f"https://ipapi.co/{ip}/json/" | |
| r = requests.get(url, timeout=2) | |
| if r.status_code == 200: | |
| data = r.json() | |
| code = data.get("country_code", "").upper() | |
| return HELPLINES.get(code, HELPLINES["DEFAULT"]) | |
| except Exception: | |
| pass | |
| return HELPLINES["DEFAULT"] | |
| def classify_emotion(text): | |
| # Try HF if available | |
| if emotion_model: | |
| try: | |
| out = emotion_model(text) | |
| # pipeline returns list or list of lists; get top label | |
| first = out[0] | |
| if isinstance(first, list): | |
| label = first[0]["label"] | |
| else: | |
| label = first["label"] | |
| return label.lower() | |
| except Exception: | |
| pass | |
| # fallback heuristics | |
| low = text.lower() | |
| if any(w in low for w in ["happy","glad","joy","great","good","awesome","fine"]): | |
| return "joy" | |
| if any(w in low for w in ["sad","down","depressed","unhappy","lonely","cry","miserable"]): | |
| return "sadness" | |
| if any(w in low for w in ["angry","mad","furious","annoyed","irritat"]): | |
| return "anger" | |
| if any(w in low for w in ["scared","afraid","anxious","panic","worried"]): | |
| return "fear" | |
| if any(w in low for w in ["love","loving","cherish","fond"]): | |
| return "love" | |
| return "neutral" | |
| # ---------- Intent detection (simple rules) ---------- | |
| def detect_intent(text): | |
| t = text.lower().strip() | |
| # Crisis | |
| if is_crisis(t): | |
| return "CRISIS" | |
| # Asking about bot | |
| if any(q in t for q in ["how are you", "how're you", "how r you", "how you doing", "are you okay", "are you mad", "are you upset", "are you mad?"]): | |
| return "QUESTION_ABOUT_BOT" | |
| # Requests for motivation/guidance | |
| if any(w in t for w in ["motivate", "motivation", "guidance", "inspire", "give me guidance", "need motivation", "help me be motivated"]): | |
| return "REQUEST_MOTIVATION" | |
| # Casual chit-chat / teasing / slang | |
| if any(w in t for w in ["lol","haha","hahaha","jk","bro","dude","whats up","what's up","have you gone mad","are you mad","r u mad","you mad"]): | |
| return "CASUAL" | |
| # If user mentions feelings -> support | |
| if any(w in t for w in ["sad","down","depressed","anxious","anxiety","lonely","hurt","upset","tired","stressed","stressing","stress"]): | |
| return "SUPPORT" | |
| # Else neutral casual fallback for short utterances | |
| if len(t.split()) <= 6: | |
| return "CASUAL" | |
| return "SUPPORT" # prefer support for longer introspective messages | |
| # ---------- Non-repetitive response manager ---------- | |
| def pick_nonrepetitive(session_slot, bucket): | |
| """Pick a reply from bucket avoiding recent repeats stored in session_slot['recent_replies']""" | |
| recent = session_slot.get("recent_replies", []) | |
| choices = [x for x in bucket if x not in recent] | |
| if not choices: | |
| # all used recently — clear memory a bit and reuse | |
| session_slot["recent_replies"] = [] | |
| choices = bucket[:] | |
| pick = random.choice(choices) | |
| # append to recent (keep last 6) | |
| recent.insert(0, pick) | |
| session_slot["recent_replies"] = recent[:6] | |
| return pick | |
| # ---------- Reply templates ---------- | |
| CASUAL_REPLY_TEMPLATES = [ | |
| "Haha, you crack me up — tell me more!", | |
| "Oh wow, that’s a curveball 😄 What made you say that?", | |
| "I’m here and very curious — go on.", | |
| "Haha, I might be a little wired but never mad — what's up?", | |
| "I love that energy. Want to tell me more about it?", | |
| "You’re funny — but seriously, how are you really?", | |
| "Haha, okay I see you. What else?" | |
| ] | |
| SUPPORT_OPENERS = [ | |
| "That sounds heavy — thank you for trusting me with that.", | |
| "I can feel how much that impacted you. I'm listening.", | |
| "You handled a lot there; I'm glad you told me.", | |
| "That must have been difficult. Tell me more, if you want." | |
| ] | |
| SUPPORT_FOLLOWUPS = [ | |
| "Would you like to talk about what might help a little today?", | |
| "How has this been affecting your daily life?", | |
| "What usually helps you when things feel this way?", | |
| "Would you prefer a calming exercise or a few practical steps?" | |
| ] | |
| MOTIVATIONAL_SNIPPETS = [ | |
| "Even small steps count — you don't need to fix everything at once.", | |
| "You’ve come so far already. One gentle step at a time.", | |
| "Rest is allowed. Healing isn’t a straight line.", | |
| "Breathe — you’re doing better than you think." | |
| ] | |
| BOT_SELF_REPLIES = [ | |
| "I'm doing well — talking to you brightens my loop! How about you?", | |
| "Feeling calm and ready to listen — how are you today?", | |
| "I’m good! Just here with an open ear for you.", | |
| "Doing okay — I was thinking about how to support you better. What’s up?" | |
| ] | |
| # ---------- OpenAI prompt builder (for mixed persona) ---------- | |
| PERSONA_TEXT = { | |
| "calm_male": "A calm masculine-tone voice: steady, grounding, gentle; use short reassuring phrases.", | |
| "deep_male": "A deep male-tone: slow, resonant, and calming.", | |
| "soothing_male": "A soothing male-tone: mellow and kind.", | |
| "gentle_female": "A gentle female-tone: tender and nurturing.", | |
| "feminine_female": "A bright feminine-tone: warm and encouraging.", | |
| "deep_female": "A deeper female-tone: soulful and empathetic.", | |
| "soothing_female": "A soothing female-tone: calm and steady.", | |
| "neutral": "A neutral friendly-tone: balanced, soft, non-gendered." | |
| } | |
| def build_openai_prompt(personality_id, session_slot): | |
| persona = PERSONA_TEXT.get(personality_id, PERSONA_TEXT["neutral"]) | |
| memory_note = "" | |
| if session_slot.get("name"): | |
| memory_note += f" The user is named {session_slot.get('name')}." | |
| if session_slot.get("last_mood"): | |
| memory_note += f" Recent mood: {session_slot.get('last_mood')}." | |
| system = ( | |
| "You are Serenity, a warm compassionate emotional support companion. " | |
| "Be empathetic, avoid repeating the same short phrases like 'I understand', and vary vocabulary. " | |
| "Keep replies concise when the user seems distressed; be chatty when the user is casual. " | |
| + persona + memory_note | |
| + " If user asks casual questions about you, answer briefly and pivot back to supporting the user." | |
| ) | |
| return system | |
| def openai_reply(user_message, personality_id, session_slot): | |
| if not OPENAI_AVAILABLE: | |
| return None | |
| system_prompt = build_openai_prompt(personality_id, session_slot) | |
| try: | |
| resp = openai.ChatCompletion.create( | |
| model = os.environ.get("OPENAI_MODEL", "gpt-4o-mini"), | |
| messages = [ | |
| {"role":"system", "content": system_prompt}, | |
| {"role":"user", "content": user_message} | |
| ], | |
| temperature = 0.85, | |
| max_tokens = 350 | |
| ) | |
| text = resp.choices[0].message.content.strip() | |
| return text | |
| except Exception: | |
| return None | |
| # ---------- Routes ---------- | |
| def index(): | |
| return send_from_directory(".", "index.html") | |
| def chat(): | |
| global memory | |
| data = request.get_json() or {} | |
| session = data.get("session") or request.remote_addr or "default_session" | |
| message = (data.get("message") or "").strip() | |
| personality = (data.get("personality") or data.get("voice_profile") or "neutral") | |
| init_flag = data.get("init", False) | |
| # ensure slot exists | |
| slot = memory.get(session, {}) | |
| now = datetime.datetime.utcnow().isoformat() | |
| if not slot: | |
| slot = {"name": None, "age": None, "last_mood": None, "last_seen": now, "recent_replies": [], "history": []} | |
| # If init requested, send greeting or follow-up | |
| if init_flag: | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| if not slot.get("name"): | |
| return jsonify({"reply":"Hey — I'm Serenity. What's your name?", "emotion":"calm", "intent":"INIT"}) | |
| else: | |
| last_mood = slot.get("last_mood") | |
| last_seen = slot.get("last_seen") | |
| try: | |
| t = datetime.datetime.fromisoformat(last_seen) | |
| if last_mood in ("sadness","anger","fear") and (datetime.datetime.utcnow() - t).days <= MEMORY_RETENTION_DAYS: | |
| return jsonify({"reply":f"Hey {slot.get('name')}, I remember you were feeling down last time. How are you today?", "emotion":"warm", "intent":"FOLLOWUP"}) | |
| except Exception: | |
| pass | |
| return jsonify({"reply":f"Welcome back {slot.get('name')} — what’s on your mind?", "emotion":"calm", "intent":"INIT"}) | |
| # If empty message | |
| if not message: | |
| return jsonify({"reply":"I'm here — whenever you're ready, tell me what's on your mind.", "emotion":"neutral", "intent":"NONE"}) | |
| # Handle awaiting name/age | |
| awaiting = slot.get("awaiting") | |
| if not slot.get("name") and not awaiting: | |
| # try to extract name | |
| name = extract_name(message) | |
| if name: | |
| slot["name"] = name | |
| slot["awaiting"] = "age" | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply":f"Nice to meet you, {name}! How old are you?", "emotion":"curious", "intent":"ASK_AGE"}) | |
| else: | |
| slot["awaiting"] = "name" | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply":"Hey — what should I call you? What's your name?", "emotion":"calm", "intent":"ASK_NAME"}) | |
| if awaiting == "name": | |
| guessed = extract_name(message) or message.split()[0].capitalize() | |
| slot["name"] = guessed | |
| slot.pop("awaiting", None) | |
| slot["awaiting"] = "age" | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply":f"Lovely, {guessed}. How old are you?", "emotion":"curious", "intent":"ASK_AGE"}) | |
| if awaiting == "age": | |
| age = extract_age(message) | |
| if age: | |
| slot["age"] = age | |
| slot.pop("awaiting", None) | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply":f"Thanks. {slot.get('name')}, how have you been feeling lately?", "emotion":"curious", "intent":"ASK_MOOD"}) | |
| else: | |
| return jsonify({"reply":"Could you tell me your age as a number (for example, 24)?", "emotion":"neutral", "intent":"ASK_AGE"}) | |
| # Crisis detection | |
| if is_crisis(message): | |
| slot["last_mood"] = "crisis" | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| helpline = helpline_for_request(request.remote_addr) | |
| reply = f"I’m really concerned about how you're feeling. You are not alone. Please consider contacting emergency services or this helpline: {helpline}" | |
| return jsonify({"reply":reply, "emotion":"crisis", "intent":"CRISIS"}) | |
| # Detect intent | |
| intent = detect_intent(message) | |
| # If user asks about the bot (casual) | |
| if intent == "QUESTION_ABOUT_BOT": | |
| # friendly, human-like small talk (Option A) | |
| bot_reply = random.choice(BOT_SELF_REPLIES) | |
| # briefly ask how user is to pivot back | |
| pivot = random.choice(["How are you doing right now?", "And how about you?"]) | |
| reply = f"{bot_reply} {pivot}" | |
| # update memory and return | |
| slot["last_mood"] = classify_emotion(message) | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": reply, "emotion": slot["last_mood"], "intent": "QUESTION_ABOUT_BOT"}) | |
| # If casual intent -> casual friendly replies (Option A) | |
| if intent == "CASUAL": | |
| # Use OpenAI if available to make it more natural | |
| if OPENAI_AVAILABLE: | |
| o = openai_reply := openai_reply = None | |
| # Use a short, casual prompt | |
| try: | |
| system = ("You are a friendly, informal companion. Answer casually, with light humor when appropriate, " | |
| "be brief and natural. Avoid repeating previous phrasing. If the user is distressed, shift to empathy.") | |
| resp = openai.ChatCompletion.create( | |
| model = os.environ.get("OPENAI_MODEL","gpt-4o-mini"), | |
| messages = [ | |
| {"role":"system", "content": system}, | |
| {"role":"user", "content": message} | |
| ], | |
| temperature = 0.8, | |
| max_tokens = 150 | |
| ) | |
| text = resp.choices[0].message.content.strip() | |
| # little safety: if the AI returns a generic empathetic one-liner only, diversify | |
| if text.lower() in ("i understand", "i see", "okay"): | |
| text = pick_nonrepetitive(slot, CASUAL_REPLY_TEMPLATES) | |
| slot["last_mood"] = classify_emotion(message) | |
| slot["last_seen"] = now | |
| # store reply to avoid repetition | |
| slot.setdefault("recent_replies", []) | |
| slot["recent_replies"].insert(0, text) | |
| slot["recent_replies"] = slot["recent_replies"][:6] | |
| slot.setdefault("history", []).append({"in": message, "out": text, "time": now, "intent": intent}) | |
| slot["history"] = slot["history"][-40:] | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": text, "emotion": slot["last_mood"], "intent": intent}) | |
| except Exception: | |
| # fallback to templates | |
| text = pick_nonrepetitive(slot, CASUAL_REPLY_TEMPLATES) | |
| slot["last_mood"] = classify_emotion(message) | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": text, "emotion": slot["last_mood"], "intent": intent}) | |
| else: | |
| text = pick_nonrepetitive(slot, CASUAL_REPLY_TEMPLATES) | |
| slot["last_mood"] = classify_emotion(message) | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": text, "emotion": slot["last_mood"], "intent": intent}) | |
| # Request motivation | |
| if intent == "REQUEST_MOTIVATION": | |
| reply = pick_nonrepetitive(slot, MOTIVATIONAL_SNIPPETS) | |
| slot["last_mood"] = classify_emotion(message) | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": reply, "emotion": slot["last_mood"], "intent": intent}) | |
| # Support (default) | |
| # Try OpenAI with persona if available | |
| if OPENAI_AVAILABLE: | |
| ai_text = openai_reply(message, personality, slot) | |
| if ai_text: | |
| # avoid robotic single-line responses | |
| if ai_text.strip().lower() in ("i understand","i see","okay","i'm sorry to hear that"): | |
| ai_text = pick_nonrepetitive(slot, SUPPORT_OPENERS) | |
| emotion = classify_emotion(message) | |
| slot["last_mood"] = emotion | |
| slot.setdefault("recent_replies", []) | |
| slot["recent_replies"].insert(0, ai_text) | |
| slot["recent_replies"] = slot["recent_replies"][:6] | |
| slot.setdefault("history", []).append({"in": message, "out": ai_text, "time": now, "intent": intent}) | |
| slot["history"] = slot["history"][-40:] | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": ai_text, "emotion": emotion, "intent": intent}) | |
| # else fall through to template fallback | |
| # Fallback supportive templated reply | |
| opener = pick_nonrepetitive(slot, SUPPORT_OPENERS) | |
| follow = pick_nonrepetitive(slot, SUPPORT_FOLLOWUPS) | |
| # Mix small chance for motivational hint | |
| if random.random() < 0.35: | |
| reply = f"{opener} {random.choice(MOTIVATIONAL_SNIPPETS)} {follow}" | |
| else: | |
| reply = f"{opener} {follow}" | |
| emotion = classify_emotion(message) | |
| slot["last_mood"] = emotion | |
| slot.setdefault("recent_replies", []) | |
| slot["recent_replies"].insert(0, reply) | |
| slot["recent_replies"] = slot["recent_replies"][:6] | |
| slot.setdefault("history", []).append({"in": message, "out": reply, "time": now, "intent": intent}) | |
| slot["history"] = slot["history"][-40:] | |
| slot["last_seen"] = now | |
| memory[session] = slot | |
| save_memory(memory) | |
| return jsonify({"reply": reply, "emotion": |