| import os |
| import streamlit as st |
| from anthropic import Anthropic |
| from dotenv import load_dotenv |
|
|
| |
| load_dotenv() |
|
|
| |
| st.set_page_config( |
| page_title="Practice Difficult Conversations", |
| page_icon="🤝", |
| layout="centered", |
| ) |
|
|
| |
| def get_api_key(): |
| |
| try: |
| if hasattr(st.secrets, "anthropic_key"): |
| return st.secrets.anthropic_key |
| except Exception as e: |
| pass |
| |
| |
| 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() |
| |
| |
| 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() |
|
|
| |
| 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 = [] |
|
|
| |
| 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) |
|
|
| |
| 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. |
| """) |
|
|
| |
| 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/) |
| |
| --- |
| """) |
|
|
| 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: |
| |
| 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.""" |
|
|
| |
| |
| 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() |
|
|
|
|
| |
| if not st.session_state.setup_complete: |
| st.info("Complete the simulation setup in the sidebar (desktop) or menu ☰ (mobile).") |
| else: |
| |
| |
| display_messages = [m for m in st.session_state.messages if m.get("role") != "system"] |
| for message in display_messages: |
| |
| role = message.get("role") |
| if role in ["user", "assistant"]: |
| with st.chat_message(role): |
| st.markdown(message["content"]) |
| |
| |
|
|
| |
| if user_prompt := st.chat_input("Type your message here... (or type 'debrief' to end simulation)"): |
| |
| st.session_state.messages.append({"role": "user", "content": user_prompt}) |
|
|
| |
| with st.chat_message("user"): |
| st.markdown(user_prompt) |
|
|
| |
| api_messages = st.session_state.messages |
|
|
| |
| with st.spinner("..."): |
| try: |
| |
| formatted_messages = [] |
| |
| |
| 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"] |
| }) |
| |
| |
| for msg in api_messages: |
| if msg["role"] != "system": |
| 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 |
|
|
| |
| st.session_state.messages.append( |
| {"role": "assistant", "content": 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}" |
| |
| st.session_state.messages.append({"role": "assistant", "content": error_message}) |
| with st.chat_message("assistant"): |
| st.markdown(error_message) |
| |
| |
| |
| |
|
|
| |
| 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): |
| |
| system_msg = next((msg for msg in st.session_state.messages if msg["role"] == "system"), None) |
|
|
| |
| conversation_transcript = "\n".join([ |
| f"{msg['role'].capitalize()}: {msg['content']}" |
| for msg in st.session_state.messages[1:] |
| ]) |
| if system_msg: |
| |
| 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" |
|
|
| |
| st.session_state.messages = [] |
| st.session_state.in_debrief = True |
|
|
| |
| 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.""" |
|
|
| |
| st.session_state.debrief_messages = [] |
| |
| try: |
| |
| 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 |
| ) |
| |
| 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() |
|
|
| |
| if st.session_state.get('in_debrief', False): |
| st.markdown("## 🤝 Let's Process Together") |
| |
| |
| for message in st.session_state.debrief_messages: |
| with st.chat_message(message["role"]): |
| st.markdown(message["content"]) |
| |
| |
| 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}") |
| |
| |
| 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() |
|
|
| |
| 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) |
|
|
|
|
|
|
|
|
|
|