jostlebot's picture
Update setup instructions to be clear for both mobile and desktop users
18cf37f
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—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("<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)