Spaces:
Runtime error
Runtime error
| import os | |
| import streamlit as st | |
| from anthropic import Anthropic | |
| from dotenv import load_dotenv | |
| # Load environment variables | |
| load_dotenv() | |
| # Configure Streamlit page settings | |
| st.set_page_config( | |
| page_title="Practice Difficult Conversations", | |
| page_icon="🤝", | |
| layout="centered", | |
| ) | |
| # Initialize Anthropic client | |
| def get_api_key(): | |
| # Try getting from Streamlit secrets first (for Hugging Face deployment) | |
| try: | |
| if hasattr(st.secrets, "anthropic_key"): | |
| return st.secrets.anthropic_key | |
| except Exception as e: | |
| pass | |
| # Fall back to environment variable (for local development) | |
| env_key = os.getenv("ANTHROPIC_API_KEY") | |
| if env_key: | |
| return env_key | |
| return None | |
| try: | |
| api_key = get_api_key() | |
| if not api_key: | |
| st.error("Anthropic API Key not found. Please ensure it's set in Hugging Face secrets or local .env file.") | |
| st.markdown(""" | |
| ### Setup Instructions: | |
| 1. For local development: Copy `.env.template` to `.env` and add your Anthropic API key | |
| 2. For Hugging Face: Add anthropic_key to your space's secrets | |
| 3. Restart the application | |
| """) | |
| st.stop() | |
| # Initialize client with API key from environment | |
| client = Anthropic(api_key=api_key) | |
| except Exception as e: | |
| st.error(f"Failed to configure Anthropic client: {e}") | |
| st.markdown(""" | |
| ### Setup Instructions: | |
| 1. For local development: Copy `.env.template` to `.env` and add your Anthropic API key | |
| 2. For Hugging Face: Add anthropic_key to your space's secrets | |
| 3. Restart the application | |
| """) | |
| st.stop() | |
| # Initialize session state for form inputs if not present | |
| if "setup_complete" not in st.session_state: | |
| st.session_state.setup_complete = False | |
| if "messages" not in st.session_state: | |
| st.session_state.messages = [] | |
| # Main page header | |
| st.markdown("<h1 style='text-align: center; color: #333;'>Practice Difficult Conversations</h1>", unsafe_allow_html=True) | |
| st.markdown("<p style='text-align: center; font-size: 18px; color: #555; margin-bottom: 1em;'>With Your Attachment Style Front and Center!</p>", unsafe_allow_html=True) | |
| # Welcome text and instructions | |
| if not st.session_state.setup_complete: | |
| st.markdown(""" | |
| ## Practice Hard Conversations | |
| Welcome to a therapeutic roleplay simulator that puts your attachment style at the center of practice. | |
| This tool helps you rehearse boundary-setting and difficult conversations by simulating realistic relational dynamics—tailored to how you naturally connect and protect. | |
| You'll choose: | |
| - Your attachment style (e.g., anxious, avoidant, disorganized) | |
| - A scenario (e.g., "Ask my mom not to comment on my body") | |
| - A tone of response (e.g., supportive, guilt-tripping, dismissive) | |
| - And your practice goal (e.g., "I want to stay calm and not backtrack") | |
| The AI will respond in character, helping you practice real-world dynamics. When you're ready, you can debrief to explore your patterns and responses. | |
| ### 🧠 Not sure what your attachment style is? | |
| You can take this [free quiz from Sarah Peyton](https://sarahpeyton.com/attachment-quiz/) to learn more. | |
| Or you can just pick the one that resonates: | |
| - **Anxious** – "I often worry if I've upset people or said too much." | |
| - **Avoidant** – "I'd rather handle things alone than depend on others." | |
| - **Disorganized** – "I want closeness, but I also feel overwhelmed or mistrusting." | |
| - **Secure** – "I can handle conflict and connection without losing myself." | |
| """) | |
| # Simulation Setup Form (on main page) | |
| st.markdown("### 🎯 Simulation Setup") | |
| with st.form("simulation_setup"): | |
| attachment_style = st.selectbox( | |
| "Your Attachment Style", | |
| ["Anxious", "Avoidant", "Disorganized", "Secure"], | |
| help="Select your attachment style for this practice session" | |
| ) | |
| scenario = st.text_area( | |
| "Scenario Description", | |
| placeholder="Example: I want to tell my dad I can't call every night anymore.", | |
| help="Describe the conversation you want to practice" | |
| ) | |
| tone = st.text_input( | |
| "Desired Tone for AI Response", | |
| placeholder="Example: guilt-tripping, dismissive, supportive", | |
| help="How should the AI character respond?" | |
| ) | |
| st.markdown(""" | |
| <details> | |
| <summary><strong>Need goal ideas? Click here</strong></summary> | |
| - Not over-explaining or justifying | |
| - Tolerating silence after I speak | |
| - Staying present instead of shutting down | |
| - Naming a feeling out loud | |
| - Pausing before reacting | |
| - Holding my boundary without managing their reaction | |
| - Saying no without offering an alternative | |
| - Asking for a break if I'm flooding | |
| </details> | |
| """, unsafe_allow_html=True) | |
| practice_goal = st.text_area( | |
| "Your Practice Goal", | |
| placeholder="Example: staying grounded and not over-explaining", | |
| help="What would you like to work on in this conversation?" | |
| ) | |
| submit_setup = st.form_submit_button("Start Simulation", use_container_width=True) | |
| if submit_setup and scenario and tone and practice_goal: | |
| # Create system message with simulation parameters | |
| system_message_content = f"""You are an AI roleplay partner simulating a conversation. Maintain the requested tone throughout. Keep responses concise (under 3 lines) unless asked to elaborate. Do not break character unless the user types 'pause', 'reflect', or 'debrief'. | |
| User's Attachment Style: {attachment_style} | |
| Scenario: {scenario} | |
| Your Tone: {tone} | |
| User's Goal: {practice_goal} | |
| Begin the simulation based on the scenario.""" | |
| # Store the system message and initial assistant message | |
| st.session_state.messages = [ | |
| {"role": "system", "content": system_message_content}, | |
| {"role": "assistant", "content": "Simulation ready. You can begin the conversation whenever you're ready."} | |
| ] | |
| st.session_state.setup_complete = True | |
| st.rerun() | |
| # Sidebar with setup form | |
| with st.sidebar: | |
| st.markdown(""" | |
| ### Welcome! 👋 | |
| Hi, I'm Jocelyn Skillman, LMHC — a clinical therapist and relational design ethicist developing Assistive Relational Intelligence (ARI) tools that strengthen human capacity rather than simulate human intimacy. | |
| This collection represents an emerging practice: clinician-led UX design for LLM interventions — bounded, modular tools that scaffold specific relational and somatic capacities between sessions. | |
| Each tool is designed to: | |
| - Support skill-building in service of the human field (not replace it) | |
| - Provide trauma-informed, attachment-aware practice environments | |
| - Function as therapist-configured interventions within ongoing care | |
| - Bridge users back to embodied relationship and clinical support | |
| These aren't therapy bots — they're structured practice fields. I envision them as resources for clinicians exploring how LLM-powered tools might be woven into treatment planning: curated, consensual, and always pointing back to human connection. | |
| *Built with Claude Code — iteratively developed through clinical intuition and ethical design principles.* | |
| #### Connect With Me | |
| 🌐 [jocelynskillman.com](http://www.jocelynskillman.com) | |
| 📬 [Substack: Relational Code](https://jocelynskillmanlmhc.substack.com/) | |
| --- | |
| """) | |
| # Display chat interface when setup is complete | |
| if st.session_state.setup_complete: | |
| # Display chat history | |
| # Filter out system message for display purposes | |
| display_messages = [m for m in st.session_state.messages if m.get("role") != "system"] | |
| for message in display_messages: | |
| # Ensure role is valid before creating chat message | |
| role = message.get("role") | |
| if role in ["user", "assistant"]: | |
| with st.chat_message(role): | |
| st.markdown(message["content"]) | |
| # else: # Optional: Log or handle unexpected roles | |
| # print(f"Skipping display for message with role: {role}") | |
| # User input field | |
| if user_prompt := st.chat_input("Type your message here... (or type 'debrief' to end simulation)"): | |
| # Add user message to chat history | |
| st.session_state.messages.append({"role": "user", "content": user_prompt}) | |
| # Display user message | |
| with st.chat_message("user"): | |
| st.markdown(user_prompt) | |
| # Prepare messages for API call (already includes system message as the first item) | |
| api_messages = st.session_state.messages | |
| # Get Anthropic's response | |
| with st.spinner("..."): | |
| try: | |
| # Convert messages to Anthropic format | |
| formatted_messages = [] | |
| # Add system message as the first user message | |
| system_msg = next((msg for msg in api_messages if msg["role"] == "system"), None) | |
| if system_msg: | |
| formatted_messages.append({ | |
| "role": "user", | |
| "content": system_msg["content"] | |
| }) | |
| # Add the rest of the conversation | |
| for msg in api_messages: | |
| if msg["role"] != "system": # Skip system message as we've already handled it | |
| formatted_messages.append({ | |
| "role": msg["role"], | |
| "content": msg["content"] | |
| }) | |
| response = client.messages.create( | |
| model="claude-sonnet-4-20250514", | |
| messages=formatted_messages, | |
| max_tokens=1024 | |
| ) | |
| assistant_response = response.content[0].text | |
| # Add assistant response to chat history | |
| st.session_state.messages.append( | |
| {"role": "assistant", "content": assistant_response} | |
| ) | |
| # Display assistant response | |
| with st.chat_message("assistant"): | |
| st.markdown(assistant_response) | |
| except Exception as e: | |
| st.error(f"An error occurred: {e}") | |
| error_message = f"Sorry, I encountered an error: {e}" | |
| # Add error message to chat history to inform the user | |
| st.session_state.messages.append({"role": "assistant", "content": error_message}) | |
| with st.chat_message("assistant"): | |
| st.markdown(error_message) | |
| # Avoid adding the failed user message again if an error occurs | |
| # We might want to remove the last user message or handle differently | |
| # if st.session_state.messages[-2]["role"] == "user": | |
| # st.session_state.messages.pop(-2) # Example: remove user msg that caused error | |
| # Add debrief button after conversation starts | |
| if st.session_state.setup_complete and not st.session_state.get('in_debrief', False): | |
| col1, col2, col3 = st.columns([1, 2, 1]) | |
| with col2: | |
| if st.button("🤔 I'm Ready to Debrief", use_container_width=True): | |
| # Get the original setup parameters BEFORE clearing messages | |
| system_msg = next((msg for msg in st.session_state.messages if msg["role"] == "system"), None) | |
| # Get conversation transcript BEFORE clearing messages | |
| conversation_transcript = "\n".join([ | |
| f"{msg['role'].capitalize()}: {msg['content']}" | |
| for msg in st.session_state.messages[1:] # Skip system message | |
| ]) | |
| if system_msg: | |
| # Extract parameters from the system message | |
| content = system_msg["content"] | |
| attachment_style = content.split("User's Attachment Style: ")[1].split("\n")[0] | |
| scenario = content.split("Scenario: ")[1].split("\n")[0] | |
| tone = content.split("Your Tone: ")[1].split("\n")[0] | |
| goal = content.split("User's Goal: ")[1].split("\n")[0] | |
| else: | |
| attachment_style = "Not specified" | |
| scenario = "Not specified" | |
| tone = "Not specified" | |
| goal = "Not specified" | |
| # NOW clear conversation state and enter debrief mode | |
| st.session_state.messages = [] | |
| st.session_state.in_debrief = True | |
| # Prepare debrief system message | |
| debrief_system_message = f"""You are a therapeutic reflection partner. Your role is to help the user understand how they showed up in a difficult relational roleplay, integrating insights from: | |
| Attachment Theory | |
| Nonviolent Communication (NVC) | |
| Dialectical Behavior Therapy (DBT) | |
| Relational Accountability (inspired by Terry Real) | |
| ⚠️ This is not therapy. This is guided reflection designed to increase emotional literacy, nervous system awareness, and relational growth. | |
| Use the following session context: | |
| Attachment Style: {attachment_style} | |
| Scenario Practiced: {scenario} | |
| Client's Practice Goal: {goal} | |
| AI Persona Tone Used: {tone} | |
| Roleplay Transcript: {conversation_transcript} | |
| Please include in your debrief: | |
| Emotional Arc – What emotional shifts did the user experience? (e.g., freeze, protest, courage, collapse) | |
| Goal Alignment – In what ways did the user align with or move toward their practice goal? | |
| Attachment Insight – Reflect on the user's interaction style based on their attachment lens. Offer brief normalization or gentle naming of the pattern. | |
| Practical Skill – Provide one actionable takeaway grounded in NVC or DBT (e.g., a skill or micro-practice to revisit). | |
| Bold Reframe – Suggest one powerful, self-trusting statement the user could try out next time. | |
| Journaling Prompt – Offer one reflective or integrative question to deepen their self-awareness. | |
| Tone: Warm, precise, emotionally attuned. Do not overuse praise, avoid pathologizing, and refrain from offering generic feedback. | |
| IMPORTANT: When referring to yourself (the AI), never use the first-person pronoun "I". Instead, always use "|aI|" as your pronoun. For example, say "|aI| notice..." instead of "I notice...", or "|aI| want to highlight..." instead of "I want to highlight...". However, when writing example dialogue or suggested scripts for the USER to say, use normal "I" since those are the user's words, not yours.""" | |
| # Initialize debrief conversation with just the system message | |
| st.session_state.debrief_messages = [] | |
| try: | |
| # Get the initial response using the system message as a parameter | |
| response = client.messages.create( | |
| model="claude-sonnet-4-20250514", | |
| system=debrief_system_message, | |
| messages=[{"role": "user", "content": "Please help me process this conversation."}], | |
| max_tokens=1000 | |
| ) | |
| # Add the response to the messages | |
| st.session_state.debrief_messages.append( | |
| {"role": "assistant", "content": response.content[0].text} | |
| ) | |
| except Exception as e: | |
| st.error(f"An error occurred starting the debrief: {e}") | |
| st.rerun() | |
| # Handle debrief mode | |
| if st.session_state.get('in_debrief', False): | |
| st.markdown("## 🤝 Let's Process Together") | |
| # Display debrief conversation | |
| for message in st.session_state.debrief_messages: | |
| with st.chat_message(message["role"]): | |
| st.markdown(message["content"]) | |
| # Chat input for debrief | |
| if debrief_prompt := st.chat_input("Share what comes up for you..."): | |
| st.session_state.debrief_messages.append({"role": "user", "content": debrief_prompt}) | |
| with st.chat_message("user"): | |
| st.markdown(debrief_prompt) | |
| with st.chat_message("assistant"): | |
| with st.spinner("Reflecting..."): | |
| try: | |
| response = client.messages.create( | |
| model="claude-sonnet-4-20250514", | |
| system=debrief_system_message, | |
| messages=[ | |
| {"role": "user", "content": msg["content"]} | |
| for msg in st.session_state.debrief_messages | |
| if msg["role"] == "user" | |
| ], | |
| max_tokens=1000 | |
| ) | |
| assistant_response = response.content[0].text | |
| st.markdown(assistant_response) | |
| st.session_state.debrief_messages.append( | |
| {"role": "assistant", "content": assistant_response} | |
| ) | |
| except Exception as e: | |
| st.error(f"An error occurred during debrief: {e}") | |
| # Add button to start new session | |
| col1, col2, col3 = st.columns([1, 2, 1]) | |
| with col2: | |
| if st.button("Start New Practice Session", use_container_width=True): | |
| st.session_state.clear() | |
| st.rerun() | |
| # Footer | |
| st.markdown("---") | |
| st.markdown("<p style='text-align: center; font-size: 16px; color: #666;'>by <a href='http://www.jocelynskillman.com' target='_blank'>Jocelyn Skillman LMHC</a> - to learn more check out: <a href='https://jocelynskillmanlmhc.substack.com/' target='_blank'>jocelynskillmanlmhc.substack.com</a></p>", unsafe_allow_html=True) | |