Spaces:
Sleeping
Sleeping
| # 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" | |
| ) | |