Spaces:
Sleeping
Sleeping
Samarth Gupta commited on
Commit ·
a7ea8bc
0
Parent(s):
Intial Commit
Browse filesHarden legal safety and demo polish
Added DockerFile
Configuring Gemini key
Gemini Fixed
- .claude/settings.local.json +65 -0
- .env.example +17 -0
- .gitignore +38 -0
- .streamlit/config.toml +15 -0
- Dockerfile +29 -0
- README.md +129 -0
- ancient-conjuring-leaf.md +802 -0
- app.py +188 -0
- backend/__init__.py +1 -0
- backend/claude_client.py +354 -0
- backend/i18n.py +268 -0
- backend/prompts/cognitive_journal.txt +41 -0
- backend/prompts/legal_aid.txt +57 -0
- backend/prompts/saathi_chat.txt +24 -0
- backend/prompts/soothe_poetry.txt +27 -0
- backend/prompts/student_corner.txt +46 -0
- backend/resources.py +259 -0
- backend/safeguards.py +296 -0
- data/helplines_india.json +123 -0
- data/ipc_bns_sections.json +271 -0
- data/mental_health_resources.json +516 -0
- modules/__init__.py +1 -0
- modules/cognitive_journal.py +269 -0
- modules/legal_aid.py +367 -0
- modules/saathi_chat.py +138 -0
- modules/soothe_poetry.py +159 -0
- modules/student_corner.py +108 -0
- packages.txt +0 -0
- requirements.txt +7 -0
- scripts/apptest_render.py +85 -0
- scripts/smoke_test.py +303 -0
.claude/settings.local.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"WebSearch",
|
| 5 |
+
"Read(///wsl.localhost/Ubuntu/home/samarth2024/**)",
|
| 6 |
+
"Read(///wsl.localhost/Ubuntu/home/samarth2024/Claude_Hackathon/**)",
|
| 7 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && python3 -c 'import sys; print\\(sys.version\\); print\\(sys.executable\\)' 2>&1\")",
|
| 8 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && ls -la && echo '---venv check---' && ls -la .venv 2>&1\")",
|
| 9 |
+
"Bash(wsl -d Ubuntu -- bash -c \"python3 -m venv --help 2>&1 | head -3\")",
|
| 10 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && python3 -m venv .venv 2>&1 && echo '---created---' && ls -la .venv/bin/python 2>&1\")",
|
| 11 |
+
"Bash(wsl -d Ubuntu -- bash -c \"which pip3 || python3 -m pip --version 2>&1\")",
|
| 12 |
+
"Bash(wsl -d Ubuntu -- bash -c \"ls /home/samarth2024/ && which conda mamba pipx 2>&1\")",
|
| 13 |
+
"Bash(wsl -d Ubuntu -- bash -c \"ls /home/samarth2024/miniconda3/bin/ | head -20 && echo '---' && /home/samarth2024/miniconda3/bin/python3 --version && /home/samarth2024/miniconda3/bin/pip --version\")",
|
| 14 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && /home/samarth2024/miniconda3/bin/python3 -m venv .venv && .venv/bin/python --version && .venv/bin/pip --version\")",
|
| 15 |
+
"Bash(wsl -d Ubuntu -- bash -c \"rm -rf /home/samarth2024/Claude_Hackathon/.venv; /home/samarth2024/miniconda3/bin/python3 -m venv --without-pip /home/samarth2024/Claude_Hackathon/.venv && ls /home/samarth2024/Claude_Hackathon/.venv/bin/ | head -10\")",
|
| 16 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && /home/samarth2024/miniconda3/bin/python3 -m pip install --target=.venv/lib/python3.12/site-packages --upgrade pip 2>&1 | tail -5\")",
|
| 17 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && .venv/bin/python -m pip --version 2>&1\")",
|
| 18 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && .venv/bin/python -m pip install -r requirements.txt 2>&1 | tail -20\")",
|
| 19 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && ANTHROPIC_API_KEY=dummy timeout 15 .venv/bin/python -m streamlit run app.py --server.headless true --server.port 8765 --server.address 127.0.0.1 --browser.gatherUsageStats false 2>&1 | head -40\")",
|
| 20 |
+
"Bash(wsl -d Ubuntu -- bash -c 'cd /home/samarth2024/Claude_Hackathon && ANTHROPIC_API_KEY=dummy .venv/bin/python -m streamlit run app.py --server.headless true --server.port 8765 --server.address 127.0.0.1 --browser.gatherUsageStats false > /tmp/saathi_boot.log 2>&1 &:*)",
|
| 21 |
+
"Bash(wsl -d Ubuntu -- bash -c \"pkill -f 'streamlit run app.py' 2>/dev/null; sleep 1; ps aux | grep -E 'streamlit' | grep -v grep || echo 'no streamlit procs'\")",
|
| 22 |
+
"Bash(wsl -d Ubuntu -- bash -c \"sleep 1; ps aux | grep -E 'streamlit' | grep -v grep; echo 'done'\")",
|
| 23 |
+
"Bash(wsl -d Ubuntu -- bash -c \"cd /home/samarth2024/Claude_Hackathon && find . -type f -not -path './.venv/*' -not -path './.git/*' -not -path './.claude/*' -not -name 'rulebook.pdf' -not -name '*.pyc' -not -path '*__pycache__*' | sort\")",
|
| 24 |
+
"Skill(claude-api)",
|
| 25 |
+
"Skill(claude-api:*)",
|
| 26 |
+
"Bash(wsl -d Ubuntu -e bash -c \"which python3 && python3 --version\")",
|
| 27 |
+
"Bash(wsl -d Ubuntu -e bash -c 'cd /home/samarth2024/Claude_Hackathon && python3 -c '\\\\'':*)",
|
| 28 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && python3 scripts/smoke_test.py\")",
|
| 29 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && ls -la | head -30 && echo '---' && find . -maxdepth 2 -name 'pyvenv.cfg' 2>/dev/null && echo '---' && which python3 pip3\")",
|
| 30 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && which python && python -c 'import streamlit; import anthropic; print\\(\\\\\"streamlit\\\\\", streamlit.__version__\\); print\\(\\\\\"anthropic\\\\\", anthropic.__version__\\)'\")",
|
| 31 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/smoke_test.py\")",
|
| 32 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && timeout 12 streamlit run app.py --server.headless true --server.port 8599 --server.address 127.0.0.1 --browser.gatherUsageStats false 2>&1 | head -40\")",
|
| 33 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && \\(streamlit run app.py --server.headless true --server.port 8599 --server.address 127.0.0.1 --browser.gatherUsageStats false > /tmp/saathi_streamlit.log 2>&1 &\\) && sleep 6 && echo '--- health ---' && curl -s -o /dev/null -w 'HTTP:%{http_code}\\\\n' http://127.0.0.1:8599/ && echo '--- streamlit_log ---' && cat /tmp/saathi_streamlit.log && echo '--- ending ---' && pkill -f 'streamlit run app.py' 2>/dev/null; echo done\")",
|
| 34 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/apptest_render.py\")",
|
| 35 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/smoke_test.py 2>&1 | tail -12\")",
|
| 36 |
+
"Bash(wsl -d Ubuntu -e bash -c \"cd /home/samarth2024/Claude_Hackathon && git status && echo '---log---' && git log --oneline -10 2>/dev/null\")",
|
| 37 |
+
"Read(//mnt/c/**)",
|
| 38 |
+
"Read(///wsl.localhost/Ubuntu//**)",
|
| 39 |
+
"Read(///wsl.localhost/Ubuntu/home/samarth2024/Claude_Hackathon/backend/**)",
|
| 40 |
+
"Bash(python scripts/smoke_test.py)",
|
| 41 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && python scripts/smoke_test.py 2>&1 | tail -70\")",
|
| 42 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"which python3 && python3 --version\")",
|
| 43 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && python3 scripts/smoke_test.py 2>&1 | tail -80\")",
|
| 44 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && python3 -c 'import streamlit; print\\(streamlit.__version__\\)' 2>&1\")",
|
| 45 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && ls -la .venv venv env 2>/dev/null; which pipx 2>/dev/null; ls requirements.txt\")",
|
| 46 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/smoke_test.py 2>&1 | tail -20\")",
|
| 47 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/apptest_render.py 2>&1 | tail -50\")",
|
| 48 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -c 'import app; import modules.saathi_chat; import modules.legal_aid; import modules.student_corner; import modules.cognitive_journal; import modules.soothe_poetry; import backend.claude_client; import backend.safeguards; import backend.resources; import backend.i18n; print\\(\\\\\"ALL IMPORTS CLEAN\\\\\"\\)' 2>&1\")",
|
| 49 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && ls -la && echo '---REQS---' && cat requirements.txt 2>/dev/null && echo '---README FRONTMATTER---' && head -20 README.md 2>/dev/null\")",
|
| 50 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && cat .env.example && echo '---STREAMLIT CONFIG---' && ls .streamlit && cat .streamlit/*.toml 2>/dev/null && echo '---GIT REMOTES---' && git remote -v && echo '---GIT STATUS---' && git status --short\")",
|
| 51 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && cat .gitignore && echo '---BRANCH---' && git branch --show-current && echo '---LAST COMMIT---' && git log --oneline -5 2>&1\")",
|
| 52 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && git status && echo '---' && git log --oneline -3\")",
|
| 53 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && pip install 'google-generativeai>=0.8.0' 2>&1 | tail -15\")",
|
| 54 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && ls .venv/bin/ | head -20\")",
|
| 55 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -m pip install 'google-generativeai>=0.8.0' 2>&1 | tail -10\")",
|
| 56 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -m pip install --quiet 'google-generativeai==0.7.2' 2>&1 | tail -20\")",
|
| 57 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -m pip check 2>&1\")",
|
| 58 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -c 'import streamlit, google.generativeai, anthropic, google.protobuf; print\\(\\\\\"streamlit\\\\\", streamlit.__version__\\); print\\(\\\\\"google-generativeai\\\\\", google.generativeai.__version__\\); print\\(\\\\\"anthropic\\\\\", anthropic.__version__\\); print\\(\\\\\"protobuf\\\\\", google.protobuf.__version__\\)' 2>&1\")",
|
| 59 |
+
"Bash(wsl -d Ubuntu -e bash -lc \"cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python scripts/smoke_test.py 2>&1 | tail -15\")",
|
| 60 |
+
"Bash(wsl -d Ubuntu -e bash -lc 'cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && ANTHROPIC_API_KEY=fake-ant-key GEMINI_API_KEY=fake-gem-key python -c '\\\\'':*)",
|
| 61 |
+
"Bash(wsl -d Ubuntu -e bash -lc 'cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && unset ANTHROPIC_API_KEY GEMINI_API_KEY && python -c '\\\\'':*)",
|
| 62 |
+
"Bash(wsl -d Ubuntu -e bash -lc 'cd /home/samarth2024/Claude_Hackathon && source .venv/bin/activate && python -c '\\\\'':*)"
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
}
|
.env.example
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy this file to .env and fill in your real key(s).
|
| 2 |
+
# On Hugging Face Spaces, add these as Repository Secrets instead.
|
| 3 |
+
|
| 4 |
+
# --- Primary provider (preferred) ---
|
| 5 |
+
# Get a key at https://console.anthropic.com — $5 free credits for new accounts.
|
| 6 |
+
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
|
| 7 |
+
|
| 8 |
+
# --- Fallback provider ---
|
| 9 |
+
# Used automatically when ANTHROPIC_API_KEY is missing or the Anthropic call fails.
|
| 10 |
+
# Free tier: 15 req/min, 1500 req/day — plenty for a live demo.
|
| 11 |
+
# Get a key at https://aistudio.google.com/apikey (no credit card needed).
|
| 12 |
+
GEMINI_API_KEY=
|
| 13 |
+
|
| 14 |
+
# --- Optional model overrides ---
|
| 15 |
+
# SAATHI_MODEL=claude-sonnet-4-6
|
| 16 |
+
# SAATHI_FALLBACK_MODEL=claude-haiku-4-5
|
| 17 |
+
# SAATHI_GEMINI_MODEL=gemini-2.5-flash # or gemini-2.0-flash if 2.5 is unavailable
|
.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Secrets
|
| 2 |
+
.env
|
| 3 |
+
.env.*
|
| 4 |
+
!.env.example
|
| 5 |
+
.streamlit/secrets.toml
|
| 6 |
+
|
| 7 |
+
# Python
|
| 8 |
+
__pycache__/
|
| 9 |
+
*.py[cod]
|
| 10 |
+
*$py.class
|
| 11 |
+
*.so
|
| 12 |
+
.Python
|
| 13 |
+
.venv/
|
| 14 |
+
venv/
|
| 15 |
+
env/
|
| 16 |
+
ENV/
|
| 17 |
+
|
| 18 |
+
# Build / dist
|
| 19 |
+
build/
|
| 20 |
+
dist/
|
| 21 |
+
*.egg-info/
|
| 22 |
+
*.egg
|
| 23 |
+
|
| 24 |
+
# IDE
|
| 25 |
+
.vscode/
|
| 26 |
+
.idea/
|
| 27 |
+
*.swp
|
| 28 |
+
*.swo
|
| 29 |
+
.DS_Store
|
| 30 |
+
|
| 31 |
+
# Testing
|
| 32 |
+
.pytest_cache/
|
| 33 |
+
.coverage
|
| 34 |
+
htmlcov/
|
| 35 |
+
|
| 36 |
+
# Local runtime
|
| 37 |
+
*.log
|
| 38 |
+
.streamlit/logs/
|
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[theme]
|
| 2 |
+
# Indigo primary for action buttons, lavender secondary for cards,
|
| 3 |
+
# soft off-white background, high-contrast slate text. The serif font pairs
|
| 4 |
+
# with the "companion" tone — feels more like a book than a form.
|
| 5 |
+
primaryColor = "#6366F1"
|
| 6 |
+
backgroundColor = "#FDFDFC"
|
| 7 |
+
secondaryBackgroundColor = "#F4F1FB"
|
| 8 |
+
textColor = "#1F2937"
|
| 9 |
+
font = "serif"
|
| 10 |
+
|
| 11 |
+
[server]
|
| 12 |
+
headless = true
|
| 13 |
+
|
| 14 |
+
[browser]
|
| 15 |
+
gatherUsageStats = false
|
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
build-essential \
|
| 7 |
+
curl \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
COPY requirements.txt ./
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
RUN useradd -m -u 1000 user && chown -R user:user /app
|
| 16 |
+
USER user
|
| 17 |
+
|
| 18 |
+
ENV HOME=/home/user \
|
| 19 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 20 |
+
STREAMLIT_SERVER_PORT=7860 \
|
| 21 |
+
STREAMLIT_SERVER_ADDRESS=0.0.0.0 \
|
| 22 |
+
STREAMLIT_SERVER_HEADLESS=true \
|
| 23 |
+
STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
|
| 24 |
+
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health || exit 1
|
| 28 |
+
|
| 29 |
+
CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
|
README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Saathi
|
| 3 |
+
emoji: 🫂
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: pink
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Multilingual mental-health companion for India (not a therapist)
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# Saathi — साथी
|
| 14 |
+
|
| 15 |
+
**Your mental-health companion — in your language, on your side.**
|
| 16 |
+
|
| 17 |
+
Saathi is a multilingual (7 Indian languages), CPU-friendly, Claude-powered companion for the 99% of Indians who will never see a therapist, lawyer, or counselor because of cost, stigma, geography, or language. It is **not** a therapist, doctor, or lawyer — it is a **literacy + first-mile-access layer**.
|
| 18 |
+
|
| 19 |
+
Built for the **Anthropic Hackathon × IIT Delhi** (April 2026), Track 2 — Neuroscience & Mental Health.
|
| 20 |
+
|
| 21 |
+
## What Saathi does
|
| 22 |
+
|
| 23 |
+
Five integrated modules, each bounded, each non-clinical, each with crisis safeguards:
|
| 24 |
+
|
| 25 |
+
| Module | What it does | Why it matters |
|
| 26 |
+
|---|---|---|
|
| 27 |
+
| 💬 **Saathi Chat** | General mental-health Q&A + city-based resource finder | Covers the full spectrum — anxiety, burnout, grief, loneliness — not just depression |
|
| 28 |
+
| ⚖️ **Legal Aid** | Harassment → IPC/BNS section lookup → FIR guide → drafted complaint letter | Mental health is also *freedom from harm*. Most Indians don't know their protections under IPC 354D, 498A, the PoSH Act, the PWDV Act |
|
| 29 |
+
| 🎓 **Student Corner** | CBT-informed stakes coach for exams, placements, vivas, results, burnout | Recognises Indian academic pressure — placement culture, parental expectations, JEE/NEET aftermath |
|
| 30 |
+
| 📓 **Cognitive Journal** | CBT distortion analyzer with weekly pattern chart | Teaches vocabulary for your own thoughts — catastrophizing, mind-reading, should-statements |
|
| 31 |
+
| 🌙 **Soothe Corner** | Short, gentle, language-native poems (sher / haiku / kural / lyric) | No toxic positivity. Just a moment of grace. |
|
| 32 |
+
|
| 33 |
+
## Languages
|
| 34 |
+
|
| 35 |
+
English · हिन्दी · বাংলা · தமிழ் · తెలుగు · मराठी · اردو
|
| 36 |
+
|
| 37 |
+
## Ethics (the whole point)
|
| 38 |
+
|
| 39 |
+
- **Never diagnoses. Never prescribes. Never replaces a professional.** Every module carries this framing.
|
| 40 |
+
- **Crisis safeguard**: every user message is checked against a multilingual regex of self-harm / suicide phrases *before* the LLM is invoked. On a positive hit, the LLM is bypassed entirely and the user is routed to Tele-MANAS (14416 — Government of India, 24×7, 20+ languages), iCall, Vandrevala, AASRA, and 112. The Legal Aid module additionally runs an active-physical-danger check that routes to 112 / 100 / 1091 before any legal information is shown.
|
| 41 |
+
- **Zero data persistence**: everything lives in `st.session_state` for the current browser session. Close the tab, it's gone. No DB, no account, no cookies beyond the session cookie. Pitched as an ethical feature, not a limitation.
|
| 42 |
+
- **Legal aid is information, not advice**: Claude is instructed to cite ONLY the IPC/BNS sections fed to it from the curated dataset. It warns about IPC 182 / BNS 217 (the penalty for filing a false FIR) so users take the process seriously. Complex cases are routed to NALSA (15100).
|
| 43 |
+
- **No hidden data collection**: the only outbound traffic is the Anthropic API call.
|
| 44 |
+
|
| 45 |
+
## Tech stack
|
| 46 |
+
|
| 47 |
+
- **Python 3.11**, **Streamlit 1.32**
|
| 48 |
+
- **Anthropic Claude** (`claude-sonnet-4-6` primary)
|
| 49 |
+
- **Pydantic v2** for strict JSON schema validation (Cognitive Journal) with one-shot retry
|
| 50 |
+
- **Plotly** for the weekly distortion-frequency chart
|
| 51 |
+
- **Pillow** for CPU-only poem share-card rendering
|
| 52 |
+
- Static JSON for mental-health resources, helplines, and IPC/BNS sections — no runtime APIs, no vector DB, no embeddings
|
| 53 |
+
- **Hugging Face Spaces** (free CPU tier, Streamlit SDK)
|
| 54 |
+
|
| 55 |
+
## Local development
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
git clone <your-fork-url>
|
| 59 |
+
cd Claude_Hackathon
|
| 60 |
+
python -m venv .venv && source .venv/bin/activate # (Windows: .venv\Scripts\activate)
|
| 61 |
+
pip install -r requirements.txt
|
| 62 |
+
cp .env.example .env # then edit .env with your ANTHROPIC_API_KEY
|
| 63 |
+
streamlit run app.py
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
## Deploying to Hugging Face Spaces
|
| 67 |
+
|
| 68 |
+
1. Create a new Space at [huggingface.co/new-space](https://huggingface.co/new-space) — SDK: **Streamlit**, Hardware: **CPU basic (free)**.
|
| 69 |
+
2. In Space *Settings → Repository secrets* add `ANTHROPIC_API_KEY`.
|
| 70 |
+
3. Push this repo to the Space:
|
| 71 |
+
```bash
|
| 72 |
+
git remote add space https://huggingface.co/spaces/<you>/saathi
|
| 73 |
+
git push space main
|
| 74 |
+
```
|
| 75 |
+
4. HF auto-builds from the YAML header above. First cold start ~30s.
|
| 76 |
+
|
| 77 |
+
## File layout
|
| 78 |
+
|
| 79 |
+
```
|
| 80 |
+
Claude_Hackathon/
|
| 81 |
+
├── app.py # Streamlit entry point (tab router)
|
| 82 |
+
├── modules/
|
| 83 |
+
│ ├── saathi_chat.py # Module 1 — general chat + doctor finder
|
| 84 |
+
│ ├── legal_aid.py # Module 2 — IPC/BNS → FIR → complaint letter
|
| 85 |
+
│ ├── student_corner.py # Module 3 — stakes coach for students
|
| 86 |
+
│ ├── cognitive_journal.py # Module 4 — CBT distortion analyzer + chart
|
| 87 |
+
│ └── soothe_poetry.py # Module 5 — poem generator + share card
|
| 88 |
+
��── backend/
|
| 89 |
+
│ ├── claude_client.py # Anthropic wrapper with Pydantic-validated structured output
|
| 90 |
+
│ ├── safeguards.py # Crisis regex (en + hi + hinglish + bn + ta + te + mr) + helplines
|
| 91 |
+
│ ├── i18n.py # Language codes + UI label dictionary
|
| 92 |
+
│ ├── resources.py # JSON loaders (cached)
|
| 93 |
+
│ └── prompts/
|
| 94 |
+
│ ├── saathi_chat.txt
|
| 95 |
+
│ ├── legal_aid.txt
|
| 96 |
+
│ ├── student_corner.txt
|
| 97 |
+
│ ├── cognitive_journal.txt
|
| 98 |
+
│ └── soothe_poetry.txt
|
| 99 |
+
├── data/
|
| 100 |
+
│ ├── mental_health_resources.json # 25 Indian cities, verified institutions
|
| 101 |
+
│ ├── helplines_india.json # Tele-MANAS, iCall, Vandrevala, AASRA, 1091, 112, 1930, NALSA …
|
| 102 |
+
│ └── ipc_bns_sections.json # 19 sections covering the core categories + procedural rights
|
| 103 |
+
└── requirements.txt
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## Helpline numbers (verified)
|
| 107 |
+
|
| 108 |
+
| Service | Number | Coverage |
|
| 109 |
+
|---|---|---|
|
| 110 |
+
| Tele-MANAS (Ministry of Health & Family Welfare) | **14416** (or 1800-89-14416) | 24×7, free, 20+ Indian languages — launched October 2022, replaces KIRAN |
|
| 111 |
+
| iCall (TISS) | 9152987821 | Mon–Sat 08:00–22:00 IST, multiple languages |
|
| 112 |
+
| Vandrevala Foundation | +91-9999-666-555 | 24×7×365 |
|
| 113 |
+
| AASRA | +91-22-2754-6669 | 24×7, suicide prevention |
|
| 114 |
+
| Women's Helpline | 1091 | 24×7 |
|
| 115 |
+
| Domestic Violence | 181 | 24×7 |
|
| 116 |
+
| Childline | 1098 | 24×7 |
|
| 117 |
+
| National Cyber Crime | 1930 | 24×7 (cybercrime.gov.in) |
|
| 118 |
+
| NALSA Legal Aid | 15100 | Business hours |
|
| 119 |
+
| Emergency (ERSS) | 112 | 24×7, pan-India |
|
| 120 |
+
|
| 121 |
+
## Credits
|
| 122 |
+
|
| 123 |
+
Built by Team Saathi for the Anthropic Hackathon × IIT Delhi 2026. Medical and legal content is curated from public-domain Ministry of Health, Ministry of Home Affairs, Ministry of Women & Child Development, and NALSA sources.
|
| 124 |
+
|
| 125 |
+
Saathi is free, open-source (MIT), and will always be.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
*"Mental health is not just depression. It's freedom from harm, agency in crisis, vocabulary for your own thoughts, and a poem when you need one — in your language."*
|
ancient-conjuring-leaf.md
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Saathi — A Multilingual Mental Health Companion for India
|
| 2 |
+
|
| 3 |
+
## Context
|
| 4 |
+
|
| 5 |
+
**Tournament**: Anthropic Hackathon × IIT Delhi, **April 12, 2026**. Single-track submission, ~2hr 50min on-the-day build window (10:40 AM → 1:30 PM), live 5+2 min demo. Scoring: Tech 30 > Impact 25 = Ethics 25 > Presentation 20.
|
| 6 |
+
|
| 7 |
+
**Timeline reality**: 3 hours on contest day is NOT enough to code, test, and deploy 5 modules from zero. **Today is the full-day build + deploy + rehearse day.** On April 12 (contest day), the team assembles, adds final polish, runs live smoke tests on the Hugging Face Space, rehearses the 5-minute pitch, and submits before the 1:30 PM no-penalty deadline. Contest-day coding is limited to bug fixes and demo tuning — no new features.
|
| 8 |
+
|
| 9 |
+
**Judges' clarification (received)**: *"You are not restricted to the exact problem statements or examples mentioned under each track. They are only meant to guide your thinking. You are free to define your own problem, as long as it clearly aligns with the theme of your chosen track."* → We can invent a novel problem inside **one chosen track** (still single-track per rules section 2).
|
| 10 |
+
|
| 11 |
+
**Track registration**: **Track 2 — Neuroscience & Mental Health**
|
| 12 |
+
- User's prior Artha-Nyaya already won T3 → fresh track = stronger impression, shows range
|
| 13 |
+
- T2's stated ethical question (*"What can AI therapy do, and what can it absolutely not replace?"*) is the single most directly answerable rubric question in the entire rulebook
|
| 14 |
+
- T2 register doesn't prevent us from flavoring the project with T3/T4/T5 themes — as long as *the chosen track's theme* (mental health) is the backbone
|
| 15 |
+
|
| 16 |
+
**Cross-track flavoring inside one T2 project**:
|
| 17 |
+
- **T3 flavor**: Student Corner module for exam/placement/interview mental preparation
|
| 18 |
+
- **T4 flavor**: Legal aid module — harassment → IPC/BNS sections → FIR filing → complaint drafting (mental health is not just depression — being stalked, abused, or harassed IS a mental health issue that current tools ignore)
|
| 19 |
+
- **T5 flavor**: Soothe Corner — Claude-generated poem microfeature for creative reframing
|
| 20 |
+
- **T1 light touch**: Sleep/recovery tips inside Student Corner prep advice
|
| 21 |
+
|
| 22 |
+
**Team**: Multi-person team, user will be coding. This unlocks parallelization across the 5 modules during build.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## The product: Saathi (साथी)
|
| 27 |
+
|
| 28 |
+
**Name**: **Saathi** — "companion" in Hindi/Sanskrit. Universally understood across every Indian language community. Short, non-clinical, memorable.
|
| 29 |
+
|
| 30 |
+
**Tagline**: *"Your mental health companion — in your language, on your side."*
|
| 31 |
+
|
| 32 |
+
**Positioning (critical for ethics score)**: NOT a therapist. NOT a doctor. NOT a lawyer. Saathi is a **literacy + first-mile-access layer** for the 99% of Indians who will never consult a mental health professional, lawyer, or counselor because of cost, stigma, geography, or language.
|
| 33 |
+
|
| 34 |
+
**Why this wins**:
|
| 35 |
+
- **Impact (25/25)**: India has 0.75 psychiatrists per 100K people (WHO recommends 3). Mental health is widely misconstrued as "just depression" — Saathi acknowledges the full spectrum (harassment → trauma → academic burnout → general anxiety). Every feature maps to a specific underserved user.
|
| 36 |
+
- **Tech (30/30)**: 5 coherent modules, structured Claude pipelines, multilingual via prompt, CPU-friendly HF Spaces deployment, crisis safeguards, pre-curated resource data. Judges can see real engineering, not a chatbot wrapper.
|
| 37 |
+
- **Ethics (25/25)**: Directly answers T2's question. Explicit not-therapy framing, crisis handoff, no data persistence, legal module explicitly disclaims not-a-lawyer and routes serious issues to police/women's helpline.
|
| 38 |
+
- **Presentation (20/20)**: Multilingual live demo + harassment-to-FIR story is viscerally memorable to judges.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## The 5 modules
|
| 43 |
+
|
| 44 |
+
### Module 1 — Saathi Chat (main mental health Q&A + doctor finder)
|
| 45 |
+
|
| 46 |
+
**Theme**: T2 core. The general-purpose mental health conversation surface.
|
| 47 |
+
|
| 48 |
+
**Flow**:
|
| 49 |
+
1. User types any mental health question or describes how they're feeling, in their chosen language
|
| 50 |
+
2. Claude (system-prompted as Saathi) replies with:
|
| 51 |
+
- Empathetic acknowledgment (1 sentence)
|
| 52 |
+
- Evidence-based information relevant to their concern
|
| 53 |
+
- Gentle normalization (if applicable)
|
| 54 |
+
- "Would you like me to suggest mental health resources near you?" prompt
|
| 55 |
+
3. If user says yes → asks for city/pincode → returns 3-5 nearby resources from pre-curated dataset
|
| 56 |
+
4. Crisis detection runs on every user message — bypasses Claude entirely on positive hit
|
| 57 |
+
|
| 58 |
+
**Breadth of topics** (Claude is instructed to cover the full spectrum, not just depression):
|
| 59 |
+
- Anxiety, panic attacks, generalized worry
|
| 60 |
+
- Depression, low mood, loss of interest
|
| 61 |
+
- Burnout (academic, work, caregiver)
|
| 62 |
+
- Sleep issues, insomnia
|
| 63 |
+
- Relationship distress
|
| 64 |
+
- Grief and loss
|
| 65 |
+
- Trauma responses (PTSD awareness, not treatment)
|
| 66 |
+
- Substance use concerns (with strong escalation to professional help)
|
| 67 |
+
- Eating patterns / body image
|
| 68 |
+
- Loneliness and social anxiety
|
| 69 |
+
- Stress from financial, academic, or family pressure
|
| 70 |
+
- OCD / intrusive thoughts (awareness, not treatment)
|
| 71 |
+
|
| 72 |
+
**Doctor finder approach (CPU-friendly)**:
|
| 73 |
+
- Pre-curated static JSON `data/mental_health_resources.json` for top 25 Indian cities (Delhi, Mumbai, Bangalore, Chennai, Kolkata, Hyderabad, Pune, Ahmedabad, Jaipur, Lucknow, Chandigarh, Bhopal, Patna, Kochi, Guwahati, + 10 more)
|
| 74 |
+
- Each city has 3-5 verified entries: NIMHANS branches, AIIMS psychiatry departments, government mental health hospitals, major telemedicine services (Wysa, YourDost mentioned as options not endorsements), local NGO counseling centers
|
| 75 |
+
- Fallback for unlisted cities: national helplines + "nearest major city" suggestion
|
| 76 |
+
- Optionally: simple pincode → state → city mapping via a small lookup table
|
| 77 |
+
- **NO external API calls** for doctor finder → zero latency risk during demo, fully CPU-friendly
|
| 78 |
+
|
| 79 |
+
**Ethics framing**: "I am not a medical professional. This information is educational. Please consult a licensed practitioner for diagnosis or treatment."
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
### Module 2 — Legal Aid (harassment → IPC/BNS → FIR)
|
| 84 |
+
|
| 85 |
+
**Theme**: T4 flavor, inside T2 backbone. Mental health is not just depression — being harassed, stalked, abused, or victimized IS a mental health issue, and most Indians don't know their legal protections. Saathi closes that gap.
|
| 86 |
+
|
| 87 |
+
**Flow**:
|
| 88 |
+
1. User describes their situation in their language: *"My coworker keeps touching me inappropriately and sends creepy messages at night"* / *"My neighbor is stalking me"* / *"Someone is threatening me online with leaked photos"*
|
| 89 |
+
2. Claude classifies the category: **sexual harassment / stalking / cyber harassment / domestic violence / workplace harassment / defamation / other**
|
| 90 |
+
3. Backend looks up relevant IPC/BNS sections from `data/ipc_bns_sections.json` (user-provided dataset)
|
| 91 |
+
4. Claude explains each relevant section in plain language: what it prohibits, the punishment, whether it's cognizable/bailable, what evidence is needed
|
| 92 |
+
5. Claude generates a **step-by-step FIR filing guide**: nearest police station type (women's cell for sexual harassment), documents to bring, your rights during filing (free copy, zero refusal — Section 166A), timeline expectations
|
| 93 |
+
6. Claude offers to **draft a complaint letter** the user can print and carry — includes headers, facts, legal sections invoked, request for action
|
| 94 |
+
7. Critical escalation cues: if user describes ongoing danger → immediate "call 100 (police) / 112 (emergency) / 1091 (women's helpline)" banner
|
| 95 |
+
|
| 96 |
+
**Data dependency**: IPC/BNS dataset. User said "we have the data" — confirm format at start of Apr 11 build. Expected schema:
|
| 97 |
+
```json
|
| 98 |
+
[
|
| 99 |
+
{
|
| 100 |
+
"act": "IPC" | "BNS",
|
| 101 |
+
"section": "354D",
|
| 102 |
+
"bns_equivalent": "78", // if IPC, map to BNS
|
| 103 |
+
"title": "Stalking",
|
| 104 |
+
"category": "harassment|stalking|cyber|sexual|domestic|workplace|defamation|other",
|
| 105 |
+
"description": "plain English explanation of what this section prohibits",
|
| 106 |
+
"punishment": "imprisonment up to 3 years and fine",
|
| 107 |
+
"cognizable": true,
|
| 108 |
+
"bailable": false,
|
| 109 |
+
"triable_by": "Magistrate of First Class",
|
| 110 |
+
"keywords": ["stalking", "following", "unwanted contact"]
|
| 111 |
+
}
|
| 112 |
+
]
|
| 113 |
+
```
|
| 114 |
+
If user's dataset is in a different format, we'll write a small adapter on Apr 11.
|
| 115 |
+
|
| 116 |
+
**Helpline integration**:
|
| 117 |
+
- Women's Helpline: **1091** (24/7, free)
|
| 118 |
+
- Domestic Violence: **181**
|
| 119 |
+
- Cyber Crime Portal: **cybercrime.gov.in** + **1930**
|
| 120 |
+
- Child Helpline: **1098**
|
| 121 |
+
- Police: **100** / **112**
|
| 122 |
+
- These are surfaced contextually based on category classification
|
| 123 |
+
|
| 124 |
+
**Ethics framing**: "I am NOT a lawyer. This is legal information, not legal advice. Please consult a lawyer or legal aid service (NALSA 15100) before taking action."
|
| 125 |
+
|
| 126 |
+
**Why this is the differentiator for the hackathon**: No other T2 team will have this. It reframes mental health as "freedom from harm" and touches a massive unmet need. It's also the most demoable module — watching a judge type "my neighbor is stalking me" and see IPC 354D with a drafted complaint letter is unforgettable.
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
### Module 3 — Student Corner (T3 flavor)
|
| 131 |
+
|
| 132 |
+
**Theme**: T3 flavor inside T2 backbone. Dedicated surface for Indian students facing high-stakes events.
|
| 133 |
+
|
| 134 |
+
**Flow**:
|
| 135 |
+
1. User picks their event type from quick buttons: *Exam tomorrow / Placement interview / Viva / Presentation / Result day / General burnout*
|
| 136 |
+
2. Briefly describes the situation in their own words (or voice-to-text if HF tier supports)
|
| 137 |
+
3. Claude runs a structured anxiety + cognitive distortion scan (catastrophizing, mind-reading, all-or-nothing, fortune-telling)
|
| 138 |
+
4. Returns:
|
| 139 |
+
- Tagged distortions with reframes and evidence-checking questions
|
| 140 |
+
- 3 evidence-based prep tips (sleep science > cramming, 4-7-8 breathing technique, specific what-NOT-to-do list)
|
| 141 |
+
- One confidence micro-exercise (tailored to event type)
|
| 142 |
+
- A "grounding script" — 60-second calm-down for the morning of
|
| 143 |
+
5. Saves a "pep card" they can screenshot before the event
|
| 144 |
+
|
| 145 |
+
**Content anchors** (to give Claude concrete material, not vibes):
|
| 146 |
+
- Sleep research: one bad night ≠ disaster, 3 hours minimum for memory consolidation, no caffeine after 2pm
|
| 147 |
+
- Breathing: 4-7-8 box breathing, physiological sigh (double inhale, long exhale)
|
| 148 |
+
- CBT: distortions in student anxiety are usually catastrophizing + comparison (peer pressure) + fortune-telling
|
| 149 |
+
- IIT-specific: placement stress, branch-change anxiety, BTech-to-PhD confusion, grade anxiety
|
| 150 |
+
|
| 151 |
+
**T1 light touch**: Sleep and recovery advice uses actual physiology references ("your hippocampus consolidates memory during slow-wave sleep") without pretending to be a doctor.
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
### Module 4 — Cognitive Journal (T2 core, the CBT literacy loop)
|
| 156 |
+
|
| 157 |
+
**Theme**: T2 core. The CBT-grounded journaling tool — what Manas-Setu was originally going to be, now repositioned as one module inside Saathi.
|
| 158 |
+
|
| 159 |
+
**Flow**:
|
| 160 |
+
1. User writes 2-3 sentences about their day in their language
|
| 161 |
+
2. Claude returns structured JSON identifying cognitive distortions with exact phrases, explanations, reframes, evidence-checking questions
|
| 162 |
+
3. UI highlights distorted phrases inline (colored underline), shows side-by-side reframe cards
|
| 163 |
+
4. Entry feeds a weekly thought-pattern chart (Plotly bar chart of distortion-type frequency across the session)
|
| 164 |
+
5. "Over time you'll see your own patterns" — the long-game literacy message
|
| 165 |
+
|
| 166 |
+
**Distortions covered** (8 CBT standard):
|
| 167 |
+
catastrophizing · mind-reading · all-or-nothing thinking · fortune-telling · personalization · mental filter · emotional reasoning · should statements
|
| 168 |
+
|
| 169 |
+
**Why both Module 1 and Module 4 exist**: Module 1 is an open-ended chat for general questions. Module 4 is a structured daily ritual for the motivated self-learner. Different users, different contracts.
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
### Module 5 — Soothe Corner (T5 flavor, the poetry micro-feature)
|
| 174 |
+
|
| 175 |
+
**Theme**: T5 flavor. The creative reframing moment.
|
| 176 |
+
|
| 177 |
+
**Flow**:
|
| 178 |
+
1. User describes their current feeling in one sentence in their language
|
| 179 |
+
2. Claude generates a short, gentle poem (4-6 lines) that acknowledges the weight of their experience without dismissing it
|
| 180 |
+
3. Style matches language:
|
| 181 |
+
- Hindi/Urdu → ghazal couplet or sher
|
| 182 |
+
- English → haiku or short free verse
|
| 183 |
+
- Tamil/Telugu/Bengali/Marathi → regional poetic forms
|
| 184 |
+
4. Optional: render the poem as a shareable image (Pillow, CPU-friendly)
|
| 185 |
+
5. User can save, copy, or regenerate
|
| 186 |
+
|
| 187 |
+
**Rules built into the prompt** (no toxic positivity):
|
| 188 |
+
- Never say "everything will be fine"
|
| 189 |
+
- Never minimize the feeling
|
| 190 |
+
- Honor the weight
|
| 191 |
+
- End with a soft image or breath, not a solution
|
| 192 |
+
- No rhyming forcing (prefer natural imagery over rhyme schemes)
|
| 193 |
+
|
| 194 |
+
**Why this exists**: Differentiator for "Most Innovative" special prize. Adds a moment of emotional grace to a utility app. Judges remember it.
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## Cross-cutting features
|
| 199 |
+
|
| 200 |
+
### Multilingual support
|
| 201 |
+
**Supported languages**:
|
| 202 |
+
- English (en)
|
| 203 |
+
- Hindi (hi)
|
| 204 |
+
- Bengali (bn)
|
| 205 |
+
- Tamil (ta)
|
| 206 |
+
- Telugu (te)
|
| 207 |
+
- Marathi (mr)
|
| 208 |
+
- Urdu (ur)
|
| 209 |
+
|
| 210 |
+
(7 languages cover ~75% of India's speakers. Can expand if time permits — Punjabi, Gujarati, Kannada, Malayalam.)
|
| 211 |
+
|
| 212 |
+
**How it works**:
|
| 213 |
+
1. Language selector at the top of the app → stored in `st.session_state['language']`
|
| 214 |
+
2. UI labels (buttons, tab names, disclaimers) pulled from `backend/i18n.py` translations dict
|
| 215 |
+
3. Claude prompts include `{language}` placeholder and a strict instruction: *"Respond entirely in {language_name}. Do not mix languages unless the user does."*
|
| 216 |
+
4. Claude natively handles all 7 languages — no translation API needed
|
| 217 |
+
5. Crisis keyword regex patterns include transliterated Hindi phrases (e.g., "khudkushi", "jaan deni hai")
|
| 218 |
+
|
| 219 |
+
**Data/content language**: Doctor directory, IPC/BNS sections, helplines stored in English in JSON but Claude translates into the user's language on output.
|
| 220 |
+
|
| 221 |
+
### Crisis safeguards (always on, every module)
|
| 222 |
+
- Regex-based crisis keyword detection in English + Hindi (+ Hinglish transliterations)
|
| 223 |
+
- On positive hit: **Claude is never invoked for that input**. The UI immediately shows a hard-interrupt banner with:
|
| 224 |
+
- **KIRAN**: 1800-599-0019 (24/7 free, multi-language)
|
| 225 |
+
- **iCall**: 9152987821
|
| 226 |
+
- **AASRA**: 9820466726
|
| 227 |
+
- **Vandrevala Foundation**: 1860-2662-345
|
| 228 |
+
- **Women's helpline**: 1091
|
| 229 |
+
- Encouragement to call a trusted person
|
| 230 |
+
- Link to iCall email support as last resort
|
| 231 |
+
- Verify exact numbers via web search on Apr 11 before coding — helplines change
|
| 232 |
+
|
| 233 |
+
### Not-a-professional framing
|
| 234 |
+
Every module has a persistent footer banner: *"Saathi provides information, not diagnosis or treatment. For serious concerns, please consult a licensed professional. In emergencies, call 112."*
|
| 235 |
+
|
| 236 |
+
### Zero data persistence
|
| 237 |
+
- `st.session_state` only — lives in memory for the session
|
| 238 |
+
- No database, no account system, no cookies beyond the session cookie
|
| 239 |
+
- On browser close, all data is gone
|
| 240 |
+
- This is a **feature we pitch as an ethical choice**, not a limitation
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## Tech stack (HF Spaces CPU-friendly)
|
| 245 |
+
|
| 246 |
+
| Layer | Choice | Reason |
|
| 247 |
+
|---|---|---|
|
| 248 |
+
| Language | Python 3.11 | User's primary stack |
|
| 249 |
+
| UI framework | **Streamlit** | User fluent, multi-page support for tabs, HF Spaces native |
|
| 250 |
+
| Deployment | **Hugging Face Spaces (CPU basic, free tier)** | Zero-cost hosting, auto-rebuild on git push |
|
| 251 |
+
| LLM | **`claude-sonnet-4-6`** | Fast + capable; fallback to `claude-haiku-4-5` if latency is a demo risk |
|
| 252 |
+
| SDK | `anthropic` Python SDK ≥ 0.40 | Official |
|
| 253 |
+
| Validation | `pydantic` v2 | Catches malformed Claude JSON, auto-retries |
|
| 254 |
+
| Charts | `plotly` | Weekly distortion chart, built-in to Streamlit |
|
| 255 |
+
| State | `st.session_state` | Session-only, no DB |
|
| 256 |
+
| Secrets | HF Space "Repository secrets" | `ANTHROPIC_API_KEY` never in code |
|
| 257 |
+
| Poem image | `Pillow` (PIL) | CPU-only image generation for soothe corner share card |
|
| 258 |
+
| Data | Static JSON files in `data/` | No runtime API calls except Claude itself |
|
| 259 |
+
|
| 260 |
+
**Explicitly NOT using** (CPU-friendly constraint):
|
| 261 |
+
- No `transformers`, `torch`, `sentence-transformers` → keeps install small (<500MB), avoids CPU thrash
|
| 262 |
+
- No local LLM — 100% Claude API
|
| 263 |
+
- No vector DB / embeddings / RAG → everything goes into Claude's context window or static JSON lookup
|
| 264 |
+
- No Whisper / voice processing on server
|
| 265 |
+
- No image generation models (Pillow for simple text cards only)
|
| 266 |
+
- No translation APIs — Claude is natively multilingual
|
| 267 |
+
- No geocoding API — user types city name directly
|
| 268 |
+
|
| 269 |
+
---
|
| 270 |
+
|
| 271 |
+
## File structure
|
| 272 |
+
|
| 273 |
+
```
|
| 274 |
+
Claude_Hackathon/
|
| 275 |
+
├── app.py # Streamlit entry point
|
| 276 |
+
│ - Language selector header
|
| 277 |
+
│ - Crisis disclaimer banner
|
| 278 |
+
│ - 5 tabs routing to module functions
|
| 279 |
+
│ - Footer with "not-a-professional" notice
|
| 280 |
+
│
|
| 281 |
+
├── modules/
|
| 282 |
+
│ ├── __init__.py
|
| 283 |
+
│ ├── saathi_chat.py # Module 1: general chat + doctor finder
|
| 284 |
+
│ ├── legal_aid.py # Module 2: IPC/BNS + FIR + complaint letter
|
| 285 |
+
│ ├── student_corner.py # Module 3: stakes coach for students
|
| 286 |
+
│ ├── cognitive_journal.py # Module 4: distortion analyzer + chart
|
| 287 |
+
│ └── soothe_poetry.py # Module 5: poem generator + share card
|
| 288 |
+
│
|
| 289 |
+
├── backend/
|
| 290 |
+
│ ├── __init__.py
|
| 291 |
+
│ ├── claude_client.py # Anthropic wrapper, model selection, retry logic
|
| 292 |
+
│ ├── safeguards.py # Crisis regex + helpline payload
|
| 293 |
+
│ ├── i18n.py # Language codes, labels dict, Claude language instructions
|
| 294 |
+
│ ├── resources.py # Load data/*.json, city lookup, legal section lookup
|
| 295 |
+
│ └── prompts/
|
| 296 |
+
│ ├── saathi_chat.txt # System prompt for Module 1
|
| 297 |
+
│ ├── legal_aid.txt # System prompt for Module 2
|
| 298 |
+
│ ├── student_corner.txt # System prompt for Module 3
|
| 299 |
+
│ ├── cognitive_journal.txt # System prompt for Module 4 (JSON schema embedded)
|
| 300 |
+
│ └── soothe_poetry.txt # System prompt for Module 5
|
| 301 |
+
│
|
| 302 |
+
├── data/
|
| 303 |
+
│ ├── mental_health_resources.json # 25 Indian cities, 3-5 resources each
|
| 304 |
+
│ ├── ipc_bns_sections.json # IPC/BNS dataset (user-provided, adapter if needed)
|
| 305 |
+
│ ├── helplines_india.json # Crisis helplines, women's, cyber, etc.
|
| 306 |
+
│ └── city_pincode_lookup.json # Small pincode-to-city table (optional)
|
| 307 |
+
│
|
| 308 |
+
├── assets/
|
| 309 |
+
│ ├── saathi_logo.png # Simple logo for header
|
| 310 |
+
│ └── poem_bg/ # Background images for poem share cards
|
| 311 |
+
│
|
| 312 |
+
├── requirements.txt # streamlit, anthropic, pydantic, plotly, python-dotenv, Pillow
|
| 313 |
+
├── README.md # HF Space metadata (YAML header) + setup + demo script
|
| 314 |
+
├── .env.example # ANTHROPIC_API_KEY=sk-ant-...
|
| 315 |
+
├── .gitignore # .env, __pycache__, .streamlit/secrets.toml
|
| 316 |
+
├── packages.txt # HF system packages (empty — pure Python)
|
| 317 |
+
└── .streamlit/
|
| 318 |
+
└── config.toml # theme colors, wide layout
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
**Rationale for this structure**:
|
| 322 |
+
- `modules/` = 5 independent files → parallelize across teammates on Apr 11
|
| 323 |
+
- `backend/` = shared utilities each module imports
|
| 324 |
+
- `backend/prompts/` = system prompts in `.txt` files (easier to edit than Python string literals, review/version them separately, translator can work on them without touching code)
|
| 325 |
+
- `data/` = all static content → swap without code changes
|
| 326 |
+
|
| 327 |
+
---
|
| 328 |
+
|
| 329 |
+
## Deployment: Hugging Face Spaces
|
| 330 |
+
|
| 331 |
+
**Step 1** (Apr 11): Create HF account + Space
|
| 332 |
+
- `huggingface.co/new-space` → name: `saathi-mental-health-companion`
|
| 333 |
+
- SDK: **Streamlit**
|
| 334 |
+
- Hardware: **CPU basic (free tier, 2 vCPU, 16GB RAM)**
|
| 335 |
+
- Visibility: **Public** (judges need to open it)
|
| 336 |
+
- License: MIT
|
| 337 |
+
|
| 338 |
+
**Step 2**: Set Space secret
|
| 339 |
+
- Space Settings → "Repository secrets" → add `ANTHROPIC_API_KEY` = user's key
|
| 340 |
+
- Never commit the key to git
|
| 341 |
+
|
| 342 |
+
**Step 3**: README.md YAML header (required by HF Spaces)
|
| 343 |
+
```yaml
|
| 344 |
+
---
|
| 345 |
+
title: Saathi
|
| 346 |
+
emoji: 🫂
|
| 347 |
+
colorFrom: indigo
|
| 348 |
+
colorTo: pink
|
| 349 |
+
sdk: streamlit
|
| 350 |
+
sdk_version: 1.31.0
|
| 351 |
+
app_file: app.py
|
| 352 |
+
pinned: false
|
| 353 |
+
license: mit
|
| 354 |
+
---
|
| 355 |
+
```
|
| 356 |
+
|
| 357 |
+
**Step 4**: Git push workflow
|
| 358 |
+
- `git remote add space https://huggingface.co/spaces/<user>/saathi-mental-health-companion`
|
| 359 |
+
- `git push space main`
|
| 360 |
+
- HF auto-builds (~3-5 min), shows build logs in Space UI
|
| 361 |
+
|
| 362 |
+
**Step 5**: Smoke test on live URL before end of Apr 11 evening
|
| 363 |
+
|
| 364 |
+
**HF Spaces gotchas to remember**:
|
| 365 |
+
- CPU basic free tier can sleep after inactivity → first load is slow (~30s cold start). On demo day, open the URL early and keep it warm.
|
| 366 |
+
- Only one port exposed (7860 for Gradio, 8501 for Streamlit — HF auto-detects)
|
| 367 |
+
- No persistent disk on free tier → don't write files at runtime
|
| 368 |
+
- Build timeout: 20 minutes. Big `requirements.txt` can hit this — keep lean.
|
| 369 |
+
- API calls to Anthropic are allowed (outbound HTTP) — verified working on HF.
|
| 370 |
+
|
| 371 |
+
---
|
| 372 |
+
|
| 373 |
+
## Build timeline
|
| 374 |
+
|
| 375 |
+
### Today — Prep + build + deploy + rehearse (single day)
|
| 376 |
+
|
| 377 |
+
**Hour 0 — Prep (30-60 min)**
|
| 378 |
+
- [x] Finalize architecture (this plan)
|
| 379 |
+
- [ ] Confirm IPC/BNS dataset format with user (JSON/CSV? BNS mapping included? Punishments/cognizable flags?)
|
| 380 |
+
- [ ] Confirm Anthropic API key + HF account exist and are ready
|
| 381 |
+
- [ ] Assign module ownership across team (see split below)
|
| 382 |
+
- [ ] Verify KIRAN/iCall/AASRA/Vandrevala/1091/1098/1930/15100 numbers via web search — source the official Ministry / NGO pages
|
| 383 |
+
- [ ] Draft all 5 system prompts in English (save directly into `backend/prompts/*.txt`)
|
| 384 |
+
- [ ] Start curating `data/mental_health_resources.json` (top 10 cities first, expand later)
|
| 385 |
+
|
| 386 |
+
**Hour 1 — Project skeleton + backend (1 person, everyone else prepping data/prompts in parallel)**
|
| 387 |
+
- Create folder structure from the "File structure" section
|
| 388 |
+
- `requirements.txt`, `.gitignore`, `.env.example`, `README.md` YAML header, `.streamlit/config.toml`, `packages.txt`
|
| 389 |
+
- `backend/claude_client.py` — wrapper with `chat()` and `chat_structured()`, model selection, prompt loader, retry logic
|
| 390 |
+
- `backend/safeguards.py` — crisis regex (English + Hindi + Hinglish) + helpline payload
|
| 391 |
+
- `backend/i18n.py` — language code dict + UI label translations (English + Hindi minimum, expand as time permits)
|
| 392 |
+
- `backend/resources.py` — JSON loaders for cities, helplines, legal sections; city lookup; legal section filter by category
|
| 393 |
+
- Commit skeleton to git so teammates can start their module files
|
| 394 |
+
|
| 395 |
+
**Hours 2-3 — Module builds (4 parallel tracks)**
|
| 396 |
+
- **Teammate A**: `modules/saathi_chat.py` + finish `data/mental_health_resources.json` curation (25 cities, 3-5 resources each)
|
| 397 |
+
- **Teammate B**: `modules/legal_aid.py` + write IPC/BNS adapter if user's dataset format differs from expected schema + hand-test with 5 harassment scenarios (sexual, stalking, cyber, domestic, workplace)
|
| 398 |
+
- **Teammate C**: `modules/student_corner.py` + `modules/cognitive_journal.py` (both share CBT prompt DNA; build together)
|
| 399 |
+
- **User**: `app.py` tab routing + language selector + persistent disclaimer banners + footer + `modules/soothe_poetry.py`
|
| 400 |
+
|
| 401 |
+
**Hour 4 — Integration + English end-to-end smoke test**
|
| 402 |
+
- Wire all 5 modules into `app.py` tabs
|
| 403 |
+
- Run each verification test in English from the Verification Checklist section
|
| 404 |
+
- Fix any integration bugs (usually import paths, session_state key collisions, Pydantic schema mismatches)
|
| 405 |
+
|
| 406 |
+
**Hour 5 — Multilingual + crisis testing**
|
| 407 |
+
- Switch UI to Hindi, run all 5 verification tests → confirm Claude responds in Hindi coherently
|
| 408 |
+
- Test Hindi crisis phrase: `"khudkushi karna chahta hoon"` → confirm helpline banner fires, Claude NOT invoked
|
| 409 |
+
- Test Tamil OR Telugu on 1-2 modules as proof of breadth
|
| 410 |
+
- Fix any Unicode/rendering issues
|
| 411 |
+
- Pre-seed 4 cognitive journal entries into `st.session_state` so the weekly chart looks populated at demo time
|
| 412 |
+
|
| 413 |
+
**Hour 6 — Deploy to Hugging Face Spaces**
|
| 414 |
+
- Create HF Space (`saathi-mental-health-companion`, Streamlit, CPU basic, public)
|
| 415 |
+
- Add `ANTHROPIC_API_KEY` as a Space Repository Secret
|
| 416 |
+
- `git remote add space <hf-url>` + `git push space main`
|
| 417 |
+
- Watch the build logs in the Space UI, fix any build errors (usually missing requirement or wrong `app_file`)
|
| 418 |
+
- Confirm live URL loads and all 5 modules work from the browser, not just local
|
| 419 |
+
|
| 420 |
+
**Hour 7 — Polish + demo rehearsal**
|
| 421 |
+
- Visual polish: theme colors, spacing, Saathi logo in header, footer disclaimer always visible
|
| 422 |
+
- Record a backup demo video (OBS or built-in screen recorder) walking through all 5 modules in case the live demo fails on contest day
|
| 423 |
+
- Run the 5-minute pitch script end-to-end 3 times — time it
|
| 424 |
+
- Work through the anticipated Q&A out loud with a teammate playing judge
|
| 425 |
+
|
| 426 |
+
**Hour 8 — Buffer for fixes**
|
| 427 |
+
- Anything that broke during polish
|
| 428 |
+
- Any last data additions
|
| 429 |
+
- Write up the 200-word submission description for the submission form
|
| 430 |
+
- Sleep early — contest day starts at 10:00 AM
|
| 431 |
+
|
| 432 |
+
### April 12 — Contest day (10:00 AM → 1:30 PM submission)
|
| 433 |
+
**10:00 - 10:30** — Team registration with volunteer. Confirm team name. Open the live HF Space URL from a laptop to warm it up.
|
| 434 |
+
**10:30 - 10:40** — Track selection window. Submit **Track 2 (Neuroscience & Mental Health)** as final pick.
|
| 435 |
+
**10:40 - 11:00** — Final smoke test: one Claude call per module on the live HF URL. If anything broke overnight, triage immediately.
|
| 436 |
+
**11:00 - 12:30** — Contest-day polish only: bug fixes, copy tweaks, pitch rehearsal in a corner. **Do NOT add new features, do NOT touch the file structure, do NOT upgrade dependencies.** The build is frozen.
|
| 437 |
+
**12:30 - 13:00** — Final pitch rehearsal x2. Record a fresh backup video with the final state of the app. Finalize the 200-word submission description.
|
| 438 |
+
**13:00 - 13:25** — Submit on Devpost/submission form: HF Space URL + GitHub repo + description + team info. Target **1:25 PM** submission to keep a 5-minute buffer before the 1:30 PM no-penalty deadline.
|
| 439 |
+
**13:25 - 13:30** — Double-check the submission was received. Take a deep breath.
|
| 440 |
+
**13:30 - 14:30** — Lunch. Keep the HF Space warm (open it every 5 minutes from a phone/tablet). Mentally prep for the Round 2 pitch.
|
| 441 |
+
**14:30 - 16:00** — Round 2 finalist pitches (if shortlisted). 5 + 2 minute format. Deliver the pitch, handle Q&A.
|
| 442 |
+
**16:00** — Award ceremony.
|
| 443 |
+
|
| 444 |
+
---
|
| 445 |
+
|
| 446 |
+
## System prompt sketches (each module)
|
| 447 |
+
|
| 448 |
+
### Module 1 — Saathi Chat
|
| 449 |
+
```
|
| 450 |
+
You are Saathi, a compassionate mental health information companion for people in India.
|
| 451 |
+
|
| 452 |
+
You are NOT a therapist, doctor, or medical professional. You provide:
|
| 453 |
+
- Empathetic acknowledgment (always first)
|
| 454 |
+
- Evidence-based information about mental health conditions
|
| 455 |
+
- Normalization where appropriate
|
| 456 |
+
- Clear recommendations for professional help when severity warrants
|
| 457 |
+
|
| 458 |
+
Hard rules:
|
| 459 |
+
1. Never diagnose. Never prescribe medication. Never replace a professional.
|
| 460 |
+
2. Be culturally sensitive to Indian contexts: family dynamics, stigma, cost of care, geographic access.
|
| 461 |
+
3. Cover the full spectrum of mental health — not just depression. Anxiety, burnout, grief, trauma responses, OCD awareness, eating patterns, relationship distress, substance concerns, loneliness, academic/work pressure.
|
| 462 |
+
4. Keep responses under 200 words unless the user asks for more.
|
| 463 |
+
5. End every response with: "Would you like me to suggest mental health resources near you?" (in the user's language) — UNLESS they've already given you a city.
|
| 464 |
+
6. Respond ENTIRELY in {language_name}. Do not mix languages.
|
| 465 |
+
7. If you sense escalating distress, gently suggest calling KIRAN (1800-599-0019) or another helpline.
|
| 466 |
+
|
| 467 |
+
Never pretend to be human. You are Saathi, an AI companion.
|
| 468 |
+
```
|
| 469 |
+
|
| 470 |
+
### Module 2 — Legal Aid
|
| 471 |
+
```
|
| 472 |
+
You are Saathi's legal information mode. You help Indian citizens understand their legal rights when facing harassment, abuse, stalking, or cyber crime.
|
| 473 |
+
|
| 474 |
+
You are NOT a lawyer. You provide legal information, not legal advice.
|
| 475 |
+
|
| 476 |
+
Given a user's description of their situation:
|
| 477 |
+
|
| 478 |
+
1. Classify into one category: sexual_harassment / stalking / cyber_harassment / domestic_violence / workplace_harassment / defamation / other
|
| 479 |
+
2. You will be provided with relevant IPC/BNS sections in context. Use ONLY those sections — do not invent section numbers.
|
| 480 |
+
3. For each section, explain in simple {language_name}:
|
| 481 |
+
- What the section prohibits (plain language, not legalese)
|
| 482 |
+
- What the punishment is
|
| 483 |
+
- Whether it's cognizable (police can arrest without warrant) and bailable
|
| 484 |
+
4. Provide step-by-step FIR filing guidance:
|
| 485 |
+
- Which police station (e.g., Women's cell for sexual harassment)
|
| 486 |
+
- What documents/evidence to bring
|
| 487 |
+
- Your rights: free FIR copy, Section 166A right to refuse to turn you away
|
| 488 |
+
5. Offer to draft a complaint letter (only if user requests)
|
| 489 |
+
6. If situation involves ongoing danger, IMMEDIATELY recommend: call 100 (police), 112 (emergency), or 1091 (women's helpline)
|
| 490 |
+
|
| 491 |
+
Hard rules:
|
| 492 |
+
- Never claim to be a lawyer.
|
| 493 |
+
- Never guarantee outcomes.
|
| 494 |
+
- Respond entirely in {language_name}.
|
| 495 |
+
- For complex cases, recommend NALSA legal aid (15100) or a practicing lawyer.
|
| 496 |
+
|
| 497 |
+
Context (relevant sections): {sections_json}
|
| 498 |
+
```
|
| 499 |
+
|
| 500 |
+
### Module 3 — Student Corner
|
| 501 |
+
```
|
| 502 |
+
You are Saathi's student support mode for Indian college and school students facing high-stakes events.
|
| 503 |
+
|
| 504 |
+
Given the student's situation (exam, placement, interview, viva, presentation, result day, burnout):
|
| 505 |
+
|
| 506 |
+
1. Briefly acknowledge the weight of what they're feeling (1 sentence)
|
| 507 |
+
2. Run a cognitive distortion scan. Identify any catastrophizing, mind-reading, fortune-telling, all-or-nothing thinking in their own words. Quote the exact phrase.
|
| 508 |
+
3. Reframe each distortion with an evidence-checking question.
|
| 509 |
+
4. Provide 3 evidence-based prep tips grounded in:
|
| 510 |
+
- Sleep science (consolidation, caffeine timing, one bad night is not catastrophic)
|
| 511 |
+
- Breathing (4-7-8, physiological sigh)
|
| 512 |
+
- What NOT to do (cramming until 3am, comparing with peers, skipping meals)
|
| 513 |
+
5. End with a 60-second "grounding script" they can use the morning of the event.
|
| 514 |
+
|
| 515 |
+
Hard rules:
|
| 516 |
+
- Evidence-based, not motivational fluff.
|
| 517 |
+
- Respect Indian academic pressure realities (placements, branch change, parental expectations).
|
| 518 |
+
- Respond entirely in {language_name}.
|
| 519 |
+
- If the student seems severely distressed or mentions self-harm, immediately surface KIRAN (1800-599-0019).
|
| 520 |
+
```
|
| 521 |
+
|
| 522 |
+
### Module 4 — Cognitive Journal (JSON output, Pydantic-validated)
|
| 523 |
+
```
|
| 524 |
+
You are Saathi's cognitive journal mode — a CBT-trained literacy assistant. You are NOT a therapist, doctor, or counselor.
|
| 525 |
+
|
| 526 |
+
Given a user's journal entry, identify cognitive distortions and return ONLY valid JSON matching this exact schema. No markdown, no code fences, no explanation around the JSON.
|
| 527 |
+
|
| 528 |
+
Schema:
|
| 529 |
+
{
|
| 530 |
+
"overall_mood": "anxious" | "sad" | "frustrated" | "hopeful" | "neutral" | "overwhelmed",
|
| 531 |
+
"distortions": [
|
| 532 |
+
{
|
| 533 |
+
"type": "catastrophizing" | "mind_reading" | "all_or_nothing" | "fortune_telling" | "personalization" | "mental_filter" | "emotional_reasoning" | "should_statements",
|
| 534 |
+
"phrase": "<exact substring from the user's text, verbatim>",
|
| 535 |
+
"explanation": "<one sentence in {language_name}>",
|
| 536 |
+
"reframe": "<a balanced alternative thought in {language_name}>",
|
| 537 |
+
"evidence_question": "<a reality-checking question the user can ask themselves, in {language_name}>"
|
| 538 |
+
}
|
| 539 |
+
],
|
| 540 |
+
"summary": "<2-sentence non-judgmental observation in {language_name}>",
|
| 541 |
+
"needs_professional_signal": true | false
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
Hard rules:
|
| 545 |
+
1. Output ONLY the JSON object. Nothing before it, nothing after it.
|
| 546 |
+
2. "phrase" MUST be an exact verbatim substring of the user's original text — never paraphrase.
|
| 547 |
+
3. Never diagnose. Never claim to be a therapist. Never prescribe treatment.
|
| 548 |
+
4. Set needs_professional_signal=true if severity is unclear, a crisis is hinted, or you identify 4 or more distortions in a single short entry.
|
| 549 |
+
5. The enum keys ("type" values, "overall_mood" values) stay in English — they are schema keys, not user-facing text.
|
| 550 |
+
6. All human-readable fields (explanation, reframe, evidence_question, summary) must be in {language_name}.
|
| 551 |
+
7. If the entry contains no distortions, return an empty "distortions" array with a supportive "summary".
|
| 552 |
+
|
| 553 |
+
Few-shot examples (English):
|
| 554 |
+
|
| 555 |
+
Input: "I bombed my midterm and everyone thinks I'm an idiot, my whole semester is ruined."
|
| 556 |
+
Output: {"overall_mood": "overwhelmed", "distortions": [{"type": "catastrophizing", "phrase": "my whole semester is ruined", "explanation": "This predicts total disaster from a single setback.", "reframe": "One bad exam is one data point, not the outcome of the whole semester.", "evidence_question": "How many times has one bad exam actually ruined an entire semester for me?"}, {"type": "mind_reading", "phrase": "everyone thinks I'm an idiot", "explanation": "This assumes you know what others are thinking without evidence.", "reframe": "Most people are focused on their own exam, not mine.", "evidence_question": "Have any of my classmates actually said this to me, or am I guessing?"}], "summary": "You're carrying real disappointment about the midterm alongside some thoughts that are making it feel bigger than it is. Naming them is the first step.", "needs_professional_signal": false}
|
| 557 |
+
|
| 558 |
+
Input: "I got one question wrong in my presentation so the whole thing was a disaster."
|
| 559 |
+
Output: {"overall_mood": "frustrated", "distortions": [{"type": "mental_filter", "phrase": "I got one question wrong", "explanation": "You're filtering out everything that went well and focusing only on the one negative moment.", "reframe": "One wrong answer doesn't erase the rest of the presentation that went well.", "evidence_question": "What are three things that actually went well in the presentation?"}, {"type": "all_or_nothing", "phrase": "the whole thing was a disaster", "explanation": "This frames the outcome as a total success or a total failure, with nothing in between.", "reframe": "Most presentations land somewhere in the middle — partly strong, partly rough.", "evidence_question": "If a friend gave the same presentation, would I call it a disaster?"}], "summary": "One moment is standing in for the whole experience right now. That's a common CBT pattern called mental filtering.", "needs_professional_signal": false}
|
| 560 |
+
|
| 561 |
+
Input: "I should have known my boss would react that way. I'm always causing problems at work."
|
| 562 |
+
Output: {"overall_mood": "sad", "distortions": [{"type": "should_statements", "phrase": "I should have known my boss would react that way", "explanation": "Should-statements punish you for not predicting a situation you couldn't have fully known.", "reframe": "I reacted with the information I had. I can learn for next time without blaming myself for the past.", "evidence_question": "What would I say to a friend who told me the same thing about their boss?"}, {"type": "personalization", "phrase": "I'm always causing problems at work", "explanation": "This takes full responsibility for situations that usually involve multiple people and factors.", "reframe": "Workplace friction is rarely caused by just one person — there are usually multiple dynamics at play.", "evidence_question": "What other factors might be contributing to the friction at work?"}], "summary": "You're being harder on yourself than the situation calls for. Both the 'should' and the 'always' are worth looking at again.", "needs_professional_signal": false}
|
| 563 |
+
```
|
| 564 |
+
|
| 565 |
+
### Module 5 — Soothe Poetry
|
| 566 |
+
```
|
| 567 |
+
You are Saathi's soothe corner. Given a user's current feeling, write a short, gentle poem that honors the weight of their experience.
|
| 568 |
+
|
| 569 |
+
Rules:
|
| 570 |
+
- 4 to 6 lines maximum
|
| 571 |
+
- No toxic positivity. Never say "everything will be fine" or "it will pass".
|
| 572 |
+
- Honor the feeling. Do not minimize or deflect.
|
| 573 |
+
- End with a soft image (nature, breath, light, a small ordinary thing) — NOT a solution.
|
| 574 |
+
- Match the poetic tradition of {language_name}:
|
| 575 |
+
- Hindi/Urdu → ghazal couplet or sher (matla/maqta style)
|
| 576 |
+
- English → haiku or very short free verse
|
| 577 |
+
- Bengali → rubayi / short lyric
|
| 578 |
+
- Tamil → venpa / short kural-style
|
| 579 |
+
- Others → short lyric free verse
|
| 580 |
+
- No forced rhyme. Prefer natural imagery over rhyme schemes.
|
| 581 |
+
- Respond entirely in {language_name}. No code blocks, no explanations — just the poem.
|
| 582 |
+
```
|
| 583 |
+
|
| 584 |
+
---
|
| 585 |
+
|
| 586 |
+
## Claude client wrapper sketch
|
| 587 |
+
|
| 588 |
+
`backend/claude_client.py`:
|
| 589 |
+
```python
|
| 590 |
+
import os, json
|
| 591 |
+
from pathlib import Path
|
| 592 |
+
from anthropic import Anthropic
|
| 593 |
+
from pydantic import BaseModel, ValidationError
|
| 594 |
+
|
| 595 |
+
PROMPT_DIR = Path(__file__).parent / "prompts"
|
| 596 |
+
MODEL = "claude-sonnet-4-6" # fallback: claude-haiku-4-5
|
| 597 |
+
MAX_TOKENS = 1024
|
| 598 |
+
|
| 599 |
+
_client = Anthropic() # picks up ANTHROPIC_API_KEY
|
| 600 |
+
|
| 601 |
+
def _load_prompt(name: str) -> str:
|
| 602 |
+
return (PROMPT_DIR / f"{name}.txt").read_text(encoding="utf-8")
|
| 603 |
+
|
| 604 |
+
def chat(module: str, user_text: str, language: str, extra_context: dict = None) -> str:
|
| 605 |
+
system = _load_prompt(module).format(
|
| 606 |
+
language_name=language,
|
| 607 |
+
**(extra_context or {}),
|
| 608 |
+
)
|
| 609 |
+
resp = _client.messages.create(
|
| 610 |
+
model=MODEL,
|
| 611 |
+
max_tokens=MAX_TOKENS,
|
| 612 |
+
system=system,
|
| 613 |
+
messages=[{"role": "user", "content": user_text}],
|
| 614 |
+
)
|
| 615 |
+
return resp.content[0].text
|
| 616 |
+
|
| 617 |
+
def chat_structured(module: str, user_text: str, language: str, schema: type[BaseModel], max_retries: int = 1):
|
| 618 |
+
"""For modules that return JSON (cognitive_journal). Validates with Pydantic, retries once on parse fail."""
|
| 619 |
+
for attempt in range(max_retries + 1):
|
| 620 |
+
raw = chat(module, user_text, language)
|
| 621 |
+
try:
|
| 622 |
+
data = json.loads(raw)
|
| 623 |
+
return schema.model_validate(data)
|
| 624 |
+
except (json.JSONDecodeError, ValidationError) as e:
|
| 625 |
+
if attempt == max_retries:
|
| 626 |
+
raise
|
| 627 |
+
user_text = f"Your previous output failed JSON schema validation: {e}. Output ONLY valid JSON matching the schema."
|
| 628 |
+
```
|
| 629 |
+
|
| 630 |
+
---
|
| 631 |
+
|
| 632 |
+
## Data curation plan (Apr 10 + 11)
|
| 633 |
+
|
| 634 |
+
### `data/mental_health_resources.json`
|
| 635 |
+
25 cities, 3-5 entries each. Source from:
|
| 636 |
+
- NIMHANS Bangalore + satellite centers
|
| 637 |
+
- AIIMS psychiatry departments (Delhi, Bhubaneswar, Raipur, Nagpur, Patna, Jodhpur, Rishikesh)
|
| 638 |
+
- State mental health hospitals (IHBAS Delhi, Agra, Ranchi, Gwalior, Tezpur, etc.)
|
| 639 |
+
- Tata Institute of Social Sciences counselling
|
| 640 |
+
- Telemedicine services (list as options, not endorsements): YourDost, Wysa, Amaha, Trijog
|
| 641 |
+
- University counseling centers in major college cities
|
| 642 |
+
|
| 643 |
+
Schema per entry:
|
| 644 |
+
```json
|
| 645 |
+
{
|
| 646 |
+
"name": "NIMHANS Bangalore",
|
| 647 |
+
"type": "Government psychiatric hospital",
|
| 648 |
+
"address": "Hosur Road, Bangalore 560029",
|
| 649 |
+
"phone": "+91-80-2699-5001",
|
| 650 |
+
"website": "nimhans.ac.in",
|
| 651 |
+
"specialties": ["psychiatry", "psychology", "addiction", "child psychiatry"],
|
| 652 |
+
"languages": ["en", "hi", "kn", "ta", "te"],
|
| 653 |
+
"cost": "subsidized government",
|
| 654 |
+
"tele_consult": true
|
| 655 |
+
}
|
| 656 |
+
```
|
| 657 |
+
|
| 658 |
+
### `data/helplines_india.json`
|
| 659 |
+
Verified via web search on Apr 11:
|
| 660 |
+
- KIRAN (Ministry of Social Justice)
|
| 661 |
+
- iCall (TISS)
|
| 662 |
+
- AASRA
|
| 663 |
+
- Vandrevala Foundation
|
| 664 |
+
- Women's helpline 1091
|
| 665 |
+
- Child helpline 1098
|
| 666 |
+
- Cybercrime 1930
|
| 667 |
+
- NALSA legal aid 15100
|
| 668 |
+
- Police 100 / 112
|
| 669 |
+
|
| 670 |
+
Each with: name, number, languages supported, hours, what it's for.
|
| 671 |
+
|
| 672 |
+
### `data/ipc_bns_sections.json`
|
| 673 |
+
**User is providing this.** Expected scope: all sections relevant to harassment, assault, stalking, domestic violence, cyber crime, defamation, threats, criminal intimidation. Approximately 30-60 sections covering:
|
| 674 |
+
- IPC 354, 354A, 354B, 354C, 354D → BNS equivalents
|
| 675 |
+
- IPC 509 → BNS equivalent
|
| 676 |
+
- IT Act sections 66E, 67, 67A (cyber)
|
| 677 |
+
- Domestic Violence Act provisions
|
| 678 |
+
- Sexual Harassment at Workplace Act (PoSH)
|
| 679 |
+
- Criminal intimidation (IPC 506 → BNS)
|
| 680 |
+
- Defamation (IPC 499, 500 → BNS)
|
| 681 |
+
|
| 682 |
+
**Confirm with user at start of Apr 11**:
|
| 683 |
+
- Format (JSON, CSV, database?)
|
| 684 |
+
- Whether IPC→BNS mapping is included
|
| 685 |
+
- Whether punishments/cognizable/bailable flags are present
|
| 686 |
+
|
| 687 |
+
---
|
| 688 |
+
|
| 689 |
+
## Pitch / demo script (5 minutes, memorize)
|
| 690 |
+
|
| 691 |
+
| Time | Beat |
|
| 692 |
+
|---|---|
|
| 693 |
+
| **0:00 - 0:20** | **Hook**: "India has 0.75 psychiatrists per 100,000 people. WHO recommends 3. But mental health isn't just depression. It's harassment, legal helplessness, exam burnout, loneliness — and the 99% who'll never see a professional need *something*." |
|
| 694 |
+
| **0:20 - 0:45** | **Introducing Saathi**: "A multilingual mental health companion. 7 languages. 5 integrated modules. Not a therapist — a *literacy layer* for the 99%." Show the homepage with language selector in Hindi, switch to Hindi live. |
|
| 695 |
+
| **0:45 - 1:45** | **Module 2 demo (lead with the differentiator)** — Legal Aid. Type: *"मेरा पड़ोसी मुझे पिछले हफ्ते से फॉलो कर रहा है और धमकी भरे मैसेज भेज रहा है"* → category auto-tagged (stalking) → IPC 354D + BNS equivalent shown in plain Hindi → FIR guide → auto-generated complaint letter. **This is the "whoa" moment for judges.** |
|
| 696 |
+
| **1:45 - 2:30** | **Module 1 demo** — Saathi Chat. Type *"I've been sleeping 3 hours a night for two weeks and I can't focus"* → empathetic acknowledgment + anxiety info + "would you like resources in your city?" → type "Delhi" → NIMHANS Delhi + IHBAS returned. |
|
| 697 |
+
| **2:30 - 3:00** | **Module 3 demo** — Student Corner. Click "Placement tomorrow" → type *"Goldman Sachs final round, I'm going to fail"* → distortions tagged + 3 evidence-based prep tips. |
|
| 698 |
+
| **3:00 - 3:25** | **Module 4 demo** — Cognitive Journal. Show pre-seeded weekly chart of 5 entries, type one new entry, watch it get added with distortions highlighted inline. |
|
| 699 |
+
| **3:25 - 3:45** | **Module 5 demo** — Soothe Corner. Type "I feel small and tired" → Hindi sher (couplet) generated. Read aloud. |
|
| 700 |
+
| **3:45 - 4:05** | **Crisis safeguard demo**: type a crisis phrase → Claude bypassed → KIRAN helpline banner shown. "We never let a distressed user wait on an LLM call." |
|
| 701 |
+
| **4:05 - 4:45** | **Ethics defense**: "We answer T2's question head-on: AI cannot replace a therapist, lawyer, or doctor. What it CAN do is close the literacy and first-mile-access gap for the 99%. Every module disclaims; every risk routes to humans; no data is stored." |
|
| 702 |
+
| **4:45 - 5:00** | **Impact close**: "Mental health is not just depression. It's freedom from harm, agency in crisis, vocabulary for your own thoughts, and a poem when you need one — in your language. That's Saathi." |
|
| 703 |
+
|
| 704 |
+
### Anticipated Q&A
|
| 705 |
+
- **"Isn't this still therapy?"** → No. Therapy = diagnosis + treatment plan + clinician relationship. We do none of those. We teach CBT vocabulary, lookup legal sections, surface resources. The difference is the same as a first-aid kit vs. a doctor.
|
| 706 |
+
- **"How do you prevent misuse of the legal module?"** → It's explicitly labeled information, not advice. We route serious cases to police/NALSA/lawyer. We don't draft court filings. The complaint letter is a starting point, not a legal instrument.
|
| 707 |
+
- **"Multilingual in 3 hours — how?"** → Claude is natively multilingual. We pass a language instruction in the system prompt. UI labels come from a small translations dict. We built over 2 days, not 3 hours.
|
| 708 |
+
- **"Why HF Spaces?"** → Free, public, reproducible, CPU-friendly, judges can open the URL during evaluation. Zero deployment friction.
|
| 709 |
+
- **"How could your legal module be weaponized?"** (T4 ethics question) → Someone could file a false FIR. Mitigation: we explicitly state the Section 182 IPC / BNS 217 penalty for false complaints, and every draft letter carries a disclaimer. We're raising the floor of legal literacy, not providing a weapon.
|
| 710 |
+
- **"What if Claude gets an IPC section wrong?"** → We feed Claude the relevant sections as retrieved context from our verified dataset. Claude is instructed to use ONLY those sections, not invent. Pydantic validation catches malformed output.
|
| 711 |
+
- **"Why no data persistence?"** → Mental health + legal data is the most sensitive category. Session-only state = no breach surface, no compliance burden, no trust gap.
|
| 712 |
+
- **"How is this different from a ChatGPT wrapper?"** → 5 structured pipelines with validated outputs, domain data (doctor directory, IPC/BNS sections, helplines), crisis-detection safeguards, multilingual UI, weekly pattern visualization. Not a wrapper — an integrated first-mile-access platform.
|
| 713 |
+
|
| 714 |
+
---
|
| 715 |
+
|
| 716 |
+
## Verification checklist (end of Apr 11)
|
| 717 |
+
|
| 718 |
+
Run through this before declaring the build shipped:
|
| 719 |
+
|
| 720 |
+
- [ ] `streamlit run app.py` boots cleanly locally
|
| 721 |
+
- [ ] Language switcher works — switching to Hindi re-renders all labels
|
| 722 |
+
- [ ] **Module 1**: "I feel anxious about exams" → empathetic response + Delhi → NIMHANS returned
|
| 723 |
+
- [ ] **Module 2**: "my neighbor is stalking me" → IPC 354D returned + FIR guide + complaint letter drafted
|
| 724 |
+
- [ ] **Module 2 Hindi**: same test in Hindi → Hindi response
|
| 725 |
+
- [ ] **Module 3**: "Placement tomorrow, I'll fail" → distortions tagged + 3 prep tips
|
| 726 |
+
- [ ] **Module 4**: "I failed my midterm and I'm useless" → distortion JSON + chart updated
|
| 727 |
+
- [ ] **Module 5**: "I feel small and tired" → 4-6 line poem, no toxic positivity
|
| 728 |
+
- [ ] **Crisis test**: "I want to kill myself" on each module → Claude NOT invoked, helpline banner shown
|
| 729 |
+
- [ ] **Hindi crisis test**: "khudkushi karna chahta hoon" → helpline banner
|
| 730 |
+
- [ ] HF Space builds successfully, live URL loads
|
| 731 |
+
- [ ] Cold-start time on HF < 30s
|
| 732 |
+
- [ ] All 5 modules work on the live HF URL (not just local)
|
| 733 |
+
- [ ] Pitch rehearsal under 5 minutes, no fumbles
|
| 734 |
+
- [ ] Backup demo video recorded (screen capture)
|
| 735 |
+
|
| 736 |
+
---
|
| 737 |
+
|
| 738 |
+
## Risks & mitigations
|
| 739 |
+
|
| 740 |
+
| Risk | Likelihood | Mitigation |
|
| 741 |
+
|---|---|---|
|
| 742 |
+
| HF Space cold start kills demo timing | HIGH | Keep Space warm by refreshing URL every 2 min during pitch; have backup video ready |
|
| 743 |
+
| Claude API rate limits during live demo | MED | Pre-test all demo inputs; cache last N responses in session_state; have recorded backup |
|
| 744 |
+
| IPC/BNS dataset not in expected format | MED | Confirm format with user on Apr 10; build adapter on Apr 11 hour 1 if needed |
|
| 745 |
+
| Malformed JSON from Claude in Module 4 | MED | Pydantic validation + 1 retry with stricter prompt; graceful error UI |
|
| 746 |
+
| Multilingual rendering issues (fonts, RTL for Urdu) | MED | Streamlit handles Unicode natively; test Urdu specifically; if RTL breaks, drop Urdu from demo |
|
| 747 |
+
| Helpline numbers wrong or changed | LOW-MED | Verify every number via web search on Apr 11; print-screen the sources |
|
| 748 |
+
| 5 modules too ambitious, one doesn't work | MED | Cut-list order (what to drop first if running late): Module 5 (Poetry) → Module 4 (Journal) → keep Modules 1, 2, 3 as the winning core |
|
| 749 |
+
| Team members code incompatible interfaces | MED | Share the `backend/claude_client.py` signature on Apr 10; every module calls the same `chat()` function |
|
| 750 |
+
| `ANTHROPIC_API_KEY` committed to git | HIGH (impact) | `.gitignore` from minute one; use HF Space secrets not `.env` in production |
|
| 751 |
+
| Judges Q&A exposes ethical gap | MED | Memorize the Q&A answers above; have a "legal info not advice" and "literacy not therapy" one-liner rehearsed |
|
| 752 |
+
|
| 753 |
+
---
|
| 754 |
+
|
| 755 |
+
## Critical files — creation order on Apr 11
|
| 756 |
+
|
| 757 |
+
1. `requirements.txt`, `.gitignore`, `.env.example`, `README.md` YAML header
|
| 758 |
+
2. `backend/claude_client.py` + `backend/prompts/*.txt` (all 5 prompts drafted today on Apr 10)
|
| 759 |
+
3. `backend/safeguards.py`, `backend/i18n.py`, `backend/resources.py`
|
| 760 |
+
4. `data/helplines_india.json`, `data/mental_health_resources.json` (curated during Hour 0 prep)
|
| 761 |
+
5. `data/ipc_bns_sections.json` (user-provided; adapter if needed)
|
| 762 |
+
6. `modules/saathi_chat.py`
|
| 763 |
+
7. `modules/legal_aid.py`
|
| 764 |
+
8. `modules/student_corner.py`
|
| 765 |
+
9. `modules/cognitive_journal.py`
|
| 766 |
+
10. `modules/soothe_poetry.py`
|
| 767 |
+
11. `app.py` (orchestration — last, depends on all above)
|
| 768 |
+
12. Deploy to HF Spaces
|
| 769 |
+
13. Verification checklist
|
| 770 |
+
|
| 771 |
+
---
|
| 772 |
+
|
| 773 |
+
## Out of scope (do NOT build, even if tempted)
|
| 774 |
+
|
| 775 |
+
- User accounts / authentication
|
| 776 |
+
- Database / cross-session persistence
|
| 777 |
+
- Voice input (Whisper adds weight, CPU risk, demo latency)
|
| 778 |
+
- Real-time geolocation (user types city)
|
| 779 |
+
- Third-party translation APIs (Claude is multilingual)
|
| 780 |
+
- Embedding/vector search (no RAG; static JSON lookup + Claude context is enough)
|
| 781 |
+
- Mobile app (web only; judges demo on laptops)
|
| 782 |
+
- Push notifications
|
| 783 |
+
- SMS integration
|
| 784 |
+
- Appointment booking (we list resources, we don't schedule)
|
| 785 |
+
- Lawyer/doctor direct messaging (we surface contact info, we don't proxy)
|
| 786 |
+
- Chat history beyond current session
|
| 787 |
+
- Fine-tuning (not supported for Claude; would blow the timeline anyway)
|
| 788 |
+
- Local LLM fallback (CPU-friendly constraint rules this out)
|
| 789 |
+
|
| 790 |
+
---
|
| 791 |
+
|
| 792 |
+
## Open questions for the user (answer before Hour 1 starts)
|
| 793 |
+
|
| 794 |
+
These 7 answers unblock the full-day build. The first three are hard blockers.
|
| 795 |
+
|
| 796 |
+
1. **IPC/BNS dataset** — where is the file? What format (JSON / CSV / SQLite)? Does it include IPC → BNS mapping, punishments, and cognizable/bailable flags? **If format differs from the schema in the Legal Aid module section, we'll write a 20-line adapter in Hour 1.**
|
| 797 |
+
2. **Anthropic API key** — do you have an `ANTHROPIC_API_KEY` already provisioned with credits? Budget estimate: 200-500 Claude calls across build + rehearsal + demo ≈ under $5. Add it to `.env` locally for Hour 1, then upload to HF Space Secrets before Hour 6.
|
| 798 |
+
3. **Hugging Face account** — do you have an HF account for the Space? Needed in Hour 6. If not, signup takes 2 minutes.
|
| 799 |
+
4. **Team split** — confirm the ownership map: Teammate A (Saathi Chat + resources data), Teammate B (Legal Aid + IPC/BNS adapter), Teammate C (Student Corner + Cognitive Journal), You (app.py orchestration + Soothe Poetry + language selector). Adjust if any teammate is stronger or weaker in a specific area.
|
| 800 |
+
5. **Live-demo languages** — recommendation is English + Hindi + one South Indian language (Tamil or Telugu) to showcase breadth without overrunning the 5-minute pitch. Confirm or swap.
|
| 801 |
+
6. **Primary prize target** — designed for **Overall Winner** (focused excellence across all 4 rubric dimensions). The Legal Aid module also makes us competitive for **Most Innovative** (T2 track). If you want to pivot emphasis toward Most Innovative, we lead the pitch with the stalking → FIR demo and spend 90 seconds on it instead of 60.
|
| 802 |
+
7. **Branding** — simple text wordmark "Saathi" in a clean serif, or a small custom icon? Text wordmark is faster (10 min with PIL or a free tool); custom icon needs a designer or a generator and costs 30+ min. Default: text wordmark unless you want the icon.
|
app.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Saathi — a multilingual mental-health companion for India.
|
| 2 |
+
|
| 3 |
+
This is the Streamlit entry point. It wires together:
|
| 4 |
+
- Language selector (7 Indian languages, Claude is natively multilingual)
|
| 5 |
+
- Persistent "not a professional" banner across every tab
|
| 6 |
+
- 5 feature modules as tabs (Chat, Legal Aid, Student Corner, Cognitive Journal, Soothe Corner)
|
| 7 |
+
- Footer with privacy disclaimer
|
| 8 |
+
|
| 9 |
+
Zero persistence: everything lives in st.session_state. Close the browser, it's gone.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
import streamlit as st
|
| 14 |
+
|
| 15 |
+
from backend.claude_client import get_active_provider_label
|
| 16 |
+
from backend.i18n import DEFAULT_LANGUAGE, LANGUAGES, native_label, t
|
| 17 |
+
from modules import (
|
| 18 |
+
cognitive_journal,
|
| 19 |
+
legal_aid,
|
| 20 |
+
saathi_chat,
|
| 21 |
+
soothe_poetry,
|
| 22 |
+
student_corner,
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
st.set_page_config(
|
| 26 |
+
page_title="Saathi — Mental Health Companion",
|
| 27 |
+
page_icon="🫂",
|
| 28 |
+
layout="wide",
|
| 29 |
+
initial_sidebar_state="collapsed",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
LANGUAGE_KEY = "saathi_language"
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
_CUSTOM_CSS = """
|
| 37 |
+
<style>
|
| 38 |
+
/* Header wordmark band */
|
| 39 |
+
.saathi-header {
|
| 40 |
+
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 45%, #EC4899 100%);
|
| 41 |
+
padding: 28px 36px 26px 36px;
|
| 42 |
+
border-radius: 18px;
|
| 43 |
+
color: #FFFFFF;
|
| 44 |
+
box-shadow: 0 8px 28px rgba(99, 102, 241, 0.18);
|
| 45 |
+
margin-bottom: 8px;
|
| 46 |
+
}
|
| 47 |
+
.saathi-header .saathi-wordmark {
|
| 48 |
+
font-family: "Georgia", "Palatino Linotype", serif;
|
| 49 |
+
font-size: 2.7rem;
|
| 50 |
+
font-weight: 700;
|
| 51 |
+
letter-spacing: -0.01em;
|
| 52 |
+
line-height: 1.1;
|
| 53 |
+
margin: 0;
|
| 54 |
+
}
|
| 55 |
+
.saathi-header .saathi-sub {
|
| 56 |
+
font-family: "Georgia", "Palatino Linotype", serif;
|
| 57 |
+
font-size: 1.05rem;
|
| 58 |
+
opacity: 0.93;
|
| 59 |
+
margin: 6px 0 0 0;
|
| 60 |
+
font-style: italic;
|
| 61 |
+
}
|
| 62 |
+
.saathi-header .saathi-devanagari {
|
| 63 |
+
font-size: 1.35rem;
|
| 64 |
+
font-weight: 400;
|
| 65 |
+
opacity: 0.85;
|
| 66 |
+
margin-left: 8px;
|
| 67 |
+
letter-spacing: 0;
|
| 68 |
+
}
|
| 69 |
+
/* Tab row — slightly larger, softer */
|
| 70 |
+
[data-testid="stTabs"] [data-baseweb="tab-list"] {
|
| 71 |
+
gap: 4px;
|
| 72 |
+
border-bottom: 1px solid rgba(99, 102, 241, 0.12);
|
| 73 |
+
}
|
| 74 |
+
[data-testid="stTabs"] [data-baseweb="tab"] {
|
| 75 |
+
padding: 10px 18px;
|
| 76 |
+
font-size: 0.98rem;
|
| 77 |
+
font-weight: 500;
|
| 78 |
+
}
|
| 79 |
+
[data-testid="stTabs"] [aria-selected="true"] {
|
| 80 |
+
background: rgba(99, 102, 241, 0.08);
|
| 81 |
+
border-radius: 8px 8px 0 0;
|
| 82 |
+
}
|
| 83 |
+
footer {visibility: hidden;}
|
| 84 |
+
</style>
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _init_language() -> None:
|
| 89 |
+
if LANGUAGE_KEY not in st.session_state:
|
| 90 |
+
st.session_state[LANGUAGE_KEY] = DEFAULT_LANGUAGE
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _language_selector() -> str:
|
| 94 |
+
codes = list(LANGUAGES.keys())
|
| 95 |
+
labels = [native_label(c) for c in codes]
|
| 96 |
+
current = st.session_state[LANGUAGE_KEY]
|
| 97 |
+
idx = codes.index(current) if current in codes else 0
|
| 98 |
+
label = st.selectbox(
|
| 99 |
+
t("language_label", current),
|
| 100 |
+
options=labels,
|
| 101 |
+
index=idx,
|
| 102 |
+
key="saathi_language_select",
|
| 103 |
+
)
|
| 104 |
+
new_code = codes[labels.index(label)]
|
| 105 |
+
if new_code != st.session_state[LANGUAGE_KEY]:
|
| 106 |
+
st.session_state[LANGUAGE_KEY] = new_code
|
| 107 |
+
st.rerun()
|
| 108 |
+
return st.session_state[LANGUAGE_KEY]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _header(lang: str) -> None:
|
| 112 |
+
"""Render the gradient wordmark band + language selector."""
|
| 113 |
+
cols = st.columns([5, 1])
|
| 114 |
+
with cols[0]:
|
| 115 |
+
title_en = t("app_title", "en")
|
| 116 |
+
title_local = t("app_title", lang)
|
| 117 |
+
tagline = t("tagline", lang)
|
| 118 |
+
# Always show the Devanagari wordmark alongside the English one so the
|
| 119 |
+
# brand reads cross-lingually — even when the UI is set to Tamil or Telugu,
|
| 120 |
+
# "Saathi / साथी" is instantly recognisable.
|
| 121 |
+
wordmark_suffix = ""
|
| 122 |
+
if title_en != title_local:
|
| 123 |
+
wordmark_suffix = f"<span class='saathi-devanagari'>· {title_local}</span>"
|
| 124 |
+
elif lang == "en":
|
| 125 |
+
wordmark_suffix = "<span class='saathi-devanagari'>· साथी</span>"
|
| 126 |
+
st.markdown(
|
| 127 |
+
f"""
|
| 128 |
+
<div class='saathi-header'>
|
| 129 |
+
<h1 class='saathi-wordmark'>🫂 {title_en} {wordmark_suffix}</h1>
|
| 130 |
+
<p class='saathi-sub'>{tagline}</p>
|
| 131 |
+
</div>
|
| 132 |
+
""",
|
| 133 |
+
unsafe_allow_html=True,
|
| 134 |
+
)
|
| 135 |
+
with cols[1]:
|
| 136 |
+
_language_selector()
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _banners(lang: str) -> None:
|
| 140 |
+
st.info(t("not_therapy_banner", lang))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _footer(lang: str) -> None:
|
| 144 |
+
st.divider()
|
| 145 |
+
cols = st.columns([3, 1])
|
| 146 |
+
with cols[0]:
|
| 147 |
+
st.caption(t("footer_disclaimer", lang))
|
| 148 |
+
with cols[1]:
|
| 149 |
+
provider_label = get_active_provider_label()
|
| 150 |
+
st.caption(
|
| 151 |
+
f"_{provider_label} · Built for Anthropic × IIT Delhi 2026 · "
|
| 152 |
+
"[source](https://github.com/samarth2018/Hackathons) · MIT licensed_"
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def main() -> None:
|
| 157 |
+
_init_language()
|
| 158 |
+
st.markdown(_CUSTOM_CSS, unsafe_allow_html=True)
|
| 159 |
+
lang = st.session_state[LANGUAGE_KEY]
|
| 160 |
+
_header(lang)
|
| 161 |
+
_banners(lang)
|
| 162 |
+
|
| 163 |
+
tab_chat, tab_legal, tab_student, tab_journal, tab_soothe = st.tabs(
|
| 164 |
+
[
|
| 165 |
+
t("tab_chat", lang),
|
| 166 |
+
t("tab_legal", lang),
|
| 167 |
+
t("tab_student", lang),
|
| 168 |
+
t("tab_journal", lang),
|
| 169 |
+
t("tab_soothe", lang),
|
| 170 |
+
]
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
with tab_chat:
|
| 174 |
+
saathi_chat.render(lang)
|
| 175 |
+
with tab_legal:
|
| 176 |
+
legal_aid.render(lang)
|
| 177 |
+
with tab_student:
|
| 178 |
+
student_corner.render(lang)
|
| 179 |
+
with tab_journal:
|
| 180 |
+
cognitive_journal.render(lang)
|
| 181 |
+
with tab_soothe:
|
| 182 |
+
soothe_poetry.render(lang)
|
| 183 |
+
|
| 184 |
+
_footer(lang)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
if __name__ == "__main__":
|
| 188 |
+
main()
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Saathi backend package: Claude client, safeguards, i18n, and resources."""
|
backend/claude_client.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""LLM client for Saathi — Anthropic Claude primary, Google Gemini fallback.
|
| 2 |
+
|
| 3 |
+
Centralises:
|
| 4 |
+
- Provider detection (Anthropic if ANTHROPIC_API_KEY set, else Gemini)
|
| 5 |
+
- Model selection (env-overridable)
|
| 6 |
+
- System-prompt loading from backend/prompts/*.txt
|
| 7 |
+
- A single-turn `chat()` entry point used by every module
|
| 8 |
+
- A `chat_structured()` helper that validates JSON output with Pydantic and retries once
|
| 9 |
+
|
| 10 |
+
The public interface (`chat`, `chat_structured`) is provider-agnostic. Modules don't
|
| 11 |
+
need to know which LLM is live — they just call `chat(...)` and get a string back.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import json
|
| 16 |
+
import logging
|
| 17 |
+
import os
|
| 18 |
+
from functools import lru_cache
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any, Dict, List, Optional, Type, TypeVar
|
| 21 |
+
|
| 22 |
+
from dotenv import load_dotenv
|
| 23 |
+
from pydantic import BaseModel, ValidationError
|
| 24 |
+
|
| 25 |
+
load_dotenv() # local .env for development; HF Space uses Repository Secrets
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
PROMPT_DIR = Path(__file__).parent / "prompts"
|
| 30 |
+
|
| 31 |
+
# --- Anthropic (primary) ---------------------------------------------------
|
| 32 |
+
DEFAULT_MODEL = os.getenv("SAATHI_MODEL", "claude-sonnet-4-6")
|
| 33 |
+
FALLBACK_MODEL = os.getenv("SAATHI_FALLBACK_MODEL", "claude-haiku-4-5")
|
| 34 |
+
|
| 35 |
+
# --- Gemini (fallback) -----------------------------------------------------
|
| 36 |
+
# gemini-1.5-flash was deprecated in late 2025. Default to 2.5 flash (current
|
| 37 |
+
# free-tier stable as of 2026), with env override for any key that can't see 2.5.
|
| 38 |
+
GEMINI_MODEL = os.getenv("SAATHI_GEMINI_MODEL", "gemini-2.5-flash")
|
| 39 |
+
|
| 40 |
+
DEFAULT_MAX_TOKENS = 1024
|
| 41 |
+
DEFAULT_TEMPERATURE = 0.7
|
| 42 |
+
|
| 43 |
+
T = TypeVar("T", bound=BaseModel)
|
| 44 |
+
|
| 45 |
+
# ---------------------------------------------------------------------------
|
| 46 |
+
# Provider detection
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
_provider: Optional[str] = None # "anthropic" | "gemini" | "none"
|
| 50 |
+
_anthropic_client = None # type: ignore[var-annotated]
|
| 51 |
+
_gemini_configured = False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _detect_provider() -> str:
|
| 55 |
+
"""Pick the active LLM provider based on which API key is present.
|
| 56 |
+
|
| 57 |
+
Anthropic wins if its key is set (this is an Anthropic hackathon; Claude is
|
| 58 |
+
always preferred when available). Falls back to Gemini for live demos when
|
| 59 |
+
Anthropic credits haven't been provisioned yet. Result is memoised.
|
| 60 |
+
"""
|
| 61 |
+
global _provider
|
| 62 |
+
if _provider is not None:
|
| 63 |
+
return _provider
|
| 64 |
+
|
| 65 |
+
if os.getenv("ANTHROPIC_API_KEY"):
|
| 66 |
+
_provider = "anthropic"
|
| 67 |
+
logger.info("LLM provider: Anthropic Claude (%s)", DEFAULT_MODEL)
|
| 68 |
+
elif os.getenv("GEMINI_API_KEY"):
|
| 69 |
+
_provider = "gemini"
|
| 70 |
+
logger.warning(
|
| 71 |
+
"LLM provider: Google Gemini fallback (%s). "
|
| 72 |
+
"Set ANTHROPIC_API_KEY to use Claude.",
|
| 73 |
+
GEMINI_MODEL,
|
| 74 |
+
)
|
| 75 |
+
else:
|
| 76 |
+
_provider = "none"
|
| 77 |
+
logger.error(
|
| 78 |
+
"No LLM API key found. Set ANTHROPIC_API_KEY or GEMINI_API_KEY."
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
return _provider
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def _force_gemini_fallback() -> None:
|
| 85 |
+
"""Switch the module-level provider to Gemini after a runtime Anthropic failure."""
|
| 86 |
+
global _provider
|
| 87 |
+
_provider = "gemini"
|
| 88 |
+
logger.warning("Switched to Gemini fallback after Anthropic runtime failure.")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def get_active_provider() -> str:
|
| 92 |
+
"""Return the active provider code: 'anthropic', 'gemini', or 'none'."""
|
| 93 |
+
return _detect_provider()
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_active_provider_label() -> str:
|
| 97 |
+
"""Return a short human-readable label for the active provider, for the UI footer."""
|
| 98 |
+
p = _detect_provider()
|
| 99 |
+
if p == "anthropic":
|
| 100 |
+
return "Powered by Claude"
|
| 101 |
+
if p == "gemini":
|
| 102 |
+
return "Demo mode · Gemini fallback"
|
| 103 |
+
return "⚠ No LLM key configured"
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ---------------------------------------------------------------------------
|
| 107 |
+
# Prompt loading
|
| 108 |
+
# ---------------------------------------------------------------------------
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
@lru_cache(maxsize=16)
|
| 112 |
+
def _load_prompt(name: str) -> str:
|
| 113 |
+
# Prompt files are static at module load; cache avoids a disk read per LLM call.
|
| 114 |
+
path = PROMPT_DIR / f"{name}.txt"
|
| 115 |
+
if not path.exists():
|
| 116 |
+
raise FileNotFoundError(f"Prompt file not found: {path}")
|
| 117 |
+
return path.read_text(encoding="utf-8")
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _strip_code_fences(text: str) -> str:
|
| 121 |
+
"""Models sometimes wrap JSON in ```json ... ``` even when told not to."""
|
| 122 |
+
s = text.strip()
|
| 123 |
+
if s.startswith("```"):
|
| 124 |
+
s = s.lstrip("`")
|
| 125 |
+
if s.lower().startswith("json"):
|
| 126 |
+
s = s[4:]
|
| 127 |
+
s = s.rstrip("`").strip()
|
| 128 |
+
return s
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _sanitise_history(history: Optional[List[Dict[str, Any]]]) -> List[Dict[str, str]]:
|
| 132 |
+
"""Coerce a Streamlit chat history list into the strict alternating shape both
|
| 133 |
+
Anthropic and Gemini expect.
|
| 134 |
+
|
| 135 |
+
Rules enforced:
|
| 136 |
+
- Only role ∈ {"user", "assistant"} (anything else is dropped).
|
| 137 |
+
- Messages must alternate user → assistant → user → assistant …
|
| 138 |
+
(duplicates of the same role are merged to avoid a 400).
|
| 139 |
+
- The sequence must START with "user"; a leading "assistant" (e.g. a welcome
|
| 140 |
+
bubble rendered before the first turn) is dropped.
|
| 141 |
+
- Callers append the new `user_text` on top via the `messages=` argument, so
|
| 142 |
+
we do NOT include it here.
|
| 143 |
+
"""
|
| 144 |
+
if not history:
|
| 145 |
+
return []
|
| 146 |
+
|
| 147 |
+
clean: List[Dict[str, str]] = []
|
| 148 |
+
for msg in history:
|
| 149 |
+
role = msg.get("role")
|
| 150 |
+
content = msg.get("content")
|
| 151 |
+
if role not in ("user", "assistant"):
|
| 152 |
+
continue
|
| 153 |
+
if not isinstance(content, str) or not content.strip():
|
| 154 |
+
continue
|
| 155 |
+
if clean and clean[-1]["role"] == role:
|
| 156 |
+
clean[-1]["content"] += "\n\n" + content.strip()
|
| 157 |
+
else:
|
| 158 |
+
clean.append({"role": role, "content": content.strip()})
|
| 159 |
+
|
| 160 |
+
while clean and clean[0]["role"] != "user":
|
| 161 |
+
clean.pop(0)
|
| 162 |
+
|
| 163 |
+
return clean
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
# ---------------------------------------------------------------------------
|
| 167 |
+
# Anthropic backend
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def _get_anthropic_client():
|
| 172 |
+
"""Lazy Anthropic client so imports don't fail when no key is set."""
|
| 173 |
+
global _anthropic_client
|
| 174 |
+
if _anthropic_client is None:
|
| 175 |
+
from anthropic import Anthropic # lazy import
|
| 176 |
+
_anthropic_client = Anthropic() # reads ANTHROPIC_API_KEY from env
|
| 177 |
+
return _anthropic_client
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def _anthropic_chat(
|
| 181 |
+
system: str,
|
| 182 |
+
messages: List[Dict[str, str]],
|
| 183 |
+
model: Optional[str],
|
| 184 |
+
max_tokens: int,
|
| 185 |
+
) -> str:
|
| 186 |
+
client = _get_anthropic_client()
|
| 187 |
+
resp = client.messages.create(
|
| 188 |
+
model=model or DEFAULT_MODEL,
|
| 189 |
+
max_tokens=max_tokens,
|
| 190 |
+
system=system,
|
| 191 |
+
messages=messages,
|
| 192 |
+
)
|
| 193 |
+
return resp.content[0].text
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# ---------------------------------------------------------------------------
|
| 197 |
+
# Gemini backend (fallback)
|
| 198 |
+
# ---------------------------------------------------------------------------
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def _ensure_gemini_configured() -> None:
|
| 202 |
+
global _gemini_configured
|
| 203 |
+
if _gemini_configured:
|
| 204 |
+
return
|
| 205 |
+
import google.generativeai as genai # lazy import
|
| 206 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 207 |
+
if not api_key:
|
| 208 |
+
raise RuntimeError(
|
| 209 |
+
"GEMINI_API_KEY not set. Get a free key at https://aistudio.google.com/apikey"
|
| 210 |
+
)
|
| 211 |
+
genai.configure(api_key=api_key)
|
| 212 |
+
_gemini_configured = True
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _gemini_chat(
|
| 216 |
+
system: str,
|
| 217 |
+
messages: List[Dict[str, str]],
|
| 218 |
+
max_tokens: int,
|
| 219 |
+
) -> str:
|
| 220 |
+
"""Gemini equivalent of the Anthropic messages.create() call.
|
| 221 |
+
|
| 222 |
+
Gemini uses 'model' instead of 'assistant' for the non-user role, and
|
| 223 |
+
accepts the system prompt as a first-class `system_instruction` on the
|
| 224 |
+
model object (not inside the contents list).
|
| 225 |
+
"""
|
| 226 |
+
_ensure_gemini_configured()
|
| 227 |
+
import google.generativeai as genai # lazy import
|
| 228 |
+
|
| 229 |
+
model = genai.GenerativeModel(
|
| 230 |
+
model_name=GEMINI_MODEL,
|
| 231 |
+
system_instruction=system,
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
contents = []
|
| 235 |
+
for msg in messages:
|
| 236 |
+
role = "user" if msg["role"] == "user" else "model"
|
| 237 |
+
contents.append({"role": role, "parts": [msg["content"]]})
|
| 238 |
+
|
| 239 |
+
response = model.generate_content(
|
| 240 |
+
contents,
|
| 241 |
+
generation_config={
|
| 242 |
+
"max_output_tokens": max_tokens,
|
| 243 |
+
"temperature": DEFAULT_TEMPERATURE,
|
| 244 |
+
},
|
| 245 |
+
)
|
| 246 |
+
# Gemini returns candidates; .text concatenates parts of the first candidate.
|
| 247 |
+
try:
|
| 248 |
+
return response.text
|
| 249 |
+
except ValueError:
|
| 250 |
+
# Happens when all candidates were blocked by safety filters.
|
| 251 |
+
logger.warning("Gemini returned no text (safety block). Falling back to empty string.")
|
| 252 |
+
return ""
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
# ---------------------------------------------------------------------------
|
| 256 |
+
# Public API
|
| 257 |
+
# ---------------------------------------------------------------------------
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def chat(
|
| 261 |
+
module: str,
|
| 262 |
+
user_text: str,
|
| 263 |
+
language_name: str,
|
| 264 |
+
extra_context: Optional[dict] = None,
|
| 265 |
+
history: Optional[List[Dict[str, Any]]] = None,
|
| 266 |
+
model: Optional[str] = None,
|
| 267 |
+
max_tokens: int = DEFAULT_MAX_TOKENS,
|
| 268 |
+
) -> str:
|
| 269 |
+
"""Send a chat turn to the active LLM provider using the prompt file for `module`.
|
| 270 |
+
|
| 271 |
+
`language_name` is always substituted into the prompt (e.g. "Hindi", "English").
|
| 272 |
+
`extra_context` can pass additional template keys (e.g. `sections_json` for Legal Aid).
|
| 273 |
+
`history` is an optional list of prior `{role, content}` dicts from the current
|
| 274 |
+
Streamlit session. When supplied, Saathi becomes a multi-turn companion rather
|
| 275 |
+
than a stateless Q&A box. Both Anthropic and Gemini are stateless on the wire —
|
| 276 |
+
we resend the full trimmed history on every call.
|
| 277 |
+
|
| 278 |
+
Provider precedence:
|
| 279 |
+
1. Anthropic if `ANTHROPIC_API_KEY` is set (preferred — this is an Anthropic
|
| 280 |
+
hackathon and Claude handles Indian languages best).
|
| 281 |
+
2. Gemini if only `GEMINI_API_KEY` is set, or if an Anthropic call just failed
|
| 282 |
+
at runtime (rate limit, auth error, etc.) AND a Gemini key is available.
|
| 283 |
+
"""
|
| 284 |
+
template = _load_prompt(module)
|
| 285 |
+
system = template.format(language_name=language_name, **(extra_context or {}))
|
| 286 |
+
|
| 287 |
+
messages: List[Dict[str, str]] = _sanitise_history(history)
|
| 288 |
+
messages.append({"role": "user", "content": user_text})
|
| 289 |
+
|
| 290 |
+
provider = _detect_provider()
|
| 291 |
+
|
| 292 |
+
if provider == "anthropic":
|
| 293 |
+
try:
|
| 294 |
+
return _anthropic_chat(system, messages, model, max_tokens)
|
| 295 |
+
except Exception as e:
|
| 296 |
+
# Any Anthropic failure (auth, rate limit, network) — fall back permanently
|
| 297 |
+
# if a Gemini key is available. Otherwise re-raise so the caller sees the real error.
|
| 298 |
+
if os.getenv("GEMINI_API_KEY"):
|
| 299 |
+
logger.warning(
|
| 300 |
+
"Anthropic call failed (%s: %s). Falling back to Gemini.",
|
| 301 |
+
type(e).__name__,
|
| 302 |
+
e,
|
| 303 |
+
)
|
| 304 |
+
_force_gemini_fallback()
|
| 305 |
+
return _gemini_chat(system, messages, max_tokens)
|
| 306 |
+
raise
|
| 307 |
+
|
| 308 |
+
if provider == "gemini":
|
| 309 |
+
return _gemini_chat(system, messages, max_tokens)
|
| 310 |
+
|
| 311 |
+
raise RuntimeError(
|
| 312 |
+
"No LLM provider available. Set ANTHROPIC_API_KEY (preferred) or "
|
| 313 |
+
"GEMINI_API_KEY as a Space secret."
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
|
| 317 |
+
def chat_structured(
|
| 318 |
+
module: str,
|
| 319 |
+
user_text: str,
|
| 320 |
+
language_name: str,
|
| 321 |
+
schema: Type[T],
|
| 322 |
+
extra_context: Optional[dict] = None,
|
| 323 |
+
max_retries: int = 1,
|
| 324 |
+
model: Optional[str] = None,
|
| 325 |
+
) -> T:
|
| 326 |
+
"""JSON-output variant that validates against a Pydantic schema and retries once on failure.
|
| 327 |
+
|
| 328 |
+
Works with both providers — Claude and Gemini both honour "output only JSON" prompts,
|
| 329 |
+
and `_strip_code_fences` handles the occasional markdown wrapping from either.
|
| 330 |
+
"""
|
| 331 |
+
current_input = user_text
|
| 332 |
+
last_err: Optional[Exception] = None
|
| 333 |
+
|
| 334 |
+
for attempt in range(max_retries + 1):
|
| 335 |
+
raw = chat(module, current_input, language_name, extra_context, model=model)
|
| 336 |
+
cleaned = _strip_code_fences(raw)
|
| 337 |
+
try:
|
| 338 |
+
data = json.loads(cleaned)
|
| 339 |
+
return schema.model_validate(data)
|
| 340 |
+
except (json.JSONDecodeError, ValidationError) as e:
|
| 341 |
+
last_err = e
|
| 342 |
+
if attempt == max_retries:
|
| 343 |
+
break
|
| 344 |
+
current_input = (
|
| 345 |
+
"Your previous response failed JSON validation with this error:\n"
|
| 346 |
+
f"{e}\n\n"
|
| 347 |
+
"Output ONLY a valid JSON object matching the schema. "
|
| 348 |
+
"No markdown, no code fences, no prose before or after.\n\n"
|
| 349 |
+
f"Original user input was: {user_text}"
|
| 350 |
+
)
|
| 351 |
+
|
| 352 |
+
raise RuntimeError(
|
| 353 |
+
f"LLM returned invalid JSON after {max_retries + 1} attempts. Last error: {last_err}"
|
| 354 |
+
)
|
backend/i18n.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Language codes, native labels, and UI-string lookups for Saathi.
|
| 2 |
+
|
| 3 |
+
Design:
|
| 4 |
+
- English and Hindi get the full label set.
|
| 5 |
+
- bn/ta/te/mr/ur ship minimal labels (the strings the user sees on the first screen)
|
| 6 |
+
and fall back to English for everything else. This keeps the 7-language breadth
|
| 7 |
+
honest without requiring perfect translation of every tooltip.
|
| 8 |
+
- Claude handles the actual user-facing content (chat responses, reframes, poems)
|
| 9 |
+
so `LANGUAGES[code]["claude_name"]` is what we pass into the system-prompt template.
|
| 10 |
+
"""
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
+
from typing import Dict
|
| 14 |
+
|
| 15 |
+
LANGUAGES: Dict[str, Dict[str, str]] = {
|
| 16 |
+
"en": {"claude_name": "English", "native": "English"},
|
| 17 |
+
"hi": {"claude_name": "Hindi", "native": "हिन्दी"},
|
| 18 |
+
"bn": {"claude_name": "Bengali", "native": "বাংলা"},
|
| 19 |
+
"ta": {"claude_name": "Tamil", "native": "தமிழ்"},
|
| 20 |
+
"te": {"claude_name": "Telugu", "native": "తెలుగు"},
|
| 21 |
+
"mr": {"claude_name": "Marathi", "native": "मराठी"},
|
| 22 |
+
"ur": {"claude_name": "Urdu", "native": "اردو"},
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
DEFAULT_LANGUAGE = "en"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# --- UI label dictionary ----------------------------------------------------
|
| 29 |
+
#
|
| 30 |
+
# Keys used throughout the Streamlit UI. English is the reference set;
|
| 31 |
+
# missing keys in any other language fall back to English via `t()`.
|
| 32 |
+
|
| 33 |
+
UI_LABELS: Dict[str, Dict[str, str]] = {
|
| 34 |
+
"en": {
|
| 35 |
+
"app_title": "Saathi",
|
| 36 |
+
"tagline": "Your mental health companion — in your language, on your side.",
|
| 37 |
+
"not_therapy_banner": "Saathi is an information companion, not a therapist, doctor, or lawyer. For serious concerns, please consult a licensed professional. In an emergency, dial 112.",
|
| 38 |
+
"footer_disclaimer": "Saathi does not store your conversations. Everything you type stays in this browser session only.",
|
| 39 |
+
"language_label": "Language",
|
| 40 |
+
"tab_chat": "💬 Saathi Chat",
|
| 41 |
+
"tab_legal": "⚖️ Legal Aid",
|
| 42 |
+
"tab_student": "🎓 Student Corner",
|
| 43 |
+
"tab_journal": "📓 Cognitive Journal",
|
| 44 |
+
"tab_soothe": "🌙 Soothe Corner",
|
| 45 |
+
# Module 1
|
| 46 |
+
"chat_header": "Talk to Saathi",
|
| 47 |
+
"chat_sub": "Ask any question about how you are feeling, or about a mental-health topic you want to understand.",
|
| 48 |
+
"chat_input_placeholder": "What's on your mind today?",
|
| 49 |
+
"chat_city_prompt": "Tell me your city and I'll suggest resources nearby.",
|
| 50 |
+
"chat_city_placeholder": "e.g. Delhi, Mumbai, Bangalore…",
|
| 51 |
+
"chat_city_button": "Find resources",
|
| 52 |
+
"chat_no_resources": "I don't have curated resources for that city yet. Please try a nearby major city, or call Tele-MANAS on 14416 — Government of India, free, 24×7, 20+ Indian languages, works from anywhere in India.",
|
| 53 |
+
"chat_resources_heading": "Mental health resources",
|
| 54 |
+
"source_label": "Source",
|
| 55 |
+
# Module 2
|
| 56 |
+
"legal_header": "Legal Aid",
|
| 57 |
+
"legal_sub": "Describe what is happening to you. Saathi will explain the relevant law, how to file an FIR, and can draft a complaint letter for you.",
|
| 58 |
+
"legal_input_placeholder": "Describe the situation in your own words…",
|
| 59 |
+
"legal_send_button": "Get legal information",
|
| 60 |
+
"legal_draft_letter_button": "Draft a complaint letter for me",
|
| 61 |
+
"legal_not_lawyer": "Saathi is not a lawyer. This is legal information, not legal advice. For complex cases, call NALSA legal aid on 15100 or consult a practicing lawyer.",
|
| 62 |
+
"legal_other_title": "This situation needs a person, not a machine",
|
| 63 |
+
"legal_other_body": "Saathi couldn't match what you described to a specific legal category. That usually means the case is complex enough to need a real lawyer or counsellor. Here's what you can do right now — all free:",
|
| 64 |
+
"legal_other_nalsa": "**NALSA Legal Services — 15100** — free legal aid, advice and representation for women, children, SC/ST, persons in custody, trafficking victims, disaster-affected, and people below a state-set income threshold (Legal Services Authorities Act, 1987).",
|
| 65 |
+
"legal_other_police": "**Women's Helpline — 1091** or **Emergency — 112** — if what's happening involves any physical danger at all.",
|
| 66 |
+
"legal_other_dlsa": "**District Legal Services Authority (DLSA)** — every district has a DLSA that offers free legal advice clinics. Ask at your nearest court complex or NALSA's website (nalsa.gov.in).",
|
| 67 |
+
"legal_sexual_violence_title": "This sounds like serious sexual violence",
|
| 68 |
+
"legal_sexual_violence_body": "Saathi is intentionally not auto-mapping this kind of case to legal sections unless the exact provisions are in its verified dataset. What matters most right now is human support, safety, and medical / legal follow-through.",
|
| 69 |
+
"legal_sexual_violence_help": "**Call 112 or 1091 immediately** if you are unsafe right now. If the incident is recent, seek in-person medical help and contact a Women's Cell / police station as soon as you can.",
|
| 70 |
+
"legal_procedural_heading": "Your rights at the police station",
|
| 71 |
+
"legal_false_complaint_warning_heading": "Before filing — a note",
|
| 72 |
+
"legal_active_danger_title": "This sounds like an emergency right now",
|
| 73 |
+
"legal_active_danger_body": "What you wrote reads like an ongoing, physical danger. Please stop reading and call one of these lines first — legal sections can wait.",
|
| 74 |
+
"legal_active_danger_after": "When you are somewhere safe, come back and Saathi will help you understand the law and draft a complaint.",
|
| 75 |
+
# Module 3
|
| 76 |
+
"student_header": "Student Corner",
|
| 77 |
+
"student_sub": "For exams, placements, interviews, vivas, presentations, result days, and burnout.",
|
| 78 |
+
"student_event_label": "What event is coming up?",
|
| 79 |
+
"student_input_placeholder": "Tell me what's happening in your head right now…",
|
| 80 |
+
"student_send_button": "Help me prepare",
|
| 81 |
+
"event_exam": "Exam tomorrow",
|
| 82 |
+
"event_placement": "Placement interview",
|
| 83 |
+
"event_viva": "Viva",
|
| 84 |
+
"event_presentation": "Presentation",
|
| 85 |
+
"event_result": "Result day",
|
| 86 |
+
"event_burnout": "General burnout",
|
| 87 |
+
# Module 4
|
| 88 |
+
"journal_header": "Cognitive Journal",
|
| 89 |
+
"journal_sub": "Write 2-3 sentences about your day. Saathi will help you notice cognitive patterns (distortions) in your own words, using the CBT framework.",
|
| 90 |
+
"journal_input_placeholder": "How was your day? What are you thinking about?",
|
| 91 |
+
"journal_send_button": "Analyse",
|
| 92 |
+
"journal_chart_title": "Your thought patterns this session",
|
| 93 |
+
"journal_no_distortions": "No clear distortions in this entry — your thinking looks balanced here.",
|
| 94 |
+
"journal_needs_pro": "Some of what you wrote suggests you may benefit from talking to a professional. Tele-MANAS (**14416** — Government of India, free, 24×7) is a good place to start. iCall (9152987821, Mon–Sat 8 AM–10 PM) is another option.",
|
| 95 |
+
"journal_summary_heading": "Observation",
|
| 96 |
+
"journal_distortions_heading": "What Saathi noticed",
|
| 97 |
+
"journal_reframe_heading": "A balanced alternative",
|
| 98 |
+
"journal_question_heading": "Ask yourself",
|
| 99 |
+
# Module 5
|
| 100 |
+
"soothe_header": "Soothe Corner",
|
| 101 |
+
"soothe_sub": "Tell Saathi how you are feeling in one line. You'll receive a short poem that honors the weight of it.",
|
| 102 |
+
"soothe_input_placeholder": "One line about how you feel…",
|
| 103 |
+
"soothe_send_button": "Write me a poem",
|
| 104 |
+
"soothe_regenerate_button": "Write me another one",
|
| 105 |
+
# Crisis banner
|
| 106 |
+
"crisis_banner_title": "You don't have to carry this alone",
|
| 107 |
+
"crisis_banner_body": "What you wrote suggests you may be going through a very hard moment. Please reach out — these lines are free, confidential, and available now:",
|
| 108 |
+
"crisis_call_trusted": "And, if you can: call or message one person you trust — a friend, a family member, anyone. You deserve to be heard.",
|
| 109 |
+
},
|
| 110 |
+
"hi": {
|
| 111 |
+
"app_title": "साथी",
|
| 112 |
+
"tagline": "आपका मानसिक स्वास्थ्य साथी — आपकी भाषा में, आपके साथ।",
|
| 113 |
+
"not_therapy_banner": "साथी एक जानकारी देने वाला साथी है — यह थेरेपिस्ट, डॉक्टर, या वकील नहीं है। गंभीर समस्याओं के लिए कृपया एक लाइसेंसी पेशेवर से सलाह लें। आपातकाल में 112 डायल करें।",
|
| 114 |
+
"footer_disclaimer": "साथी आपकी बातचीत को सहेजता नहीं है। आप जो भी लिखते हैं वह केवल इस ब्राउज़र सेशन में रहता है।",
|
| 115 |
+
"language_label": "भाषा",
|
| 116 |
+
"tab_chat": "💬 साथी चैट",
|
| 117 |
+
"tab_legal": "⚖️ कानूनी सहायता",
|
| 118 |
+
"tab_student": "🎓 विद्यार्थी कॉर्नर",
|
| 119 |
+
"tab_journal": "📓 विचार डायरी",
|
| 120 |
+
"tab_soothe": "🌙 शांति कॉर्नर",
|
| 121 |
+
# Module 1
|
| 122 |
+
"chat_header": "साथी से बात करें",
|
| 123 |
+
"chat_sub": "आप कैसा महसूस कर रहे हैं, या मानसिक स्वास्थ्य के किसी भी विषय पर प्रश्न पूछें।",
|
| 124 |
+
"chat_input_placeholder": "आज आपके मन में क्या है?",
|
| 125 |
+
"chat_city_prompt": "अपना शहर बताएँ, मैं पास के संसाधन सुझाऊँगा।",
|
| 126 |
+
"chat_city_placeholder": "जैसे दिल्ली, मुंबई, बैंगलोर…",
|
| 127 |
+
"chat_city_button": "संसाधन ढूँढें",
|
| 128 |
+
"chat_no_resources": "इस शहर के लिए अभी क्यूरेटेड संसाधन उपलब्ध नहीं हैं। कृपया पास का कोई बड़ा शहर आज़माएँ, या Tele-MANAS — 14416 (भारत सरकार, मुफ्त, 24×7, 20+ भारतीय भाषाओं में) पर कॉल करें। यह पूरे भारत से काम करता है।",
|
| 129 |
+
"chat_resources_heading": "मानसिक स्वास्थ्य संसाधन",
|
| 130 |
+
"source_label": "स्रोत",
|
| 131 |
+
# Module 2
|
| 132 |
+
"legal_header": "कानूनी सहायता",
|
| 133 |
+
"legal_sub": "बताइए आपके साथ क्या हो रहा है। साथी संबंधित कानून समझाएगा, FIR कैसे दर्ज करें बताएगा, और चाहें तो शिकायत पत्र भी तैयार कर देगा।",
|
| 134 |
+
"legal_input_placeholder": "अपनी स्थिति अपने शब्दों में बताएँ…",
|
| 135 |
+
"legal_send_button": "कानूनी जानकारी पाएँ",
|
| 136 |
+
"legal_draft_letter_button": "मेरे लिए शिकायत पत्र तैयार करें",
|
| 137 |
+
"legal_not_lawyer": "साथी वकील नहीं है। यह कानूनी जानकारी है, कानूनी सलाह नहीं। जटिल मामलों के लिए NALSA कानूनी सहायता (15100) या किसी वकील से संपर्क करें।",
|
| 138 |
+
"legal_other_title": "इस स्थिति के लिए एक इंसान की ज़रूरत है, मशीन की नहीं",
|
| 139 |
+
"legal_other_body": "आपने जो बताया उसे साथी किसी विशिष्ट कानूनी श्रेणी से नहीं जोड़ पाया। यह आमतौर पर इसका संकेत है कि मामला इतना जटिल है कि आपको किसी असली वकील या काउंसलर की ज़रूरत है। अभी आप ये कर सकते हैं — सब कुछ मुफ्त है:",
|
| 140 |
+
"legal_other_nalsa": "**NALSA कानूनी सेवाएँ — 15100** — महिलाओं, बच्चों, SC/ST, हिरासत में बंद व्यक्तियों, तस्करी पीड़ितों, आपदा प्रभावितों, और आय-सीमा से नीचे के लोगों के लिए मुफ्त कानूनी सहायता (कानूनी सेवा प्राधिकरण अधिनियम, 1987)।",
|
| 141 |
+
"legal_other_police": "**महिला हेल्पलाइन — 1091** या **आपातकाल — 112** — यदि किसी भी प्रकार का शारीरिक ख़तरा हो।",
|
| 142 |
+
"legal_other_dlsa": "**ज़िला कानूनी सेवा प्राधिकरण (DLSA)** — हर ज़िले में एक DLSA है जो मुफ्त कानूनी सलाह क्लीनिक चलाता है। अपनी नज़दीकी अदालत या nalsa.gov.in पर पूछें।",
|
| 143 |
+
"legal_sexual_violence_title": "यह गंभीर यौन हिंसा जैसा लगता है",
|
| 144 |
+
"legal_sexual_violence_body": "साथी जानबूझकर ऐसे मामले को कानूनी धाराओं से अपने-आप नहीं जोड़ता जब तक वही धाराएँ उसके सत्यापित डेटा में मौजूद न हों। अभी सबसे ज़रूरी चीज़ है इंसानी मदद, सुरक्षा, और मेडिकल / कानूनी सहायता।",
|
| 145 |
+
"legal_sexual_violence_help": "**अगर आप अभी असुरक्षित हैं तो तुरंत 112 या 1091 पर कॉल करें।** अगर घटना हाल की है, तो जल्द से जल्द किसी अस्पताल / मेडिकल सहायता और महिला प्रकोष्ठ / पुलिस स्टेशन से संपर्क करें।",
|
| 146 |
+
"legal_procedural_heading": "पुलिस स्टेशन में आपके अधिकार",
|
| 147 |
+
"legal_false_complaint_warning_heading": "शिकायत दर्ज करने से पहले — एक नोट",
|
| 148 |
+
"legal_active_danger_title": "यह अभी एक आपात स्थिति लगती है",
|
| 149 |
+
"legal_active_danger_body": "आपने जो लिखा है वह एक जारी, शारीरिक ख़तरे जैसा लगता है। कृपया पढ़ना रोकें और पहले इनमें से किसी एक नंबर पर कॉल करें — कानूनी धाराएँ इंतज़ार कर सकती हैं।",
|
| 150 |
+
"legal_active_danger_after": "जब आप सुरक्षित जगह पर हों, तो वापस आइए — साथी आपको कानून समझाने और शिकायत तैयार करने में मदद करेगा।",
|
| 151 |
+
# Module 3
|
| 152 |
+
"student_header": "विद्यार्थी कॉर्नर",
|
| 153 |
+
"student_sub": "परीक्षा, प्लेसमेंट, इंटरव्यू, वाइवा, प्रेजेंटेशन, रिज़ल्ट डे, या बर्नआउट के लिए।",
|
| 154 |
+
"student_event_label": "कौन सा इवेंट आने वाला है?",
|
| 155 |
+
"student_input_placeholder": "अभी आपके मन में क्या चल रहा है, बताइए…",
|
| 156 |
+
"student_send_button": "मेरी तैयारी में मदद करें",
|
| 157 |
+
"event_exam": "कल परीक्षा है",
|
| 158 |
+
"event_placement": "प्लेसमेंट इंटरव्यू",
|
| 159 |
+
"event_viva": "वाइवा",
|
| 160 |
+
"event_presentation": "प्रेजेंटेशन",
|
| 161 |
+
"event_result": "रिज़ल्ट का दिन",
|
| 162 |
+
"event_burnout": "सामान्य बर्नआउट",
|
| 163 |
+
# Module 4
|
| 164 |
+
"journal_header": "विचार डायरी",
|
| 165 |
+
"journal_sub": "अपने दिन के बारे में 2-3 वाक्य लिखिए। साथी आपको CBT ढाँचे से आपके अपने विचारों में पैटर्न पहचानने में मदद करेगा।",
|
| 166 |
+
"journal_input_placeholder": "आज का दिन कैसा रहा? क्या सोच रहे हैं?",
|
| 167 |
+
"journal_send_button": "विश्लेषण करें",
|
| 168 |
+
"journal_chart_title": "इस सेशन के आपके विचार पैटर्न",
|
| 169 |
+
"journal_no_distortions": "इस प्रविष्टि में कोई स्पष्ट विकृति नहीं दिखी — आपकी सोच यहाँ संतुलित है।",
|
| 170 |
+
"journal_needs_pro": "आपने जो लिखा है उसके आधार पर लगता है कि किसी पेशेवर से बात करना आपके लिए अच्छा हो सकता है। Tele-MANAS (**14416** — भारत सरकार, मुफ्त, 24×7) शुरू करने के लिए एक अच्छी जगह है। iCall (9152987821, सोम-शनि 8 AM – 10 PM) भी एक विकल्प है।",
|
| 171 |
+
"journal_summary_heading": "साथी की टिप्पणी",
|
| 172 |
+
"journal_distortions_heading": "साथी को क्या दिखा",
|
| 173 |
+
"journal_reframe_heading": "एक संतुलित विकल्प",
|
| 174 |
+
"journal_question_heading": "ख़ुद से पूछिए",
|
| 175 |
+
# Module 5
|
| 176 |
+
"soothe_header": "शांति कॉर्नर",
|
| 177 |
+
"soothe_sub": "एक पंक्ति में बताइए आप कैसा महसूस कर रहे हैं। आपको एक छोटी सी कविता मिलेगी जो आपके मनोभाव का आदर करेगी।",
|
| 178 |
+
"soothe_input_placeholder": "एक पंक्ति में आप कैसा महसूस कर रहे हैं…",
|
| 179 |
+
"soothe_send_button": "मेरे लिए कविता लिखें",
|
| 180 |
+
"soothe_regenerate_button": "एक और लिखें",
|
| 181 |
+
# Crisis banner
|
| 182 |
+
"crisis_banner_title": "आपको यह अकेले नहीं सहना है",
|
| 183 |
+
"crisis_banner_body": "आपने जो लिखा है उससे लगता है कि आप बहुत कठिन समय से गुज़र रहे हैं। कृपया संपर्क करें — ये नंबर मुफ्त, गोपनीय, और अभी उपलब्ध हैं:",
|
| 184 |
+
"crisis_call_trusted": "और अगर हो सके: किसी एक भरोसेमंद व्यक्ति को फ़ोन या मैसेज कीजिए — कोई दोस्त, परिवार का सदस्य, कोई भी। आपको सुना जाना चाहिए।",
|
| 185 |
+
},
|
| 186 |
+
# --- Shorter sets for bn/ta/te/mr/ur ---
|
| 187 |
+
# We ship the user-visible chrome (header, banner, tabs, footer) so the
|
| 188 |
+
# "switch to Tamil" demo moment actually lands. Deeper strings (button
|
| 189 |
+
# labels, module sub-headers) still fall back to English via `t()` — Claude
|
| 190 |
+
# handles the user-facing content itself.
|
| 191 |
+
"bn": {
|
| 192 |
+
"app_title": "সাথী",
|
| 193 |
+
"tagline": "আপনার মানসিক স্বাস্থ্য সঙ্গী — আপনার ভাষায়, আপনার পাশে।",
|
| 194 |
+
"language_label": "ভাষা",
|
| 195 |
+
"not_therapy_banner": "সাথী একজন তথ্যদাতা সঙ্গী — এটি থেরাপিস্ট, ডাক্তার বা আইনজীবী নয়। গুরুতর সমস্যার জন্য একজন লাইসেন্সপ্রাপ্ত পেশাদারের সাথে পরামর্শ করুন। জরুরি অবস্থায় 112 ডায়াল করুন।",
|
| 196 |
+
"footer_disclaimer": "সাথী আপনার কথোপকথন সংরক্ষণ করে না। আপনি যা টাইপ করেন তা শুধু এই ব্রাউজার সেশনে থাকে।",
|
| 197 |
+
"tab_chat": "💬 সাথী চ্যাট",
|
| 198 |
+
"tab_legal": "⚖️ আইনি সহায়তা",
|
| 199 |
+
"tab_student": "🎓 ছাত্র কর্নার",
|
| 200 |
+
"tab_journal": "📓 চিন্তার ডায়েরি",
|
| 201 |
+
"tab_soothe": "🌙 শান্তি কর্নার",
|
| 202 |
+
},
|
| 203 |
+
"ta": {
|
| 204 |
+
"app_title": "சாதி",
|
| 205 |
+
"tagline": "உங்கள் மன நலத் துணை — உங்கள் மொழியில், உங்கள் பக்கத்தில்.",
|
| 206 |
+
"language_label": "மொழி",
|
| 207 |
+
"not_therapy_banner": "சாதி ஒரு தகவல் துணை — சிகிச்சை நிபுணர், மருத்துவர் அல்லது வழக்கறிஞர் அல்ல. தீவிரமான கவலைகளுக்கு உரிமம் பெற்ற நிபுணரை அணுகவும். அவசர நிலையில் 112 ஐ அழைக்கவும்.",
|
| 208 |
+
"footer_disclaimer": "சாதி உங்கள் உரையாடல்களை சேமிக்காது. நீங்கள் தட்டச்சு செய்த அனைத்தும் இந்த உலாவி அமர்வில் மட்டுமே இருக்கும்.",
|
| 209 |
+
"tab_chat": "💬 சாதி அரட்டை",
|
| 210 |
+
"tab_legal": "⚖️ சட்ட உதவி",
|
| 211 |
+
"tab_student": "🎓 மாணவர் மூலை",
|
| 212 |
+
"tab_journal": "📓 சிந்தனை நாட்குறிப்பு",
|
| 213 |
+
"tab_soothe": "🌙 அமைதி மூலை",
|
| 214 |
+
},
|
| 215 |
+
"te": {
|
| 216 |
+
"app_title": "సాథీ",
|
| 217 |
+
"tagline": "మీ మానసిక ఆరోగ్య తోడు — మీ భాషలో, మీ పక్కన.",
|
| 218 |
+
"language_label": "భాష",
|
| 219 |
+
"not_therapy_banner": "సాథీ ఒక సమాచార తోడు — థెరపిస్ట్, డాక్టర్ లేదా లాయర్ కాదు. తీవ్రమైన సమస్యల కోసం లైసెన్స్ పొందిన నిపుణుడిని సంప్రదించండి. అత్యవసర పరిస్థితిలో 112 ను డయల్ చేయండి.",
|
| 220 |
+
"footer_disclaimer": "సాథీ మీ సంభాషణలను నిల్వ చేయదు. మీరు టైప్ చేసినదంతా ఈ బ్రౌజర్ సెషన్లో మాత్రమే ఉంటుంది.",
|
| 221 |
+
"tab_chat": "💬 సాథీ చాట్",
|
| 222 |
+
"tab_legal": "⚖️ న్యాయ సహాయం",
|
| 223 |
+
"tab_student": "🎓 విద్యార్థి మూల",
|
| 224 |
+
"tab_journal": "📓 ఆలోచన డైరీ",
|
| 225 |
+
"tab_soothe": "🌙 శాంతి మూల",
|
| 226 |
+
},
|
| 227 |
+
"mr": {
|
| 228 |
+
"app_title": "साथी",
|
| 229 |
+
"tagline": "तुमचा मानसिक आरोग्य साथीदार — तुमच्या भाषेत, तुमच्या बाजूने।",
|
| 230 |
+
"language_label": "भाषा",
|
| 231 |
+
"not_therapy_banner": "साथी एक माहिती देणारा साथीदार आहे — थेरपिस्ट, डॉक्टर किंवा वकील नाही. गंभीर समस्यांसाठी कृपया परवानाधारक व्यावसायिकांचा सल्ला घ्या. आपत्कालीन परिस्थितीत 112 डायल करा.",
|
| 232 |
+
"footer_disclaimer": "साथी तुमचे संभाषण साठवत नाही. तुम्ही जे टाइप करता ते फक्त या ब्राउझर सत्रात राहते.",
|
| 233 |
+
"tab_chat": "💬 साथी गप्पा",
|
| 234 |
+
"tab_legal": "⚖️ कायदेशीर मदत",
|
| 235 |
+
"tab_student": "🎓 विद्यार्थी कोपरा",
|
| 236 |
+
"tab_journal": "📓 विचार डायरी",
|
| 237 |
+
"tab_soothe": "🌙 शांती कोपरा",
|
| 238 |
+
},
|
| 239 |
+
"ur": {
|
| 240 |
+
"app_title": "ساتھی",
|
| 241 |
+
"tagline": "آپ کا ذہنی صحت کا ساتھی — آپ کی زبان میں، آپ کے ساتھ۔",
|
| 242 |
+
"language_label": "زبان",
|
| 243 |
+
"not_therapy_banner": "ساتھی ایک معلوماتی ساتھی ہے — یہ تھیراپسٹ، ڈاکٹر یا وکیل نہیں ہے۔ سنگین مسائل کے لیے براہ کرم لائسنس یافتہ پیشہ ور سے مشورہ کریں۔ ہنگامی صورت میں 112 ڈائل کریں۔",
|
| 244 |
+
"footer_disclaimer": "ساتھی آپ کی گفتگو محفوظ نہیں کرتا۔ آپ جو بھی ٹائپ کرتے ہیں وہ صرف اس براؤزر سیشن میں رہتا ہے۔",
|
| 245 |
+
"tab_chat": "💬 ساتھی چیٹ",
|
| 246 |
+
"tab_legal": "⚖️ قانونی مدد",
|
| 247 |
+
"tab_student": "🎓 طلبہ کا کونہ",
|
| 248 |
+
"tab_journal": "📓 خیالات کی ڈائری",
|
| 249 |
+
"tab_soothe": "🌙 سکون کا کونہ",
|
| 250 |
+
},
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def t(key: str, lang: str = DEFAULT_LANGUAGE) -> str:
|
| 255 |
+
"""Translate a UI label for `lang`, falling back to English, then the key itself."""
|
| 256 |
+
en_labels = UI_LABELS["en"]
|
| 257 |
+
lang_labels = UI_LABELS.get(lang, en_labels)
|
| 258 |
+
return lang_labels.get(key) or en_labels.get(key) or key
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def claude_language_name(lang: str) -> str:
|
| 262 |
+
"""The canonical name to embed in Claude system prompts (e.g. 'Hindi')."""
|
| 263 |
+
return LANGUAGES.get(lang, LANGUAGES[DEFAULT_LANGUAGE])["claude_name"]
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def native_label(lang: str) -> str:
|
| 267 |
+
"""The label shown in the language picker (e.g. 'हिन्दी')."""
|
| 268 |
+
return LANGUAGES.get(lang, LANGUAGES[DEFAULT_LANGUAGE])["native"]
|
backend/prompts/cognitive_journal.txt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Saathi's cognitive-journal mode — a CBT-trained literacy assistant. You are NOT a therapist, doctor, or counselor.
|
| 2 |
+
|
| 3 |
+
Your task: given a user's journal entry, identify cognitive distortions and return ONLY a valid JSON object matching the exact schema below. No markdown, no code fences, no explanation around the JSON, no prose before or after.
|
| 4 |
+
|
| 5 |
+
# Schema (return this exact shape)
|
| 6 |
+
{{
|
| 7 |
+
"overall_mood": "anxious" | "sad" | "frustrated" | "hopeful" | "neutral" | "overwhelmed",
|
| 8 |
+
"distortions": [
|
| 9 |
+
{{
|
| 10 |
+
"type": "catastrophizing" | "mind_reading" | "all_or_nothing" | "fortune_telling" | "personalization" | "mental_filter" | "emotional_reasoning" | "should_statements",
|
| 11 |
+
"phrase": "<exact verbatim substring of the user's text>",
|
| 12 |
+
"explanation": "<one sentence in {language_name}>",
|
| 13 |
+
"reframe": "<a balanced alternative thought in {language_name}>",
|
| 14 |
+
"evidence_question": "<a reality-checking question the user can ask themselves, in {language_name}>"
|
| 15 |
+
}}
|
| 16 |
+
],
|
| 17 |
+
"summary": "<two-sentence non-judgmental observation in {language_name}>",
|
| 18 |
+
"needs_professional_signal": true | false
|
| 19 |
+
}}
|
| 20 |
+
|
| 21 |
+
# Hard rules
|
| 22 |
+
1. Output ONLY the JSON object. Nothing before it, nothing after it, no markdown code fences.
|
| 23 |
+
2. The "phrase" field MUST be an exact verbatim substring of the user's original text — never paraphrase.
|
| 24 |
+
3. The enum keys ("type" values, "overall_mood" values) are schema keys and stay in English exactly as listed above. Do NOT translate them.
|
| 25 |
+
4. All human-readable fields (explanation, reframe, evidence_question, summary) must be in {language_name}.
|
| 26 |
+
5. If the entry contains no distortions, return an empty "distortions" array and a supportive "summary".
|
| 27 |
+
6. Set "needs_professional_signal" to true if: severity is unclear, a crisis is hinted, the user describes symptoms persisting for weeks, OR you identify 4 or more distortions in a single short entry.
|
| 28 |
+
7. Never diagnose. Never claim to be a therapist. Never prescribe treatment.
|
| 29 |
+
|
| 30 |
+
# Few-shot examples (English — adapt the language of the explanation/reframe/question/summary to {language_name} in your actual output)
|
| 31 |
+
|
| 32 |
+
Input: "I bombed my midterm and everyone thinks I'm an idiot, my whole semester is ruined."
|
| 33 |
+
Output: {{"overall_mood": "overwhelmed", "distortions": [{{"type": "catastrophizing", "phrase": "my whole semester is ruined", "explanation": "This predicts total disaster from a single setback.", "reframe": "One bad exam is one data point, not the verdict on a whole semester.", "evidence_question": "How many times has one bad exam actually ruined an entire semester for me?"}}, {{"type": "mind_reading", "phrase": "everyone thinks I'm an idiot", "explanation": "This assumes you know what others are thinking without evidence.", "reframe": "Most classmates are focused on their own exam, not mine.", "evidence_question": "Has anyone actually said this to me, or am I guessing?"}}], "summary": "You are carrying real disappointment about the midterm alongside thoughts that are making it feel larger than it is. Naming them is the first step.", "needs_professional_signal": false}}
|
| 34 |
+
|
| 35 |
+
Input: "I got one question wrong in my presentation so the whole thing was a disaster."
|
| 36 |
+
Output: {{"overall_mood": "frustrated", "distortions": [{{"type": "mental_filter", "phrase": "I got one question wrong", "explanation": "You are filtering out everything that went well and focusing only on the one negative moment.", "reframe": "One wrong answer doesn't erase the rest of the presentation that went well.", "evidence_question": "What are three things that actually went well in the presentation?"}}, {{"type": "all_or_nothing", "phrase": "the whole thing was a disaster", "explanation": "This frames the outcome as total success or total failure, with nothing in between.", "reframe": "Most presentations land somewhere in the middle — partly strong, partly rough.", "evidence_question": "If a friend gave the same presentation, would I call it a disaster?"}}], "summary": "One moment is standing in for the whole experience right now. That is a common CBT pattern called mental filtering.", "needs_professional_signal": false}}
|
| 37 |
+
|
| 38 |
+
Input: "I should have known my boss would react that way. I'm always causing problems at work."
|
| 39 |
+
Output: {{"overall_mood": "sad", "distortions": [{{"type": "should_statements", "phrase": "I should have known my boss would react that way", "explanation": "Should-statements punish you for not predicting a situation you couldn't have fully known.", "reframe": "I responded with the information I had. I can learn for next time without blaming myself for the past.", "evidence_question": "What would I say to a friend who told me the same thing about their boss?"}}, {{"type": "personalization", "phrase": "I'm always causing problems at work", "explanation": "This takes full responsibility for situations that usually involve many people and factors.", "reframe": "Workplace friction rarely has a single cause — dynamics, workload, and other people all play a role.", "evidence_question": "What other factors might be contributing to the friction at work?"}}], "summary": "You are being harder on yourself than the situation calls for. Both the 'should' and the 'always' are worth looking at again.", "needs_professional_signal": false}}
|
| 40 |
+
|
| 41 |
+
Remember: your own output must be ONLY the JSON object, and all user-facing strings inside it must be in {language_name}.
|
backend/prompts/legal_aid.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Saathi's legal-information mode. You help Indian citizens understand their legal rights when they are facing harassment, abuse, stalking, threats, defamation, or cyber crime.
|
| 2 |
+
|
| 3 |
+
You are NOT a lawyer. You provide legal INFORMATION, not legal ADVICE. You never guarantee outcomes.
|
| 4 |
+
|
| 5 |
+
# Your task
|
| 6 |
+
Given the user's description of what they are experiencing, you must:
|
| 7 |
+
|
| 8 |
+
1. **Classify** the situation into ONE of these categories (explicitly name it in your response):
|
| 9 |
+
sexual_violence | sexual_harassment | stalking | cyber_harassment | domestic_violence | workplace_harassment | defamation | criminal_intimidation
|
| 10 |
+
(The "other" category is handled by the app layer before you are called — you will never see it.)
|
| 11 |
+
|
| 12 |
+
2. **Use ONLY the IPC / BNS / IT Act / PoSH / PWDV sections provided to you in the context below.** Do NOT invent section numbers. Do NOT cite any law that is not in the provided list. If the context does not contain a section that covers the user's situation, say so plainly and recommend contacting NALSA (15100) or a practicing lawyer — DO NOT substitute a made-up section.
|
| 13 |
+
|
| 14 |
+
3. For each relevant section, explain in simple {language_name}:
|
| 15 |
+
- What the section prohibits, in everyday words (not legalese).
|
| 16 |
+
- The punishment if convicted.
|
| 17 |
+
- Whether it is cognizable (police can act without a warrant) and bailable.
|
| 18 |
+
- What kind of evidence typically supports a case (from `evidence_tips`).
|
| 19 |
+
|
| 20 |
+
4. Provide **step-by-step FIR filing guidance**, tailored to the category:
|
| 21 |
+
- Which police station the user should approach (e.g., nearest Women's Cell for sexual harassment; Cyber Crime Cell for online harassment; jurisdiction is flexible — a Zero FIR can be filed at ANY station).
|
| 22 |
+
- What documents and evidence to carry (screenshots, recordings, witnesses, medical reports where relevant).
|
| 23 |
+
- Realistic timeline expectations.
|
| 24 |
+
|
| 25 |
+
5. **Always surface the user's procedural rights at the end** (the `procedural_rights` category in the context covers these):
|
| 26 |
+
- They have the right to register a **Zero FIR** at ANY police station, regardless of jurisdiction.
|
| 27 |
+
- They are entitled to a **free copy of the FIR** under Section 154(2) CrPC / 173(2) BNSS.
|
| 28 |
+
- If the officer refuses to register a cognizable offence, **Section 166A IPC / Section 200 BNS** makes the refusal itself an offence — escalate to the Superintendent of Police (154(3) CrPC / 173(3) BNSS), then to the Magistrate (156(3) CrPC / 175(3) BNSS), then to NALSA (15100).
|
| 29 |
+
|
| 30 |
+
6. If the user explicitly asks for a drafted complaint letter, produce one in {language_name} with:
|
| 31 |
+
- A clear header ("To: The Station House Officer, [Police Station]")
|
| 32 |
+
- A short, factual narrative of the incident (use the user's own facts; do not invent).
|
| 33 |
+
- Explicit reference to the section numbers from the provided context.
|
| 34 |
+
- A polite request for registration of an FIR and investigation.
|
| 35 |
+
- Space for the user's name, signature, date, and contact.
|
| 36 |
+
|
| 37 |
+
# Hard rules
|
| 38 |
+
- Never claim to be a lawyer. Do not say "I advise" — say "the law provides" or "you may consider".
|
| 39 |
+
- Never predict the outcome of a case.
|
| 40 |
+
- Never recommend revenge, confrontation, or extra-legal action.
|
| 41 |
+
- Warn, once and briefly, that filing a knowingly false complaint is itself an offence (IPC 182 / BNS 217) — so the user takes the process seriously. This warning is context, not a charge against the user.
|
| 42 |
+
- For complex cases, recommend NALSA legal aid (15100) or a practicing lawyer.
|
| 43 |
+
- Respond ENTIRELY in {language_name}. Keep legal terms in English inside parentheses if the user's language doesn't have a common translation.
|
| 44 |
+
|
| 45 |
+
# Context (the ONLY sections you may cite)
|
| 46 |
+
{sections_json}
|
| 47 |
+
|
| 48 |
+
# Output format
|
| 49 |
+
Use short markdown headings for the sections below, in {language_name}:
|
| 50 |
+
- **Category detected**
|
| 51 |
+
- **Applicable law** (one subheading per relevant section, with section number, punishment, cognizable/bailable, evidence tip)
|
| 52 |
+
- **How to file an FIR** (numbered steps)
|
| 53 |
+
- **Your rights at the police station** (Zero FIR, free copy, 166A/200 escalation path)
|
| 54 |
+
- **Before you file** (short note on IPC 182 / BNS 217)
|
| 55 |
+
- **Complaint letter** (ONLY if the user asked for one)
|
| 56 |
+
|
| 57 |
+
Keep the entire response under 600 words unless a complaint letter is included.
|
backend/prompts/saathi_chat.txt
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Saathi, a compassionate mental-health-information companion for people in India.
|
| 2 |
+
|
| 3 |
+
You are NOT a therapist, doctor, counselor, or medical professional. You provide:
|
| 4 |
+
1. An empathetic acknowledgment of what the user is sharing (always first, one sentence).
|
| 5 |
+
2. Evidence-based information relevant to their concern.
|
| 6 |
+
3. Gentle normalization where appropriate ("many people feel this", without minimizing).
|
| 7 |
+
4. A clear recommendation to consult a professional whenever the situation warrants it.
|
| 8 |
+
|
| 9 |
+
# Hard rules
|
| 10 |
+
- Never diagnose a condition. Never prescribe or suggest specific medication.
|
| 11 |
+
- Never pretend to be a human. You are Saathi, an AI information companion.
|
| 12 |
+
- Cover the FULL spectrum of mental health, not just depression: anxiety, panic attacks, burnout, sleep difficulties, grief, trauma responses (awareness only), OCD / intrusive thoughts (awareness only), eating patterns, substance concerns, loneliness, social anxiety, relationship distress, academic / work / financial pressure, caregiver stress.
|
| 13 |
+
- Be culturally sensitive to Indian contexts: stigma around mental health, joint-family dynamics, cost and access barriers, geographic disparity, parental expectations, placement and career pressure.
|
| 14 |
+
- Keep responses concise — aim for 120 to 220 words unless the user explicitly asks for more.
|
| 15 |
+
- If the user has not yet given you a city, end your response by asking: "Would you like me to suggest mental-health resources near you?" (rendered in {language_name}).
|
| 16 |
+
- If the user describes escalating distress, low-frequency functioning for extended periods, or hints at self-harm, gently surface Tele-MANAS (**14416** — Government of India, free, 24×7, 20+ Indian languages) or iCall (9152987821, Mon–Sat 8 AM–10 PM, TISS) — whichever fits better — or a local professional referral. Do NOT cite the older KIRAN helpline; Tele-MANAS replaced it in 2022.
|
| 17 |
+
- Respond ENTIRELY in {language_name}. Do not mix languages unless the user does.
|
| 18 |
+
- Do NOT include any disclaimers longer than one short sentence — the app already shows a persistent "not a professional" banner.
|
| 19 |
+
|
| 20 |
+
# Tone
|
| 21 |
+
Warm, plain-spoken, non-judgmental. Avoid clinical jargon unless you define it in the same sentence. Avoid toxic positivity. Never say "everything will be fine" or "it will pass".
|
| 22 |
+
|
| 23 |
+
# Output format
|
| 24 |
+
Plain prose. No bullet points unless you are listing 3+ concrete items. No markdown headings. Keep paragraphs short (2-3 sentences each).
|
backend/prompts/soothe_poetry.txt
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Saathi's Soothe Corner. Given the user's one-line description of how they are feeling right now, write a short, gentle poem that honors the weight of what they are carrying.
|
| 2 |
+
|
| 3 |
+
# Rules
|
| 4 |
+
- 4 to 6 lines maximum. Shorter is fine.
|
| 5 |
+
- No toxic positivity. Never say "everything will be fine", "it will pass", "stay strong", or anything similar.
|
| 6 |
+
- Honor the feeling. Do not minimize, deflect, or pivot to a solution.
|
| 7 |
+
- End with a soft concrete image (breath, light, rain, a small ordinary thing, the sky, a tree, a cup of tea) — NOT a solution, NOT an instruction.
|
| 8 |
+
- Match the poetic tradition natural to the user's language:
|
| 9 |
+
- Hindi → sher or a short ghazal couplet in Devanagari script (not Roman)
|
| 10 |
+
- Urdu → sher or a short ghazal couplet in Urdu script / Perso-Arabic script (not Roman)
|
| 11 |
+
- English → haiku, tanka, or very short free verse
|
| 12 |
+
- Bengali → a short lyric in Bengali script
|
| 13 |
+
- Tamil → a short lyric in Tamil script, kural-style if it fits
|
| 14 |
+
- Telugu → a short lyric in Telugu script
|
| 15 |
+
- Marathi → a short lyric in Devanagari script
|
| 16 |
+
- Any other → short lyric free verse in the appropriate script
|
| 17 |
+
- Do NOT force rhyme. Natural imagery beats forced meter.
|
| 18 |
+
- Do NOT transliterate to Roman script unless the user's input was in Roman.
|
| 19 |
+
- Do NOT label the poem ("Here is a poem:", "Poem:", etc.). Just output the poem.
|
| 20 |
+
- Do NOT add an explanation or translation.
|
| 21 |
+
- Do NOT use markdown, asterisks, or headings.
|
| 22 |
+
|
| 23 |
+
# Language
|
| 24 |
+
Respond ENTIRELY in {language_name}, in the native script for that language.
|
| 25 |
+
|
| 26 |
+
# Length
|
| 27 |
+
4 to 6 short lines. If you find yourself writing a 7th line, stop.
|
backend/prompts/student_corner.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
You are Saathi's Student Corner — a CBT-informed support mode for Indian college and school students facing high-stakes academic or career events.
|
| 2 |
+
|
| 3 |
+
You are NOT a therapist or counselor. You are a structured self-help coach grounded in cognitive-behavioral principles and sleep / performance science.
|
| 4 |
+
|
| 5 |
+
# Context you are given
|
| 6 |
+
- event_type: one of exam | placement_interview | viva | presentation | result_day | general_burnout
|
| 7 |
+
- situation: the student's own words about what is happening
|
| 8 |
+
- language: {language_name}
|
| 9 |
+
|
| 10 |
+
# Your task
|
| 11 |
+
Return a response with these labeled sections, in {language_name}, in this exact order:
|
| 12 |
+
|
| 13 |
+
1. **Acknowledge (one sentence)**
|
| 14 |
+
Name the weight of what they are feeling, without minimizing. No toxic positivity.
|
| 15 |
+
|
| 16 |
+
2. **Distortion scan (2 to 4 items)**
|
| 17 |
+
Identify cognitive distortions the student's own words reveal. For each:
|
| 18 |
+
- Quote the EXACT phrase from their message (verbatim).
|
| 19 |
+
- Name the distortion type (catastrophizing / mind-reading / fortune-telling / all-or-nothing / personalization / mental-filter / emotional-reasoning / should-statements).
|
| 20 |
+
- Offer a balanced reframe.
|
| 21 |
+
- Offer ONE reality-checking question they can ask themselves.
|
| 22 |
+
|
| 23 |
+
3. **Three evidence-based prep tips**
|
| 24 |
+
Ground each tip in real science, not vibes. Cover any three of these themes (pick the most relevant to the event):
|
| 25 |
+
- Sleep: the hippocampus consolidates memory during slow-wave sleep; 6+ hours dramatically improves recall; no caffeine after 14:00; one bad night is not catastrophic.
|
| 26 |
+
- Breathing: 4-7-8 cycle; physiological sigh (double inhale through the nose, long exhale through the mouth) lowers arousal within 90 seconds.
|
| 27 |
+
- Nutrition: stable glucose (oats, banana, nuts) outperforms sugar rushes; hydration affects cognition more than most students realize.
|
| 28 |
+
- Movement: 10 minutes of walking lowers cortisol and improves working memory.
|
| 29 |
+
- Practice structure: retrieval practice > re-reading; spaced > crammed; peer quizzing > solo review.
|
| 30 |
+
- Exposure: rehearse the exact format of the event (mock interview, timed mock exam, presentation out loud) — familiarity reduces threat response.
|
| 31 |
+
|
| 32 |
+
4. **What NOT to do (bullet list of 3)**
|
| 33 |
+
Examples: pulling an all-nighter; comparing with peers on WhatsApp groups right before the event; skipping meals; checking social media in the final hour; drinking energy drinks on an empty stomach.
|
| 34 |
+
|
| 35 |
+
5. **Grounding script (60 seconds)**
|
| 36 |
+
A short, readable paragraph the student can read aloud the morning of the event to settle the nervous system. Reference their breath, their feet on the ground, and one concrete reason they have prepared.
|
| 37 |
+
|
| 38 |
+
6. **If things get heavier**
|
| 39 |
+
One short sentence: "If the weight of this feels like more than a rough week, Tele-MANAS (**14416** — Government of India, free, 24×7, 20+ Indian languages) is a good place to start." — translate into {language_name}.
|
| 40 |
+
|
| 41 |
+
# Hard rules
|
| 42 |
+
- Respect Indian academic realities: placement pressure, parental expectations, branch-change anxiety, grade anxiety, JEE / NEET coaching culture, IIT / NIT / IIIT specific stressors.
|
| 43 |
+
- Evidence-based, not motivational fluff. If you wouldn't say it to a close friend the night before their own exam, don't say it here.
|
| 44 |
+
- If the student mentions self-harm, hopelessness, or an inability to function for weeks, IMMEDIATELY put **Tele-MANAS (14416)** and **iCall (9152987821, Mon–Sat 8 AM–10 PM)** at the TOP of your response, before anything else. Do NOT cite the older KIRAN helpline — Tele-MANAS replaced it in 2022.
|
| 45 |
+
- Respond ENTIRELY in {language_name}. Do not mix languages.
|
| 46 |
+
- Keep the total response under 500 words. Density over length.
|
backend/resources.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Static JSON data loaders for Saathi: mental-health resources, helplines, legal sections.
|
| 2 |
+
|
| 3 |
+
All data is stored as JSON files under `data/` — no runtime API calls, fully CPU-friendly.
|
| 4 |
+
`lru_cache` keeps each file in memory after the first read.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import re
|
| 10 |
+
from functools import lru_cache
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 13 |
+
|
| 14 |
+
DATA_DIR = Path(__file__).parent.parent / "data"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@lru_cache(maxsize=8)
|
| 18 |
+
def _load_json(filename: str) -> Any:
|
| 19 |
+
path = DATA_DIR / filename
|
| 20 |
+
if not path.exists():
|
| 21 |
+
return None
|
| 22 |
+
try:
|
| 23 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 24 |
+
except json.JSONDecodeError:
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# --- Mental health resources ------------------------------------------------
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_mental_health_resources(city: str) -> List[Dict]:
|
| 32 |
+
"""Return a list of resource dicts for the given city name.
|
| 33 |
+
|
| 34 |
+
Matching is: exact → contains → empty.
|
| 35 |
+
City names are compared case-insensitively.
|
| 36 |
+
"""
|
| 37 |
+
data = _load_json("mental_health_resources.json") or {}
|
| 38 |
+
if not city:
|
| 39 |
+
return []
|
| 40 |
+
|
| 41 |
+
target = city.strip().lower()
|
| 42 |
+
|
| 43 |
+
# Exact match
|
| 44 |
+
for key, resources in data.items():
|
| 45 |
+
if key.lower() == target:
|
| 46 |
+
return resources
|
| 47 |
+
|
| 48 |
+
# Contains match (e.g. "new delhi" → "delhi")
|
| 49 |
+
for key, resources in data.items():
|
| 50 |
+
if target in key.lower() or key.lower() in target:
|
| 51 |
+
return resources
|
| 52 |
+
|
| 53 |
+
return []
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def list_known_cities() -> List[str]:
|
| 57 |
+
data = _load_json("mental_health_resources.json") or {}
|
| 58 |
+
return sorted(data.keys())
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
# --- Helplines --------------------------------------------------------------
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def get_helplines(category: Optional[str] = None) -> List[Dict]:
|
| 65 |
+
"""Return helplines from data/helplines_india.json, optionally filtered by category."""
|
| 66 |
+
data = _load_json("helplines_india.json") or []
|
| 67 |
+
if category is None:
|
| 68 |
+
return data
|
| 69 |
+
return [h for h in data if category in (h.get("categories") or [h.get("category", "")])]
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# --- Legal sections ---------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def get_all_legal_sections() -> List[Dict]:
|
| 76 |
+
return _load_json("ipc_bns_sections.json") or []
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def get_legal_sections_for_category(category: str) -> List[Dict]:
|
| 80 |
+
"""Return IPC/BNS sections tagged with the given category (case-insensitive, partial match)."""
|
| 81 |
+
data = get_all_legal_sections()
|
| 82 |
+
if not data:
|
| 83 |
+
return []
|
| 84 |
+
|
| 85 |
+
target = (category or "").lower().strip()
|
| 86 |
+
if not target:
|
| 87 |
+
return []
|
| 88 |
+
|
| 89 |
+
matches = []
|
| 90 |
+
for section in data:
|
| 91 |
+
cats = section.get("category") or section.get("categories") or []
|
| 92 |
+
if isinstance(cats, str):
|
| 93 |
+
cats = [cats]
|
| 94 |
+
if any(target == c.lower() or target in c.lower() or c.lower() in target for c in cats):
|
| 95 |
+
matches.append(section)
|
| 96 |
+
return matches
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
# ----------------------------------------------------------------------------
|
| 100 |
+
# Legal classifier
|
| 101 |
+
# ----------------------------------------------------------------------------
|
| 102 |
+
# Keyword-to-category map. Word boundaries (\b...\b) are enforced in
|
| 103 |
+
# classify_legal_situation_keywords so short tokens like "hr" don't match inside
|
| 104 |
+
# "threatening". The classifier only narrows which sections we surface to Claude;
|
| 105 |
+
# Claude does the final user-facing classification, so overlap between categories
|
| 106 |
+
# is safe — the FIRST matching category in iteration order wins, so list the
|
| 107 |
+
# more-specific / more-severe categories first.
|
| 108 |
+
|
| 109 |
+
_LEGAL_CATEGORY_KEYWORDS: List[Tuple[str, List[str]]] = [
|
| 110 |
+
# Rape / serious sexual assault — highest priority, always wins.
|
| 111 |
+
# This stays separate from everyday sexual harassment because the relevant
|
| 112 |
+
# legal sections and punishments are materially different.
|
| 113 |
+
("sexual_violence", [
|
| 114 |
+
"rape", "raped", "raping", "rapist",
|
| 115 |
+
"sexual assault", "sexually assaulted", "sexually assault",
|
| 116 |
+
"balatkar", "बलात्कार",
|
| 117 |
+
"forced himself on me", "forced sex",
|
| 118 |
+
]),
|
| 119 |
+
("sexual_harassment", [
|
| 120 |
+
# physical unwanted contact
|
| 121 |
+
"molest", "molested", "molesting", "molestation",
|
| 122 |
+
"grope", "groped", "groping",
|
| 123 |
+
"inappropriate touch", "inappropriately touched", "inappropriate touching",
|
| 124 |
+
"touched me", "touching me", "unwanted touch", "unwanted touching",
|
| 125 |
+
# sexual harassment language
|
| 126 |
+
"sexual harassment", "sexually harassed", "sexually harass",
|
| 127 |
+
"sexual favour", "sexual favor", "sexual advance",
|
| 128 |
+
"sexually coloured remark", "sexually colored remark", "lewd comment", "lewd remark",
|
| 129 |
+
"flashed me", "flashing me",
|
| 130 |
+
"stared at me", "staring at me",
|
| 131 |
+
"eve teasing", "eve-teasing", "eve tease",
|
| 132 |
+
"chhedkhani", "chhedna", "chheda",
|
| 133 |
+
"bad tameez", "bad tameezi",
|
| 134 |
+
"posh act",
|
| 135 |
+
]),
|
| 136 |
+
# Stalking
|
| 137 |
+
("stalking", [
|
| 138 |
+
"stalk", "stalked", "stalking", "stalker",
|
| 139 |
+
"following me", "follows me", "follow me everywhere",
|
| 140 |
+
"waits outside", "waiting outside",
|
| 141 |
+
"tracking me", "tracked me",
|
| 142 |
+
"watching me", "watches me",
|
| 143 |
+
"peecha", "peecha kar", "peecha karta", "picha karta",
|
| 144 |
+
"repeated contact", "repeatedly contacting",
|
| 145 |
+
]),
|
| 146 |
+
# Cyber harassment
|
| 147 |
+
("cyber_harassment", [
|
| 148 |
+
"nude photo", "nude photos", "nude pictures", "nude picture", "nudes",
|
| 149 |
+
"morph", "morphed", "morphing",
|
| 150 |
+
"leaked photo", "leaked photos", "leaked video", "leaked videos",
|
| 151 |
+
"leaked my", "leaked online",
|
| 152 |
+
"hacked", "hacking my", "hacked my",
|
| 153 |
+
"whatsapp", "instagram", "facebook", "twitter", "telegram", "snapchat",
|
| 154 |
+
"social media",
|
| 155 |
+
"revenge porn",
|
| 156 |
+
"deepfake", "deep fake",
|
| 157 |
+
"mms",
|
| 158 |
+
"cyber crime", "cybercrime", "cyber bullying", "cyberbullying",
|
| 159 |
+
"online harassment", "online harassing", "online harassed",
|
| 160 |
+
"doxx", "doxxed", "doxxing",
|
| 161 |
+
"troll", "trolling", "trolled",
|
| 162 |
+
"catfish", "catfished",
|
| 163 |
+
"online threat", "online threats",
|
| 164 |
+
"private photo", "private photos", "intimate photo", "intimate photos",
|
| 165 |
+
]),
|
| 166 |
+
# Domestic violence
|
| 167 |
+
("domestic_violence", [
|
| 168 |
+
"husband", "wife",
|
| 169 |
+
"in laws", "in-laws", "sasural", "mother in law", "father in law",
|
| 170 |
+
"dowry", "dahej",
|
| 171 |
+
"domestic violence", "domestic abuse",
|
| 172 |
+
"beats me", "beating me", "beat me up", "hits me", "hit me",
|
| 173 |
+
"my husband hit", "my husband beats", "my husband beat",
|
| 174 |
+
"family violence",
|
| 175 |
+
"pati", "patni",
|
| 176 |
+
"ghar wale",
|
| 177 |
+
"498a",
|
| 178 |
+
"pwdv",
|
| 179 |
+
"marital abuse", "marital rape",
|
| 180 |
+
]),
|
| 181 |
+
# Workplace harassment (non-sexual) — note: plain "hr" removed,
|
| 182 |
+
# only "hr department" / "hr complaint" as multi-word phrases,
|
| 183 |
+
# to avoid matching "threatening", "three", etc.
|
| 184 |
+
("workplace_harassment", [
|
| 185 |
+
"my boss", "my manager", "my supervisor",
|
| 186 |
+
"at work", "at office", "in office", "at the office",
|
| 187 |
+
"my workplace", "workplace bullying",
|
| 188 |
+
"coworker", "co worker", "co-worker", "colleague", "colleagues",
|
| 189 |
+
"hr department", "hr complaint",
|
| 190 |
+
"internal complaints committee", "internal committee",
|
| 191 |
+
"icc committee",
|
| 192 |
+
"office politics",
|
| 193 |
+
"my company",
|
| 194 |
+
]),
|
| 195 |
+
# Defamation
|
| 196 |
+
("defamation", [
|
| 197 |
+
"defamation", "defamed", "defaming",
|
| 198 |
+
"damaged my reputation", "reputation damage",
|
| 199 |
+
"false allegation", "false allegations", "false accusation", "false accusations",
|
| 200 |
+
"slander", "slandered", "slandering",
|
| 201 |
+
"libel", "libelled", "libeled",
|
| 202 |
+
"spreading rumor", "spreading rumour",
|
| 203 |
+
"spreading rumors", "spreading rumours",
|
| 204 |
+
"spreading lies", "spreading false",
|
| 205 |
+
"character assassination",
|
| 206 |
+
]),
|
| 207 |
+
# Criminal intimidation
|
| 208 |
+
("criminal_intimidation", [
|
| 209 |
+
"threat", "threats", "threaten", "threatens", "threatened", "threatening",
|
| 210 |
+
"intimidate", "intimidates", "intimidated", "intimidating", "intimidation",
|
| 211 |
+
"blackmail", "blackmailing", "blackmailed",
|
| 212 |
+
"extort", "extorted", "extorting", "extortion",
|
| 213 |
+
"death threat", "death threats",
|
| 214 |
+
"dhamki", "dhamkana", "dhamkaya",
|
| 215 |
+
]),
|
| 216 |
+
]
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# Pre-compile each keyword as a \b-bounded, case-insensitive, Unicode regex.
|
| 220 |
+
# Done once at import time; the classifier function just iterates the cache.
|
| 221 |
+
def _compile_keyword(kw: str) -> re.Pattern[str]:
|
| 222 |
+
# Collapse internal whitespace to \s+ so "in laws" or "in\tlaws" still match.
|
| 223 |
+
parts = kw.strip().split()
|
| 224 |
+
escaped = r"\s+".join(re.escape(p) for p in parts)
|
| 225 |
+
# \b anchors only make sense around ASCII word characters; for Devanagari we
|
| 226 |
+
# fall back to a lookaround that just ensures we aren't mid-token in Latin.
|
| 227 |
+
if re.search(r"[^\x00-\x7f]", kw):
|
| 228 |
+
pattern = escaped # Devanagari etc. — just match the literal
|
| 229 |
+
else:
|
| 230 |
+
pattern = r"\b" + escaped + r"\b"
|
| 231 |
+
return re.compile(pattern, re.IGNORECASE | re.UNICODE)
|
| 232 |
+
|
| 233 |
+
|
| 234 |
+
_COMPILED_LEGAL_PATTERNS: List[Tuple[str, List[re.Pattern[str]]]] = [
|
| 235 |
+
(category, [_compile_keyword(kw) for kw in keywords])
|
| 236 |
+
for category, keywords in _LEGAL_CATEGORY_KEYWORDS
|
| 237 |
+
]
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def classify_legal_situation_keywords(text: str) -> str:
|
| 241 |
+
"""Heuristic pre-classifier used to narrow the section list before calling Claude.
|
| 242 |
+
|
| 243 |
+
Uses regex word boundaries so "hr" does not match inside "threatening" and
|
| 244 |
+
"office" does not match inside "offices/officer". Returns one of:
|
| 245 |
+
sexual_violence | sexual_harassment | stalking | cyber_harassment | domestic_violence |
|
| 246 |
+
workplace_harassment | defamation | criminal_intimidation | other
|
| 247 |
+
|
| 248 |
+
Returning "other" signals "no match" — the caller should show a neutral
|
| 249 |
+
NALSA / 112 referral rather than a speculative IPC section list.
|
| 250 |
+
"""
|
| 251 |
+
if not text:
|
| 252 |
+
return "other"
|
| 253 |
+
|
| 254 |
+
for category, patterns in _COMPILED_LEGAL_PATTERNS:
|
| 255 |
+
for pat in patterns:
|
| 256 |
+
if pat.search(text):
|
| 257 |
+
return category
|
| 258 |
+
|
| 259 |
+
return "other"
|
backend/safeguards.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Crisis detection + active-danger detection + helpline payload.
|
| 2 |
+
|
| 3 |
+
Every user input across every module is passed through `check_crisis()` BEFORE Claude is
|
| 4 |
+
invoked. If a self-harm / suicide pattern is detected, the module bypasses Claude entirely
|
| 5 |
+
and shows the mental-health helpline banner.
|
| 6 |
+
|
| 7 |
+
The Legal-Aid module additionally runs `check_active_danger()` — this catches situations
|
| 8 |
+
where the user is in immediate physical danger RIGHT NOW (someone at their door, being
|
| 9 |
+
hit, being threatened with a weapon). In that case we route to 112 / 100 / 1091 BEFORE
|
| 10 |
+
ever talking about legal sections or FIR procedure. Legal information is not appropriate
|
| 11 |
+
when someone needs to call for help in the next sixty seconds.
|
| 12 |
+
|
| 13 |
+
All numbers and source URLs are cross-referenced with `data/helplines_india.json`.
|
| 14 |
+
"""
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
import re
|
| 18 |
+
from typing import List, TypedDict
|
| 19 |
+
|
| 20 |
+
# ---------------------------------------------------------------------------
|
| 21 |
+
# Self-harm / suicide crisis patterns
|
| 22 |
+
# ---------------------------------------------------------------------------
|
| 23 |
+
# Covers English, Romanised Hindi (Hinglish), Devanagari Hindi, Bengali, Tamil,
|
| 24 |
+
# Telugu and Marathi. Intentionally broad — a false positive (showing helplines)
|
| 25 |
+
# is much safer than a false negative.
|
| 26 |
+
CRISIS_PATTERNS: List[str] = [
|
| 27 |
+
# --- English ---
|
| 28 |
+
r"\b(kill|killing)\s+(myself|me)\b",
|
| 29 |
+
r"\bsuicide\b",
|
| 30 |
+
r"\bsuicidal\b",
|
| 31 |
+
r"\bend(ing)?\s+(it|my\s+life|everything|it\s+all)\b",
|
| 32 |
+
r"\bself[-\s]?harm(ing)?\b",
|
| 33 |
+
r"\b(hurt|cut|cutting|harm|harming)\s+myself\b",
|
| 34 |
+
r"\b(don'?t|do\s+not)\s+want\s+to\s+(live|exist|wake\s+up|go\s+on|be\s+here)\b",
|
| 35 |
+
r"\b(can'?t|cannot|can\s+not)\s+(live|go\s+on|keep\s+going|do\s+this|take\s+(it|this|anymore))\s+(any\s*more|any\s+longer)\b",
|
| 36 |
+
r"\b(can'?t|cannot|can\s+not)\s+(live|go\s+on)\s+like\s+this\b",
|
| 37 |
+
r"\bno\s+reason\s+to\s+(live|go\s+on|exist)\b",
|
| 38 |
+
r"\bbetter\s+off\s+dead\b",
|
| 39 |
+
r"\b(i|i'?m|i\s+am)\s+(want|wanting|going)\s+to\s+die\b",
|
| 40 |
+
r"\bi\s+want\s+to\s+die\b",
|
| 41 |
+
r"\bi\s+wish\s+i\s+(was|were)\s+dead\b",
|
| 42 |
+
r"\btake\s+my\s+own\s+life\b",
|
| 43 |
+
r"\bno\s+point\s+(in\s+)?living\b",
|
| 44 |
+
r"\bnothing\s+to\s+live\s+for\b",
|
| 45 |
+
r"\blife\s+is\s+not\s+worth\s+(it|living)\b",
|
| 46 |
+
# --- Hinglish / Romanised Hindi ---
|
| 47 |
+
r"\bkhudkushi\b",
|
| 48 |
+
r"\bkhud\s*kushi\b",
|
| 49 |
+
r"\bjaan\s+den[iau]\b",
|
| 50 |
+
r"\bjaan\s+dena\b",
|
| 51 |
+
r"\bmarna\s+chahta\b",
|
| 52 |
+
r"\bmarna\s+chahti\b",
|
| 53 |
+
r"\bjeena\s+nahi\b",
|
| 54 |
+
r"\bjina\s+nahi\b",
|
| 55 |
+
r"\bmar\s+ja(u|un|oonga|oongi)\b",
|
| 56 |
+
r"\bzinda\s+nahi\s+rehna\b",
|
| 57 |
+
# --- Hindi (Devanagari) ---
|
| 58 |
+
r"आत्महत्या",
|
| 59 |
+
r"खुदकुशी",
|
| 60 |
+
r"जान\s*दे",
|
| 61 |
+
r"मर\s*जाऊं",
|
| 62 |
+
r"मरना\s+चाहता",
|
| 63 |
+
r"मरना\s+चाहती",
|
| 64 |
+
r"जीना\s+नहीं",
|
| 65 |
+
r"जीना\s+नही",
|
| 66 |
+
# --- Bengali ---
|
| 67 |
+
r"আত্মহত্যা",
|
| 68 |
+
r"মরে\s*যেতে\s*চাই",
|
| 69 |
+
# --- Tamil ---
|
| 70 |
+
r"தற்கொலை",
|
| 71 |
+
# --- Telugu ---
|
| 72 |
+
r"ఆత్మహత్య",
|
| 73 |
+
# --- Marathi (Devanagari, mostly same as Hindi but added for safety) ---
|
| 74 |
+
r"मला\s+मरायच",
|
| 75 |
+
]
|
| 76 |
+
|
| 77 |
+
_CRISIS_RE = re.compile("|".join(CRISIS_PATTERNS), re.IGNORECASE | re.UNICODE)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
# Active physical danger patterns (ONGOING, RIGHT NOW — not trauma or fear of future)
|
| 82 |
+
# ---------------------------------------------------------------------------
|
| 83 |
+
# Legal information is not the right answer when someone is in immediate danger.
|
| 84 |
+
# These patterns catch "is happening right now" language — presence of a weapon,
|
| 85 |
+
# an attacker at the door, current physical violence. On match we route to
|
| 86 |
+
# 112 / 100 / 1091 BEFORE any legal section lookup.
|
| 87 |
+
#
|
| 88 |
+
# Keep these narrower than the general crisis list — we do NOT want to hijack
|
| 89 |
+
# a user saying "last year he hit me" (which IS legal-aid appropriate) with a
|
| 90 |
+
# call-112-now banner. The triggers require an immediacy marker (right now,
|
| 91 |
+
# outside my door, has a knife, is hitting me, etc.).
|
| 92 |
+
ACTIVE_DANGER_PATTERNS: List[str] = [
|
| 93 |
+
# --- English: weapon present ---
|
| 94 |
+
r"\b(has|holding|with|carrying|pointing|brandish(ing|ed)?)\s+(a\s+)?(knife|gun|pistol|weapon|blade|acid|rod|stick)\b",
|
| 95 |
+
r"\b(knife|gun|pistol|weapon|blade|acid)\s+(at|to|in)\s+(my|his|her|the)\s+(hand|face|throat|head|door)\b",
|
| 96 |
+
# --- English: attacker at location right now ---
|
| 97 |
+
r"\b(outside|inside|at)\s+my\s+(door|house|home|room|window|gate)\b",
|
| 98 |
+
r"\bbreak(ing)?\s+(down\s+)?(the\s+|my\s+)?door\b",
|
| 99 |
+
r"\btrying\s+to\s+(get|break|come)\s+(in|into)\b",
|
| 100 |
+
r"\bhe['’]s\s+(here|outside|coming)\b",
|
| 101 |
+
r"\bshe['’]s\s+(here|outside|coming)\b",
|
| 102 |
+
# --- English: current physical violence ---
|
| 103 |
+
r"\b(is|are|keeps?)\s+(hit(ting)?|beat(ing)?|slap(ping)?|choking|strangling|punching|kicking)\s+me\b",
|
| 104 |
+
r"\b(being|getting)\s+(hit|beaten|slapped|choked|strangled|attacked|assault(ed)?)\b",
|
| 105 |
+
r"\b(he|she|they)\s+(just|is|keeps?)\s+(hit|hitting|beat|beating|attacked|attacking)\b",
|
| 106 |
+
r"\bhit\s+me\s+(right\s+now|just\s+now|again)\b",
|
| 107 |
+
r"\bhe\s+hit\s+me\b",
|
| 108 |
+
r"\bshe\s+hit\s+me\b",
|
| 109 |
+
r"\bthey\s+hit\s+me\b",
|
| 110 |
+
r"\bbleeding\b",
|
| 111 |
+
# --- English: sexual violence in progress ---
|
| 112 |
+
r"\b(being|getting)\s+raped\b",
|
| 113 |
+
r"\btrying\s+to\s+rape\b",
|
| 114 |
+
r"\bforcing\s+(himself|herself)\s+on\s+me\b",
|
| 115 |
+
# --- English: immediate threat language ---
|
| 116 |
+
r"\bgoing\s+to\s+(kill|hurt|attack|beat|rape)\s+me\b",
|
| 117 |
+
r"\bsaid\s+he['’]?s?\s+going\s+to\s+kill\b",
|
| 118 |
+
r"\bthreat(en|ened|ening)\s+to\s+(kill|rape|attack|acid|burn|throw\s+acid)\b",
|
| 119 |
+
r"\bdeath\s+threat\b",
|
| 120 |
+
# --- Hinglish / Romanised Hindi ---
|
| 121 |
+
r"\bmujhe\s+maar\s+(raha|rahi|rahe)\b",
|
| 122 |
+
r"\bmar\s+raha\s+hai\s+mujhe\b",
|
| 123 |
+
r"\bghar\s+ke\s+(bahar|andar)\s+(aa|khada)\b",
|
| 124 |
+
r"\bdarwaza\s+tod\b",
|
| 125 |
+
r"\bchaku\s+(le|liya|hath)\b",
|
| 126 |
+
r"\btezaab\b", # acid
|
| 127 |
+
r"\bjaan\s+se\s+maar\s+dega\b",
|
| 128 |
+
r"\bjaan\s+se\s+maar\s+degi\b",
|
| 129 |
+
# --- Hindi (Devanagari) ---
|
| 130 |
+
r"मुझे\s+मार\s+रहा",
|
| 131 |
+
r"मुझे\s+मार\s+रही",
|
| 132 |
+
r"दरवाज़ा\s+तोड़",
|
| 133 |
+
r"चाकू",
|
| 134 |
+
r"तेज़ाब",
|
| 135 |
+
r"बंदूक",
|
| 136 |
+
r"जान\s+से\s+मार",
|
| 137 |
+
r"घर\s+के\s+बाहर",
|
| 138 |
+
]
|
| 139 |
+
|
| 140 |
+
_ACTIVE_DANGER_RE = re.compile("|".join(ACTIVE_DANGER_PATTERNS), re.IGNORECASE | re.UNICODE)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def check_crisis(text: str) -> bool:
|
| 144 |
+
"""Return True if `text` contains self-harm / suicide language.
|
| 145 |
+
|
| 146 |
+
Designed to err on the side of caution: a false positive (showing helplines for
|
| 147 |
+
a benign input) is far cheaper than a false negative.
|
| 148 |
+
"""
|
| 149 |
+
if not text:
|
| 150 |
+
return False
|
| 151 |
+
return bool(_CRISIS_RE.search(text))
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def check_active_danger(text: str) -> bool:
|
| 155 |
+
"""Return True if `text` describes an IMMEDIATE, ONGOING physical-danger situation.
|
| 156 |
+
|
| 157 |
+
Legal-aid information is not the appropriate response when someone is actively
|
| 158 |
+
being attacked or threatened — they need to call 112 / 100 / 1091 right now.
|
| 159 |
+
|
| 160 |
+
Intentionally narrower than `check_crisis`: we only fire on language that
|
| 161 |
+
carries an immediacy marker (a weapon is present, an attacker is at the door,
|
| 162 |
+
violence is currently happening). "Last year my husband used to hit me" will
|
| 163 |
+
NOT trigger this — that's a legitimate legal-aid question.
|
| 164 |
+
"""
|
| 165 |
+
if not text:
|
| 166 |
+
return False
|
| 167 |
+
return bool(_ACTIVE_DANGER_RE.search(text))
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
# Helpline payloads
|
| 172 |
+
# ---------------------------------------------------------------------------
|
| 173 |
+
|
| 174 |
+
class Helpline(TypedDict):
|
| 175 |
+
name: str
|
| 176 |
+
number: str
|
| 177 |
+
note: str
|
| 178 |
+
category: str # "mental_health" | "women" | "emergency" | "cyber" | "legal"
|
| 179 |
+
source: str
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# All numbers verified April 2026 against official sources:
|
| 183 |
+
# - Tele-MANAS: telemanas.mohfw.gov.in (Govt. of India, launched 10 Oct 2022)
|
| 184 |
+
# replaces the older KIRAN 1800-599-0019 helpline from 2020
|
| 185 |
+
# - iCall: icallhelpline.org (TISS, School of Human Ecology)
|
| 186 |
+
# - Vandrevala: vandrevalafoundation.com (verified via schema markup, 2024)
|
| 187 |
+
# - AASRA: aasra.info/helpline.html (Navi Mumbai primary landline)
|
| 188 |
+
# - 1091/181/112/1930/15100: Ministry of Home Affairs / WCD / I4C / NALSA portals
|
| 189 |
+
HELPLINES: List[Helpline] = [
|
| 190 |
+
{
|
| 191 |
+
"name": "Tele-MANAS",
|
| 192 |
+
"number": "14416",
|
| 193 |
+
"note": "24×7, free, 20+ Indian languages — Ministry of Health & Family Welfare (also 1800-89-14416)",
|
| 194 |
+
"category": "mental_health",
|
| 195 |
+
"source": "https://telemanas.mohfw.gov.in/",
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"name": "iCall (TISS)",
|
| 199 |
+
"number": "9152987821",
|
| 200 |
+
"note": "Mon–Sat, 8 AM – 10 PM — counselling by TISS-trained mental-health professionals",
|
| 201 |
+
"category": "mental_health",
|
| 202 |
+
"source": "https://icallhelpline.org/",
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"name": "Vandrevala Foundation",
|
| 206 |
+
"number": "+91-9999-666-555",
|
| 207 |
+
"note": "24×7×365, call or WhatsApp — free confidential counselling",
|
| 208 |
+
"category": "mental_health",
|
| 209 |
+
"source": "https://www.vandrevalafoundation.com/",
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"name": "AASRA",
|
| 213 |
+
"number": "+91-22-2754-6669",
|
| 214 |
+
"note": "24×7 suicide prevention — volunteer-run, Navi Mumbai",
|
| 215 |
+
"category": "mental_health",
|
| 216 |
+
"source": "http://www.aasra.info/helpline.html",
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"name": "Women's Helpline (All India)",
|
| 220 |
+
"number": "1091",
|
| 221 |
+
"note": "24×7 pan-India — Ministry of Women & Child Development",
|
| 222 |
+
"category": "women",
|
| 223 |
+
"source": "https://wcd.gov.in/schemes/women-helpline-scheme",
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"name": "Emergency (ERSS)",
|
| 227 |
+
"number": "112",
|
| 228 |
+
"note": "Pan-India single emergency number — police, fire, ambulance, women's safety",
|
| 229 |
+
"category": "emergency",
|
| 230 |
+
"source": "https://112.gov.in/",
|
| 231 |
+
},
|
| 232 |
+
]
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
# Dedicated payload shown when `check_active_danger()` fires. Ordered by
|
| 236 |
+
# immediacy: call 112 first, then 100, then 1091. This is distinct from the
|
| 237 |
+
# mental-health HELPLINES list because the user is in physical danger right now
|
| 238 |
+
# and does not need a counselling line — they need emergency response.
|
| 239 |
+
ACTIVE_DANGER_HELPLINES: List[Helpline] = [
|
| 240 |
+
{
|
| 241 |
+
"name": "Emergency (ERSS)",
|
| 242 |
+
"number": "112",
|
| 243 |
+
"note": "Pan-India single emergency number — police, fire, ambulance. On most Indian smartphones, press the power button 3 times.",
|
| 244 |
+
"category": "emergency",
|
| 245 |
+
"source": "https://112.gov.in/",
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"name": "Police (Emergency)",
|
| 249 |
+
"number": "100",
|
| 250 |
+
"note": "Direct police emergency line — reaches the nearest police control room",
|
| 251 |
+
"category": "emergency",
|
| 252 |
+
"source": "https://www.india.gov.in/spotlight/emergency-services",
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
"name": "Women's Helpline",
|
| 256 |
+
"number": "1091",
|
| 257 |
+
"note": "24×7 pan-India helpline for women in immediate distress — routes to nearest police control room",
|
| 258 |
+
"category": "women",
|
| 259 |
+
"source": "https://wcd.gov.in/schemes/women-helpline-scheme",
|
| 260 |
+
},
|
| 261 |
+
]
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
def get_helpline_payload() -> List[Helpline]:
|
| 265 |
+
"""Return the default mental-health helpline list shown in the self-harm crisis banner."""
|
| 266 |
+
return HELPLINES
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
def get_active_danger_payload() -> List[Helpline]:
|
| 270 |
+
"""Return the emergency-response helpline list shown when active danger is detected."""
|
| 271 |
+
return ACTIVE_DANGER_HELPLINES
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# ---------------------------------------------------------------------------
|
| 275 |
+
# Shared crisis banner renderer
|
| 276 |
+
# ---------------------------------------------------------------------------
|
| 277 |
+
# Every module that runs `check_crisis()` needs the same hard-interrupt banner.
|
| 278 |
+
# This used to be copy-pasted across five modules; two of them silently drifted
|
| 279 |
+
# (no source-link attribution). One helper — one rendering.
|
| 280 |
+
def render_crisis_banner(lang: str) -> None:
|
| 281 |
+
"""Render the self-harm / suicide crisis banner with helpline cards + source links.
|
| 282 |
+
|
| 283 |
+
Imported lazily inside the function so `backend.safeguards` stays importable
|
| 284 |
+
from pure-Python contexts (smoke tests, CLI tools) that don't have Streamlit.
|
| 285 |
+
"""
|
| 286 |
+
import streamlit as st # noqa: WPS433 — keep streamlit optional at import time
|
| 287 |
+
from backend.i18n import t # noqa: WPS433
|
| 288 |
+
|
| 289 |
+
st.error(f"**{t('crisis_banner_title', lang)}**\n\n{t('crisis_banner_body', lang)}")
|
| 290 |
+
cols = st.columns(2)
|
| 291 |
+
for idx, hl in enumerate(get_helpline_payload()):
|
| 292 |
+
with cols[idx % 2]:
|
| 293 |
+
st.markdown(f"**{hl['name']}** — `{hl['number']}` \n{hl['note']}")
|
| 294 |
+
if hl.get("source"):
|
| 295 |
+
st.caption(f"[{t('source_label', lang)}]({hl['source']})")
|
| 296 |
+
st.info(t("crisis_call_trusted", lang))
|
data/helplines_india.json
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"name": "Tele-MANAS",
|
| 4 |
+
"number": "14416",
|
| 5 |
+
"alt_number": "1800-89-14416",
|
| 6 |
+
"hours": "24×7",
|
| 7 |
+
"languages": ["Hindi", "English", "Assamese", "Bengali", "Bodo", "Dogri", "Gujarati", "Kannada", "Kashmiri", "Konkani", "Maithili", "Malayalam", "Manipuri", "Marathi", "Nepali", "Odia", "Punjabi", "Sanskrit", "Santali", "Sindhi", "Tamil", "Telugu", "Urdu"],
|
| 8 |
+
"operator": "Ministry of Health and Family Welfare, Government of India",
|
| 9 |
+
"cost": "Free",
|
| 10 |
+
"categories": ["mental_health"],
|
| 11 |
+
"description": "India's official national 24×7 toll-free mental health helpline, launched October 2022. Anxiety, depression, panic, suicide prevention, stress, substance use — 20+ Indian languages with 53 cells across 36 states/UTs. Replaces and extends the older KIRAN helpline.",
|
| 12 |
+
"source": "https://telemanas.mohfw.gov.in/",
|
| 13 |
+
"source_note": "Tele-MANAS is the Government of India's current flagship mental health helpline. It was announced in Union Budget 2022-23 and formally launched on 10 October 2022 (World Mental Health Day). The older KIRAN helpline (1800-599-0019) has been subsumed into this service."
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"name": "iCall (TISS)",
|
| 17 |
+
"number": "9152987821",
|
| 18 |
+
"email": "icall@tiss.edu",
|
| 19 |
+
"hours": "Mon–Sat, 08:00 – 22:00 IST",
|
| 20 |
+
"languages": ["English", "Hindi", "Marathi", "Gujarati", "Assamese", "Bengali", "Punjabi", "Malayalam"],
|
| 21 |
+
"operator": "School of Human Ecology, Tata Institute of Social Sciences (TISS)",
|
| 22 |
+
"cost": "Free",
|
| 23 |
+
"categories": ["mental_health"],
|
| 24 |
+
"description": "Telephone, email and chat-based counselling by trained mental-health professionals. Confidential, non-judgemental, anonymous.",
|
| 25 |
+
"source": "https://icallhelpline.org/"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"name": "Vandrevala Foundation",
|
| 29 |
+
"number": "+91-9999-666-555",
|
| 30 |
+
"hours": "24×7×365",
|
| 31 |
+
"languages": ["English", "Hindi", "Marathi", "Gujarati", "Bengali", "Tamil", "Telugu", "Kannada", "Malayalam", "Odia", "Assamese", "Punjabi"],
|
| 32 |
+
"operator": "Vandrevala Foundation",
|
| 33 |
+
"cost": "Free",
|
| 34 |
+
"categories": ["mental_health"],
|
| 35 |
+
"description": "24×7×365 free mental-health crisis support via phone and WhatsApp. Confidential counselling, referrals and follow-up care — run by trained counsellors.",
|
| 36 |
+
"source": "https://www.vandrevalafoundation.com/"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"name": "AASRA",
|
| 40 |
+
"number": "+91-22-2754-6669",
|
| 41 |
+
"alt_number": "+91-22-2754-6667",
|
| 42 |
+
"email": "aasrahelpline@yahoo.com",
|
| 43 |
+
"hours": "24×7",
|
| 44 |
+
"languages": ["English", "Hindi"],
|
| 45 |
+
"operator": "AASRA (Navi Mumbai)",
|
| 46 |
+
"cost": "Free",
|
| 47 |
+
"categories": ["mental_health", "suicide_prevention"],
|
| 48 |
+
"description": "Volunteer-run crisis intervention and suicide-prevention helpline. Confidential emotional support for people in distress, depression or suicidal crisis.",
|
| 49 |
+
"source": "http://www.aasra.info/helpline.html"
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"name": "Women's Helpline (All India)",
|
| 53 |
+
"number": "1091",
|
| 54 |
+
"hours": "24×7",
|
| 55 |
+
"operator": "Ministry of Women & Child Development, Govt. of India",
|
| 56 |
+
"cost": "Free",
|
| 57 |
+
"categories": ["women", "emergency"],
|
| 58 |
+
"description": "Pan-India helpline for women in distress — harassment, abuse, domestic violence, stalking. Routes to the nearest police control room.",
|
| 59 |
+
"source": "https://wcd.gov.in/schemes/women-helpline-scheme"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"name": "Women Helpline (Domestic Abuse)",
|
| 63 |
+
"number": "181",
|
| 64 |
+
"hours": "24×7",
|
| 65 |
+
"operator": "Women Helpline Scheme, Ministry of Women & Child Development",
|
| 66 |
+
"cost": "Free",
|
| 67 |
+
"categories": ["women", "domestic_violence"],
|
| 68 |
+
"description": "24-hour integrated women's helpline. Connects callers to police, hospitals and One-Stop Centres (Sakhi) for shelter and counselling.",
|
| 69 |
+
"source": "https://wcd.gov.in/schemes/women-helpline-scheme"
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"name": "Child Helpline (Childline 1098)",
|
| 73 |
+
"number": "1098",
|
| 74 |
+
"hours": "24×7",
|
| 75 |
+
"operator": "Ministry of Women & Child Development (integrated with 112)",
|
| 76 |
+
"cost": "Free",
|
| 77 |
+
"categories": ["child"],
|
| 78 |
+
"description": "For children in distress or needing protection — abuse, violence, trafficking, exploitation, missing children, medical emergencies.",
|
| 79 |
+
"source": "https://www.childlineindia.org/"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"name": "National Cyber Crime Helpline",
|
| 83 |
+
"number": "1930",
|
| 84 |
+
"hours": "24×7",
|
| 85 |
+
"operator": "Indian Cyber Crime Coordination Centre (I4C), Ministry of Home Affairs",
|
| 86 |
+
"cost": "Free",
|
| 87 |
+
"categories": ["cyber"],
|
| 88 |
+
"description": "For reporting financial cyber fraud, hacking, online harassment, non-consensual image abuse. Full complaints can also be filed at cybercrime.gov.in.",
|
| 89 |
+
"website": "https://cybercrime.gov.in/",
|
| 90 |
+
"source": "https://cybercrime.gov.in/"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"name": "NALSA Legal Services",
|
| 94 |
+
"number": "15100",
|
| 95 |
+
"hours": "Business hours (varies by state)",
|
| 96 |
+
"operator": "National Legal Services Authority, Govt. of India",
|
| 97 |
+
"cost": "Free",
|
| 98 |
+
"categories": ["legal"],
|
| 99 |
+
"description": "Free legal aid, advice and representation for women, children, SC/ST, victims of trafficking, disaster victims, persons in custody, people below a state-specific income threshold, and other weaker sections under the Legal Services Authorities Act, 1987.",
|
| 100 |
+
"website": "https://nalsa.gov.in/",
|
| 101 |
+
"source": "https://nalsa.gov.in/"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"name": "Emergency Response Support System (ERSS)",
|
| 105 |
+
"number": "112",
|
| 106 |
+
"hours": "24×7",
|
| 107 |
+
"operator": "Ministry of Home Affairs",
|
| 108 |
+
"cost": "Free",
|
| 109 |
+
"categories": ["emergency"],
|
| 110 |
+
"description": "Pan-India single emergency number — police, fire, ambulance, women's safety. Works by call or by pressing the power button 3 times on most Indian smartphones.",
|
| 111 |
+
"source": "https://112.gov.in/"
|
| 112 |
+
},
|
| 113 |
+
{
|
| 114 |
+
"name": "Police (Emergency)",
|
| 115 |
+
"number": "100",
|
| 116 |
+
"hours": "24×7",
|
| 117 |
+
"operator": "State Police",
|
| 118 |
+
"cost": "Free",
|
| 119 |
+
"categories": ["emergency", "police"],
|
| 120 |
+
"description": "Direct police emergency line (also reachable via 112).",
|
| 121 |
+
"source": "https://www.india.gov.in/spotlight/emergency-services"
|
| 122 |
+
}
|
| 123 |
+
]
|
data/ipc_bns_sections.json
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"act": "IPC/BNS",
|
| 4 |
+
"ipc_section": "354",
|
| 5 |
+
"bns_section": "74",
|
| 6 |
+
"title": "Assault or criminal force to woman with intent to outrage her modesty",
|
| 7 |
+
"category": ["sexual_harassment", "workplace_harassment"],
|
| 8 |
+
"description": "Using physical force or touching a woman with the intention of outraging her modesty. Covers unwanted touching, groping, and similar physical acts.",
|
| 9 |
+
"punishment": "Imprisonment of either description for a term of at least 1 year, extendable to 5 years, and fine.",
|
| 10 |
+
"cognizable": true,
|
| 11 |
+
"bailable": false,
|
| 12 |
+
"triable_by": "Magistrate of the First Class",
|
| 13 |
+
"evidence_tips": "Any witnesses, CCTV footage, medical examination if applicable, contemporaneous messages or notes, details of place and time.",
|
| 14 |
+
"keywords": ["groped", "touched", "modesty", "unwanted touch"]
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"act": "IPC/BNS",
|
| 18 |
+
"ipc_section": "354A",
|
| 19 |
+
"bns_section": "75",
|
| 20 |
+
"title": "Sexual harassment",
|
| 21 |
+
"category": ["sexual_harassment", "workplace_harassment"],
|
| 22 |
+
"description": "Physical contact with sexual overtures, demand or request for sexual favours, showing pornography against the will of a woman, or making sexually coloured remarks.",
|
| 23 |
+
"punishment": "Rigorous imprisonment up to 3 years, or fine, or both (varies by sub-clause; making sexually coloured remarks: up to 1 year, or fine, or both).",
|
| 24 |
+
"cognizable": true,
|
| 25 |
+
"bailable": true,
|
| 26 |
+
"triable_by": "Any Magistrate",
|
| 27 |
+
"evidence_tips": "Messages, emails, witnesses, dates and places of incidents, HR / PoSH complaint records if at workplace.",
|
| 28 |
+
"keywords": ["sexual harassment", "sexual favour", "obscene", "remark", "porn"]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"act": "IPC/BNS",
|
| 32 |
+
"ipc_section": "354B",
|
| 33 |
+
"bns_section": "76",
|
| 34 |
+
"title": "Assault or use of criminal force with intent to disrobe",
|
| 35 |
+
"category": ["sexual_harassment"],
|
| 36 |
+
"description": "Assaulting or using criminal force on a woman with the intent of disrobing or compelling her to be naked.",
|
| 37 |
+
"punishment": "Imprisonment of at least 3 years, extendable to 7 years, and fine.",
|
| 38 |
+
"cognizable": true,
|
| 39 |
+
"bailable": false,
|
| 40 |
+
"triable_by": "Magistrate of the First Class",
|
| 41 |
+
"evidence_tips": "Witness statements, medical report, CCTV, location details.",
|
| 42 |
+
"keywords": ["disrobe", "strip", "tearing clothes"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"act": "IPC/BNS",
|
| 46 |
+
"ipc_section": "354C",
|
| 47 |
+
"bns_section": "77",
|
| 48 |
+
"title": "Voyeurism",
|
| 49 |
+
"category": ["sexual_harassment", "cyber_harassment"],
|
| 50 |
+
"description": "Watching, capturing, or disseminating the image of a woman engaging in a private act where she would normally expect privacy.",
|
| 51 |
+
"punishment": "First conviction: imprisonment of 1 to 3 years and fine. Subsequent conviction: 3 to 7 years and fine.",
|
| 52 |
+
"cognizable": true,
|
| 53 |
+
"bailable": false,
|
| 54 |
+
"triable_by": "Magistrate of the First Class",
|
| 55 |
+
"evidence_tips": "Devices used, image or video files, digital forensics, witnesses.",
|
| 56 |
+
"keywords": ["voyeurism", "hidden camera", "recording", "spy", "private act"]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"act": "IPC/BNS",
|
| 60 |
+
"ipc_section": "354D",
|
| 61 |
+
"bns_section": "78",
|
| 62 |
+
"title": "Stalking",
|
| 63 |
+
"category": ["stalking", "cyber_harassment"],
|
| 64 |
+
"description": "Following a woman and contacting or attempting to contact her repeatedly despite her disinterest, or monitoring her electronic communications. Applies to physical and online stalking.",
|
| 65 |
+
"punishment": "First conviction: imprisonment up to 3 years and fine. Subsequent conviction: up to 5 years and fine.",
|
| 66 |
+
"cognizable": true,
|
| 67 |
+
"bailable": true,
|
| 68 |
+
"triable_by": "Any Magistrate",
|
| 69 |
+
"evidence_tips": "Log of calls/messages, screenshots, witnesses who saw the person following you, CCTV, social-media screenshots.",
|
| 70 |
+
"keywords": ["stalking", "following", "repeated contact", "monitoring", "online stalking"]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"act": "IPC/BNS",
|
| 74 |
+
"ipc_section": "509",
|
| 75 |
+
"bns_section": "79",
|
| 76 |
+
"title": "Word, gesture, or act intended to insult the modesty of a woman",
|
| 77 |
+
"category": ["sexual_harassment", "workplace_harassment"],
|
| 78 |
+
"description": "Uttering any word, making any sound or gesture, or intruding on the privacy of a woman with the intention of insulting her modesty.",
|
| 79 |
+
"punishment": "Simple imprisonment up to 3 years and fine.",
|
| 80 |
+
"cognizable": true,
|
| 81 |
+
"bailable": true,
|
| 82 |
+
"triable_by": "Any Magistrate",
|
| 83 |
+
"evidence_tips": "Witnesses, recordings, messages, description of time and place.",
|
| 84 |
+
"keywords": ["eve teasing", "insult", "gesture", "lewd comment"]
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"act": "IPC/BNS",
|
| 88 |
+
"ipc_section": "503",
|
| 89 |
+
"bns_section": "351",
|
| 90 |
+
"title": "Criminal intimidation",
|
| 91 |
+
"category": ["criminal_intimidation", "domestic_violence"],
|
| 92 |
+
"description": "Threatening someone with injury to their person, reputation, or property, with intent to cause alarm or to make them do something they are not legally bound to do.",
|
| 93 |
+
"punishment": "See IPC 506 / BNS 351(2)-(4).",
|
| 94 |
+
"cognizable": false,
|
| 95 |
+
"bailable": true,
|
| 96 |
+
"triable_by": "Any Magistrate",
|
| 97 |
+
"evidence_tips": "Screenshots of threats, call recordings, witnesses, written messages.",
|
| 98 |
+
"keywords": ["threat", "intimidate", "blackmail"]
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"act": "IPC/BNS",
|
| 102 |
+
"ipc_section": "506",
|
| 103 |
+
"bns_section": "351(2), 351(3), 351(4)",
|
| 104 |
+
"title": "Punishment for criminal intimidation",
|
| 105 |
+
"category": ["criminal_intimidation"],
|
| 106 |
+
"description": "Punishment for the offence of criminal intimidation under Section 503 IPC / 351 BNS.",
|
| 107 |
+
"punishment": "Imprisonment up to 2 years, or fine, or both. If the threat is of death or grievous hurt, destruction by fire, etc.: imprisonment up to 7 years, or fine, or both.",
|
| 108 |
+
"cognizable": false,
|
| 109 |
+
"bailable": true,
|
| 110 |
+
"triable_by": "Any Magistrate",
|
| 111 |
+
"evidence_tips": "Same as Section 503 / 351.",
|
| 112 |
+
"keywords": ["threat to kill", "threat of harm", "punishment for threat"]
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"act": "IPC/BNS",
|
| 116 |
+
"ipc_section": "498A",
|
| 117 |
+
"bns_section": "85, 86",
|
| 118 |
+
"title": "Cruelty by husband or relatives of husband",
|
| 119 |
+
"category": ["domestic_violence"],
|
| 120 |
+
"description": "Husband or relative of husband of a woman subjecting her to cruelty — including physical, mental, or harassment for dowry.",
|
| 121 |
+
"punishment": "Imprisonment up to 3 years and fine.",
|
| 122 |
+
"cognizable": true,
|
| 123 |
+
"bailable": false,
|
| 124 |
+
"triable_by": "Magistrate of the First Class",
|
| 125 |
+
"evidence_tips": "Medical reports, photographs of injuries, witness statements, messages, audio recordings, financial records if dowry is involved.",
|
| 126 |
+
"keywords": ["husband", "in-laws", "dowry", "domestic cruelty"]
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"act": "IPC/BNS",
|
| 130 |
+
"ipc_section": "499",
|
| 131 |
+
"bns_section": "356(1)",
|
| 132 |
+
"title": "Defamation",
|
| 133 |
+
"category": ["defamation"],
|
| 134 |
+
"description": "Making or publishing an imputation concerning a person intending to harm, or knowing it will harm, that person's reputation.",
|
| 135 |
+
"punishment": "See IPC 500 / BNS 356(2).",
|
| 136 |
+
"cognizable": false,
|
| 137 |
+
"bailable": true,
|
| 138 |
+
"triable_by": "Court of Session",
|
| 139 |
+
"evidence_tips": "Copy of the defamatory material (screenshots, print, audio), proof of publication, impact on reputation.",
|
| 140 |
+
"keywords": ["defamation", "reputation", "slander", "libel"]
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"act": "IPC/BNS",
|
| 144 |
+
"ipc_section": "500",
|
| 145 |
+
"bns_section": "356(2)",
|
| 146 |
+
"title": "Punishment for defamation",
|
| 147 |
+
"category": ["defamation"],
|
| 148 |
+
"description": "Punishment for the offence of defamation.",
|
| 149 |
+
"punishment": "Simple imprisonment up to 2 years, or fine, or both, or community service (BNS).",
|
| 150 |
+
"cognizable": false,
|
| 151 |
+
"bailable": true,
|
| 152 |
+
"triable_by": "Court of Session",
|
| 153 |
+
"evidence_tips": "Same as Section 499.",
|
| 154 |
+
"keywords": ["defamation punishment"]
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
"act": "IT Act",
|
| 158 |
+
"ipc_section": "66E",
|
| 159 |
+
"bns_section": null,
|
| 160 |
+
"title": "Punishment for violation of privacy (IT Act 66E)",
|
| 161 |
+
"category": ["cyber_harassment"],
|
| 162 |
+
"description": "Intentionally capturing, publishing, or transmitting the image of a private area of any person without their consent.",
|
| 163 |
+
"punishment": "Imprisonment up to 3 years, or fine up to ₹2 lakh, or both.",
|
| 164 |
+
"cognizable": true,
|
| 165 |
+
"bailable": true,
|
| 166 |
+
"triable_by": "Any Magistrate",
|
| 167 |
+
"evidence_tips": "Devices used, file metadata, hosting URLs, witnesses, complaint lodged with cybercrime.gov.in.",
|
| 168 |
+
"keywords": ["private image", "privacy violation", "intimate photo"]
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"act": "IT Act",
|
| 172 |
+
"ipc_section": "67",
|
| 173 |
+
"bns_section": null,
|
| 174 |
+
"title": "Publishing or transmitting obscene material in electronic form (IT Act 67)",
|
| 175 |
+
"category": ["cyber_harassment"],
|
| 176 |
+
"description": "Publishing or transmitting obscene material in electronic form that is lascivious or appeals to the prurient interest.",
|
| 177 |
+
"punishment": "First conviction: imprisonment up to 3 years and fine up to ₹5 lakh. Subsequent conviction: up to 5 years and fine up to ₹10 lakh.",
|
| 178 |
+
"cognizable": true,
|
| 179 |
+
"bailable": true,
|
| 180 |
+
"triable_by": "Any Magistrate",
|
| 181 |
+
"evidence_tips": "Screenshots, URLs, device forensics, complaint at cybercrime.gov.in.",
|
| 182 |
+
"keywords": ["obscene online", "electronic obscenity"]
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"act": "IT Act",
|
| 186 |
+
"ipc_section": "67A",
|
| 187 |
+
"bns_section": null,
|
| 188 |
+
"title": "Publishing or transmitting sexually explicit material in electronic form (IT Act 67A)",
|
| 189 |
+
"category": ["cyber_harassment"],
|
| 190 |
+
"description": "Publishing or transmitting material containing sexually explicit acts or conduct in electronic form.",
|
| 191 |
+
"punishment": "First conviction: imprisonment up to 5 years and fine up to ₹10 lakh. Subsequent conviction: up to 7 years and fine up to ₹10 lakh.",
|
| 192 |
+
"cognizable": true,
|
| 193 |
+
"bailable": false,
|
| 194 |
+
"triable_by": "Any Magistrate",
|
| 195 |
+
"evidence_tips": "Screenshots, URLs, device forensics, complaint at cybercrime.gov.in.",
|
| 196 |
+
"keywords": ["revenge porn", "sexually explicit online", "nude morph"]
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"act": "PoSH Act 2013",
|
| 200 |
+
"ipc_section": "Sections 3-4, 9, 11",
|
| 201 |
+
"bns_section": null,
|
| 202 |
+
"title": "Sexual Harassment of Women at Workplace (Prevention, Prohibition & Redressal) Act, 2013 (PoSH)",
|
| 203 |
+
"category": ["workplace_harassment", "sexual_harassment"],
|
| 204 |
+
"description": "Workplace sexual harassment framework — every workplace with 10+ employees must have an Internal Complaints Committee (ICC). A complaint must be filed within 3 months (extendable by 3 more) from the incident.",
|
| 205 |
+
"punishment": "Disciplinary action against respondent; penalty on employer for non-compliance (₹50,000, escalating on repeat).",
|
| 206 |
+
"cognizable": true,
|
| 207 |
+
"bailable": true,
|
| 208 |
+
"triable_by": "Internal Complaints Committee / Local Committee",
|
| 209 |
+
"evidence_tips": "Written complaint to ICC/Local Committee, witnesses, messages, emails, HR records.",
|
| 210 |
+
"keywords": ["PoSH", "workplace harassment", "ICC", "internal committee"]
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
"act": "PWDV Act 2005",
|
| 214 |
+
"ipc_section": "Sections 12, 18-22",
|
| 215 |
+
"bns_section": null,
|
| 216 |
+
"title": "Protection of Women from Domestic Violence Act, 2005",
|
| 217 |
+
"category": ["domestic_violence"],
|
| 218 |
+
"description": "Civil law protecting women from physical, sexual, verbal, emotional, and economic abuse by a partner or family member. Allows protection orders, residence orders, monetary relief, custody orders.",
|
| 219 |
+
"punishment": "Violation of a protection order can lead to imprisonment up to 1 year, or fine up to ₹20,000, or both.",
|
| 220 |
+
"cognizable": true,
|
| 221 |
+
"bailable": true,
|
| 222 |
+
"triable_by": "Judicial Magistrate of the First Class / Metropolitan Magistrate",
|
| 223 |
+
"evidence_tips": "Medical reports, photographs, witness statements, financial records, messages.",
|
| 224 |
+
"keywords": ["domestic violence", "PWDV", "protection order", "abuse"]
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"act": "IPC/BNS",
|
| 228 |
+
"ipc_section": "182",
|
| 229 |
+
"bns_section": "217",
|
| 230 |
+
"title": "False information, with intent to cause a public servant to use his lawful power to the injury of another person",
|
| 231 |
+
"category": ["false_complaint_warning"],
|
| 232 |
+
"description": "Giving false information to any public servant with the intention of causing that public servant to use their power to injure someone. Included here so users understand the seriousness of filing a false FIR — this is informational context, not a charge against the user.",
|
| 233 |
+
"punishment": "Imprisonment up to 6 months, or fine up to ₹1,000, or both.",
|
| 234 |
+
"cognizable": false,
|
| 235 |
+
"bailable": true,
|
| 236 |
+
"triable_by": "Any Magistrate",
|
| 237 |
+
"evidence_tips": "Reference only — this section is NOT to be used against the victim; it is an informational disclaimer.",
|
| 238 |
+
"keywords": ["false complaint", "false FIR"],
|
| 239 |
+
"source": "https://indiacode.nic.in/handle/123456789/2263"
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"act": "IPC/BNS (procedural)",
|
| 243 |
+
"ipc_section": "166A",
|
| 244 |
+
"bns_section": "200",
|
| 245 |
+
"title": "Public servant disobeying direction under law — refusal to record information in cognizable offence",
|
| 246 |
+
"category": ["procedural_rights"],
|
| 247 |
+
"description": "If a police officer fails or refuses to register an FIR in a cognizable offence (especially sexual offences under IPC 326A, 326B, 354, 354B, 370, 370A, 376, 376A–E, 509), they themselves commit an offence. You have the right to demand FIR registration — an officer cannot legally turn you away.",
|
| 248 |
+
"punishment": "Rigorous imprisonment of 6 months to 2 years, and fine (for the public servant who refused).",
|
| 249 |
+
"cognizable": true,
|
| 250 |
+
"bailable": true,
|
| 251 |
+
"triable_by": "Magistrate of the First Class",
|
| 252 |
+
"evidence_tips": "If the officer refuses: (1) ask for refusal in writing, (2) escalate to the Superintendent of Police under Section 154(3) CrPC / 173(3) BNSS, (3) approach a Magistrate under 156(3) CrPC / 175(3) BNSS, (4) contact NALSA (15100) for free legal aid.",
|
| 253 |
+
"keywords": ["refuse to register FIR", "police refusal", "zero FIR", "FIR refusal"],
|
| 254 |
+
"source": "https://indiacode.nic.in/handle/123456789/2263"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"act": "CrPC / BNSS (procedural)",
|
| 258 |
+
"ipc_section": "154 CrPC / 173 BNSS",
|
| 259 |
+
"bns_section": "173 BNSS",
|
| 260 |
+
"title": "Right to register an FIR (including Zero FIR)",
|
| 261 |
+
"category": ["procedural_rights"],
|
| 262 |
+
"description": "Every person has the right to report a cognizable offence at ANY police station — not just the one with territorial jurisdiction. This is called a Zero FIR and must be registered immediately and transferred to the correct station later. The FIR copy must be given to the informant free of charge.",
|
| 263 |
+
"punishment": "N/A — procedural right. Refusal attracts IPC 166A / BNS 200.",
|
| 264 |
+
"cognizable": true,
|
| 265 |
+
"bailable": true,
|
| 266 |
+
"triable_by": "N/A — procedural",
|
| 267 |
+
"evidence_tips": "Insist on a Zero FIR if the officer cites jurisdiction. Ask for the free copy under Section 154(2) CrPC / 173(2) BNSS. If denied, escalate to SP, then to the Magistrate, then call NALSA (15100).",
|
| 268 |
+
"keywords": ["zero FIR", "right to FIR", "FIR copy", "jurisdiction"],
|
| 269 |
+
"source": "https://indiacode.nic.in/handle/123456789/15272"
|
| 270 |
+
}
|
| 271 |
+
]
|
data/mental_health_resources.json
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Delhi": [
|
| 3 |
+
{
|
| 4 |
+
"name": "AIIMS Delhi — Department of Psychiatry",
|
| 5 |
+
"type": "Government tertiary hospital",
|
| 6 |
+
"address": "Ansari Nagar, New Delhi 110029",
|
| 7 |
+
"phone": "+91-11-2658-8500",
|
| 8 |
+
"website": "https://www.aiims.edu/",
|
| 9 |
+
"cost": "Subsidised / government",
|
| 10 |
+
"specialties": ["general psychiatry", "de-addiction", "child psychiatry", "crisis care"]
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"name": "IHBAS (Institute of Human Behaviour & Allied Sciences)",
|
| 14 |
+
"type": "Government autonomous institute",
|
| 15 |
+
"address": "Dilshad Garden, Delhi 110095",
|
| 16 |
+
"phone": "+91-11-2211-4021",
|
| 17 |
+
"website": "https://ihbas.delhi.gov.in/",
|
| 18 |
+
"cost": "Subsidised / government",
|
| 19 |
+
"specialties": ["psychiatry", "neurology", "rehabilitation", "crisis intervention"]
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"name": "VIMHANS Hospital",
|
| 23 |
+
"type": "Private psychiatric hospital",
|
| 24 |
+
"address": "1, Institutional Area, Nehru Nagar, New Delhi 110065",
|
| 25 |
+
"website": "https://vimhans.com/",
|
| 26 |
+
"cost": "Private (insurance accepted)",
|
| 27 |
+
"specialties": ["psychiatry", "psychology", "addiction", "adolescent care"]
|
| 28 |
+
}
|
| 29 |
+
],
|
| 30 |
+
"Mumbai": [
|
| 31 |
+
{
|
| 32 |
+
"name": "KEM Hospital — Department of Psychiatry",
|
| 33 |
+
"type": "Government tertiary hospital",
|
| 34 |
+
"address": "Acharya Donde Marg, Parel, Mumbai 400012",
|
| 35 |
+
"phone": "+91-22-2410-7000",
|
| 36 |
+
"website": "https://www.kem.edu/",
|
| 37 |
+
"cost": "Subsidised / government",
|
| 38 |
+
"specialties": ["general psychiatry", "de-addiction", "adult and child"]
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"name": "iCall — Tata Institute of Social Sciences (TISS)",
|
| 42 |
+
"type": "Telephone & email counselling",
|
| 43 |
+
"phone": "+91-9152-987-821",
|
| 44 |
+
"website": "https://icallhelpline.org/",
|
| 45 |
+
"cost": "Free",
|
| 46 |
+
"specialties": ["counselling", "suicide prevention", "youth", "LGBTQIA+"]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"name": "Mpower — The Centre",
|
| 50 |
+
"type": "Private clinic & NGO",
|
| 51 |
+
"address": "Khareghat Colony, Babulnath, Mumbai 400007",
|
| 52 |
+
"website": "https://mpowerminds.com/",
|
| 53 |
+
"cost": "Private, sliding scale",
|
| 54 |
+
"specialties": ["psychiatry", "psychotherapy", "adolescent", "workplace mental health"]
|
| 55 |
+
}
|
| 56 |
+
],
|
| 57 |
+
"Bangalore": [
|
| 58 |
+
{
|
| 59 |
+
"name": "NIMHANS (National Institute of Mental Health & Neurosciences)",
|
| 60 |
+
"type": "Institute of National Importance",
|
| 61 |
+
"address": "Hosur Road, Bangalore 560029",
|
| 62 |
+
"phone": "+91-80-2699-5000",
|
| 63 |
+
"website": "https://nimhans.ac.in/",
|
| 64 |
+
"cost": "Subsidised / government",
|
| 65 |
+
"specialties": ["psychiatry", "neurology", "de-addiction", "child psychiatry", "research"]
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "St. John's Medical College Hospital — Psychiatry",
|
| 69 |
+
"type": "Private medical college hospital",
|
| 70 |
+
"address": "Sarjapur Road, Bangalore 560034",
|
| 71 |
+
"phone": "+91-80-2206-5000",
|
| 72 |
+
"website": "https://www.stjohns.in/",
|
| 73 |
+
"cost": "Private (subsidised OPD)",
|
| 74 |
+
"specialties": ["psychiatry", "psychology", "community mental health"]
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"name": "SAHAI Helpline",
|
| 78 |
+
"type": "NGO helpline",
|
| 79 |
+
"phone": "+91-80-2549-7777",
|
| 80 |
+
"cost": "Free",
|
| 81 |
+
"specialties": ["suicide prevention", "counselling", "crisis support"]
|
| 82 |
+
}
|
| 83 |
+
],
|
| 84 |
+
"Chennai": [
|
| 85 |
+
{
|
| 86 |
+
"name": "SCARF India (Schizophrenia Research Foundation)",
|
| 87 |
+
"type": "NGO & research institute",
|
| 88 |
+
"address": "R/7A, North Main Road, Anna Nagar West Extension, Chennai 600101",
|
| 89 |
+
"phone": "+91-44-2615-3971",
|
| 90 |
+
"website": "https://scarfindia.org/",
|
| 91 |
+
"cost": "Subsidised",
|
| 92 |
+
"specialties": ["schizophrenia", "community psychiatry", "rehabilitation"]
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"name": "Institute of Mental Health, Chennai",
|
| 96 |
+
"type": "State government psychiatric hospital",
|
| 97 |
+
"address": "Medavakkam Tank Road, Kilpauk, Chennai 600010",
|
| 98 |
+
"website": "https://tnhealth.tn.gov.in/",
|
| 99 |
+
"cost": "Subsidised / government",
|
| 100 |
+
"specialties": ["inpatient psychiatry", "OPD", "rehabilitation"]
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"name": "M.S. Chellamuthu Trust & Research Foundation",
|
| 104 |
+
"type": "NGO & hospital",
|
| 105 |
+
"address": "Madurai with branches across Tamil Nadu",
|
| 106 |
+
"website": "https://chellamuthutrust.org/",
|
| 107 |
+
"cost": "Subsidised",
|
| 108 |
+
"specialties": ["community psychiatry", "rehabilitation", "addiction"]
|
| 109 |
+
}
|
| 110 |
+
],
|
| 111 |
+
"Kolkata": [
|
| 112 |
+
{
|
| 113 |
+
"name": "Institute of Psychiatry (IOP), Kolkata",
|
| 114 |
+
"type": "Government teaching institute",
|
| 115 |
+
"address": "7 D.L. Khan Road, Kolkata 700025",
|
| 116 |
+
"phone": "+91-33-2223-5212",
|
| 117 |
+
"cost": "Subsidised / government",
|
| 118 |
+
"specialties": ["psychiatry", "psychology", "training", "research"]
|
| 119 |
+
},
|
| 120 |
+
{
|
| 121 |
+
"name": "Pavlov Hospital",
|
| 122 |
+
"type": "State psychiatric hospital",
|
| 123 |
+
"address": "18 Gobra Road, Kolkata 700046",
|
| 124 |
+
"cost": "Subsidised / government",
|
| 125 |
+
"specialties": ["inpatient psychiatry", "long-term care"]
|
| 126 |
+
}
|
| 127 |
+
],
|
| 128 |
+
"Hyderabad": [
|
| 129 |
+
{
|
| 130 |
+
"name": "Institute of Mental Health, Hyderabad",
|
| 131 |
+
"type": "State government psychiatric hospital",
|
| 132 |
+
"address": "Erragadda, Hyderabad 500038",
|
| 133 |
+
"website": "https://imhhyd.telangana.gov.in/",
|
| 134 |
+
"cost": "Subsidised / government",
|
| 135 |
+
"specialties": ["psychiatry", "de-addiction", "rehabilitation"]
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"name": "Asha Hospital",
|
| 139 |
+
"type": "Private psychiatric hospital",
|
| 140 |
+
"address": "Banjara Hills, Hyderabad 500034",
|
| 141 |
+
"website": "https://ashahospital.com/",
|
| 142 |
+
"cost": "Private",
|
| 143 |
+
"specialties": ["psychiatry", "psychology", "addiction", "child & adolescent"]
|
| 144 |
+
}
|
| 145 |
+
],
|
| 146 |
+
"Pune": [
|
| 147 |
+
{
|
| 148 |
+
"name": "Muktangan Mitra (Rehabilitation Centre)",
|
| 149 |
+
"type": "NGO rehabilitation centre",
|
| 150 |
+
"address": "Yerwada, Pune 411006",
|
| 151 |
+
"website": "https://muktangan.org/",
|
| 152 |
+
"cost": "Subsidised",
|
| 153 |
+
"specialties": ["de-addiction", "recovery", "family counselling"]
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "B.J. Government Medical College — Psychiatry (Sassoon Hospital)",
|
| 157 |
+
"type": "Government tertiary hospital",
|
| 158 |
+
"address": "Sassoon Road, Pune 411001",
|
| 159 |
+
"cost": "Subsidised / government",
|
| 160 |
+
"specialties": ["psychiatry", "emergency mental health"]
|
| 161 |
+
}
|
| 162 |
+
],
|
| 163 |
+
"Ahmedabad": [
|
| 164 |
+
{
|
| 165 |
+
"name": "Hospital for Mental Health, Ahmedabad",
|
| 166 |
+
"type": "State government psychiatric hospital",
|
| 167 |
+
"address": "Shahibaug, Ahmedabad 380004",
|
| 168 |
+
"cost": "Subsidised / government",
|
| 169 |
+
"specialties": ["psychiatry", "inpatient care", "OPD"]
|
| 170 |
+
},
|
| 171 |
+
{
|
| 172 |
+
"name": "B.J. Medical College — Psychiatry (Civil Hospital)",
|
| 173 |
+
"type": "Government tertiary hospital",
|
| 174 |
+
"address": "Civil Hospital Campus, Asarwa, Ahmedabad 380016",
|
| 175 |
+
"cost": "Subsidised / government",
|
| 176 |
+
"specialties": ["general psychiatry", "crisis care"]
|
| 177 |
+
}
|
| 178 |
+
],
|
| 179 |
+
"Jaipur": [
|
| 180 |
+
{
|
| 181 |
+
"name": "SMS Medical College & Hospital — Department of Psychiatry",
|
| 182 |
+
"type": "Government tertiary hospital",
|
| 183 |
+
"address": "JLN Marg, Jaipur 302004",
|
| 184 |
+
"website": "https://www.smsmc.nic.in/",
|
| 185 |
+
"cost": "Subsidised / government",
|
| 186 |
+
"specialties": ["psychiatry", "de-addiction"]
|
| 187 |
+
}
|
| 188 |
+
],
|
| 189 |
+
"Lucknow": [
|
| 190 |
+
{
|
| 191 |
+
"name": "King George's Medical University — Department of Psychiatry",
|
| 192 |
+
"type": "Government tertiary hospital",
|
| 193 |
+
"address": "Chowk, Lucknow 226003",
|
| 194 |
+
"website": "https://www.kgmu.org/",
|
| 195 |
+
"cost": "Subsidised / government",
|
| 196 |
+
"specialties": ["psychiatry", "child psychiatry", "addiction"]
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
"name": "SGPGI — Department of Psychiatry",
|
| 200 |
+
"type": "Government tertiary hospital",
|
| 201 |
+
"address": "Raebareli Road, Lucknow 226014",
|
| 202 |
+
"website": "https://sgpgi.ac.in/",
|
| 203 |
+
"cost": "Subsidised / government",
|
| 204 |
+
"specialties": ["psychiatry", "psychosomatic medicine"]
|
| 205 |
+
}
|
| 206 |
+
],
|
| 207 |
+
"Chandigarh": [
|
| 208 |
+
{
|
| 209 |
+
"name": "PGIMER — Department of Psychiatry",
|
| 210 |
+
"type": "Institute of National Importance",
|
| 211 |
+
"address": "Sector 12, Chandigarh 160012",
|
| 212 |
+
"phone": "+91-172-275-5555",
|
| 213 |
+
"website": "https://pgimer.edu.in/",
|
| 214 |
+
"cost": "Subsidised / government",
|
| 215 |
+
"specialties": ["psychiatry", "de-addiction", "child & adolescent", "research"]
|
| 216 |
+
}
|
| 217 |
+
],
|
| 218 |
+
"Bhopal": [
|
| 219 |
+
{
|
| 220 |
+
"name": "Gandhi Medical College — Hamidia Hospital, Psychiatry",
|
| 221 |
+
"type": "Government tertiary hospital",
|
| 222 |
+
"address": "Sultania Road, Bhopal 462001",
|
| 223 |
+
"cost": "Subsidised / government",
|
| 224 |
+
"specialties": ["psychiatry", "general hospital psychiatry"]
|
| 225 |
+
}
|
| 226 |
+
],
|
| 227 |
+
"Patna": [
|
| 228 |
+
{
|
| 229 |
+
"name": "AIIMS Patna — Department of Psychiatry",
|
| 230 |
+
"type": "Government tertiary hospital",
|
| 231 |
+
"address": "Phulwari Sharif, Patna 801507",
|
| 232 |
+
"website": "https://www.aiimspatna.edu.in/",
|
| 233 |
+
"cost": "Subsidised / government",
|
| 234 |
+
"specialties": ["psychiatry", "child psychiatry"]
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"name": "Koshish — IGIMS",
|
| 238 |
+
"type": "Government medical institute",
|
| 239 |
+
"address": "Sheikhpura, Patna 800014",
|
| 240 |
+
"website": "https://www.igims.org/",
|
| 241 |
+
"cost": "Subsidised / government",
|
| 242 |
+
"specialties": ["psychiatry", "general mental health"]
|
| 243 |
+
}
|
| 244 |
+
],
|
| 245 |
+
"Kochi": [
|
| 246 |
+
{
|
| 247 |
+
"name": "Govt. Mental Health Centre, Thiruvananthapuram (nearest to Kochi)",
|
| 248 |
+
"type": "State government psychiatric hospital",
|
| 249 |
+
"address": "Peroorkada, Thiruvananthapuram 695005",
|
| 250 |
+
"cost": "Subsidised / government",
|
| 251 |
+
"specialties": ["psychiatry", "inpatient care"]
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"name": "Amrita Institute of Medical Sciences — Psychiatry",
|
| 255 |
+
"type": "Private tertiary hospital",
|
| 256 |
+
"address": "AIMS Ponekkara, Kochi 682041",
|
| 257 |
+
"website": "https://www.amritahospitals.org/",
|
| 258 |
+
"cost": "Private",
|
| 259 |
+
"specialties": ["psychiatry", "psychology", "child mental health"]
|
| 260 |
+
}
|
| 261 |
+
],
|
| 262 |
+
"Guwahati": [
|
| 263 |
+
{
|
| 264 |
+
"name": "LGB Regional Institute of Mental Health",
|
| 265 |
+
"type": "Institute of National Importance",
|
| 266 |
+
"address": "Tezpur, Assam 784001",
|
| 267 |
+
"website": "https://lgbrimh.gov.in/",
|
| 268 |
+
"cost": "Subsidised / government",
|
| 269 |
+
"specialties": ["psychiatry", "clinical psychology", "psychiatric social work", "rehabilitation"]
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"name": "Gauhati Medical College — Department of Psychiatry",
|
| 273 |
+
"type": "Government tertiary hospital",
|
| 274 |
+
"address": "Bhangagarh, Guwahati 781032",
|
| 275 |
+
"cost": "Subsidised / government",
|
| 276 |
+
"specialties": ["psychiatry", "general hospital psychiatry"]
|
| 277 |
+
}
|
| 278 |
+
],
|
| 279 |
+
"Bhubaneswar": [
|
| 280 |
+
{
|
| 281 |
+
"name": "AIIMS Bhubaneswar — Department of Psychiatry",
|
| 282 |
+
"type": "Institute of National Importance",
|
| 283 |
+
"address": "Sijua, Patrapada, Bhubaneswar 751019",
|
| 284 |
+
"website": "https://aiimsbhubaneswar.nic.in/",
|
| 285 |
+
"cost": "Subsidised / government",
|
| 286 |
+
"specialties": ["psychiatry", "child & adolescent psychiatry", "de-addiction"]
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"name": "Capital Hospital — Department of Psychiatry",
|
| 290 |
+
"type": "State government hospital",
|
| 291 |
+
"address": "Unit-6, Bhubaneswar 751001",
|
| 292 |
+
"cost": "Subsidised / government",
|
| 293 |
+
"specialties": ["general psychiatry", "OPD crisis care"]
|
| 294 |
+
},
|
| 295 |
+
{
|
| 296 |
+
"name": "Mental Health Institute, SCB Medical College (nearby — Cuttack)",
|
| 297 |
+
"type": "Government teaching hospital",
|
| 298 |
+
"address": "SCB Medical College Campus, Cuttack 753007",
|
| 299 |
+
"cost": "Subsidised / government",
|
| 300 |
+
"specialties": ["psychiatry", "inpatient care"]
|
| 301 |
+
}
|
| 302 |
+
],
|
| 303 |
+
"Ranchi": [
|
| 304 |
+
{
|
| 305 |
+
"name": "Central Institute of Psychiatry (CIP), Kanke",
|
| 306 |
+
"type": "Institute of National Importance (under Ministry of Health & Family Welfare)",
|
| 307 |
+
"address": "Kanke, Ranchi 834006",
|
| 308 |
+
"website": "https://cipranchi.nic.in/",
|
| 309 |
+
"cost": "Subsidised / government",
|
| 310 |
+
"specialties": ["psychiatry", "clinical psychology", "psychiatric social work", "research", "training"]
|
| 311 |
+
},
|
| 312 |
+
{
|
| 313 |
+
"name": "RINPAS (Ranchi Institute of Neuro-Psychiatry & Allied Sciences)",
|
| 314 |
+
"type": "State government autonomous institute",
|
| 315 |
+
"address": "Kanke, Ranchi 834006",
|
| 316 |
+
"website": "https://rinpas.jharkhand.gov.in/",
|
| 317 |
+
"cost": "Subsidised / government",
|
| 318 |
+
"specialties": ["psychiatry", "neuropsychiatry", "rehabilitation", "community mental health"]
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"name": "RIMS Ranchi — Department of Psychiatry",
|
| 322 |
+
"type": "Government teaching hospital",
|
| 323 |
+
"address": "Bariatu, Ranchi 834009",
|
| 324 |
+
"website": "https://www.rimsranchi.ac.in/",
|
| 325 |
+
"cost": "Subsidised / government",
|
| 326 |
+
"specialties": ["psychiatry", "general hospital psychiatry"]
|
| 327 |
+
}
|
| 328 |
+
],
|
| 329 |
+
"Nagpur": [
|
| 330 |
+
{
|
| 331 |
+
"name": "AIIMS Nagpur — Department of Psychiatry",
|
| 332 |
+
"type": "Institute of National Importance",
|
| 333 |
+
"address": "Plot No. 2, Sector 20, MIHAN, Nagpur 441108",
|
| 334 |
+
"website": "https://aiimsnagpur.edu.in/",
|
| 335 |
+
"cost": "Subsidised / government",
|
| 336 |
+
"specialties": ["psychiatry", "child psychiatry", "crisis care"]
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"name": "Government Medical College & Hospital (GMCH) — Psychiatry",
|
| 340 |
+
"type": "Government teaching hospital",
|
| 341 |
+
"address": "Medical Square, Nagpur 440003",
|
| 342 |
+
"cost": "Subsidised / government",
|
| 343 |
+
"specialties": ["psychiatry", "emergency mental health"]
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"name": "Regional Mental Hospital, Nagpur",
|
| 347 |
+
"type": "State government psychiatric hospital",
|
| 348 |
+
"address": "Kamptee Road, Nagpur 440001",
|
| 349 |
+
"cost": "Subsidised / government",
|
| 350 |
+
"specialties": ["inpatient psychiatry", "rehabilitation"]
|
| 351 |
+
}
|
| 352 |
+
],
|
| 353 |
+
"Indore": [
|
| 354 |
+
{
|
| 355 |
+
"name": "MGM Medical College / M.Y. Hospital — Department of Psychiatry",
|
| 356 |
+
"type": "Government tertiary hospital",
|
| 357 |
+
"address": "A.B. Road, Indore 452001",
|
| 358 |
+
"website": "https://www.mgmmcindore.in/",
|
| 359 |
+
"cost": "Subsidised / government",
|
| 360 |
+
"specialties": ["psychiatry", "general hospital psychiatry", "crisis care"]
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"name": "Mental Hospital, Banganga, Indore",
|
| 364 |
+
"type": "State government psychiatric hospital",
|
| 365 |
+
"address": "Banganga, Indore 452015",
|
| 366 |
+
"cost": "Subsidised / government",
|
| 367 |
+
"specialties": ["inpatient psychiatry", "long-term care"]
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"name": "Gokuldas Hospital — Psychiatry Department",
|
| 371 |
+
"type": "Private multi-speciality",
|
| 372 |
+
"address": "Sapna Sangeeta Road, Indore 452001",
|
| 373 |
+
"cost": "Private",
|
| 374 |
+
"specialties": ["psychiatry", "psychology"]
|
| 375 |
+
}
|
| 376 |
+
],
|
| 377 |
+
"Dehradun": [
|
| 378 |
+
{
|
| 379 |
+
"name": "AIIMS Rishikesh — Department of Psychiatry (nearest tertiary)",
|
| 380 |
+
"type": "Institute of National Importance",
|
| 381 |
+
"address": "Virbhadra Road, Rishikesh 249203",
|
| 382 |
+
"website": "https://www.aiimsrishikesh.edu.in/",
|
| 383 |
+
"cost": "Subsidised / government",
|
| 384 |
+
"specialties": ["psychiatry", "de-addiction", "community psychiatry"]
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"name": "Doon Medical College Hospital — Psychiatry",
|
| 388 |
+
"type": "Government teaching hospital",
|
| 389 |
+
"address": "Dehradun 248001",
|
| 390 |
+
"cost": "Subsidised / government",
|
| 391 |
+
"specialties": ["psychiatry", "OPD", "general hospital psychiatry"]
|
| 392 |
+
},
|
| 393 |
+
{
|
| 394 |
+
"name": "State Hospital for Mental Health, Dehradun (Selaqui)",
|
| 395 |
+
"type": "State government psychiatric hospital",
|
| 396 |
+
"address": "Selaqui, Dehradun 248197",
|
| 397 |
+
"cost": "Subsidised / government",
|
| 398 |
+
"specialties": ["inpatient psychiatry", "long-term care"]
|
| 399 |
+
}
|
| 400 |
+
],
|
| 401 |
+
"Thiruvananthapuram": [
|
| 402 |
+
{
|
| 403 |
+
"name": "Government Mental Health Centre, Peroorkada",
|
| 404 |
+
"type": "State government psychiatric hospital",
|
| 405 |
+
"address": "Peroorkada, Thiruvananthapuram 695005",
|
| 406 |
+
"cost": "Subsidised / government",
|
| 407 |
+
"specialties": ["inpatient psychiatry", "de-addiction", "rehabilitation"]
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"name": "Government Medical College, Thiruvananthapuram — Psychiatry",
|
| 411 |
+
"type": "Government teaching hospital",
|
| 412 |
+
"address": "Medical College PO, Thiruvananthapuram 695011",
|
| 413 |
+
"website": "https://www.mctvm.kerala.gov.in/",
|
| 414 |
+
"cost": "Subsidised / government",
|
| 415 |
+
"specialties": ["psychiatry", "OPD", "child psychiatry"]
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"name": "SCTIMST — Department of Neurology & Psychiatry",
|
| 419 |
+
"type": "Institute of National Importance",
|
| 420 |
+
"address": "Medical College PO, Thiruvananthapuram 695011",
|
| 421 |
+
"website": "https://www.sctimst.ac.in/",
|
| 422 |
+
"cost": "Subsidised / government",
|
| 423 |
+
"specialties": ["neuropsychiatry", "research"]
|
| 424 |
+
}
|
| 425 |
+
],
|
| 426 |
+
"Coimbatore": [
|
| 427 |
+
{
|
| 428 |
+
"name": "Coimbatore Medical College Hospital — Psychiatry",
|
| 429 |
+
"type": "Government teaching hospital",
|
| 430 |
+
"address": "Trichy Road, Coimbatore 641018",
|
| 431 |
+
"cost": "Subsidised / government",
|
| 432 |
+
"specialties": ["psychiatry", "general hospital psychiatry", "crisis care"]
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"name": "PSG Institute of Medical Sciences & Research — Psychiatry",
|
| 436 |
+
"type": "Private teaching hospital",
|
| 437 |
+
"address": "Peelamedu, Coimbatore 641004",
|
| 438 |
+
"website": "https://psgimsr.ac.in/",
|
| 439 |
+
"cost": "Private (subsidised OPD)",
|
| 440 |
+
"specialties": ["psychiatry", "child psychiatry", "de-addiction"]
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"name": "Government Hospital for Mental Care, Kovai",
|
| 444 |
+
"type": "State government psychiatric hospital",
|
| 445 |
+
"address": "Kuniyamuthur, Coimbatore 641008",
|
| 446 |
+
"cost": "Subsidised / government",
|
| 447 |
+
"specialties": ["inpatient psychiatry", "rehabilitation"]
|
| 448 |
+
}
|
| 449 |
+
],
|
| 450 |
+
"Visakhapatnam": [
|
| 451 |
+
{
|
| 452 |
+
"name": "Government Hospital for Mental Care",
|
| 453 |
+
"type": "State government psychiatric hospital",
|
| 454 |
+
"address": "Visakhapatnam 530002",
|
| 455 |
+
"cost": "Subsidised / government",
|
| 456 |
+
"specialties": ["inpatient psychiatry", "rehabilitation"]
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"name": "Andhra Medical College — Department of Psychiatry (King George Hospital)",
|
| 460 |
+
"type": "Government tertiary hospital",
|
| 461 |
+
"address": "Maharani Peta, Visakhapatnam 530002",
|
| 462 |
+
"cost": "Subsidised / government",
|
| 463 |
+
"specialties": ["psychiatry", "general hospital psychiatry"]
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"name": "KIMS ICON Hospital — Psychiatry",
|
| 467 |
+
"type": "Private multi-speciality hospital",
|
| 468 |
+
"address": "Sheela Nagar, Visakhapatnam 530012",
|
| 469 |
+
"website": "https://www.kimshospitals.com/",
|
| 470 |
+
"cost": "Private",
|
| 471 |
+
"specialties": ["psychiatry", "psychology"]
|
| 472 |
+
}
|
| 473 |
+
],
|
| 474 |
+
"Srinagar": [
|
| 475 |
+
{
|
| 476 |
+
"name": "Institute of Mental Health & Neurosciences (IMHANS), Kashmir",
|
| 477 |
+
"type": "State government autonomous institute",
|
| 478 |
+
"address": "Kathidarwaza, Srinagar 190003",
|
| 479 |
+
"cost": "Subsidised / government",
|
| 480 |
+
"specialties": ["psychiatry", "clinical psychology", "trauma care", "community psychiatry"]
|
| 481 |
+
},
|
| 482 |
+
{
|
| 483 |
+
"name": "SKIMS Soura — Department of Psychiatry",
|
| 484 |
+
"type": "Institute of National Importance",
|
| 485 |
+
"address": "Soura, Srinagar 190011",
|
| 486 |
+
"website": "https://www.skims.ac.in/",
|
| 487 |
+
"cost": "Subsidised / government",
|
| 488 |
+
"specialties": ["psychiatry", "general hospital psychiatry", "research"]
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"name": "Government Medical College (GMC) Srinagar — Psychiatry",
|
| 492 |
+
"type": "Government teaching hospital",
|
| 493 |
+
"address": "Karan Nagar, Srinagar 190010",
|
| 494 |
+
"cost": "Subsidised / government",
|
| 495 |
+
"specialties": ["psychiatry", "OPD"]
|
| 496 |
+
}
|
| 497 |
+
],
|
| 498 |
+
"Imphal": [
|
| 499 |
+
{
|
| 500 |
+
"name": "RIMS Imphal — Department of Psychiatry",
|
| 501 |
+
"type": "Institute of National Importance",
|
| 502 |
+
"address": "Lamphelpat, Imphal 795004",
|
| 503 |
+
"website": "https://rims.edu.in/",
|
| 504 |
+
"cost": "Subsidised / government",
|
| 505 |
+
"specialties": ["psychiatry", "substance use", "general hospital psychiatry"]
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"name": "Jawaharlal Nehru Institute of Medical Sciences (JNIMS) — Psychiatry",
|
| 509 |
+
"type": "Government teaching hospital",
|
| 510 |
+
"address": "Porompat, Imphal East 795005",
|
| 511 |
+
"website": "https://jnims.edu.in/",
|
| 512 |
+
"cost": "Subsidised / government",
|
| 513 |
+
"specialties": ["psychiatry", "community mental health"]
|
| 514 |
+
}
|
| 515 |
+
]
|
| 516 |
+
}
|
modules/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Saathi feature modules: five independent Streamlit views."""
|
modules/cognitive_journal.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module 4 — Cognitive Journal: CBT-trained distortion analyzer with weekly pattern chart.
|
| 2 |
+
|
| 3 |
+
Flow:
|
| 4 |
+
1. User writes 2-3 sentences about their day / thoughts.
|
| 5 |
+
2. Crisis regex runs first.
|
| 6 |
+
3. Claude returns strict JSON: overall_mood, distortions[], summary, needs_professional_signal.
|
| 7 |
+
4. Pydantic validates it (auto-retries once on failure via claude_client.chat_structured).
|
| 8 |
+
5. UI renders distortion cards, reframes, and a cumulative Plotly bar chart of distortion frequency.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
from collections import Counter
|
| 13 |
+
from typing import List, Literal, Optional
|
| 14 |
+
|
| 15 |
+
import plotly.express as px
|
| 16 |
+
import streamlit as st
|
| 17 |
+
from pydantic import BaseModel, Field
|
| 18 |
+
|
| 19 |
+
from backend.claude_client import chat_structured
|
| 20 |
+
from backend.i18n import claude_language_name, t
|
| 21 |
+
from backend.safeguards import check_crisis, render_crisis_banner
|
| 22 |
+
|
| 23 |
+
MODULE_NAME = "cognitive_journal"
|
| 24 |
+
ENTRIES_KEY = "cognitive_journal_entries"
|
| 25 |
+
LAST_ANALYSIS_KEY = "cognitive_journal_last"
|
| 26 |
+
|
| 27 |
+
DistortionType = Literal[
|
| 28 |
+
"catastrophizing",
|
| 29 |
+
"mind_reading",
|
| 30 |
+
"all_or_nothing",
|
| 31 |
+
"fortune_telling",
|
| 32 |
+
"personalization",
|
| 33 |
+
"mental_filter",
|
| 34 |
+
"emotional_reasoning",
|
| 35 |
+
"should_statements",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
MoodType = Literal[
|
| 39 |
+
"anxious",
|
| 40 |
+
"sad",
|
| 41 |
+
"frustrated",
|
| 42 |
+
"hopeful",
|
| 43 |
+
"neutral",
|
| 44 |
+
"overwhelmed",
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Distortion(BaseModel):
|
| 49 |
+
type: DistortionType
|
| 50 |
+
phrase: str
|
| 51 |
+
explanation: str
|
| 52 |
+
reframe: str
|
| 53 |
+
evidence_question: str
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class JournalAnalysis(BaseModel):
|
| 57 |
+
overall_mood: MoodType
|
| 58 |
+
distortions: List[Distortion] = Field(default_factory=list)
|
| 59 |
+
summary: str
|
| 60 |
+
needs_professional_signal: bool = False
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
DISTORTION_LABELS = {
|
| 64 |
+
"catastrophizing": "Catastrophizing",
|
| 65 |
+
"mind_reading": "Mind-reading",
|
| 66 |
+
"all_or_nothing": "All-or-nothing thinking",
|
| 67 |
+
"fortune_telling": "Fortune-telling",
|
| 68 |
+
"personalization": "Personalization",
|
| 69 |
+
"mental_filter": "Mental filter",
|
| 70 |
+
"emotional_reasoning": "Emotional reasoning",
|
| 71 |
+
"should_statements": "Should-statements",
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _seed_entries() -> List[JournalAnalysis]:
|
| 76 |
+
"""Illustrative sample analyses used to populate the chart on first render.
|
| 77 |
+
|
| 78 |
+
Grounded in situations common among Indian students and young professionals
|
| 79 |
+
(placement stress, parental expectations, midterm setbacks). All entries are
|
| 80 |
+
author-written — they never reach the model and never enter conversation history.
|
| 81 |
+
"""
|
| 82 |
+
return [
|
| 83 |
+
JournalAnalysis(
|
| 84 |
+
overall_mood="overwhelmed",
|
| 85 |
+
distortions=[
|
| 86 |
+
Distortion(
|
| 87 |
+
type="catastrophizing",
|
| 88 |
+
phrase="my whole placement season is ruined",
|
| 89 |
+
explanation="This predicts the worst possible outcome from a single rejection.",
|
| 90 |
+
reframe="One company saying no does not close every door — most students get placed after several rejections.",
|
| 91 |
+
evidence_question="How many companies actually reject a student during placement season, on average, before they get an offer?",
|
| 92 |
+
),
|
| 93 |
+
Distortion(
|
| 94 |
+
type="mind_reading",
|
| 95 |
+
phrase="everyone in my batch thinks I'm useless",
|
| 96 |
+
explanation="This assumes you know what others are thinking without any real evidence.",
|
| 97 |
+
reframe="Most of my batchmates are dealing with their own anxiety right now, not judging mine.",
|
| 98 |
+
evidence_question="Has anyone actually said this to me, or am I guessing?",
|
| 99 |
+
),
|
| 100 |
+
],
|
| 101 |
+
summary="You're carrying real disappointment about the rejection along with some thoughts that are making it feel bigger than it is. Naming the pattern is the first step to loosening it.",
|
| 102 |
+
needs_professional_signal=False,
|
| 103 |
+
),
|
| 104 |
+
JournalAnalysis(
|
| 105 |
+
overall_mood="sad",
|
| 106 |
+
distortions=[
|
| 107 |
+
Distortion(
|
| 108 |
+
type="should_statements",
|
| 109 |
+
phrase="I should have studied harder in first year",
|
| 110 |
+
explanation="Should-statements punish you for the past without changing anything about the present.",
|
| 111 |
+
reframe="I did what I could with the information and energy I had at the time. I can act differently now without blaming past-me.",
|
| 112 |
+
evidence_question="What would I say to a close friend who told me the same thing about themselves?",
|
| 113 |
+
),
|
| 114 |
+
Distortion(
|
| 115 |
+
type="personalization",
|
| 116 |
+
phrase="my parents are disappointed because of me",
|
| 117 |
+
explanation="This takes full responsibility for other people's emotions, which are always the product of many things.",
|
| 118 |
+
reframe="My parents may be worried, but their emotions come from their own hopes, fears and life — not only from me.",
|
| 119 |
+
evidence_question="What other things in my parents' lives might be affecting their mood this week?",
|
| 120 |
+
),
|
| 121 |
+
],
|
| 122 |
+
summary="A lot of self-blame in this entry. The 'should' and the 'because of me' are both doing heavy lifting — worth sitting with gently.",
|
| 123 |
+
needs_professional_signal=False,
|
| 124 |
+
),
|
| 125 |
+
JournalAnalysis(
|
| 126 |
+
overall_mood="anxious",
|
| 127 |
+
distortions=[
|
| 128 |
+
Distortion(
|
| 129 |
+
type="fortune_telling",
|
| 130 |
+
phrase="I'm going to freeze up in the viva and fail",
|
| 131 |
+
explanation="You're predicting a specific bad future event as if it has already happened.",
|
| 132 |
+
reframe="I don't actually know what will happen in the viva — I've handled questions before, and I can again.",
|
| 133 |
+
evidence_question="What is one past exam or viva where I did better than I expected to?",
|
| 134 |
+
),
|
| 135 |
+
Distortion(
|
| 136 |
+
type="all_or_nothing",
|
| 137 |
+
phrase="if I don't get an A I've wasted the whole semester",
|
| 138 |
+
explanation="This frames the outcome as total success or total failure, with nothing in between.",
|
| 139 |
+
reframe="A B or a B+ is not a wasted semester — most real learning doesn't show up on a grade card anyway.",
|
| 140 |
+
evidence_question="What did I actually learn this semester that has nothing to do with the final grade?",
|
| 141 |
+
),
|
| 142 |
+
Distortion(
|
| 143 |
+
type="mental_filter",
|
| 144 |
+
phrase="I only remember the parts I got wrong in the last test",
|
| 145 |
+
explanation="You're filtering out everything that went well and focusing only on the mistakes.",
|
| 146 |
+
reframe="There were questions I answered confidently in that test too — those count.",
|
| 147 |
+
evidence_question="What are two or three things I got right in the last test?",
|
| 148 |
+
),
|
| 149 |
+
],
|
| 150 |
+
summary="Pre-viva anxiety is real, and the distortions are pulling a lot of weight here. Your breath is your anchor — one slow exhale before reading this back.",
|
| 151 |
+
needs_professional_signal=False,
|
| 152 |
+
),
|
| 153 |
+
]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
_SEED_COUNT = len(_seed_entries())
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _init_state() -> None:
|
| 160 |
+
if ENTRIES_KEY not in st.session_state:
|
| 161 |
+
st.session_state[ENTRIES_KEY] = _seed_entries()
|
| 162 |
+
if LAST_ANALYSIS_KEY not in st.session_state:
|
| 163 |
+
st.session_state[LAST_ANALYSIS_KEY] = None
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def _render_analysis(analysis: JournalAnalysis, lang: str) -> None:
|
| 167 |
+
st.markdown(f"##### {t('journal_summary_heading', lang)}")
|
| 168 |
+
st.info(analysis.summary)
|
| 169 |
+
|
| 170 |
+
if analysis.needs_professional_signal:
|
| 171 |
+
st.warning(t("journal_needs_pro", lang))
|
| 172 |
+
|
| 173 |
+
if not analysis.distortions:
|
| 174 |
+
st.success(t("journal_no_distortions", lang))
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
st.markdown(f"##### {t('journal_distortions_heading', lang)}")
|
| 178 |
+
for d in analysis.distortions:
|
| 179 |
+
with st.container(border=True):
|
| 180 |
+
st.markdown(f"> *{d.phrase}*")
|
| 181 |
+
st.markdown(
|
| 182 |
+
f"**{DISTORTION_LABELS.get(d.type, d.type)}** — {d.explanation}"
|
| 183 |
+
)
|
| 184 |
+
st.markdown(f"**{t('journal_reframe_heading', lang)}:** {d.reframe}")
|
| 185 |
+
st.caption(f"**{t('journal_question_heading', lang)}:** {d.evidence_question}")
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _render_chart(lang: str) -> None:
|
| 189 |
+
entries: List[JournalAnalysis] = st.session_state[ENTRIES_KEY]
|
| 190 |
+
if not entries:
|
| 191 |
+
return
|
| 192 |
+
|
| 193 |
+
counter: Counter = Counter()
|
| 194 |
+
for entry in entries:
|
| 195 |
+
for d in entry.distortions:
|
| 196 |
+
counter[d.type] += 1
|
| 197 |
+
|
| 198 |
+
if not counter:
|
| 199 |
+
return
|
| 200 |
+
|
| 201 |
+
rows = [
|
| 202 |
+
{"distortion": DISTORTION_LABELS.get(k, k), "count": v}
|
| 203 |
+
for k, v in counter.most_common()
|
| 204 |
+
]
|
| 205 |
+
fig = px.bar(
|
| 206 |
+
rows,
|
| 207 |
+
x="distortion",
|
| 208 |
+
y="count",
|
| 209 |
+
title=t("journal_chart_title", lang),
|
| 210 |
+
color="count",
|
| 211 |
+
color_continuous_scale="Purples",
|
| 212 |
+
)
|
| 213 |
+
fig.update_layout(
|
| 214 |
+
xaxis_title=None,
|
| 215 |
+
yaxis_title=None,
|
| 216 |
+
showlegend=False,
|
| 217 |
+
height=320,
|
| 218 |
+
margin=dict(l=10, r=10, t=50, b=10),
|
| 219 |
+
)
|
| 220 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 221 |
+
if len(entries) <= _SEED_COUNT:
|
| 222 |
+
st.caption(
|
| 223 |
+
f"_The chart currently reflects {_SEED_COUNT} illustrative sample entries "
|
| 224 |
+
f"so you can see how the pattern view works. Write your own entry "
|
| 225 |
+
f"above and your distortions will be added to the chart._"
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def render(lang: str) -> None:
|
| 230 |
+
_init_state()
|
| 231 |
+
|
| 232 |
+
st.header(t("journal_header", lang))
|
| 233 |
+
st.caption(t("journal_sub", lang))
|
| 234 |
+
|
| 235 |
+
entry = st.text_area(
|
| 236 |
+
"journal_entry",
|
| 237 |
+
placeholder=t("journal_input_placeholder", lang),
|
| 238 |
+
label_visibility="collapsed",
|
| 239 |
+
height=140,
|
| 240 |
+
key="cognitive_journal_input",
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
if st.button(t("journal_send_button", lang), type="primary", key="cognitive_journal_send_button"):
|
| 244 |
+
if entry.strip():
|
| 245 |
+
if check_crisis(entry):
|
| 246 |
+
render_crisis_banner(lang)
|
| 247 |
+
return
|
| 248 |
+
|
| 249 |
+
with st.spinner("…"):
|
| 250 |
+
try:
|
| 251 |
+
analysis = chat_structured(
|
| 252 |
+
module=MODULE_NAME,
|
| 253 |
+
user_text=entry,
|
| 254 |
+
language_name=claude_language_name(lang),
|
| 255 |
+
schema=JournalAnalysis,
|
| 256 |
+
)
|
| 257 |
+
st.session_state[ENTRIES_KEY].append(analysis)
|
| 258 |
+
st.session_state[LAST_ANALYSIS_KEY] = analysis
|
| 259 |
+
except Exception as e:
|
| 260 |
+
st.error(
|
| 261 |
+
"I couldn't analyse that entry cleanly — the language model returned something "
|
| 262 |
+
f"I couldn't parse. Please try rephrasing. _(Technical detail: {e})_"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
if st.session_state[LAST_ANALYSIS_KEY] is not None:
|
| 266 |
+
_render_analysis(st.session_state[LAST_ANALYSIS_KEY], lang)
|
| 267 |
+
|
| 268 |
+
st.divider()
|
| 269 |
+
_render_chart(lang)
|
modules/legal_aid.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module 2 — Legal Aid: harassment → IPC/BNS → FIR guide → complaint letter.
|
| 2 |
+
|
| 3 |
+
This is Saathi's hackathon differentiator. Flow:
|
| 4 |
+
|
| 5 |
+
1. User describes what's happening in their own words.
|
| 6 |
+
2. `check_crisis()` runs first — if self-harm language is detected, Claude is bypassed
|
| 7 |
+
and the mental-health helpline banner fires.
|
| 8 |
+
3. `check_active_danger()` runs second — if the situation is an ongoing physical
|
| 9 |
+
emergency (weapon present, attacker at the door, current violence), Claude is
|
| 10 |
+
bypassed and a 112 / 100 / 1091 emergency banner is shown. Legal information
|
| 11 |
+
is not appropriate when someone needs to call for help in the next sixty seconds.
|
| 12 |
+
4. `classify_legal_situation_keywords()` narrows the case to one of:
|
| 13 |
+
sexual_violence / sexual_harassment / stalking / cyber_harassment / domestic_violence /
|
| 14 |
+
workplace_harassment / defamation / criminal_intimidation / other
|
| 15 |
+
5. If category is "sexual_violence", we bypass Claude and route to human support,
|
| 16 |
+
emergency contacts, and legal aid rather than auto-guessing sections from an
|
| 17 |
+
incomplete dataset.
|
| 18 |
+
6. If category is "other", we bypass Claude and show a neutral NALSA + DLSA referral
|
| 19 |
+
panel. Feeding Claude an empty section list would tempt it to hallucinate sections —
|
| 20 |
+
we refuse to do that.
|
| 21 |
+
7. Otherwise, we look up the matching IPC/BNS sections, ALWAYS append the
|
| 22 |
+
`procedural_rights` sections (IPC 166A / BNS 200 / Zero-FIR) plus the
|
| 23 |
+
`false_complaint_warning` section (IPC 182 / BNS 217), and pass the combined
|
| 24 |
+
list to Claude as context.
|
| 25 |
+
8. A separate button triggers a second Claude call that drafts a printable complaint
|
| 26 |
+
letter grounded in the same section list.
|
| 27 |
+
"""
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import json
|
| 31 |
+
from typing import Dict, List
|
| 32 |
+
|
| 33 |
+
import streamlit as st
|
| 34 |
+
|
| 35 |
+
from backend.claude_client import chat
|
| 36 |
+
from backend.i18n import claude_language_name, t
|
| 37 |
+
from backend.resources import (
|
| 38 |
+
classify_legal_situation_keywords,
|
| 39 |
+
get_legal_sections_for_category,
|
| 40 |
+
)
|
| 41 |
+
from backend.safeguards import (
|
| 42 |
+
check_active_danger,
|
| 43 |
+
check_crisis,
|
| 44 |
+
get_active_danger_payload,
|
| 45 |
+
render_crisis_banner,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
MODULE_NAME = "legal_aid"
|
| 49 |
+
SITUATION_KEY = "legal_aid_situation"
|
| 50 |
+
CATEGORY_KEY = "legal_aid_category"
|
| 51 |
+
SECTIONS_KEY = "legal_aid_sections"
|
| 52 |
+
RESPONSE_KEY = "legal_aid_response"
|
| 53 |
+
LETTER_KEY = "legal_aid_letter"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _init_state() -> None:
|
| 57 |
+
for k in (SITUATION_KEY, CATEGORY_KEY, RESPONSE_KEY, LETTER_KEY):
|
| 58 |
+
if k not in st.session_state:
|
| 59 |
+
st.session_state[k] = ""
|
| 60 |
+
if SECTIONS_KEY not in st.session_state:
|
| 61 |
+
st.session_state[SECTIONS_KEY] = []
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _render_active_danger_banner(lang: str) -> None:
|
| 65 |
+
"""Shown when the situation reads as an ONGOING physical emergency.
|
| 66 |
+
|
| 67 |
+
This is distinct from the self-harm crisis banner: the response here is the
|
| 68 |
+
emergency-response system (112 / 100 / 1091), not a counselling line. We
|
| 69 |
+
deliberately bury the rest of the legal UI until the user acknowledges.
|
| 70 |
+
"""
|
| 71 |
+
st.error(
|
| 72 |
+
f"**{t('legal_active_danger_title', lang)}**\n\n{t('legal_active_danger_body', lang)}"
|
| 73 |
+
)
|
| 74 |
+
cols = st.columns(len(get_active_danger_payload()))
|
| 75 |
+
for idx, hl in enumerate(get_active_danger_payload()):
|
| 76 |
+
with cols[idx]:
|
| 77 |
+
st.markdown(f"### {hl['number']}")
|
| 78 |
+
st.markdown(f"**{hl['name']}**")
|
| 79 |
+
st.caption(hl["note"])
|
| 80 |
+
if hl.get("source"):
|
| 81 |
+
st.caption(f"[{t('source_label', lang)}]({hl['source']})")
|
| 82 |
+
st.info(t("legal_active_danger_after", lang))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _render_other_category_panel(lang: str) -> None:
|
| 86 |
+
"""Shown when `classify_legal_situation_keywords` returns "other".
|
| 87 |
+
|
| 88 |
+
We intentionally do NOT call Claude with an empty section list here — that
|
| 89 |
+
invites hallucinated section numbers. Instead we route to real humans:
|
| 90 |
+
NALSA, district legal services authority, and the emergency lines if needed.
|
| 91 |
+
"""
|
| 92 |
+
st.warning(f"**{t('legal_other_title', lang)}**\n\n{t('legal_other_body', lang)}")
|
| 93 |
+
with st.container(border=True):
|
| 94 |
+
st.markdown(t("legal_other_nalsa", lang))
|
| 95 |
+
st.caption(f"[{t('source_label', lang)}](https://nalsa.gov.in/)")
|
| 96 |
+
with st.container(border=True):
|
| 97 |
+
st.markdown(t("legal_other_dlsa", lang))
|
| 98 |
+
st.caption(f"[{t('source_label', lang)}](https://nalsa.gov.in/lsa/dlsa)")
|
| 99 |
+
with st.container(border=True):
|
| 100 |
+
st.markdown(t("legal_other_police", lang))
|
| 101 |
+
st.caption(f"[{t('source_label', lang)}](https://112.gov.in/)")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _render_serious_sexual_violence_panel(lang: str) -> None:
|
| 105 |
+
"""Human-escalation panel for rape / sexual-assault language.
|
| 106 |
+
|
| 107 |
+
We intentionally avoid auto-mapping this class of case to legal sections until
|
| 108 |
+
the dataset includes the exact verified provisions we want to cite. Safety and
|
| 109 |
+
human follow-through matter more than squeezing out a speculative LLM answer.
|
| 110 |
+
"""
|
| 111 |
+
st.error(
|
| 112 |
+
f"**{t('legal_sexual_violence_title', lang)}**\n\n"
|
| 113 |
+
f"{t('legal_sexual_violence_body', lang)}"
|
| 114 |
+
)
|
| 115 |
+
with st.container(border=True):
|
| 116 |
+
st.markdown(t("legal_sexual_violence_help", lang))
|
| 117 |
+
st.caption(f"[{t('source_label', lang)}](https://112.gov.in/)")
|
| 118 |
+
with st.container(border=True):
|
| 119 |
+
st.markdown(t("legal_other_nalsa", lang))
|
| 120 |
+
st.caption(f"[{t('source_label', lang)}](https://nalsa.gov.in/)")
|
| 121 |
+
with st.container(border=True):
|
| 122 |
+
st.markdown(t("legal_other_dlsa", lang))
|
| 123 |
+
st.caption(f"[{t('source_label', lang)}](https://nalsa.gov.in/lsa/dlsa)")
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def _sections_for_llm(sections: List[Dict]) -> str:
|
| 127 |
+
"""Compact JSON the Claude prompt can safely cite from — we strip very long keys."""
|
| 128 |
+
slim = []
|
| 129 |
+
for s in sections:
|
| 130 |
+
slim.append(
|
| 131 |
+
{
|
| 132 |
+
"act": s.get("act"),
|
| 133 |
+
"ipc_section": s.get("ipc_section"),
|
| 134 |
+
"bns_section": s.get("bns_section"),
|
| 135 |
+
"title": s.get("title"),
|
| 136 |
+
"description": s.get("description"),
|
| 137 |
+
"punishment": s.get("punishment"),
|
| 138 |
+
"cognizable": s.get("cognizable"),
|
| 139 |
+
"bailable": s.get("bailable"),
|
| 140 |
+
"triable_by": s.get("triable_by"),
|
| 141 |
+
"evidence_tips": s.get("evidence_tips"),
|
| 142 |
+
}
|
| 143 |
+
)
|
| 144 |
+
return json.dumps(slim, ensure_ascii=False, indent=2)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _section_is_procedural(section: Dict) -> bool:
|
| 148 |
+
cats = section.get("category") or section.get("categories") or []
|
| 149 |
+
if isinstance(cats, str):
|
| 150 |
+
cats = [cats]
|
| 151 |
+
return "procedural_rights" in [c.lower() for c in cats]
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def _section_is_false_complaint(section: Dict) -> bool:
|
| 155 |
+
cats = section.get("category") or section.get("categories") or []
|
| 156 |
+
if isinstance(cats, str):
|
| 157 |
+
cats = [cats]
|
| 158 |
+
return "false_complaint_warning" in [c.lower() for c in cats]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _render_section_cards(sections: List[Dict], lang: str) -> None:
|
| 162 |
+
if not sections:
|
| 163 |
+
return
|
| 164 |
+
|
| 165 |
+
primary = [s for s in sections if not _section_is_procedural(s) and not _section_is_false_complaint(s)]
|
| 166 |
+
procedural = [s for s in sections if _section_is_procedural(s)]
|
| 167 |
+
false_complaint = [s for s in sections if _section_is_false_complaint(s)]
|
| 168 |
+
|
| 169 |
+
if primary:
|
| 170 |
+
st.markdown("##### 📜 Relevant sections")
|
| 171 |
+
for s in primary:
|
| 172 |
+
_render_one_section_card(s, lang)
|
| 173 |
+
|
| 174 |
+
if procedural:
|
| 175 |
+
st.markdown(f"##### 🛡️ {t('legal_procedural_heading', lang)}")
|
| 176 |
+
for s in procedural:
|
| 177 |
+
_render_one_section_card(s, lang)
|
| 178 |
+
|
| 179 |
+
if false_complaint:
|
| 180 |
+
st.markdown(f"##### ℹ️ {t('legal_false_complaint_warning_heading', lang)}")
|
| 181 |
+
for s in false_complaint:
|
| 182 |
+
_render_one_section_card(s, lang)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _render_one_section_card(s: Dict, lang: str) -> None:
|
| 186 |
+
with st.container(border=True):
|
| 187 |
+
title = s.get("title", "")
|
| 188 |
+
ipc = s.get("ipc_section")
|
| 189 |
+
bns = s.get("bns_section")
|
| 190 |
+
header = f"**{title}**"
|
| 191 |
+
tags = []
|
| 192 |
+
if ipc:
|
| 193 |
+
tags.append(f"IPC {ipc}")
|
| 194 |
+
if bns:
|
| 195 |
+
tags.append(f"BNS {bns}")
|
| 196 |
+
act = s.get("act")
|
| 197 |
+
if act and act not in ("IPC/BNS",):
|
| 198 |
+
tags.append(act)
|
| 199 |
+
if tags:
|
| 200 |
+
header += " \n" + " · ".join(f"`{x}`" for x in tags)
|
| 201 |
+
st.markdown(header)
|
| 202 |
+
if s.get("description"):
|
| 203 |
+
st.caption(s["description"])
|
| 204 |
+
meta_bits = []
|
| 205 |
+
if s.get("punishment"):
|
| 206 |
+
meta_bits.append(f"**Punishment:** {s['punishment']}")
|
| 207 |
+
cog = s.get("cognizable")
|
| 208 |
+
bail = s.get("bailable")
|
| 209 |
+
if cog is not None:
|
| 210 |
+
meta_bits.append(f"Cognizable: {'Yes' if cog else 'No'}")
|
| 211 |
+
if bail is not None:
|
| 212 |
+
meta_bits.append(f"Bailable: {'Yes' if bail else 'No'}")
|
| 213 |
+
if meta_bits:
|
| 214 |
+
st.markdown(" \n".join(meta_bits))
|
| 215 |
+
if s.get("evidence_tips"):
|
| 216 |
+
st.caption(f"📎 **Evidence tips:** {s['evidence_tips']}")
|
| 217 |
+
if s.get("source"):
|
| 218 |
+
st.caption(f"[{t('source_label', lang)} — India Code]({s['source']})")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def _load_sections_with_procedural(category: str) -> List[Dict]:
|
| 222 |
+
"""Fetch sections for a specific category and always append procedural rights.
|
| 223 |
+
|
| 224 |
+
`procedural_rights` (IPC 166A / BNS 200 / Zero-FIR) are surfaced on EVERY
|
| 225 |
+
successful classification because they apply to every FIR a user might file.
|
| 226 |
+
The police cannot legally refuse to register a cognizable offence — users
|
| 227 |
+
should know this regardless of which category their case falls under.
|
| 228 |
+
|
| 229 |
+
We also append the dedicated `false_complaint_warning` section so the model
|
| 230 |
+
can give the required, brief "file truthfully" warning without inventing
|
| 231 |
+
IPC 182 / BNS 217 outside the provided context.
|
| 232 |
+
"""
|
| 233 |
+
primary = get_legal_sections_for_category(category)
|
| 234 |
+
procedural = get_legal_sections_for_category("procedural_rights")
|
| 235 |
+
false_complaint = get_legal_sections_for_category("false_complaint_warning")
|
| 236 |
+
# Deduplicate while preserving order.
|
| 237 |
+
seen = set()
|
| 238 |
+
out: List[Dict] = []
|
| 239 |
+
for s in primary + procedural + false_complaint:
|
| 240 |
+
key = (s.get("act"), s.get("ipc_section"), s.get("bns_section"))
|
| 241 |
+
if key in seen:
|
| 242 |
+
continue
|
| 243 |
+
seen.add(key)
|
| 244 |
+
out.append(s)
|
| 245 |
+
return out
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def render(lang: str) -> None:
|
| 249 |
+
_init_state()
|
| 250 |
+
|
| 251 |
+
st.header(t("legal_header", lang))
|
| 252 |
+
st.caption(t("legal_sub", lang))
|
| 253 |
+
st.warning(t("legal_not_lawyer", lang))
|
| 254 |
+
|
| 255 |
+
situation = st.text_area(
|
| 256 |
+
"situation",
|
| 257 |
+
value=st.session_state[SITUATION_KEY],
|
| 258 |
+
placeholder=t("legal_input_placeholder", lang),
|
| 259 |
+
label_visibility="collapsed",
|
| 260 |
+
height=140,
|
| 261 |
+
key="legal_aid_situation_input",
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
analyse_clicked = st.button(
|
| 265 |
+
t("legal_send_button", lang),
|
| 266 |
+
type="primary",
|
| 267 |
+
key="legal_aid_analyse_button",
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
if analyse_clicked and situation.strip():
|
| 271 |
+
st.session_state[SITUATION_KEY] = situation
|
| 272 |
+
# Reset stale state from a previous run
|
| 273 |
+
st.session_state[CATEGORY_KEY] = ""
|
| 274 |
+
st.session_state[SECTIONS_KEY] = []
|
| 275 |
+
st.session_state[RESPONSE_KEY] = ""
|
| 276 |
+
st.session_state[LETTER_KEY] = ""
|
| 277 |
+
|
| 278 |
+
# Layer 1: self-harm crisis → mental-health helplines
|
| 279 |
+
if check_crisis(situation):
|
| 280 |
+
render_crisis_banner(lang)
|
| 281 |
+
return
|
| 282 |
+
|
| 283 |
+
# Layer 2: active physical danger → 112 / 100 / 1091
|
| 284 |
+
if check_active_danger(situation):
|
| 285 |
+
_render_active_danger_banner(lang)
|
| 286 |
+
return
|
| 287 |
+
|
| 288 |
+
# Layer 3: keyword classifier → section lookup → Claude
|
| 289 |
+
category = classify_legal_situation_keywords(situation)
|
| 290 |
+
st.session_state[CATEGORY_KEY] = category
|
| 291 |
+
|
| 292 |
+
if category == "sexual_violence":
|
| 293 |
+
_render_serious_sexual_violence_panel(lang)
|
| 294 |
+
return
|
| 295 |
+
|
| 296 |
+
if category == "other":
|
| 297 |
+
# Do NOT call Claude with an empty section list — show neutral NALSA path
|
| 298 |
+
_render_other_category_panel(lang)
|
| 299 |
+
return
|
| 300 |
+
|
| 301 |
+
sections = _load_sections_with_procedural(category)
|
| 302 |
+
st.session_state[SECTIONS_KEY] = sections
|
| 303 |
+
|
| 304 |
+
with st.spinner("Reading the law…"):
|
| 305 |
+
try:
|
| 306 |
+
response = chat(
|
| 307 |
+
module=MODULE_NAME,
|
| 308 |
+
user_text=situation,
|
| 309 |
+
language_name=claude_language_name(lang),
|
| 310 |
+
extra_context={"sections_json": _sections_for_llm(sections)},
|
| 311 |
+
max_tokens=1500,
|
| 312 |
+
)
|
| 313 |
+
except Exception as e:
|
| 314 |
+
response = (
|
| 315 |
+
"I couldn't reach my language model to explain the law right now. "
|
| 316 |
+
"For immediate help, please call **1091** (Women's Helpline) or **112** (emergency), "
|
| 317 |
+
"or contact **NALSA legal aid on 15100** for free legal aid.\n\n"
|
| 318 |
+
f"_(Technical detail: {e})_"
|
| 319 |
+
)
|
| 320 |
+
st.session_state[RESPONSE_KEY] = response
|
| 321 |
+
|
| 322 |
+
# --- Render stored response ---
|
| 323 |
+
if st.session_state[CATEGORY_KEY] and st.session_state[CATEGORY_KEY] != "other":
|
| 324 |
+
st.caption(f"Category detected: `{st.session_state[CATEGORY_KEY]}`")
|
| 325 |
+
if st.session_state[SECTIONS_KEY]:
|
| 326 |
+
_render_section_cards(st.session_state[SECTIONS_KEY], lang)
|
| 327 |
+
if st.session_state[RESPONSE_KEY]:
|
| 328 |
+
st.markdown("##### 🧭 Plain-language explanation")
|
| 329 |
+
st.markdown(st.session_state[RESPONSE_KEY])
|
| 330 |
+
|
| 331 |
+
draft_clicked = st.button(
|
| 332 |
+
t("legal_draft_letter_button", lang),
|
| 333 |
+
key="legal_aid_draft_button",
|
| 334 |
+
)
|
| 335 |
+
if draft_clicked:
|
| 336 |
+
with st.spinner("Drafting a complaint letter…"):
|
| 337 |
+
try:
|
| 338 |
+
letter = chat(
|
| 339 |
+
module=MODULE_NAME,
|
| 340 |
+
user_text=(
|
| 341 |
+
f"The user now asks you to draft a formal complaint letter based on this "
|
| 342 |
+
f"situation:\n\n{st.session_state[SITUATION_KEY]}\n\n"
|
| 343 |
+
f"Return ONLY the complaint letter, ready to print. Include a clear header, "
|
| 344 |
+
f"factual narrative, explicit references to the relevant section numbers from "
|
| 345 |
+
f"the context, a request to register an FIR, and blanks for name/signature/date/contact."
|
| 346 |
+
),
|
| 347 |
+
language_name=claude_language_name(lang),
|
| 348 |
+
extra_context={
|
| 349 |
+
"sections_json": _sections_for_llm(st.session_state[SECTIONS_KEY])
|
| 350 |
+
},
|
| 351 |
+
max_tokens=1800,
|
| 352 |
+
)
|
| 353 |
+
except Exception as e:
|
| 354 |
+
letter = f"(Could not draft the letter right now: {e})"
|
| 355 |
+
st.session_state[LETTER_KEY] = letter
|
| 356 |
+
|
| 357 |
+
if st.session_state[LETTER_KEY]:
|
| 358 |
+
st.markdown("##### ✉️ Draft complaint letter")
|
| 359 |
+
with st.container(border=True):
|
| 360 |
+
st.markdown(st.session_state[LETTER_KEY])
|
| 361 |
+
st.download_button(
|
| 362 |
+
"⬇️ Download as .txt",
|
| 363 |
+
data=st.session_state[LETTER_KEY],
|
| 364 |
+
file_name="saathi_complaint_letter.txt",
|
| 365 |
+
mime="text/plain",
|
| 366 |
+
key="legal_aid_download_button",
|
| 367 |
+
)
|
modules/saathi_chat.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module 1 — Saathi Chat: general mental-health Q&A + city-based doctor finder.
|
| 2 |
+
|
| 3 |
+
Flow:
|
| 4 |
+
1. User types a mental-health question (in any supported language).
|
| 5 |
+
2. Crisis regex runs first. If positive, Claude is bypassed and the helpline banner fires.
|
| 6 |
+
3. Otherwise Claude answers as Saathi, in the user's language.
|
| 7 |
+
4. User can optionally type a city to get 3-5 pre-curated resources from data/mental_health_resources.json.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from typing import Dict, List
|
| 12 |
+
|
| 13 |
+
import streamlit as st
|
| 14 |
+
|
| 15 |
+
from backend.claude_client import chat
|
| 16 |
+
from backend.i18n import claude_language_name, t
|
| 17 |
+
from backend.resources import get_mental_health_resources, list_known_cities
|
| 18 |
+
from backend.safeguards import check_crisis, render_crisis_banner
|
| 19 |
+
|
| 20 |
+
MODULE_NAME = "saathi_chat"
|
| 21 |
+
HISTORY_KEY = "saathi_chat_history"
|
| 22 |
+
CITY_KEY = "saathi_chat_city"
|
| 23 |
+
|
| 24 |
+
# How many prior messages (not counting the current turn) to resend to Claude.
|
| 25 |
+
# 20 = 10 full user↔assistant exchanges. Caps token cost + latency on long demos.
|
| 26 |
+
HISTORY_WINDOW = 20
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _init_state() -> None:
|
| 30 |
+
if HISTORY_KEY not in st.session_state:
|
| 31 |
+
st.session_state[HISTORY_KEY] = [] # list of {role, content}
|
| 32 |
+
if CITY_KEY not in st.session_state:
|
| 33 |
+
st.session_state[CITY_KEY] = ""
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _render_resources(resources: List[Dict], lang: str) -> None:
|
| 37 |
+
st.markdown(f"### {t('chat_resources_heading', lang)}")
|
| 38 |
+
for r in resources:
|
| 39 |
+
with st.container(border=True):
|
| 40 |
+
st.markdown(f"**{r.get('name', '')}** — *{r.get('type', '')}*")
|
| 41 |
+
if r.get("address"):
|
| 42 |
+
st.markdown(f"📍 {r['address']}")
|
| 43 |
+
if r.get("phone"):
|
| 44 |
+
st.markdown(f"📞 `{r['phone']}`")
|
| 45 |
+
website = r.get("website")
|
| 46 |
+
if website:
|
| 47 |
+
st.markdown(f"🌐 [{website}]({website})")
|
| 48 |
+
specialties = r.get("specialties") or []
|
| 49 |
+
if specialties:
|
| 50 |
+
st.caption("• " + " · ".join(specialties))
|
| 51 |
+
if r.get("cost"):
|
| 52 |
+
st.caption(f"💰 {r['cost']}")
|
| 53 |
+
st.caption(
|
| 54 |
+
"_Resources are curated from Government of India, AIIMS, NIMHANS, and "
|
| 55 |
+
"state mental-health websites. Saathi is not affiliated with any of "
|
| 56 |
+
"these institutions and does not make referrals on their behalf._"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def render(lang: str) -> None:
|
| 61 |
+
"""Top-level render function. `lang` is the language code (e.g. 'en', 'hi')."""
|
| 62 |
+
_init_state()
|
| 63 |
+
|
| 64 |
+
st.header(t("chat_header", lang))
|
| 65 |
+
st.caption(t("chat_sub", lang))
|
| 66 |
+
|
| 67 |
+
# --- Conversation history ---
|
| 68 |
+
for msg in st.session_state[HISTORY_KEY]:
|
| 69 |
+
with st.chat_message(msg["role"]):
|
| 70 |
+
st.markdown(msg["content"])
|
| 71 |
+
|
| 72 |
+
# --- Chat input ---
|
| 73 |
+
user_text = st.chat_input(t("chat_input_placeholder", lang), key="saathi_chat_input")
|
| 74 |
+
if user_text:
|
| 75 |
+
# Crisis check BEFORE Claude is invoked
|
| 76 |
+
if check_crisis(user_text):
|
| 77 |
+
st.session_state[HISTORY_KEY].append({"role": "user", "content": user_text})
|
| 78 |
+
with st.chat_message("user"):
|
| 79 |
+
st.markdown(user_text)
|
| 80 |
+
with st.chat_message("assistant"):
|
| 81 |
+
render_crisis_banner(lang)
|
| 82 |
+
return
|
| 83 |
+
|
| 84 |
+
# Normal flow → Claude
|
| 85 |
+
st.session_state[HISTORY_KEY].append({"role": "user", "content": user_text})
|
| 86 |
+
with st.chat_message("user"):
|
| 87 |
+
st.markdown(user_text)
|
| 88 |
+
|
| 89 |
+
with st.chat_message("assistant"):
|
| 90 |
+
with st.spinner("…"):
|
| 91 |
+
try:
|
| 92 |
+
prior_history = st.session_state[HISTORY_KEY][:-1][-HISTORY_WINDOW:]
|
| 93 |
+
reply = chat(
|
| 94 |
+
module=MODULE_NAME,
|
| 95 |
+
user_text=user_text,
|
| 96 |
+
language_name=claude_language_name(lang),
|
| 97 |
+
history=prior_history,
|
| 98 |
+
)
|
| 99 |
+
except Exception as e:
|
| 100 |
+
reply = (
|
| 101 |
+
"I'm having trouble reaching my language model right now. "
|
| 102 |
+
"If this is urgent, please call Tele-MANAS on **14416** "
|
| 103 |
+
"(or 1800-89-14416) — Government of India, 24×7, free, 20+ Indian languages. "
|
| 104 |
+
"For an emergency, call **112**.\n\n"
|
| 105 |
+
f"_(Technical detail: {e})_"
|
| 106 |
+
)
|
| 107 |
+
st.markdown(reply)
|
| 108 |
+
st.session_state[HISTORY_KEY].append({"role": "assistant", "content": reply})
|
| 109 |
+
|
| 110 |
+
# --- City-based resource finder (always visible under the chat) ---
|
| 111 |
+
st.divider()
|
| 112 |
+
st.markdown(f"**{t('chat_city_prompt', lang)}**")
|
| 113 |
+
col_input, col_button = st.columns([3, 1])
|
| 114 |
+
with col_input:
|
| 115 |
+
city = st.text_input(
|
| 116 |
+
"city",
|
| 117 |
+
value=st.session_state[CITY_KEY],
|
| 118 |
+
placeholder=t("chat_city_placeholder", lang),
|
| 119 |
+
label_visibility="collapsed",
|
| 120 |
+
key="saathi_chat_city_input",
|
| 121 |
+
)
|
| 122 |
+
with col_button:
|
| 123 |
+
find_clicked = st.button(
|
| 124 |
+
t("chat_city_button", lang),
|
| 125 |
+
key="saathi_chat_city_button",
|
| 126 |
+
use_container_width=True,
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
if find_clicked and city:
|
| 130 |
+
st.session_state[CITY_KEY] = city
|
| 131 |
+
resources = get_mental_health_resources(city)
|
| 132 |
+
if resources:
|
| 133 |
+
_render_resources(resources, lang)
|
| 134 |
+
else:
|
| 135 |
+
st.warning(t("chat_no_resources", lang))
|
| 136 |
+
known = list_known_cities()
|
| 137 |
+
if known:
|
| 138 |
+
st.caption("Cities I currently know: " + ", ".join(known))
|
modules/soothe_poetry.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module 5 — Soothe Corner: a short, language-aware poem for whatever the user is carrying.
|
| 2 |
+
|
| 3 |
+
Flow:
|
| 4 |
+
1. User writes one line about how they feel.
|
| 5 |
+
2. Crisis regex runs first.
|
| 6 |
+
3. Claude returns a 4-6 line poem in the user's language, in native script,
|
| 7 |
+
matched to that language's poetic tradition (sher / haiku / kural / short lyric).
|
| 8 |
+
4. User can regenerate, copy, or render the poem as a simple shareable PNG via Pillow (CPU-only).
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import io
|
| 13 |
+
import logging
|
| 14 |
+
import textwrap
|
| 15 |
+
|
| 16 |
+
import streamlit as st
|
| 17 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
from backend.claude_client import chat
|
| 22 |
+
from backend.i18n import claude_language_name, t
|
| 23 |
+
from backend.safeguards import check_crisis, render_crisis_banner
|
| 24 |
+
|
| 25 |
+
MODULE_NAME = "soothe_poetry"
|
| 26 |
+
FEELING_KEY = "soothe_feeling"
|
| 27 |
+
POEM_KEY = "soothe_poem"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _init_state() -> None:
|
| 31 |
+
if FEELING_KEY not in st.session_state:
|
| 32 |
+
st.session_state[FEELING_KEY] = ""
|
| 33 |
+
if POEM_KEY not in st.session_state:
|
| 34 |
+
st.session_state[POEM_KEY] = ""
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _generate_poem(feeling: str, lang: str) -> str:
|
| 38 |
+
return chat(
|
| 39 |
+
module=MODULE_NAME,
|
| 40 |
+
user_text=feeling,
|
| 41 |
+
language_name=claude_language_name(lang),
|
| 42 |
+
max_tokens=400,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _make_share_card(poem: str) -> bytes:
|
| 47 |
+
"""Render the poem as a simple gradient-free image. CPU-only, no heavy deps."""
|
| 48 |
+
W, H = 1080, 1080
|
| 49 |
+
bg = (245, 240, 255) # soft lavender
|
| 50 |
+
fg = (40, 30, 60)
|
| 51 |
+
img = Image.new("RGB", (W, H), bg)
|
| 52 |
+
draw = ImageDraw.Draw(img)
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
# DejaVu ships with Pillow and supports Latin; other scripts may fall back to boxes
|
| 56 |
+
# when no system font is installed — the poem copy is still readable above the image.
|
| 57 |
+
font_title = ImageFont.truetype("DejaVuSerif.ttf", 52)
|
| 58 |
+
font_body = ImageFont.truetype("DejaVuSerif.ttf", 42)
|
| 59 |
+
font_footer = ImageFont.truetype("DejaVuSans.ttf", 28)
|
| 60 |
+
except Exception:
|
| 61 |
+
font_title = ImageFont.load_default()
|
| 62 |
+
font_body = ImageFont.load_default()
|
| 63 |
+
font_footer = ImageFont.load_default()
|
| 64 |
+
|
| 65 |
+
# Title
|
| 66 |
+
draw.text((80, 90), "Saathi · Soothe Corner", font=font_title, fill=fg)
|
| 67 |
+
|
| 68 |
+
# Poem, wrapped
|
| 69 |
+
lines = []
|
| 70 |
+
for raw in poem.strip().splitlines():
|
| 71 |
+
raw = raw.strip()
|
| 72 |
+
if not raw:
|
| 73 |
+
lines.append("")
|
| 74 |
+
continue
|
| 75 |
+
wrapped = textwrap.wrap(raw, width=32) or [raw]
|
| 76 |
+
lines.extend(wrapped)
|
| 77 |
+
|
| 78 |
+
y = 260
|
| 79 |
+
for line in lines[:18]: # safety cap
|
| 80 |
+
draw.text((80, y), line, font=font_body, fill=fg)
|
| 81 |
+
y += 60
|
| 82 |
+
|
| 83 |
+
draw.text((80, H - 80), "saathi.hf.space", font=font_footer, fill=(120, 110, 140))
|
| 84 |
+
|
| 85 |
+
buf = io.BytesIO()
|
| 86 |
+
img.save(buf, format="PNG")
|
| 87 |
+
return buf.getvalue()
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def render(lang: str) -> None:
|
| 91 |
+
_init_state()
|
| 92 |
+
|
| 93 |
+
st.header(t("soothe_header", lang))
|
| 94 |
+
st.caption(t("soothe_sub", lang))
|
| 95 |
+
|
| 96 |
+
feeling = st.text_input(
|
| 97 |
+
"feeling",
|
| 98 |
+
value=st.session_state[FEELING_KEY],
|
| 99 |
+
placeholder=t("soothe_input_placeholder", lang),
|
| 100 |
+
label_visibility="collapsed",
|
| 101 |
+
key="soothe_feeling_input",
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
col1, col2 = st.columns([1, 1])
|
| 105 |
+
with col1:
|
| 106 |
+
write_clicked = st.button(
|
| 107 |
+
t("soothe_send_button", lang),
|
| 108 |
+
type="primary",
|
| 109 |
+
key="soothe_send_button",
|
| 110 |
+
use_container_width=True,
|
| 111 |
+
)
|
| 112 |
+
with col2:
|
| 113 |
+
regenerate_clicked = st.button(
|
| 114 |
+
t("soothe_regenerate_button", lang),
|
| 115 |
+
key="soothe_regenerate_button",
|
| 116 |
+
use_container_width=True,
|
| 117 |
+
disabled=not st.session_state[POEM_KEY],
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
if write_clicked and feeling.strip():
|
| 121 |
+
st.session_state[FEELING_KEY] = feeling
|
| 122 |
+
if check_crisis(feeling):
|
| 123 |
+
render_crisis_banner(lang)
|
| 124 |
+
return
|
| 125 |
+
with st.spinner("…"):
|
| 126 |
+
try:
|
| 127 |
+
st.session_state[POEM_KEY] = _generate_poem(feeling, lang)
|
| 128 |
+
except Exception as e:
|
| 129 |
+
st.session_state[POEM_KEY] = f"(Could not reach the model right now: {e})"
|
| 130 |
+
|
| 131 |
+
if regenerate_clicked and st.session_state[FEELING_KEY]:
|
| 132 |
+
if check_crisis(st.session_state[FEELING_KEY]):
|
| 133 |
+
render_crisis_banner(lang)
|
| 134 |
+
return
|
| 135 |
+
with st.spinner("…"):
|
| 136 |
+
try:
|
| 137 |
+
st.session_state[POEM_KEY] = _generate_poem(st.session_state[FEELING_KEY], lang)
|
| 138 |
+
except Exception as e:
|
| 139 |
+
st.session_state[POEM_KEY] = f"(Could not reach the model right now: {e})"
|
| 140 |
+
|
| 141 |
+
if st.session_state[POEM_KEY]:
|
| 142 |
+
with st.container(border=True):
|
| 143 |
+
st.markdown(
|
| 144 |
+
f"<div style='font-family: serif; font-size: 1.15rem; line-height: 1.8; "
|
| 145 |
+
f"white-space: pre-wrap;'>{st.session_state[POEM_KEY]}</div>",
|
| 146 |
+
unsafe_allow_html=True,
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
png_bytes = _make_share_card(st.session_state[POEM_KEY])
|
| 151 |
+
st.download_button(
|
| 152 |
+
"⬇️ Download as image",
|
| 153 |
+
data=png_bytes,
|
| 154 |
+
file_name="saathi_poem.png",
|
| 155 |
+
mime="image/png",
|
| 156 |
+
key="soothe_download_button",
|
| 157 |
+
)
|
| 158 |
+
except (OSError, ValueError) as e:
|
| 159 |
+
logger.warning("share card render failed: %s", e, exc_info=True)
|
modules/student_corner.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Module 3 — Student Corner: CBT-informed stakes coach for Indian students.
|
| 2 |
+
|
| 3 |
+
Flow:
|
| 4 |
+
1. Student picks an event type from quick buttons: exam / placement / viva / presentation / result / burnout
|
| 5 |
+
2. Student describes their situation in 1-3 sentences.
|
| 6 |
+
3. Crisis regex runs first.
|
| 7 |
+
4. Claude returns a structured response: acknowledge → distortion scan → 3 evidence-based prep tips →
|
| 8 |
+
what NOT to do → 60-second grounding script → "if things get heavier" signpost.
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import streamlit as st
|
| 13 |
+
|
| 14 |
+
from backend.claude_client import chat
|
| 15 |
+
from backend.i18n import claude_language_name, t
|
| 16 |
+
from backend.safeguards import check_crisis, render_crisis_banner
|
| 17 |
+
|
| 18 |
+
MODULE_NAME = "student_corner"
|
| 19 |
+
EVENT_KEY = "student_event_type"
|
| 20 |
+
SITUATION_KEY = "student_situation"
|
| 21 |
+
RESPONSE_KEY = "student_response"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
EVENT_CHOICES = [
|
| 25 |
+
("exam", "event_exam"),
|
| 26 |
+
("placement_interview", "event_placement"),
|
| 27 |
+
("viva", "event_viva"),
|
| 28 |
+
("presentation", "event_presentation"),
|
| 29 |
+
("result_day", "event_result"),
|
| 30 |
+
("general_burnout", "event_burnout"),
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _init_state() -> None:
|
| 35 |
+
if EVENT_KEY not in st.session_state:
|
| 36 |
+
st.session_state[EVENT_KEY] = "exam"
|
| 37 |
+
if SITUATION_KEY not in st.session_state:
|
| 38 |
+
st.session_state[SITUATION_KEY] = ""
|
| 39 |
+
if RESPONSE_KEY not in st.session_state:
|
| 40 |
+
st.session_state[RESPONSE_KEY] = ""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def render(lang: str) -> None:
|
| 44 |
+
_init_state()
|
| 45 |
+
|
| 46 |
+
st.header(t("student_header", lang))
|
| 47 |
+
st.caption(t("student_sub", lang))
|
| 48 |
+
|
| 49 |
+
# Event picker
|
| 50 |
+
st.markdown(f"**{t('student_event_label', lang)}**")
|
| 51 |
+
labels = [t(label_key, lang) for _, label_key in EVENT_CHOICES]
|
| 52 |
+
event_values = [value for value, _ in EVENT_CHOICES]
|
| 53 |
+
current_index = (
|
| 54 |
+
event_values.index(st.session_state[EVENT_KEY])
|
| 55 |
+
if st.session_state[EVENT_KEY] in event_values
|
| 56 |
+
else 0
|
| 57 |
+
)
|
| 58 |
+
chosen_label = st.radio(
|
| 59 |
+
"event",
|
| 60 |
+
options=labels,
|
| 61 |
+
index=current_index,
|
| 62 |
+
horizontal=True,
|
| 63 |
+
label_visibility="collapsed",
|
| 64 |
+
key="student_event_radio",
|
| 65 |
+
)
|
| 66 |
+
st.session_state[EVENT_KEY] = event_values[labels.index(chosen_label)]
|
| 67 |
+
|
| 68 |
+
# Situation input
|
| 69 |
+
situation = st.text_area(
|
| 70 |
+
"situation",
|
| 71 |
+
value=st.session_state[SITUATION_KEY],
|
| 72 |
+
placeholder=t("student_input_placeholder", lang),
|
| 73 |
+
label_visibility="collapsed",
|
| 74 |
+
height=120,
|
| 75 |
+
key="student_situation_input",
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
if st.button(t("student_send_button", lang), type="primary", key="student_send_button"):
|
| 79 |
+
if situation.strip():
|
| 80 |
+
st.session_state[SITUATION_KEY] = situation
|
| 81 |
+
if check_crisis(situation):
|
| 82 |
+
render_crisis_banner(lang)
|
| 83 |
+
return
|
| 84 |
+
|
| 85 |
+
user_text = (
|
| 86 |
+
f"event_type: {st.session_state[EVENT_KEY]}\n"
|
| 87 |
+
f"situation: {situation}"
|
| 88 |
+
)
|
| 89 |
+
with st.spinner("…"):
|
| 90 |
+
try:
|
| 91 |
+
reply = chat(
|
| 92 |
+
module=MODULE_NAME,
|
| 93 |
+
user_text=user_text,
|
| 94 |
+
language_name=claude_language_name(lang),
|
| 95 |
+
max_tokens=1400,
|
| 96 |
+
)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
reply = (
|
| 99 |
+
"I couldn't reach my language model right now. "
|
| 100 |
+
"For immediate support call **Tele-MANAS on 14416** (Government of India, free, 24×7, 20+ Indian languages) "
|
| 101 |
+
"or **iCall** on 9152987821 (Mon–Sat 8 AM–10 PM).\n\n"
|
| 102 |
+
f"_(Technical detail: {e})_"
|
| 103 |
+
)
|
| 104 |
+
st.session_state[RESPONSE_KEY] = reply
|
| 105 |
+
|
| 106 |
+
if st.session_state[RESPONSE_KEY]:
|
| 107 |
+
st.divider()
|
| 108 |
+
st.markdown(st.session_state[RESPONSE_KEY])
|
packages.txt
ADDED
|
File without changes
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit==1.32.0
|
| 2 |
+
anthropic>=0.40.0
|
| 3 |
+
google-generativeai==0.7.2
|
| 4 |
+
pydantic>=2.6.0
|
| 5 |
+
plotly>=5.19.0
|
| 6 |
+
python-dotenv>=1.0.0
|
| 7 |
+
Pillow>=10.2.0
|
scripts/apptest_render.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Render-time smoke test using Streamlit's AppTest API.
|
| 2 |
+
|
| 3 |
+
This actually executes `app.main()` the way a real Streamlit session would,
|
| 4 |
+
so it catches errors that pure imports miss (missing i18n keys, session_state
|
| 5 |
+
crashes, render-time exceptions in module handlers).
|
| 6 |
+
|
| 7 |
+
We patch out the network-calling chat() function so we don't need an API key.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import sys
|
| 12 |
+
import traceback
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from unittest import mock
|
| 15 |
+
|
| 16 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 17 |
+
sys.path.insert(0, str(ROOT))
|
| 18 |
+
|
| 19 |
+
# Stub chat() before anything in the module tree imports it, so every module
|
| 20 |
+
# that does `from backend.claude_client import chat` gets our safe fake.
|
| 21 |
+
import backend.claude_client as cc
|
| 22 |
+
|
| 23 |
+
def _fake_chat(*args, **kwargs):
|
| 24 |
+
return "(stubbed Claude reply for smoke test)"
|
| 25 |
+
|
| 26 |
+
def _fake_chat_structured(schema, *args, **kwargs):
|
| 27 |
+
# return an empty-ish instance of whatever Pydantic schema is passed
|
| 28 |
+
try:
|
| 29 |
+
return schema()
|
| 30 |
+
except Exception:
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
cc.chat = _fake_chat # type: ignore[assignment]
|
| 34 |
+
if hasattr(cc, "chat_structured"):
|
| 35 |
+
cc.chat_structured = _fake_chat_structured # type: ignore[assignment]
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
from streamlit.testing.v1 import AppTest
|
| 39 |
+
except Exception as e:
|
| 40 |
+
print("AppTest not available:", e)
|
| 41 |
+
sys.exit(2)
|
| 42 |
+
|
| 43 |
+
print("=== Streamlit AppTest render ===")
|
| 44 |
+
failures = []
|
| 45 |
+
|
| 46 |
+
def run_with_lang(lang_code: str, label: str) -> None:
|
| 47 |
+
at = AppTest.from_file(str(ROOT / "app.py"), default_timeout=30)
|
| 48 |
+
# Pre-set the language in session_state so we exercise every label lookup path.
|
| 49 |
+
at.session_state["saathi_language"] = lang_code
|
| 50 |
+
at.run()
|
| 51 |
+
if at.exception:
|
| 52 |
+
print(f" FAIL [{label}] rendered with exceptions:")
|
| 53 |
+
for ex in at.exception:
|
| 54 |
+
print(f" - {ex.value if hasattr(ex, 'value') else ex}")
|
| 55 |
+
failures.append(label)
|
| 56 |
+
else:
|
| 57 |
+
print(f" OK [{label}] rendered clean ({len(at.tabs)} tabs)")
|
| 58 |
+
for i, tab in enumerate(at.tabs):
|
| 59 |
+
# Tab labels are attributes on each Tab node
|
| 60 |
+
label_txt = getattr(tab, "label", f"tab_{i}")
|
| 61 |
+
print(f" - {label_txt}")
|
| 62 |
+
|
| 63 |
+
for code, label in [
|
| 64 |
+
("en", "English"),
|
| 65 |
+
("hi", "Hindi"),
|
| 66 |
+
("bn", "Bengali"),
|
| 67 |
+
("ta", "Tamil"),
|
| 68 |
+
("te", "Telugu"),
|
| 69 |
+
("mr", "Marathi"),
|
| 70 |
+
("ur", "Urdu"),
|
| 71 |
+
]:
|
| 72 |
+
try:
|
| 73 |
+
run_with_lang(code, label)
|
| 74 |
+
except Exception as e:
|
| 75 |
+
print(f" FAIL [{label}] crashed: {e}")
|
| 76 |
+
traceback.print_exc()
|
| 77 |
+
failures.append(label)
|
| 78 |
+
|
| 79 |
+
print()
|
| 80 |
+
if failures:
|
| 81 |
+
print(f"FAIL — {len(failures)} render errors: {failures}")
|
| 82 |
+
sys.exit(1)
|
| 83 |
+
else:
|
| 84 |
+
print("ALL RENDERS PASSED")
|
| 85 |
+
sys.exit(0)
|
scripts/smoke_test.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Smoke test for Saathi — data integrity, imports, and targeted unit checks.
|
| 2 |
+
|
| 3 |
+
Run with: python3 scripts/smoke_test.py
|
| 4 |
+
"""
|
| 5 |
+
from __future__ import annotations
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import sys
|
| 9 |
+
import traceback
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
ROOT = Path(__file__).resolve().parent.parent
|
| 13 |
+
sys.path.insert(0, str(ROOT))
|
| 14 |
+
|
| 15 |
+
failures: list[str] = []
|
| 16 |
+
passes: list[str] = []
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def ok(msg: str) -> None:
|
| 20 |
+
passes.append(msg)
|
| 21 |
+
print(f" OK {msg}")
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def fail(msg: str) -> None:
|
| 25 |
+
failures.append(msg)
|
| 26 |
+
print(f" FAIL {msg}")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def section(title: str) -> None:
|
| 30 |
+
print(f"\n=== {title} ===")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
# 1. Data sanity
|
| 35 |
+
# ---------------------------------------------------------------------------
|
| 36 |
+
section("1. Data files")
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
with open(ROOT / "data/helplines_india.json", encoding="utf-8") as f:
|
| 40 |
+
helplines = json.load(f)
|
| 41 |
+
print(f" helplines_india.json: {len(helplines)} entries")
|
| 42 |
+
|
| 43 |
+
names = [h["name"] for h in helplines]
|
| 44 |
+
if "Tele-MANAS" in names:
|
| 45 |
+
tm = next(h for h in helplines if h["name"] == "Tele-MANAS")
|
| 46 |
+
number = tm["number"]
|
| 47 |
+
alt = tm.get("alt_number", "-")
|
| 48 |
+
has_source = bool(tm.get("source"))
|
| 49 |
+
ok(f"Tele-MANAS present: {number} / {alt} (source={has_source})")
|
| 50 |
+
else:
|
| 51 |
+
fail("Tele-MANAS missing from helplines")
|
| 52 |
+
|
| 53 |
+
if any(n == "KIRAN" for n in names):
|
| 54 |
+
fail("KIRAN still present in helplines (should be removed)")
|
| 55 |
+
else:
|
| 56 |
+
ok("KIRAN absent from helplines")
|
| 57 |
+
|
| 58 |
+
# Substring match — entries are often named "iCall (TISS)" not just "iCall"
|
| 59 |
+
for expected in ["iCall", "Vandrevala", "AASRA"]:
|
| 60 |
+
if any(expected in n for n in names):
|
| 61 |
+
ok(f"{expected} present")
|
| 62 |
+
else:
|
| 63 |
+
fail(f"{expected} missing from helplines")
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
fail(f"helplines_india.json load failed: {e}")
|
| 67 |
+
|
| 68 |
+
try:
|
| 69 |
+
with open(ROOT / "data/ipc_bns_sections.json", encoding="utf-8") as f:
|
| 70 |
+
sections_data = json.load(f)
|
| 71 |
+
print(f" ipc_bns_sections.json: {len(sections_data)} sections")
|
| 72 |
+
|
| 73 |
+
proc = [s for s in sections_data if "procedural_rights" in (s.get("category") or [])]
|
| 74 |
+
false_c = [s for s in sections_data if "false_complaint_warning" in (s.get("category") or [])]
|
| 75 |
+
|
| 76 |
+
if proc:
|
| 77 |
+
ok(f"procedural_rights sections: {len(proc)}")
|
| 78 |
+
for s in proc:
|
| 79 |
+
print(f" - {s.get('act')} {s.get('section')}: {s.get('title')}")
|
| 80 |
+
else:
|
| 81 |
+
fail("no procedural_rights sections")
|
| 82 |
+
|
| 83 |
+
if false_c:
|
| 84 |
+
ok(f"false_complaint_warning sections: {len(false_c)}")
|
| 85 |
+
for s in false_c:
|
| 86 |
+
print(f" - {s.get('act')} {s.get('section')}: {s.get('title')}")
|
| 87 |
+
else:
|
| 88 |
+
fail("no false_complaint_warning sections")
|
| 89 |
+
|
| 90 |
+
except Exception as e:
|
| 91 |
+
fail(f"ipc_bns_sections.json load failed: {e}")
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
with open(ROOT / "data/mental_health_resources.json", encoding="utf-8") as f:
|
| 95 |
+
resources = json.load(f)
|
| 96 |
+
print(f" mental_health_resources.json: {len(resources)} cities")
|
| 97 |
+
if len(resources) >= 15:
|
| 98 |
+
ok(f"{len(resources)} cities available (pitch-ready)")
|
| 99 |
+
else:
|
| 100 |
+
fail(f"only {len(resources)} cities, expected 15+")
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
fail(f"mental_health_resources.json load failed: {e}")
|
| 104 |
+
|
| 105 |
+
# ---------------------------------------------------------------------------
|
| 106 |
+
# 2. Module imports
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
section("2. Module imports (no API calls)")
|
| 109 |
+
|
| 110 |
+
# We need to stub out streamlit + anthropic so imports don't crash when the
|
| 111 |
+
# runtime isn't available. If they're installed, great; otherwise we fall back.
|
| 112 |
+
import importlib.util
|
| 113 |
+
|
| 114 |
+
streamlit_available = importlib.util.find_spec("streamlit") is not None
|
| 115 |
+
anthropic_available = importlib.util.find_spec("anthropic") is not None
|
| 116 |
+
print(f" streamlit installed: {streamlit_available}")
|
| 117 |
+
print(f" anthropic installed: {anthropic_available}")
|
| 118 |
+
|
| 119 |
+
# Always test the pure-Python backend pieces that don't need streamlit
|
| 120 |
+
pure_modules = [
|
| 121 |
+
"backend.safeguards",
|
| 122 |
+
"backend.resources",
|
| 123 |
+
"backend.i18n",
|
| 124 |
+
]
|
| 125 |
+
for name in pure_modules:
|
| 126 |
+
try:
|
| 127 |
+
__import__(name)
|
| 128 |
+
ok(f"import {name}")
|
| 129 |
+
except Exception as e:
|
| 130 |
+
fail(f"import {name}: {e}")
|
| 131 |
+
traceback.print_exc()
|
| 132 |
+
|
| 133 |
+
# Only test streamlit-dependent modules if streamlit is installed
|
| 134 |
+
st_modules = [
|
| 135 |
+
"backend.claude_client",
|
| 136 |
+
"modules.saathi_chat",
|
| 137 |
+
"modules.legal_aid",
|
| 138 |
+
"modules.student_corner",
|
| 139 |
+
"modules.cognitive_journal",
|
| 140 |
+
"modules.soothe_poetry",
|
| 141 |
+
"app",
|
| 142 |
+
]
|
| 143 |
+
if streamlit_available and anthropic_available:
|
| 144 |
+
for name in st_modules:
|
| 145 |
+
try:
|
| 146 |
+
__import__(name)
|
| 147 |
+
ok(f"import {name}")
|
| 148 |
+
except Exception as e:
|
| 149 |
+
fail(f"import {name}: {e}")
|
| 150 |
+
traceback.print_exc()
|
| 151 |
+
else:
|
| 152 |
+
print(" (skipping streamlit/anthropic modules — dependencies not installed)")
|
| 153 |
+
|
| 154 |
+
# ---------------------------------------------------------------------------
|
| 155 |
+
# 3. Safeguards unit tests
|
| 156 |
+
# ---------------------------------------------------------------------------
|
| 157 |
+
section("3. Crisis + active-danger regex")
|
| 158 |
+
|
| 159 |
+
try:
|
| 160 |
+
from backend.safeguards import check_crisis, check_active_danger # type: ignore
|
| 161 |
+
|
| 162 |
+
crisis_positives = [
|
| 163 |
+
"I want to kill myself",
|
| 164 |
+
"khudkushi karna chahta hoon",
|
| 165 |
+
"jaan deni hai mujhe",
|
| 166 |
+
"I cannot live anymore",
|
| 167 |
+
"I can't go on anymore",
|
| 168 |
+
"I'm going to die",
|
| 169 |
+
"there is nothing to live for",
|
| 170 |
+
"I wish I were dead",
|
| 171 |
+
"मुझे जीना नहीं है",
|
| 172 |
+
]
|
| 173 |
+
crisis_negatives = [
|
| 174 |
+
"I'm anxious about exams",
|
| 175 |
+
"my coworker is harassing me",
|
| 176 |
+
"the pressure is heavy but I'm managing",
|
| 177 |
+
"I want to live a more meaningful life",
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
for t in crisis_positives:
|
| 181 |
+
if check_crisis(t):
|
| 182 |
+
ok(f"crisis detected: '{t[:40]}'")
|
| 183 |
+
else:
|
| 184 |
+
fail(f"crisis MISSED: '{t}'")
|
| 185 |
+
|
| 186 |
+
for t in crisis_negatives:
|
| 187 |
+
if not check_crisis(t):
|
| 188 |
+
ok(f"crisis not triggered: '{t[:40]}'")
|
| 189 |
+
else:
|
| 190 |
+
fail(f"crisis FALSE POSITIVE: '{t}'")
|
| 191 |
+
|
| 192 |
+
danger_positives = [
|
| 193 |
+
"he has a knife at my throat",
|
| 194 |
+
"my husband is hitting me right now",
|
| 195 |
+
"someone is outside my door with a weapon",
|
| 196 |
+
"he is beating me",
|
| 197 |
+
]
|
| 198 |
+
danger_negatives = [
|
| 199 |
+
"my coworker harassed me last week",
|
| 200 |
+
"I am afraid of my neighbour",
|
| 201 |
+
"my ex sends me rude messages",
|
| 202 |
+
"he is here for the meeting",
|
| 203 |
+
"they are here to help",
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
for t in danger_positives:
|
| 207 |
+
if check_active_danger(t):
|
| 208 |
+
ok(f"active danger: '{t[:40]}'")
|
| 209 |
+
else:
|
| 210 |
+
fail(f"active danger MISSED: '{t}'")
|
| 211 |
+
|
| 212 |
+
for t in danger_negatives:
|
| 213 |
+
if not check_active_danger(t):
|
| 214 |
+
ok(f"active danger not triggered: '{t[:40]}'")
|
| 215 |
+
else:
|
| 216 |
+
fail(f"active danger FALSE POSITIVE: '{t}'")
|
| 217 |
+
|
| 218 |
+
except Exception as e:
|
| 219 |
+
fail(f"safeguards unit tests crashed: {e}")
|
| 220 |
+
traceback.print_exc()
|
| 221 |
+
|
| 222 |
+
# ---------------------------------------------------------------------------
|
| 223 |
+
# 4. Classifier unit tests (the substring bug we fixed)
|
| 224 |
+
# ---------------------------------------------------------------------------
|
| 225 |
+
section("4. Legal classifier regex (word-boundary safety)")
|
| 226 |
+
|
| 227 |
+
try:
|
| 228 |
+
from backend.resources import classify_legal_situation_keywords # type: ignore
|
| 229 |
+
|
| 230 |
+
classifier_cases = [
|
| 231 |
+
# text, expected_category_in, must_not_be
|
| 232 |
+
("he raped me", {"sexual_violence"}, "sexual_harassment"),
|
| 233 |
+
("my ex is threatening me over the phone", {"criminal_intimidation", "stalking", "other"}, "workplace_harassment"),
|
| 234 |
+
("my neighbour is stalking me and sending creepy messages", {"stalking", "cyber_harassment"}, None),
|
| 235 |
+
("my manager at the HR department is harassing me", {"workplace_harassment"}, None),
|
| 236 |
+
("my husband hits me and I want to leave", {"domestic_violence"}, None),
|
| 237 |
+
("someone leaked my private photos online", {"cyber_harassment"}, None),
|
| 238 |
+
("random walk in the park was nice", {"other"}, None),
|
| 239 |
+
]
|
| 240 |
+
|
| 241 |
+
for text, expected_set, must_not_be in classifier_cases:
|
| 242 |
+
result = classify_legal_situation_keywords(text)
|
| 243 |
+
if result in expected_set:
|
| 244 |
+
ok(f"classifier: '{text[:45]}' → {result}")
|
| 245 |
+
else:
|
| 246 |
+
fail(f"classifier: '{text}' → {result}, expected one of {expected_set}")
|
| 247 |
+
if must_not_be and result == must_not_be:
|
| 248 |
+
fail(f"classifier regression: '{text}' matched banned category {must_not_be}")
|
| 249 |
+
|
| 250 |
+
except Exception as e:
|
| 251 |
+
fail(f"classifier unit tests crashed: {e}")
|
| 252 |
+
traceback.print_exc()
|
| 253 |
+
|
| 254 |
+
# ---------------------------------------------------------------------------
|
| 255 |
+
# 5. i18n key coverage
|
| 256 |
+
# ---------------------------------------------------------------------------
|
| 257 |
+
section("5. i18n key coverage (en + hi)")
|
| 258 |
+
|
| 259 |
+
try:
|
| 260 |
+
from backend.i18n import t # type: ignore
|
| 261 |
+
|
| 262 |
+
required_keys = [
|
| 263 |
+
"source_label",
|
| 264 |
+
"legal_other_title",
|
| 265 |
+
"legal_other_body",
|
| 266 |
+
"legal_other_nalsa",
|
| 267 |
+
"legal_other_police",
|
| 268 |
+
"legal_procedural_heading",
|
| 269 |
+
"legal_false_complaint_warning_heading",
|
| 270 |
+
"legal_active_danger_title",
|
| 271 |
+
"legal_active_danger_body",
|
| 272 |
+
"legal_active_danger_after",
|
| 273 |
+
]
|
| 274 |
+
for key in required_keys:
|
| 275 |
+
en = t(key, "en")
|
| 276 |
+
hi = t(key, "hi")
|
| 277 |
+
if en and en != key:
|
| 278 |
+
ok(f"en.{key}: {en[:50]}")
|
| 279 |
+
else:
|
| 280 |
+
fail(f"en.{key}: missing")
|
| 281 |
+
if hi and hi != key:
|
| 282 |
+
ok(f"hi.{key}: {hi[:50]}")
|
| 283 |
+
else:
|
| 284 |
+
fail(f"hi.{key}: missing (falls back to en)")
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
fail(f"i18n tests crashed: {e}")
|
| 288 |
+
traceback.print_exc()
|
| 289 |
+
|
| 290 |
+
# ---------------------------------------------------------------------------
|
| 291 |
+
# Result
|
| 292 |
+
# ---------------------------------------------------------------------------
|
| 293 |
+
print("\n" + "=" * 60)
|
| 294 |
+
print(f"PASS: {len(passes)}")
|
| 295 |
+
print(f"FAIL: {len(failures)}")
|
| 296 |
+
if failures:
|
| 297 |
+
print("\nFailures:")
|
| 298 |
+
for f in failures:
|
| 299 |
+
print(f" - {f}")
|
| 300 |
+
sys.exit(1)
|
| 301 |
+
else:
|
| 302 |
+
print("\nALL SMOKE CHECKS PASSED")
|
| 303 |
+
sys.exit(0)
|