""" Implicit Needs Detection for Ghost Malone Maps emotional patterns + context keywords to underlying psychological needs. STRATEGIC FRAMEWORK: Based on Self-Determination Theory (Ryan & Deci) + Maslow's Hierarchy 5 Core Needs: 1. AUTONOMY - Control, choice, self-direction 2. CONNECTION - Belonging, relationships, being seen 3. SECURITY - Safety, clarity, predictability 4. REST - Recovery, boundaries, capacity 5. RECOGNITION - Validation, competence, mattering Context Detection Strategy: - BARRIERS: What's blocking the need? (can't, won't let, prevented) - DEFICITS: What's missing? (nobody, nothing, no one) - THREATS: What's at risk? (unsafe, unstable, unpredictable) - OVERLOAD: What's too much? (overwhelmed, exhausted, too many) - DISMISSAL: What's being ignored? (dismissed, invisible, unappreciated) Constitutional AI Principle: Respects human dignity by treating emotions as signals of legitimate needs, not surface-level problems. """ from typing import List, Dict import re # STRATEGIC CONTEXT PATTERNS - organized by psychological dimension CONTEXT_PATTERNS = { # ===== AUTONOMY CONTEXTS ===== # Barriers to self-direction "blocked": r"(can't|won't let|prevented|stopped|blocked|restricted|controlled|controlling me|forced|have to|must|don't give|won't give|not letting|not allowed|doesn't give|pressured to|feel pressured)", "powerless": r"(powerless|helpless|trapped|stuck|no choice|no control|out of.{0,15}control|feels.{0,15}out of control|don't have the tools|not much opportunity|no say)", "dismissed": r"(ignored|dismissed|disregarded|invalidated|didn't listen|brushed off|shut down|talked over|doesn't consider|don't consider|nobody trusts|don't trust|people ignore|ignore me|cut me off|looked.{0,15}through me|right through me)", "underutilized": r"(know i can|i can do|capable of|able to do|qualified|skills|talent|waste|potential|not enough challenge|bored|underused|so boring|below my ability|way below|could do.{0,15}more|living up to)", "micromanaged": r"(micromanag|hovering|checking|breathing down|second-guess|don't trust me|checks every|watching every)", # ===== CONNECTION CONTEXTS ===== # Deficits in belonging/relationships "isolated": r"(alone|lonely|isolated|by myself|left out|excluded|without me|on my own|don't have anyone|no one to talk|invisible to|no one there|nobody there)", "rejected": r"(rejected|abandoned|left me|left behind|they left|ghosted|dumped|unwanted|unloved|won't talk|ignoring|avoiding me|cut off|don't.{0,15}respond|doesn't.{0,15}respond|won't.{0,15}respond|not.{0,15}respond(?:ed|ing)?|hasn't.{0,15}respond|didn't.{0,15}respond|not.{0,15}reply|not.{0,15}repl(?:ied|ying)|not.{0,15}answer(?:ed|ing)?|avoid me|seem to avoid|wasn't invited|didn't invite|unanswered|go unanswered|make excuses|not texting.{0,15}back|won't text)", "misunderstood": r"(misunderstood|don't get it|don't understand|not listening|not hearing|missing the point|doesn't.{0,15}listen|won't.{0,15}listen|different language|speaking a different|doesn't.{0,15}hear|misinterpret|nobody.{0,15}understands|nobody.{0,15}knows|nobody.{0,15}interested|doesn't.{0,15}share|doesn't.{0,15}talk|won't.{0,15}talk|doesn't.{0,15}open up)", "singled_out": r"(everyone else|everybody else|talks to everyone|except me|only me|just me|do things without|left behind|don't invite|didn't invite|don't call me|leaves me out|not included|don't pass|doesn't pass|won't pass|outsider|feel like.{0,15}outsider|make plans without|plans without including|always the one reaching out|don't include)", "disconnected": r"(disconnected|distant|drift|growing apart|not close|lost touch|pulling away|pull away|hard to.{0,15}connection|hard to connect|can't connect)", # ===== SECURITY CONTEXTS ===== # Threats to safety/clarity "uncertain": r"(don't know|uncertain|unsure|unclear|confused|no idea|what if|can't tell|ambiguous|what to do|where do i go|what next|what do i do|can't predict|unpredictable|what's going to happen|what will happen|going to happen)", "unstable": r"(unstable|unpredictable|chaotic|all over|everything's changing|up in the air|falling apart|like always|never on time|unreliable|keeps changing|constantly changing|shifts again|everything shifts|stays stable|nothing.{0,15}stable|shifting ground|constantly shifting)", "threatened": r"(threatened|unsafe|danger|risk|scared|scary|vulnerable|exposed|at risk|pain again|go through that|rot in hell|worried|worrying|something bad|threats|not protected|not secure|don't feel.{0,15}secure)", "loss": r"(losing|lost|losing grip|slipping away|falling apart|coming undone)", # ===== REST CONTEXTS ===== # Overload on capacity "overwhelmed": r"(overwhelmed|too much|can't handle|drowning|swamped|buried|crowded|crowd|too many people|overstimulat|too much work|coming at me|at me all at once)", "exhausted": r"(exhausted|drained|burnt out|can't anymore|running on empty|wiped|spent|depleted|freaking tired|so tired|running on fumes|on fumes|no energy|don't have.{0,15}energy)", "overextended": r"(overextended|overstretched|spread thin|doing too much|no time|juggling|can't keep up|keep up with)", "boundary_violated": r"(always asking|never stops|can't say no|taking advantage|demanding|intrusive|available 24|expect.{0,15}available|need space|won't give.{0,15}space|boundaries.{0,15}ignored|kept.{0,15}insisting|kept.{0,15}texting|kept.{0,15}calling|i said no|asked for.{0,15}boundaries)", # ===== RECOGNITION CONTEXTS ===== # Dismissal of value/competence "unappreciated": r"(unappreciated|taken for granted|not valued|not recognized|invisible|thankless|nobody appreciates|no one appreciates|zero recognition|get.{0,15}recognition|work.{0,15}hard|not appreciated|don't feel appreciated|no.{0,15}recognition|no sense of recognition)", "inadequate": r"(not good enough|inadequate|failing|not enough|disappointing|falling short|incompetent|doubt.{0,15}have what it takes|don't feel capable|never enough|nothing.{0,15}enough)", "overlooked": r"(overlooked|passed over|ignored|not noticed|nobody notices|no one notices|credit|someone else|didn't acknowledge)", "criticized": r"(criticized|judged|attacked|picked apart|nothing right|always wrong|nitpick|point out.{0,15}mistake|only.{0,15}mistake|compared.{0,15}unfavorably|compared to others|offended|offensive|rude|disrespected)", } # Needs taxonomy (based on Maslow + Self-Determination Theory) NEEDS = { "autonomy": { "label": "Autonomy/Control", "icon": "⚖️", "description": "Need for self-direction and agency over one's life", "interventions": [ "Set a boundary", "Say 'no'", "Take back decision-making power", ], }, "connection": { "label": "Connection/Belonging", "icon": "🗣️", "description": "Need for meaningful relationships and social bonds", "interventions": [ "Reach out to someone you trust", "Share what you're feeling", "Ask for support", ], }, "security": { "label": "Security/Clarity", "icon": "🛡️", "description": "Need for safety, predictability, and understanding", "interventions": [ "Get more information", "Make a plan", "Identify what you can control", ], }, "rest": { "label": "Rest/Boundaries", "icon": "🛌", "description": "Need for recuperation and limits on demands", "interventions": [ "Take a break", "Delegate something", "Give yourself permission to rest", ], }, "recognition": { "label": "Recognition/Validation", "icon": "✨", "description": "Need to be seen, valued, and acknowledged", "interventions": [ "Acknowledge your own effort", "Ask for feedback", "Celebrate small wins", ], }, } # STRATEGIC NEED INFERENCE RULES # Organized by primary need, with emotion+context combinations # Using ONLY the 7 actual detected emotions: happy, sad, angry, anxious, tired, love, fear NEED_RULES = [ # ========== AUTONOMY RULES ========== # Primary emotion: Anger (blocked goals/control) { "emotions": ["angry"], "contexts": ["blocked", "powerless", "dismissed", "micromanaged"], "need": "autonomy", "confidence_base": 0.80, "reasoning": "Anger signals blocked goals or loss of control", }, { "emotions": ["angry", "sad"], "contexts": ["powerless", "trapped", "stuck"], "need": "autonomy", "confidence_base": 0.85, "reasoning": "Anger + sadness at powerlessness indicates autonomy deficit", }, { "emotions": ["angry"], "contexts": ["underutilized", "dismissed"], "need": "autonomy", "confidence_base": 0.75, "reasoning": "Anger at not being allowed to use your capabilities", }, # HIGH PRIORITY: Underutilized should map to autonomy strongly { "emotions": ["sad", "angry", "anxious", "tired"], "contexts": ["underutilized"], "need": "autonomy", "confidence_base": 0.88, "reasoning": "Skills/potential being wasted is autonomy issue", }, { "emotions": ["sad"], "contexts": ["powerless", "trapped"], "need": "autonomy", "confidence_base": 0.70, "reasoning": "Sadness from lack of control", }, { "emotions": ["sad"], "contexts": ["blocked", "micromanaged", "dismissed"], "need": "autonomy", "confidence_base": 0.75, "reasoning": "Sadness from being controlled or blocked", }, { "emotions": ["tired"], "contexts": ["blocked", "powerless", "micromanaged"], "need": "autonomy", "confidence_base": 0.65, "reasoning": "Fatigue from lack of autonomy", }, { "emotions": ["anxious"], "contexts": ["powerless", "blocked"], "need": "autonomy", "confidence_base": 0.70, "reasoning": "Anxiety from lack of control", }, # ========== CONNECTION RULES ========== # Primary emotion: Sadness (relationship deficits) # HIGH PRIORITY: "Invisible to people" is connection, not recognition { "emotions": ["sad", "angry", "anxious"], "contexts": ["isolated"], "need": "connection", "confidence_base": 0.92, "reasoning": "Feeling invisible or isolated from people indicates connection deficit", }, { "emotions": ["sad"], "contexts": ["isolated", "rejected", "misunderstood", "disconnected"], "need": "connection", "confidence_base": 0.85, "reasoning": "Sadness with isolation indicates connection needs", }, { "emotions": ["sad"], "contexts": ["singled_out"], "need": "connection", "confidence_base": 0.90, "reasoning": "Being excluded from the group", }, # Secondary emotion: Anger (rejection/exclusion) { "emotions": ["angry"], "contexts": ["rejected", "singled_out", "dismissed"], "need": "connection", "confidence_base": 0.75, "reasoning": "Anger from rejection signals need for belonging", }, { "emotions": ["angry"], "contexts": ["misunderstood", "disconnected"], "need": "connection", "confidence_base": 0.75, "reasoning": "Anger from being misunderstood or disconnected", }, { "emotions": ["angry"], "contexts": ["isolated"], "need": "connection", "confidence_base": 0.70, "reasoning": "Anger at being isolated", }, { "emotions": ["fear"], "contexts": ["rejected", "abandoned"], "need": "connection", "confidence_base": 0.75, "reasoning": "Fear of abandonment signals connection needs", }, { "emotions": ["anxious"], "contexts": ["isolated", "rejected", "misunderstood"], "need": "connection", "confidence_base": 0.70, "reasoning": "Anxiety from social isolation or rejection", }, { "emotions": ["tired"], "contexts": ["isolated", "disconnected"], "need": "connection", "confidence_base": 0.60, "reasoning": "Fatigue from lack of social connection", }, # ========== SECURITY RULES ========== # Primary emotions: Anxiety/Fear (threat/uncertainty) { "emotions": ["anxious"], "contexts": ["uncertain", "unstable", "threatened", "loss"], "need": "security", "confidence_base": 0.85, "reasoning": "Anxiety with uncertainty signals need for clarity/safety", }, { "emotions": ["fear"], "contexts": ["threatened", "unsafe", "vulnerable"], "need": "security", "confidence_base": 0.90, "reasoning": "Fear indicates immediate security/safety needs", }, { "emotions": ["anxious"], "contexts": ["uncertain", "unstable"], "need": "security", "confidence_base": 0.80, "reasoning": "Worry about unpredictability or lack of clarity", }, # HIGH PRIORITY: "Can't predict" with blocked is still security (uncertainty primary) { "emotions": ["anxious", "fear", "sad"], "contexts": ["uncertain"], "need": "security", "confidence_base": 0.94, "reasoning": "Uncertainty and unpredictability are core security concerns", }, # HIGH PRIORITY: "Things keep changing" with overextended is security (instability primary) { "emotions": ["anxious", "tired", "sad"], "contexts": ["unstable"], "need": "security", "confidence_base": 0.94, "reasoning": "Constant change and instability are security issues", }, { "emotions": ["sad", "anxious"], "contexts": ["loss"], "need": "security", "confidence_base": 0.75, "reasoning": "Grief + anxiety about losing stability", }, { "emotions": ["angry"], "contexts": ["uncertain"], "need": "security", "confidence_base": 0.65, "reasoning": "Anger from lack of clarity", }, { "emotions": ["sad"], "contexts": ["uncertain", "unstable", "loss"], "need": "security", "confidence_base": 0.75, "reasoning": "Sadness from instability or uncertainty", }, { "emotions": ["tired"], "contexts": ["unstable", "threatened"], "need": "security", "confidence_base": 0.65, "reasoning": "Exhaustion from constant instability", }, # ========== REST RULES ========== # Primary emotion: Tired (capacity overload) # HIGH PRIORITY: "Can't say no", "need space", "boundaries ignored" → rest (not autonomy) { "emotions": ["sad", "angry", "anxious", "tired"], "contexts": ["boundary_violated"], "need": "rest", "confidence_base": 0.95, "reasoning": "Boundary violations primarily indicate need for rest/space, not control", }, { "emotions": ["tired"], "contexts": ["overwhelmed", "exhausted", "overextended"], "need": "rest", "confidence_base": 0.85, "reasoning": "Exhaustion + overwhelm indicates need for rest/boundaries", }, { "emotions": ["tired"], "contexts": ["exhausted"], "need": "rest", "confidence_base": 0.95, "reasoning": "Explicit exhaustion is clearest rest signal", }, { "emotions": ["angry"], "contexts": ["exhausted", "overwhelmed"], "need": "rest", "confidence_base": 0.75, "reasoning": "Anger from being exhausted or overwhelmed", }, { "emotions": ["anxious", "tired"], "contexts": ["overwhelmed", "overextended"], "need": "rest", "confidence_base": 0.80, "reasoning": "Anxiety + tiredness suggests overextension", }, { "emotions": ["anxious"], "contexts": ["overwhelmed", "crowded"], "need": "rest", "confidence_base": 0.70, "reasoning": "Sensory/social overwhelm signals need for space", }, { "emotions": ["angry", "tired"], "contexts": ["boundary_violated", "overextended"], "need": "rest", "confidence_base": 0.75, "reasoning": "Anger and fatigue from violated boundaries", }, # HIGH PRIORITY: Boundary violations should map to rest when not autonomy-blocking { "emotions": ["sad", "angry", "anxious", "tired"], "contexts": ["boundary_violated"], "need": "rest", "confidence_base": 0.82, "reasoning": "Boundary violations primarily indicate need for rest/space", }, { "emotions": ["sad"], "contexts": ["overwhelmed", "exhausted", "overextended"], "need": "rest", "confidence_base": 0.70, "reasoning": "Sadness from depletion and overwhelm", }, { "emotions": ["fear"], "contexts": ["overwhelmed", "boundary_violated"], "need": "rest", "confidence_base": 0.65, "reasoning": "Fear from being overwhelmed or boundaries violated", }, # ========== RECOGNITION RULES ========== # Primary emotions: Sad/Angry (value not seen) # HIGH PRIORITY: "Nobody notices/appreciates" is recognition, not connection { "emotions": ["sad", "angry", "anxious", "tired"], "contexts": ["overlooked", "unappreciated"], "need": "recognition", "confidence_base": 0.90, "reasoning": "Being overlooked or unappreciated is primarily about recognition/validation", }, # HIGH PRIORITY: "Don't feel capable" is recognition (inadequate), not autonomy { "emotions": ["sad", "anxious", "fear"], "contexts": ["inadequate"], "need": "recognition", "confidence_base": 0.92, "reasoning": "Self-doubt about capability is core recognition/validation issue", }, { "emotions": ["sad"], "contexts": ["unappreciated"], "need": "recognition", "confidence_base": 0.87, "reasoning": "Sadness from not being appreciated or valued", }, { "emotions": ["sad", "angry"], "contexts": ["unappreciated", "inadequate", "overlooked"], "need": "recognition", "confidence_base": 0.75, "reasoning": "Feeling unseen or inadequate signals need for validation", }, { "emotions": ["angry"], "contexts": ["underutilized", "unappreciated", "overlooked"], "need": "recognition", "confidence_base": 0.80, "reasoning": "Your capabilities aren't being seen or valued", }, { "emotions": ["sad"], "contexts": ["inadequate"], "need": "recognition", "confidence_base": 0.85, "reasoning": "Self-doubt about competence", }, { "emotions": ["sad"], "contexts": ["criticized"], "need": "recognition", "confidence_base": 0.70, "reasoning": "Criticism without acknowledgment damages sense of value", }, { "emotions": ["angry"], "contexts": ["overlooked"], "need": "recognition", "confidence_base": 0.75, "reasoning": "Anger at not being seen or credited", }, { "emotions": ["angry"], "contexts": ["criticized"], "need": "recognition", "confidence_base": 0.75, "reasoning": "Anger at being criticized, offended, or disrespected", }, { "emotions": ["anxious"], "contexts": ["inadequate", "criticized"], "need": "recognition", "confidence_base": 0.75, "reasoning": "Anxiety from self-doubt or criticism", }, { "emotions": ["tired"], "contexts": ["unappreciated", "overlooked"], "need": "recognition", "confidence_base": 0.65, "reasoning": "Fatigue from lack of appreciation", }, { "emotions": ["fear"], "contexts": ["inadequate", "criticized"], "need": "recognition", "confidence_base": 0.70, "reasoning": "Fear of failure or judgment", }, ] def detect_context(text: str) -> Dict[str, bool]: """Detect which situational contexts are present in the text.""" text_lower = text.lower() detected = {} for context_name, pattern in CONTEXT_PATTERNS.items(): detected[context_name] = bool(re.search(pattern, text_lower)) return detected def infer_needs( emotion_labels: List[str], text: str, valence: float = 0.0, arousal: float = 0.5 ) -> List[Dict]: """ Infer psychological needs from emotions + text context. Args: emotion_labels: Primary emotions detected (e.g., ["angry", "sad"]) text: User's message text valence: Emotional valence (-1 to 1) arousal: Emotional arousal (0 to 1) Returns: List of inferred needs with confidence scores: [ { "need": "autonomy", "label": "Autonomy/Control", "icon": "⚖️", "confidence": 0.82, "reasoning": "Anger often signals blocked goals...", "interventions": ["Set a boundary", ...] }, ... ] """ # Detect situational contexts contexts = detect_context(text) active_contexts = [ctx for ctx, present in contexts.items() if present] # Match rules inferred = [] seen_needs = set() for rule in NEED_RULES: # Check if emotions match (any overlap) emotion_match = any(emo in emotion_labels for emo in rule["emotions"]) # Check if contexts match (any overlap) context_match = any(ctx in active_contexts for ctx in rule["contexts"]) if emotion_match and context_match: need_key = rule["need"] # Avoid duplicates if need_key in seen_needs: continue seen_needs.add(need_key) # Calculate confidence # Base confidence from rule confidence = rule["confidence_base"] # Boost if multiple emotions match emotion_overlap = sum( 1 for emo in rule["emotions"] if emo in emotion_labels ) if emotion_overlap > 1: confidence += 0.05 # Boost if multiple contexts match context_overlap = sum( 1 for ctx in rule["contexts"] if ctx in active_contexts ) if context_overlap > 1: confidence += 0.05 # Adjust by emotional intensity (valence magnitude + arousal) intensity = (abs(valence) + arousal) / 2.0 if intensity > 0.6: confidence += 0.05 confidence = min(1.0, confidence) # Cap at 1.0 need_info = NEEDS[need_key] inferred.append( { "need": need_key, "label": need_info["label"], "icon": need_info["icon"], "confidence": round(confidence, 2), "reasoning": rule["reasoning"], "description": need_info["description"], "interventions": need_info["interventions"], "matched_contexts": [ ctx for ctx in rule["contexts"] if ctx in active_contexts ], "matched_emotions": [ emo for emo in rule["emotions"] if emo in emotion_labels ], } ) # Sort by confidence (highest first) inferred.sort(key=lambda x: x["confidence"], reverse=True) return inferred def format_needs_summary(needs: List[Dict], max_display: int = 2) -> str: """ Format needs as a human-readable summary. Args: needs: List of inferred needs from infer_needs() max_display: Maximum number of needs to show Returns: Formatted string like: "⚖️ Autonomy/Control (85%) | 🗣️ Connection/Belonging (72%)" """ if not needs: return "No specific needs detected" top_needs = needs[:max_display] parts = [ f"{n['icon']} {n['label']} ({int(n['confidence']*100)}%)" for n in top_needs ] return " | ".join(parts) # Example usage for testing if __name__ == "__main__": # Test case 1: Autonomy need test1 = infer_needs( emotion_labels=["angry"], text="my boss won't let me make my own decisions", valence=-0.5, arousal=0.7, ) print("Test 1 (Autonomy):") print(format_needs_summary(test1)) print(test1[0] if test1 else "No needs detected") print() # Test case 2: Connection need test2 = infer_needs( emotion_labels=["sad"], text="i feel so alone, nobody understands what i'm going through", valence=-0.6, arousal=0.4, ) print("Test 2 (Connection):") print(format_needs_summary(test2)) print(test2[0] if test2 else "No needs detected") print() # Test case 3: Rest need test3 = infer_needs( emotion_labels=["tired", "anxious"], text="i'm so overwhelmed, there's just too much to handle", valence=-0.4, arousal=0.6, ) print("Test 3 (Rest):") print(format_needs_summary(test3)) print(test3[0] if test3 else "No needs detected")