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("

Practice Difficult Conversations

", unsafe_allow_html=True) st.markdown("

With Your Attachment Style Front and Center!

", unsafe_allow_html=True) # Welcome text and instructions if not st.session_state.setup_complete: st.markdown(""" ## Practice Hard Conversations—Safely. 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://www.yourresonantself.com/attachment-assessment) 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." Complete the simulation setup in the sidebar (desktop) or menu ☰ (mobile) to begin your practice session. """) # Sidebar with setup form with st.sidebar: st.markdown(""" ### Welcome! 👋 Hi, I'm Jocelyn Skillman, LMHC — a clinical therapist, relational design ethicist, and creator of experimental tools that explore how AI can support (not replace) human care. Each tool in this collection is thoughtfully designed to: - Extend therapeutic support between sessions - Model emotional safety and relational depth - Help clients and clinicians rehearse courage, regulation, and repair - Stay grounded in trauma-informed, developmentally sensitive frameworks I use powerful language models like Anthropic's Claude for these tools, chosen for their ability to simulate nuanced human interaction and responsiveness to emotionally complex prompts. As a practicing therapist, I imagine these resources being especially helpful to clinicians like myself — companions in the work of tending to others with insight, warmth, and care. #### Connect With Me 🌐 [jocelynskillman.com](http://www.jocelynskillman.com) 📬 [Substack: Relational Code](https://jocelynskillmanlmhc.substack.com/) --- """) 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?" ) 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") 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 # OpenAI expects the system message as the first message in the list 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 # No need to store system_message separately in session state anymore # if "system_message" in st.session_state: # del st.session_state["system_message"] st.rerun() # Display simulation status if not st.session_state.setup_complete: st.info("Complete the simulation setup in the sidebar (desktop) or menu ☰ (mobile).") else: # 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-3-opus-20240229", 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): # Clear previous conversation state st.session_state.messages = [] st.session_state.in_debrief = True # Get the original setup parameters system_msg = next((msg for msg in st.session_state.messages if msg["role"] == "system"), None) 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" # Get conversation transcript conversation_transcript = "\n".join([ f"{msg['role'].capitalize()}: {msg['content']}" for msg in st.session_state.messages[1:] # Skip system message ]) # 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.""" # 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-3-opus-20240229", 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-3-opus-20240229", 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("

by Jocelyn Skillman LMHC - to learn more check out: jocelynskillmanlmhc.substack.com

", unsafe_allow_html=True)