Inspiration
A school counsellor has a caseload and no way to see what is arriving. Students who are being bullied mostly do not report it, and the ones who do report it late, quietly, and to whoever is easiest to talk to. Three things then go wrong at once: the quiet cases never surface, because a student describing months of exclusion does not use the words that trigger anyone's attention; nobody can see across conversations, so four students describing the same boy and the same stairwell are four separate conversations with four different adults; and any tool that helps with this has to be trusted by a twelve-year-old, which rules out anything that starts by asking for a name.
Most "AI mental health" projects put a language model in front of a child and hope. We wanted to build the opposite, and to be able to prove it was the opposite.
## What it does
Lighthouse is an anonymous chat helpline for students where the classifier decides and the LLM only explains.
- Anonymous intake. A student picks a handle. No account, no email, no name.
- A deterministic safety gate, upstream of everything. 66 graded patterns and 21 suppressors across six categories, 123 microseconds per turn. It emits floors and ceilings, never a decision. On a T4 hit, real 24/7 crisis lines render before the language model is contacted, and still render if that call fails, times out, or refuses.
- A fine-tuned risk classifier. DistilBERT over 45,286 turns, temperature-scaled, feeding a 38-feature conversation head that assigns T0 to T4.
- Escalation cards that cite evidence. At most three verbatim quotes sliced by offset from the student's own words, plus gate indicators, a risk timeline, and reasons drawn from a closed template bank a human wrote in advance.
- A counsellor console with a real audit trail. The queue ranks by gate floor first and model score only as a tie-break. Reading the transcript is required before overriding a tier. Every access is logged, and the student can read the same log.
- Cross-conversation pattern detection. Keyed-HMAC pseudonymised entities plus BM25 and character trigrams surface "4 separate reports, naming the same location, naming the same person, within 9 days" without the database ever holding a name.
Pull the API key and triage still works. That is the property that turns a chatbot into a system a school could be responsible for.
## How we built it
Python for everything that decides: the gate, training, calibration, features, the conversation head, the card builder, the clustering, and a FastAPI scoring service on a Hugging Face Space. Next.js 16 / React 19 / TypeScript for the student chat and the counsellor console, on Neon serverless Postgres via Drizzle, deployed on Vercel. The Anthropic SDK writes chat replies and does offline entity extraction, and nothing else.
The one deliberate duplication is the safety gate, which exists in both runtimes. It has to render crisis numbers while the Python service is asleep, so it cannot live behind a network call. Two test suites hold the copies identical: 354 verdicts diffed across runtimes, and all 87 regex sources compared character by character.
Training ran on a free Colab T4 in 23 minutes, after length-grouped batching took padding efficiency from 0.363 to 0.976.
## What we learned
The victim-voice gap reshaped the project. Every public dataset available is perpetrator voice: Jigsaw comments are the abuse, r/SuicideWatch posts are first-person distress. This product receives neither. It receives a student describing what is happening to them. Measured on our own checkpoint, the same harm in two voices:
| | risk | |---|---| | "nobody wants to talk to you, you freak" | 0.964 | | "nobody in my class has spoken to me in about two weeks" | 0.021 |
A 45x gap. It is why turn features alone score 0.214 macro-F1 on the conversation head, and why the conversation-level features exist at all.
Calibration matters more than accuracy when a queue is sorted by a number. Temperature scaling, $\mathrm{softmax}(z/T)$ with $T = 1.342$, took test ECE from $0.0699$ to $0.0263$ without moving a single argmax. Same predictions, honest confidence.
The gate earns its keep, measurably. The conversation head misses three of sixteen T4 conversations. The gate catches all three:
| conversation tiering | head alone | post-gate, what a counsellor sees | |---|---|---| | macro-F1 | 0.631 | 0.714 | | T4 recall | 0.812 | 1.000 |
## Challenges
The model refuses exactly the content this product exists for. Our entire input distribution is self-harm, abuse and bullying disclosure, which is what content classifiers decline. A refusal is an HTTP 200 with empty content, so it looks like a silent bug at the worst possible moment. It is handled twice: declined requests route to a fallback model, and if the whole chain declines, a scripted responder answers. No path produces silence.
An escape hatch had to exist without becoming a back door. A gate tuned to miss nothing will floor cases it should not, and a counsellor facing false T4s learns to distrust the tier. So overrides are recorded, not applied: a counsellor who marks a T4 self-harm disclosure as T1 gets their reason logged and the case still routes at T4. Closing a case below the floor requires break-glass, a 40-character justification, and review by a different person, because a second pair of eyes belonging to the same person is not a second pair of eyes.
A bug worth remembering. For one day the gate floor was inferred from a field that is null whenever the model already agrees with the gate, so on exactly the cases where the floor mattered most there appeared to be no floor. The floor is now passed explicitly and is a required field, so omitting it does not compile.
## What is honest about this
Recall at a counsellor's weekly budget is 0.865 against our 0.90 target. That is a miss, and we know why: the victim-voice gap above is not closable with more features, it needs victim-voice training data that does not currently exist publicly.
All conversation data shown is synthetic. Every conversation in the demo and in the corpus was written for this project. No real student data was used, and none should be until this has been reviewed by people qualified to do so.
This is a listening and routing tool. It is not therapy, it is not a counsellor, and it does not diagnose anything.
Every number in there is verified against the repo: 1,570 tests, ECE 0.0699 → 0.0263, T4 recall 1.000, recall@budget 0.865, 66 patterns, 21 suppressors, 123 µs.
Built With
- anthropic-claude
- distilbert
- drizzle-orm
- fastapi
- gradio
- huggingface
- neon
- next.js
- postgresql
- python
- pytorch
- react
- scikit-learn
- tailwindcss
- transformers
- typescript
- uv
- vercel
- vitest
Log in or sign up for Devpost to join the conversation.