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 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("""
Need goal ideas? Click here - 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
""", 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("

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

", unsafe_allow_html=True)