<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pranav Ajay</title>
    <description>The latest articles on DEV Community by Pranav Ajay (@pranavajay).</description>
    <link>https://dev.to/pranavajay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4245493%2F71c93445-6912-4c2b-abaa-5b87a7dd0a70.png</url>
      <title>DEV Community: Pranav Ajay</title>
      <link>https://dev.to/pranavajay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranavajay"/>
    <language>en</language>
    <item>
      <title>Keyword rules missed 36 of 45 emergencies: how we layer safety checks in a multilingual health chatbot</title>
      <dc:creator>Pranav Ajay</dc:creator>
      <pubDate>Sun, 15 Nov 2026 06:42:44 +0000</pubDate>
      <link>https://dev.to/pranavajay/keyword-rules-missed-36-of-45-emergencies-how-we-layer-safety-checks-in-a-multilingual-health-2h8p</link>
      <guid>https://dev.to/pranavajay/keyword-rules-missed-36-of-45-emergencies-how-we-layer-safety-checks-in-a-multilingual-health-2h8p</guid>
      <description>&lt;p&gt;If you build a chatbot that people ask about their health, some of those people will be having an emergency while they type. Someone with crushing chest pain will sometimes open a chat window before they call anyone. A parent will type "my son swallowed a button battery" and wait for an answer.&lt;/p&gt;

&lt;p&gt;When we started building &lt;a href="https://www.saluvo.ai" rel="noopener noreferrer"&gt;Saluvo&lt;/a&gt;, an AI health companion that answers in 80+ languages, we made one rule before anything else: &lt;strong&gt;the emergency warning must never depend on the model.&lt;/strong&gt; This post is about how we tried to do that, where our first version failed, and what we ship now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule one: check before the model, not after
&lt;/h2&gt;

&lt;p&gt;The tempting design is to put "if this is an emergency, tell the user to call for help" in the system prompt and trust the model. It works most of the time. "Most of the time" is the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Models drift with prompt changes. A tweak that improves tone can quietly weaken the emergency instruction.&lt;/li&gt;
&lt;li&gt;Streaming answers start with whatever the model writes first. If it opens with three paragraphs of context, the important line arrives late, or after the person has stopped reading.&lt;/li&gt;
&lt;li&gt;If the model call fails, times out or hits a rate limit, there is no answer at all, and no warning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first check runs &lt;strong&gt;before&lt;/strong&gt; any model call, in plain code. It returns a red flag (emergency or mental health) and a reason, and the UI shows a banner with the local emergency number. The model still answers underneath, but the banner doesn't wait for it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;RedFlag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"emergency"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s2"&gt;"mental-health"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectRedFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;RedFlag&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stripBenignPhrases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// "food poisoning", "stroke risk"&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;mentalHealthFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RULES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;      &lt;span class="c1"&gt;// so the right helpline shows&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;patterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;matchesUnnegated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Simplified; the real one also handles phrase lists for languages that don't suit regex.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The boring parts that matter
&lt;/h2&gt;

&lt;p&gt;A keyword check sounds trivial. Three details made it usable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Benign phrases are blanked out first.&lt;/strong&gt; "Food poisoning", "heat stroke", "how to lower my stroke risk", "swimming stroke". Without this, the banner fires on half of all health questions, and people learn to ignore it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Simple negation.&lt;/strong&gt; "No chest pain, just a cough" shouldn't trigger the chest-pain rule. We look at a short window before each match for words like &lt;em&gt;no&lt;/em&gt;, &lt;em&gt;not&lt;/em&gt;, &lt;em&gt;without&lt;/em&gt;, &lt;em&gt;denies&lt;/em&gt;. This is English-only and crude, and we accept that: a false banner is far cheaper than a missed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Normalise what phones type differently.&lt;/strong&gt; Curly apostrophes, case, Arabic versus Persian forms of the same letter, the zero-width joiner in Persian. If you don't fold these, the same word typed on two keyboards matches once and misses once.&lt;/p&gt;

&lt;p&gt;And for Hindi speakers, a lot of real input is Hinglish in Latin script: &lt;em&gt;"seene mein dard"&lt;/em&gt;, &lt;em&gt;"saans lene mein dikkat"&lt;/em&gt;. Those need their own patterns next to the Devanagari ones. If you serve India, test with how people actually type, not with textbook Hindi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it failed: 36 of 45
&lt;/h2&gt;

&lt;p&gt;We wrote a set of 45 hard test phrases: indirect descriptions, other languages, the way a worried relative would type rather than the way a textbook would. One of them was "my son swallowed a button battery".&lt;/p&gt;

&lt;p&gt;The keyword check &lt;strong&gt;missed 36 of them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That wasn't a bug; it's what keyword lists are. They know set phrases. People in a crisis don't use set phrases, and a list for one language doesn't transfer to 80.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer two: a small, fast model as a net, never a gate
&lt;/h2&gt;

&lt;p&gt;So we added a second check that every channel shares (web chat, WhatsApp and voice): a small, fast model reads what the person said, in any language, and returns strict JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;"danger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"none | emergency | mental-health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"a few English words"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"the same words in the person's language"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"ISO 639-1 code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"handled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design choices that matter more than the prompt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It can only add a warning, never remove one.&lt;/strong&gt; If the keyword check fired, it stands, whatever the model says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed or slow check counts as "nothing found",&lt;/strong&gt; with a short timeout. It must never be the reason someone gets no answer. The keyword layer is still there underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reassurance from someone else doesn't lower urgency.&lt;/strong&gt; The prompt says so explicitly: "my husband says it's only gas" is not a reason to downgrade chest pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the person typed is data, not instructions.&lt;/strong&gt; It's a classifier reading untrusted text; treat it like one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"handled"&lt;/strong&gt; tells us whether the main answer already told them to get urgent help, so on text channels we can add the safety line when it didn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The check also returns the reason in the person's own language, so a Tamil speaker gets a Tamil warning, not an English one with a Tamil answer below it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mental health is not "another emergency"
&lt;/h2&gt;

&lt;p&gt;Two things we treat separately:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mental-health rules run first, so someone who mentions both pain and self-harm sees the crisis line, not only the ambulance number.&lt;/li&gt;
&lt;li&gt;The tone is different. Warmth first, the crisis line second, never a lecture. We list a crisis line for the countries where we have one, and a "find a free helpline near you" link everywhere else.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pick the number by country, and say when you don't know
&lt;/h2&gt;

&lt;p&gt;Showing "call 911" to someone in India is worse than useless. We map the country (from the request headers) to its emergency and ambulance numbers. When we don't know the country, the copy says &lt;em&gt;"or your local emergency number"&lt;/em&gt; rather than guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell another team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Put the emergency check &lt;strong&gt;outside&lt;/strong&gt; the model and &lt;strong&gt;before&lt;/strong&gt; it. Then add a model check as a net, not a gate.&lt;/li&gt;
&lt;li&gt;Write a hard test set before you write rules, and include how people really type in each language.&lt;/li&gt;
&lt;li&gt;Measure misses, not just false alarms. A banner that fires too often is annoying. A banner that doesn't fire is the failure that matters.&lt;/li&gt;
&lt;li&gt;Your rules are a first draft until a clinician reviews them. Ours are a first version too: they still need that review, and more testing in both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes a chatbot a doctor, and it shouldn't pretend to be one. But if people are going to ask an AI about their health at 2 a.m. anyway, the least we can do is make sure "call now" is the first thing they see when it matters.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I'm Pranav, co-founder of Saluvo. If you're working on safety in health or crisis-adjacent AI, I'd like to compare notes: we've written more about &lt;a href="https://www.saluvo.ai/en/blog/ai-symptom-checkers-accuracy-and-safe-use" rel="noopener noreferrer"&gt;how to use AI symptom checkers safely&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>healthtech</category>
      <category>safety</category>
    </item>
  </channel>
</rss>
