|
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, 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: |
|
|
|
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-3-opus-20240229", |
|
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): |
|
|
|
st.session_state.messages = [] |
|
st.session_state.in_debrief = True |
|
|
|
|
|
system_msg = next((msg for msg in st.session_state.messages if msg["role"] == "system"), None) |
|
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" |
|
|
|
|
|
conversation_transcript = "\n".join([ |
|
f"{msg['role'].capitalize()}: {msg['content']}" |
|
for msg in st.session_state.messages[1:] |
|
]) |
|
|
|
|
|
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-3-opus-20240229", |
|
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-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}") |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|