Spaces:
Running
Running
| #!/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") | |
| def get_event_loop(): | |
| """Create and cache event loop.""" | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| return loop | |
| 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="<b>Message %{text}</b><br>Valence: %{x:.2f}<br>Arousal: %{y:.2f}<br>Emotions: %{customdata}<extra></extra>", | |
| customdata=labels, | |
| showlegend=False, | |
| ) | |
| ) | |
| # Add quadrant labels | |
| quadrant_labels = [ | |
| dict(x=0.5, y=0.75, text="High Arousal<br>Positive", color="green"), | |
| dict(x=-0.5, y=0.75, text="High Arousal<br>Negative", color="red"), | |
| dict(x=0.5, y=0.25, text="Low Arousal<br>Positive", color="lightgreen"), | |
| dict(x=-0.5, y=0.25, text="Low Arousal<br>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.* | |
| """) | |