Chatbot-2 / app.py
Shresthh03's picture
Update app.py
26242a6 verified
raw
history blame
20.1 kB
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 ----------
@app.route("/")
def index():
return send_from_directory(".", "index.html")
@app.route("/chat", methods=["POST"])
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":