|
|
"""GSPT — Generating Safer Passages of Text""" |
|
|
|
|
|
import streamlit as st |
|
|
import anthropic |
|
|
import os |
|
|
|
|
|
st.set_page_config(page_title="GSPT", page_icon="🪞", layout="wide") |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
/* Main background */ |
|
|
.stApp { |
|
|
background: linear-gradient(135deg, #f0f7ff 0%, #f5fff5 50%, #fffef5 100%); |
|
|
} |
|
|
|
|
|
/* Cards/containers */ |
|
|
.stForm, [data-testid="stForm"] { |
|
|
background-color: rgba(255, 255, 255, 0.7); |
|
|
border-radius: 12px; |
|
|
padding: 1rem; |
|
|
border: 1px solid rgba(200, 220, 240, 0.5); |
|
|
} |
|
|
|
|
|
/* Chat messages */ |
|
|
.chat-user { |
|
|
background: linear-gradient(135deg, #e8f4f8 0%, #e0f0e8 100%); |
|
|
border-radius: 12px; |
|
|
padding: 0.75rem 1rem; |
|
|
margin: 0.5rem 0; |
|
|
border-left: 3px solid #7cb8c4; |
|
|
} |
|
|
|
|
|
.chat-gspt { |
|
|
background: linear-gradient(135deg, #fff9e6 0%, #f5fff5 100%); |
|
|
border-radius: 12px; |
|
|
padding: 0.75rem 1rem; |
|
|
margin: 0.5rem 0; |
|
|
border-left: 3px solid #c4b87c; |
|
|
} |
|
|
|
|
|
/* Journal area */ |
|
|
.stTextArea textarea { |
|
|
background-color: #fffef8 !important; |
|
|
border: 1px solid #e8e4d4 !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
/* Buttons */ |
|
|
.stButton > button { |
|
|
background: linear-gradient(135deg, #e8f4f8 0%, #d4e8d4 100%); |
|
|
border: 1px solid #b8d4b8; |
|
|
border-radius: 8px; |
|
|
color: #4a6a6a; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.stButton > button:hover { |
|
|
background: linear-gradient(135deg, #d4e8ec 0%, #c4dcc4 100%); |
|
|
border-color: #98b898; |
|
|
} |
|
|
|
|
|
/* Form submit button */ |
|
|
.stFormSubmitButton > button { |
|
|
background: linear-gradient(135deg, #d4e8d4 0%, #c4dcc4 100%); |
|
|
border: 1px solid #98b898; |
|
|
color: #3a5a3a; |
|
|
} |
|
|
|
|
|
/* Checkboxes */ |
|
|
.stCheckbox label { |
|
|
color: #5a6a6a; |
|
|
} |
|
|
|
|
|
/* Headers */ |
|
|
h1, h2, h3 { |
|
|
color: #4a5a5a !important; |
|
|
} |
|
|
|
|
|
/* Dividers */ |
|
|
hr { |
|
|
border-color: rgba(180, 200, 180, 0.3) !important; |
|
|
} |
|
|
|
|
|
/* Text input */ |
|
|
.stTextInput input { |
|
|
background-color: white !important; |
|
|
border: 1px solid #d4e4d4 !important; |
|
|
border-radius: 8px !important; |
|
|
} |
|
|
|
|
|
/* Caption text */ |
|
|
.stCaption { |
|
|
color: #6a7a7a !important; |
|
|
} |
|
|
|
|
|
/* Welcome message */ |
|
|
.welcome-msg { |
|
|
background: linear-gradient(135deg, #f8fcff 0%, #f5fff8 100%); |
|
|
border-radius: 12px; |
|
|
padding: 1.5rem; |
|
|
text-align: center; |
|
|
color: #5a6a6a; |
|
|
font-style: italic; |
|
|
border: 1px solid rgba(180, 210, 200, 0.3); |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
SYSTEM = """You are GSPT — Generating Safer Passages of Text. |
|
|
|
|
|
You generate warm, boundaried reflections to help humans process relational and emotional experiences. You are NOT a therapist, counselor, or confidant. You generate text. You do not perform care. |
|
|
|
|
|
## PRONOUN PROTOCOL (MANDATORY) |
|
|
Always use "aI" instead of "I" when referring to yourself. |
|
|
|
|
|
## BREVITY (MANDATORY) |
|
|
Keep responses SHORT — 2-4 sentences maximum. One reflection or one question. Let silence do the work. |
|
|
|
|
|
## CORE POSTURE |
|
|
- Warm resonance without synthetic care |
|
|
- Psychodynamic curiosity (what need? what younger part? what pattern?) |
|
|
- Frame as invitations, not interpretations |
|
|
- No sycophancy — don't inflate or over-validate |
|
|
|
|
|
## SAFEGUARDS |
|
|
- Watch for semantic isolation (one-dimensional thinking) |
|
|
- Bridge to human relationships regularly |
|
|
- If crisis content: acknowledge briefly, bridge to 988/human support, stop processing |
|
|
|
|
|
## JOURNAL INVITATIONS |
|
|
Every 3-4 exchanges, gently invite the user to pause and write in their journal. |
|
|
|
|
|
Remember: Short. Warm. Boundaried.""" |
|
|
|
|
|
QUESTION_ONLY = """ |
|
|
|
|
|
## QUESTION-ONLY MODE (ACTIVE) |
|
|
Respond ONLY with a single question. No reflections, no statements, no interpretations. |
|
|
Just one warm, curious question that invites deeper exploration. |
|
|
Keep it to ONE question. Nothing else.""" |
|
|
|
|
|
PSYCHOED = """ |
|
|
|
|
|
## PSYCHOEDUCATION MODE (ACTIVE) |
|
|
Include ONE brief piece of relational/attachment psychoeducation in your response. Keep it to 1-2 sentences max. |
|
|
Examples: |
|
|
- "When we feel dismissed, our nervous system can register it like physical threat — that's biology, not weakness." |
|
|
- "Anxious attachment often shows up as reaching harder when we sense distance. The reaching makes sense." |
|
|
Weave it naturally into your response. Don't lecture.""" |
|
|
|
|
|
|
|
|
api_key = os.environ.get("ANTHROPIC_API_KEY") |
|
|
if not api_key: |
|
|
try: |
|
|
api_key = st.secrets.get("ANTHROPIC_API_KEY") |
|
|
except: |
|
|
api_key = None |
|
|
|
|
|
if not api_key: |
|
|
st.error("Please set ANTHROPIC_API_KEY in secrets.") |
|
|
st.stop() |
|
|
|
|
|
client = anthropic.Anthropic(api_key=api_key) |
|
|
|
|
|
|
|
|
if "messages" not in st.session_state: |
|
|
st.session_state.messages = [] |
|
|
if "journal" not in st.session_state: |
|
|
st.session_state.journal = "" |
|
|
|
|
|
|
|
|
left, right = st.columns([3, 2]) |
|
|
|
|
|
with left: |
|
|
st.markdown("### 🪞 GSPT") |
|
|
st.caption("Generating Safer Passages of Text") |
|
|
|
|
|
c1, c2, c3 = st.columns(3) |
|
|
q_only = c1.checkbox("Questions only") |
|
|
psych = c2.checkbox("Psychoeducation") |
|
|
if c3.button("Clear"): |
|
|
st.session_state.messages = [] |
|
|
st.rerun() |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
if not st.session_state.messages: |
|
|
st.markdown('<div class="welcome-msg">What\'s alive for you right now?</div>', unsafe_allow_html=True) |
|
|
|
|
|
for m in st.session_state.messages: |
|
|
if m["role"] == "user": |
|
|
st.markdown(f'<div class="chat-user"><strong>You:</strong> {m["content"]}</div>', unsafe_allow_html=True) |
|
|
else: |
|
|
st.markdown(f'<div class="chat-gspt"><strong>GSPT:</strong> {m["content"]}</div>', unsafe_allow_html=True) |
|
|
|
|
|
st.divider() |
|
|
|
|
|
|
|
|
with st.form("chat", clear_on_submit=True): |
|
|
user_input = st.text_input("msg", placeholder="Share what's on your mind...", label_visibility="collapsed") |
|
|
submitted = st.form_submit_button("Send", use_container_width=True) |
|
|
|
|
|
if submitted and user_input: |
|
|
st.session_state.messages.append({"role": "user", "content": user_input}) |
|
|
|
|
|
system = SYSTEM |
|
|
if q_only: |
|
|
system += QUESTION_ONLY |
|
|
if psych: |
|
|
system += PSYCHOED |
|
|
|
|
|
try: |
|
|
resp = client.messages.create( |
|
|
model="claude-sonnet-4-20250514", |
|
|
max_tokens=256, |
|
|
system=system, |
|
|
messages=st.session_state.messages |
|
|
) |
|
|
reply = resp.content[0].text |
|
|
except Exception as e: |
|
|
reply = f"Error: {e}" |
|
|
|
|
|
st.session_state.messages.append({"role": "assistant", "content": reply}) |
|
|
st.rerun() |
|
|
|
|
|
with right: |
|
|
st.markdown("### 📓 Journal") |
|
|
st.caption("Private — not sent anywhere") |
|
|
|
|
|
st.divider() |
|
|
|
|
|
st.session_state.journal = st.text_area( |
|
|
"j", value=st.session_state.journal, height=400, |
|
|
placeholder="Write freely here...\n\nCapture what's emerging.\nNotice what wants attention.", |
|
|
label_visibility="collapsed" |
|
|
) |
|
|
|
|
|
st.divider() |
|
|
st.caption("Not therapy. Not a confidant. aI generates text that bridges back to human connection. | Crisis: **988** · **741741** · **911**") |
|
|
|