#!/usr/bin/env python3 """Ghost Malone: MCP-powered emotional intelligence chatbot - Streamlit version""" import json import asyncio import os from dotenv import load_dotenv import streamlit as st import plotly.graph_objects as go from utils.orchestrator import get_orchestrator load_dotenv() # Page config st.set_page_config( page_title="Ghost Malone", page_icon="๐Ÿ‘ป", layout="wide" ) # Clear memory on startup for fresh conversations if os.path.exists("memory.json"): os.remove("memory.json") print("๐Ÿงน Cleared previous memory for fresh start") @st.cache_resource def get_event_loop(): """Create and cache event loop.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop @st.cache_resource def boot_orchestrator(): """Bootstrap the orchestrator with all MCP servers.""" loop = get_event_loop() orchestrator = loop.run_until_complete(get_orchestrator()) print("๐Ÿงฐ Ghost Malone orchestrator initialized") return orchestrator def run_async(coro): """Run async coroutine in the persistent event loop.""" loop = get_event_loop() return loop.run_until_complete(coro) def create_emotion_plot(emotion_arc): """Create a Plotly scatter plot showing emotions on valence/arousal grid.""" if not emotion_arc or not emotion_arc.get("trajectory"): # Empty plot with quadrant labels fig = go.Figure() fig.add_trace( go.Scatter( x=[0], y=[0.5], mode="text", text=["No emotional data yet"], textfont=dict(size=14, color="gray"), showlegend=False, ) ) fig.update_layout( title="Emotional Trajectory (Russell's Circumplex)", xaxis=dict(range=[-1, 1], title="Valence (negative โ† โ†’ positive)", zeroline=True), yaxis=dict(range=[0, 1], title="Arousal (calm โ† โ†’ activated)", zeroline=True), height=400, template="plotly_white", ) return fig trajectory = emotion_arc["trajectory"] x_vals = [p["valence"] for p in trajectory] y_vals = [p["arousal"] for p in trajectory] labels = [p["labels"] for p in trajectory] fig = go.Figure() # Plot trajectory with numbered markers fig.add_trace( go.Scatter( x=x_vals, y=y_vals, mode="lines+markers+text", marker=dict(size=12, color="purple", line=dict(width=2, color="white")), line=dict(color="purple", width=2), text=[str(i + 1) for i in range(len(x_vals))], textposition="top center", textfont=dict(size=10, color="white"), hovertemplate="Message %{text}
Valence: %{x:.2f}
Arousal: %{y:.2f}
Emotions: %{customdata}", customdata=labels, showlegend=False, ) ) # Add quadrant labels quadrant_labels = [ dict(x=0.5, y=0.75, text="High Arousal
Positive", color="green"), dict(x=-0.5, y=0.75, text="High Arousal
Negative", color="red"), dict(x=0.5, y=0.25, text="Low Arousal
Positive", color="lightgreen"), dict(x=-0.5, y=0.25, text="Low Arousal
Negative", color="pink"), ] for label in quadrant_labels: fig.add_annotation( x=label["x"], y=label["y"], text=label["text"], showarrow=False, font=dict(size=11, color=label["color"]), bgcolor="rgba(255,255,255,0.7)", borderpad=4, ) fig.update_layout( title="Emotional Trajectory (Russell's Circumplex)", xaxis=dict(range=[-1, 1], title="Valence (negative โ† โ†’ positive)", zeroline=True), yaxis=dict(range=[0, 1], title="Arousal (calm โ† โ†’ activated)", zeroline=True), height=400, template="plotly_white", ) return fig async def process_message(user_message, min_confidence, min_arousal, min_depth): """Process user message through the MCP pipeline.""" orchestrator = boot_orchestrator() # Run the emotional pipeline result = await orchestrator.call_tool( "run_emotional_pipeline", { "user_input": user_message, "intervention_config": { "min_confidence": min_confidence, "min_arousal": min_arousal, "min_depth": min_depth, }, }, ) # Parse results content = result.content[0].text if result.content else "{}" data = json.loads(content) return { "response": data.get("response", "I'm processing your message..."), "emotion": data.get("emotion", {}), "need": data.get("need", {}), "intervention": data.get("intervention"), "emotion_arc": data.get("emotion_arc", {}), } # Initialize session state if "messages" not in st.session_state: st.session_state.messages = [] # Title st.title("๐Ÿ‘ป Ghost Malone") st.markdown("*Emotion-aware MCP chatbot with constitutional alignment*") # Sidebar controls with st.sidebar: st.header("โš™๏ธ Intervention Settings") st.markdown("Control when Ghost Malone offers interventions:") min_confidence = st.slider( "Confidence Threshold", min_value=0.0, max_value=1.0, value=0.7, step=0.05, help="Minimum confidence in emotion detection" ) min_arousal = st.slider( "Arousal Threshold", min_value=0.0, max_value=1.0, value=0.4, step=0.05, help="Minimum emotional arousal level" ) min_depth = st.slider( "Conversation Depth", min_value=1, max_value=5, value=2, step=1, help="Minimum number of messages before interventions" ) st.markdown("---") if st.button("๐Ÿงน Clear History"): st.session_state.messages = [] if os.path.exists("memory.json"): os.remove("memory.json") st.rerun() # Main chat area col1, col2 = st.columns([2, 1]) with col1: st.subheader("๐Ÿ’ฌ Chat") # Display chat history for msg in st.session_state.messages: with st.chat_message(msg["role"]): st.markdown(msg["content"]) # Show emotion/need for user messages if msg["role"] == "user" and "metadata" in msg: meta = msg["metadata"] if meta.get("emotion"): st.caption(f"๐ŸŽญ {meta['emotion'].get('labels', '')} (valence: {meta['emotion'].get('valence', 0):.2f}, arousal: {meta['emotion'].get('arousal', 0):.2f})") if meta.get("need"): st.caption(f"๐ŸŽฏ Need: {meta['need'].get('primary_need', '')} ({meta['need'].get('confidence', 0):.2f})") # Show intervention for assistant messages if msg["role"] == "assistant" and "intervention" in msg: if msg["intervention"]: with st.expander("๐Ÿ’ก Intervention Offered"): st.markdown(msg["intervention"]) # Chat input if prompt := st.chat_input("How are you feeling?"): # Add user message st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # Process message with st.chat_message("assistant"): with st.spinner("Thinking..."): result = run_async(process_message(prompt, min_confidence, min_arousal, min_depth)) st.markdown(result["response"]) # Update user message metadata st.session_state.messages[-1]["metadata"] = { "emotion": result["emotion"], "need": result["need"] } # Show metadata if result.get("emotion"): st.caption(f"๐ŸŽญ {result['emotion'].get('labels', '')} (valence: {result['emotion'].get('valence', 0):.2f}, arousal: {result['emotion'].get('arousal', 0):.2f})") if result.get("need"): st.caption(f"๐ŸŽฏ Need: {result['need'].get('primary_need', '')} ({result['need'].get('confidence', 0):.2f})") # Show intervention if present if result.get("intervention"): with st.expander("๐Ÿ’ก Intervention Offered"): st.markdown(result["intervention"]) # Add assistant message st.session_state.messages.append({ "role": "assistant", "content": result["response"], "intervention": result.get("intervention") }) st.rerun() with col2: st.subheader("๐Ÿ“Š Emotional Trajectory") # Get latest emotion arc from last message if st.session_state.messages: # Process current state to get emotion arc try: with st.spinner("Updating chart..."): # Get emotion arc by making a dummy call orchestrator = boot_orchestrator() arc_result = run_async(orchestrator.call_tool("get_emotion_arc", {})) arc_content = arc_result.content[0].text if arc_result.content else "{}" emotion_arc = json.loads(arc_content) fig = create_emotion_plot(emotion_arc) st.plotly_chart(fig, use_container_width=True) except Exception as e: st.error(f"Could not load trajectory: {e}") else: fig = create_emotion_plot({}) st.plotly_chart(fig, use_container_width=True) # Footer st.markdown("---") st.markdown(""" ### ๐ŸŽฏ Try These Examples **Connection needs:** "I feel so isolated and alone" **Autonomy needs:** "I feel so trapped and powerless" **Security needs:** "Everything feels so uncertain" **Rest needs:** "I'm so burnt out and exhausted" **Recognition needs:** "Nobody notices all the work I do" ๐Ÿ’ก *Interventions appear on the 2nd message when thresholds are met.* """)