|
|
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 |
|
|
|
|
|
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." |
|
|
""") |
|
|
|
|
|
|
|
|
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(""" |
|
|
<details> |
|
|
<summary><strong>Need goal ideas? Click here</strong></summary> |
|
|
|
|
|
- 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 |
|
|
</details> |
|
|
""", 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: |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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/) |
|
|
|
|
|
--- |
|
|
""") |
|
|
|
|
|
|
|
|
if st.session_state.setup_complete: |
|
|
|
|
|
|
|
|
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. |
|
|
|
|
|
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.""" |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|