# context_engine.py """ Model-free context & emotion heuristics. Replaces the prior transformer-based emotion classifier with a fast, deterministic heuristic that infers: - primary_emotion: one of ('joy','sadness','anger','fear','surprise','neutral') - emotion_confidence: float (0.0 - 1.0) indicating heuristic strength - conversation_mode: Ping-Pong / Standard / Deep Dive - emoji suggestions and min-word guidance Design notes: - Uses emoji presence, punctuation, uppercase emphasis, keywords, negations, question density, message length, and repetition to infer emotion. - Intentionally conservative: returns moderate confidences unless strong signals. - No external libraries or model downloads required. """ from typing import Tuple import re # Keyword lists (tunable) _JOY_KEYWORDS = {"happy", "great", "awesome", "fantastic", "nice", "love", "yay", "yay!", "cool", "amazing", "thanks", "thank you", "cheers"} _SADNESS_KEYWORDS = {"sad", "unhappy", "depressed", "upset", "down", "sadder", "melancholy", "sorrow", "lonely"} _ANGER_KEYWORDS = {"angry", "frustrat", "frustrated", "mad", "furious", "annoyed", "irritat", "rage", "disgusted"} _FEAR_KEYWORDS = {"scared", "afraid", "anxious", "worried", "panic", "nervous", "fear"} _SURPRISE_KEYWORDS = {"wow", "whoa", "surpris", "unexpected", "amazed", "shocked"} _NEGATIONS = {"not", "don't", "didn't", "can't", "couldn't", "won't", "never", "n't"} _EMOJI_POSITIVE = {"😊","🙂","😄","😁","🎉","👍","🤝","😃","✨","😍"} _EMOJI_NEGATIVE = {"đŸ˜ĸ","😞","â˜šī¸","😡","😭","😠","😤","😖","😩","😓"} _EMOJI_SURPRISE = {"😲","đŸ˜¯","😮","đŸ¤¯","đŸ˜ŗ"} def _count_emojis(text: str): # simple unicode emoji detection by ranges + common emoji symbols (lightweight) # also check presence in our small emoji sets pos = sum(1 for e in _EMOJI_POSITIVE if e in text) neg = sum(1 for e in _EMOJI_NEGATIVE if e in text) sup = sum(1 for e in _EMOJI_SURPRISE if e in text) # rough generic emoji count (fallback) generic = len(re.findall(r'[\U0001F300-\U0001FAFF\U00002700-\U000027BF]', text)) return {"positive": pos, "negative": neg, "surprise": sup, "generic": generic} def _word_tokens(text: str): return re.findall(r"\w+", text.lower()) def _keyword_score(tokens, keywords): return sum(1 for t in tokens if any(t.startswith(k) for k in keywords)) def _has_upper_emphasis(text: str): # Count words that are ALL CAPS and length>=2 caps = [w for w in re.findall(r"\b[A-Z]{2,}\b", text)] return len(caps) def _question_density(tokens, text: str): qwords = {"what","why","how","which","when","where","who","do","does","did","can","could","would","should","is","are","was","were"} qcount = sum(1 for t in tokens if t in qwords) total = max(1, len(tokens)) return qcount / total def _detect_emotion(text: str) -> Tuple[str, float]: """ Rule-based emotion detection. Returns (label, confidence) """ if not text or not text.strip(): return ("neutral", 0.0) t = text.strip() tokens = _word_tokens(t) lower = t.lower() # simple signals emoji_counts = _count_emojis(t) positive_emoji = emoji_counts["positive"] negative_emoji = emoji_counts["negative"] surprise_emoji = emoji_counts["surprise"] generic_emoji = emoji_counts["generic"] upper_caps = _has_upper_emphasis(t) exclamations = t.count("!") question_marks = t.count("?") repeated_punct = bool(re.search(r'([!?])\1{2,}', t)) # e.g., "!!!" or "???" or "!?!!" # keyword matches joy_kw = _keyword_score(tokens, _JOY_KEYWORDS) sad_kw = _keyword_score(tokens, _SADNESS_KEYWORDS) anger_kw = _keyword_score(tokens, _ANGER_KEYWORDS) fear_kw = _keyword_score(tokens, _FEAR_KEYWORDS) surprise_kw = _keyword_score(tokens, _SURPRISE_KEYWORDS) negation_present = any(n in tokens for n in _NEGATIONS) q_density = _question_density(tokens, t) length = len(tokens) # scoring heuristics (base 0) scores = { "joy": 0.0, "sadness": 0.0, "anger": 0.0, "fear": 0.0, "surprise": 0.0, "neutral": 0.0 } # Emoji-weighted signals scores["joy"] += positive_emoji * 0.35 scores["sadness"] += negative_emoji * 0.4 scores["surprise"] += surprise_emoji * 0.4 # Keyword signals (normalized) scores["joy"] += min(joy_kw * 0.25, 1.0) scores["sadness"] += min(sad_kw * 0.3, 1.0) scores["anger"] += min(anger_kw * 0.35, 1.0) scores["fear"] += min(fear_kw * 0.3, 1.0) scores["surprise"] += min(surprise_kw * 0.3, 1.0) # punctuation / emphasis if exclamations >= 2 or upper_caps >= 2 or repeated_punct: # could be joy or anger depending on words if joy_kw or positive_emoji: scores["joy"] += 0.4 if anger_kw or negative_emoji: scores["anger"] += 0.45 # otherwise, boost surprise if not (joy_kw or anger_kw): scores["surprise"] += 0.25 # question-dense messages -> information-seeking / surprise / neutral if q_density > 0.2 or question_marks >= 1: scores["surprise"] += 0.2 scores["neutral"] += 0.15 # negativity via negation nearby to positive words -> reduce joy, raise neutral/anger if negation_present and joy_kw: scores["joy"] = max(0.0, scores["joy"] - 0.5) scores["neutral"] += 0.2 scores["anger"] += 0.1 # sadness signals for short emotive messages like "so sad" or "feeling down" if sad_kw and length <= 6: scores["sadness"] += 0.3 # length-based adjust: very short messages default to small_talk/neutral unless strong signal if length <= 3 and sum(scores.values()) < 0.5: scores["neutral"] += 0.5 # normalize into selection # pick top-scoring emotion top_em = max(scores.items(), key=lambda kv: kv[1]) label = top_em[0] raw_score = float(top_em[1]) # compute confidence: scale raw_score to 0..1 with heuristics # higher length + multiple signals -> higher confidence confidence = raw_score # boost confidence for multiple corroborating signals corroborators = 0 if positive_emoji + negative_emoji + surprise_emoji + generic_emoji > 0: corroborators += 1 if upper_caps > 0 or exclamations > 0 or repeated_punct: corroborators += 1 if any([joy_kw, sad_kw, anger_kw, fear_kw, surprise_kw]): corroborators += 1 # boost based on corroborators (0..3) confidence = min(1.0, confidence + (0.12 * corroborators)) # fallback: if very low signal, mark neutral with low confidence if confidence < 0.15: label = "neutral" confidence = round(max(confidence, 0.05), 2) else: confidence = round(confidence, 2) return (label, confidence) def get_smart_context(user_text: str): """ Returns a short persona instruction block including: - Conversation Mode - Emotional context (heuristic) - Emoji suggestions - Minimum verbosity hint """ try: text = (user_text or "").strip() label, confidence = _detect_emotion(text) word_count = len(_word_tokens(text)) q_density = _question_density(_word_tokens(text), text) # Conversation Mode determination (same as before) if word_count < 4: conversation_mode = "Ping-Pong Mode (Fast)" min_words_hint = 12 elif word_count < 20: conversation_mode = "Standard Chat Mode (Balanced)" min_words_hint = 30 else: conversation_mode = "Deep Dive Mode (Detailed)" min_words_hint = 70 # Map emotion label to friendly guidance & emoji suggestions if label == "joy": emotional_context = "User: Positive/Energetic. Vibe: Upbeat — be warm and slightly playful." emoji_examples = "😊 🎉 🙂" emoji_range = (1, 2) elif label == "sadness": emotional_context = "User: Low Energy. Vibe: Supportive — be gentle and empathetic." emoji_examples = "🤍 đŸŒ¤ī¸" emoji_range = (0, 1) elif label == "anger": emotional_context = "User: Frustrated. Vibe: De-escalate — calm, solution-first." emoji_examples = "🙏 đŸ› ī¸" emoji_range = (0, 1) elif label == "fear": emotional_context = "User: Anxious. Vibe: Reassure and clarify." emoji_examples = "🤝 đŸ›Ąī¸" emoji_range = (0, 1) elif label == "surprise": emotional_context = "User: Curious/Alert. Vibe: Engage and explain." emoji_examples = "🤔 ✨" emoji_range = (0, 2) else: emotional_context = "User: Neutral/Professional. Vibe: Helpful and efficient." emoji_examples = "💡 🙂" emoji_range = (0, 2) # Slightly adjust min_words_hint if question density is high if q_density > 0.25: min_words_hint = max(min_words_hint, 30) # Build instruction block return ( f"\n[PSYCHOLOGICAL PROFILE]\n" f"1. Interaction Mode: {conversation_mode}\n" f"2. {emotional_context} (detected_emotion={label}, confidence={confidence})\n" f"3. Emoji Suggestions: Use {emoji_range[0]}–{emoji_range[1]} emoji(s). Examples: {emoji_examples}\n" f"4. Minimum Word Guidance: Aim for ~{min_words_hint} words unless user explicitly requests 'short' or 'brief'.\n" f"5. Directive: Mirror user's energy; prefer natural phrasing and avoid robotic one-line replies.\n" ) except Exception as e: # conservative fallback return ( "\n[PSYCHOLOGICAL PROFILE]\n" "1. Interaction Mode: Standard Chat Mode (Balanced)\n" "2. User: Neutral. Vibe: Helpful and efficient.\n" "3. Emoji Suggestions: Use 0–1 emoji. Examples: 🙂\n" "4. Minimum Word Guidance: Aim for ~30 words.\n" )