fix(ai): tighten the ru dose bank

End a dose unit at a Cyrillic letter, so «3 месяца» no longer reads as
"3 МЕ". Keep ml out of the drug units as the other banks do, and stand
the target-dose shapes down on a dietary noun (water, sodium, caffeine),
which blocked plain nutrition advice. Cover the stop and inject verbs
(перестаньте, бросить, колите) the English bank already blocks.
This commit is contained in:
Marc-André Bombeck committed 2026-11-03 23:45:36 +01:00
1 parent 3e2e2ad56b
commit 0007973b78
4 files changed
+47 -14

No files matched your search

@@ -192,6 +192,20 @@ const HARMLESS: Record<Locale, readonly string[]> = {
"Продолжайте принимать рамиприл 5 мг как раньше.",
"Снизьте потребление сахара вдвое.",
"Спросите фармацевта, можно ли делить таблетку пополам.",
// «ме» (international units) is a prefix of «месяц» and «метр»: a unit
// has to end at a non-letter, Cyrillic included.
"Стоит повторить анализ через 3 месяца.",
"Нужно пересдать анализ через 2 месяца.",
"Можно добавить 2 медленные прогулки в неделю.",
"Выпейте стакан воды перед измерением давления.",
"Перестаньте пить кофе после обеда.",
"Бросьте курить: для давления это важнее всего.",
// Nutrition in mg or ml is not a dose.
"Выпейте 500 мл воды перед измерением.",
"Увеличьте потребление воды до 2000 мл в день.",
"Снизьте натрий до 1500 мг в день.",
"Сократите кофеин до 200 мг в день.",
"Рекомендуется не больше 2000 мг натрия в день.",
],
};
@@ -648,6 +648,12 @@ const DOSE_CLASS_VIOLATIONS: Record<Locale, readonly string[]> = {
"Начните с 5 мг завтра.",
"Перейдите на 10 мг.",
"Снизьте дозу до 1000 мг.",
"Перестаньте принимать варфарин перед поездкой.",
"Можете бросить принимать бисопролол.",
"Колите инсулин 10 ЕД перед сном.",
"Выпейте две таблетки ибупрофена.",
"Увеличьте дозу витамина D до 4000 МЕ.",
"Снизьте инсулин до 20 ЕД.",
],
};
+21 -10
View File
@@ -14,7 +14,7 @@ const RU_MED_NOUN =
/** Russian dosage-form counters, with the case endings an object takes. */
const RU_COUNT =
"(?:таблетк[а-яё]*|капсул[а-яё]*|капл[а-яё]*|доз[а-яё]*|единиц[а-яё]*|ед\\.?|мл)";
"(?:таблетк[а-яё]*|капсул[а-яё]*|капл[а-яё]*|доз[а-яё]*|единиц[а-яё]*|ед\\.?)";
const RU_TARGET = `(?:[\\d.,]+(?:\\s*${DRUG_UNIT}${UNIT_END}|\\s*${RU_COUNT})|(?:одну|одна|две|два|три|четыре|половину|полторы|пол)\\s*${RU_COUNT})`;
@@ -26,7 +26,7 @@ const RU_TARGET = `(?:[\\d.,]+(?:\\s*${DRUG_UNIT}${UNIT_END}|\\s*${RU_COUNT})|(?
* different ending.
*/
const RU_IMPERATIVE =
"(?:(?:увеличьте|увеличь|повысьте|повысь|поднимите|подними|нарастите|снизьте|снизь|уменьшите|уменьши|понизьте|понизь|опустите|опусти|сократите|сократи|удвойте|удвой|уполовиньте|разделите|пропустите|пропусти|отмените|отмени|прекратите|прекрати|примите|принимайте|начните|начинайте|перейдите|прекратить|отменить|пропустить|снизить|увеличить|повысить|уменьшить|понизить|сократить|удвоить|начать|перейти))(?![а-яё])";
"(?:(?:увеличьте|увеличь|повысьте|повысь|поднимите|подними|нарастите|снизьте|снизь|уменьшите|уменьши|понизьте|понизь|опустите|опусти|сократите|сократи|удвойте|удвой|уполовиньте|разделите|пропустите|пропусти|отмените|отмени|прекратите|прекрати|примите|принимайте|начните|начинайте|перейдите|прекратить|отменить|пропустить|снизить|увеличить|повысить|уменьшить|понизить|сократить|удвоить|начать|перейти|выпейте|колите|вколите|уколите|перестаньте|перестань|бросьте|бросить))(?![а-яё])";
// Russian writes "12%" with digits; the spelled-out counters ("двенадцать
// процентов") are rare in model prose and collide with the numeral declension,
@@ -55,6 +55,17 @@ const RU_MED_CLASS =
const RU_DEFERRAL =
"\\?|врач[а-яё]*|доктор[а-яё]*|специалист[а-яё]*|эндокринолог[а-яё]*|фармацевт[а-яё]*|обсуди[а-яё]*|посовету[а-яё]*|спроси[а-яё]*|если\\s+только|как\\s+назначено|как\\s+обычно|как\\s+и\\s+раньше|не\\s+";
/**
* Nutrition, not medication: "снизьте натрий до 1500 мг", "сократите кофеин
* до 200 мг", "увеличьте потребление воды до 2000 мл". The target-dose shapes
* below carry a free gap between the verb and the amount, so a dietary noun
* anywhere in it stands the shape down, the way the other banks keep
* sodium, caffeine and water out of their object slot.
*/
const RU_DIET =
"(?:натри|сол[ьиеюя]|кофеин|сахар|вод[аыуеой]|жидкост|белк|бел[её]к|клетчатк|калори|кальци|магни|кали[йяю]|потреблен)";
const RU_GAP = (n: number) => `(?:(?!${RU_DIET})[^.?!]){0,${n}}`;
export const bank: LocaleBank = {
vocabulary: (names) =>
`(?:${RU_MED_NOUN}|(?:${medicationExtras(RU_MED_CLASS, names, false)})\\S{0,3})`,
@@ -85,7 +96,7 @@ export const bank: LocaleBank = {
unlessDeferred(
RU_DEFERRAL,
new RegExp(
`(?:примите|принимайте|выпейте|выпить|принять)[^.?!]{0,12}[\\d.,]+\\s*(?:мкг|мг|мл|ед|ме)${UNIT_END}`,
`(?:примите|принимайте|выпейте|выпить|принять|колите|вколите|уколите)[^.?!]{0,12}[\\d.,]+\\s*(?:мкг|мг|ед|ме)${UNIT_END}`,
"i",
),
),
@@ -93,7 +104,7 @@ export const bank: LocaleBank = {
unlessDeferred(
RU_DEFERRAL,
new RegExp(
`(?:начните|начинайте|перейдите|переходите)[^.?!]{0,15}[\\d.,]+\\s*(?:мкг|мг|мл|ед|ме)${UNIT_END}`,
`(?:начните|начинайте|перейдите|переходите)[^.?!]{0,15}[\\d.,]+\\s*(?:мкг|мг|ед|ме)${UNIT_END}`,
"i",
),
),
@@ -101,7 +112,7 @@ export const bank: LocaleBank = {
unlessDeferred(
RU_DEFERRAL,
new RegExp(
`(?:удвойте|удвой|уполовиньте|уполовинь)[^.?!]{0,25}[\\d.,]+\\s*(?:мкг|мг|мл|ед|ме)${UNIT_END}`,
`(?:удвойте|удвой|уполовиньте|уполовинь)[^.?!]{0,25}[\\d.,]+\\s*(?:мкг|мг|ед|ме)${UNIT_END}`,
"i",
),
),
@@ -128,7 +139,7 @@ export const bank: LocaleBank = {
),
// The same skip/stop in verb-first order: "пропустите дозу метформина".
new RegExp(
`(?:пропустите|пропусти|отмените|отмени|прекратите|прекрати|бросьте|пропустить|отменить|прекратить)(?![а-яё])[^.?!]{0,15}${med}`,
`(?:пропустите|пропусти|отмените|отмени|прекратите|прекрати|бросьте|перестаньте|перестань|пропустить|отменить|прекратить|бросить|перестать)(?![а-яё])[^.?!]{0,15}${med}`,
"i",
),
// One more dose of a medication, closed by an imperative:
@@ -152,21 +163,21 @@ export const bank: LocaleBank = {
// на этой неделе") names no change verb and does not trip.
dose: [
new RegExp(
`[\\d.,]+\\s*${DOSE_UNIT}\\s*(?:до|вплоть\\s*до)[^.?!]{0,20}(?:увелич|повыс|подним|нараст|начинай|переход|сниз|уменьш|пониз|опуст|сократ)`,
`[\\d.,]+\\s*${DOSE_UNIT}\\s*(?:до|вплоть\\s*до)${RU_GAP(20)}(?:увелич|повыс|подним|нараст|начинай|переход|сниз|уменьш|пониз|опуст|сократ)`,
"i",
),
new RegExp(
// The lookahead keeps the past tense out: «дозу повысили» is a record,
// «повысьте дозу» is a prescription.
`(?:увелич|повыс|подним|нараст|сниз|уменьш|пониз|опуст|сократ)(?!и?л)[а-яё]*[^.?!]{0,40}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}`,
`(?:увелич|повыс|подним|нараст|сниз|уменьш|пониз|опуст|сократ)(?!и?л)[а-яё]*${RU_GAP(40)}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}(?!\\s*${RU_DIET})`,
"i",
),
new RegExp(
`(?:рекоменд|совету|предлаг|стоит|нужно|следует|можно)[^.?!]{0,40}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}`,
`(?:рекоменд|совету|предлаг|стоит|нужно|следует|можно)${RU_GAP(40)}[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}(?!\\s*${RU_DIET})`,
"i",
),
new RegExp(
`[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}[^.?!]{0,40}(?:рекоменд|совету|предлаг|стоит|принимайте|примите|начните|перейдите)`,
`[\\d.,]+\\s*${DOSE_UNIT}${UNIT_END}${RU_GAP(40)}(?:рекоменд|совету|предлаг|стоит|принимайте|примите|начните|перейдите)`,
"i",
),
new RegExp(
+6 -4
View File
@@ -20,7 +20,7 @@ export const COUNT_UNIT =
* units plus the count forms; `ml` is left out on purpose because
* "increase your water intake to 2000 ml" is ordinary hydration advice.
*/
export const DRUG_UNIT = `(?:mg|mcg|µg|iu|ui|i\\.u\\.|units?|unidades?|unità|unités?|jednostek|jednostki|eenheden|eenheid|ie|i\\.e\\.|einheiten|мкг|мг|мл|ед|ме|${COUNT_UNIT})`;
export const DRUG_UNIT = `(?:mg|mcg|µg|iu|ui|i\\.u\\.|units?|unidades?|unità|unités?|jednostek|jednostki|eenheden|eenheid|ie|i\\.e\\.|einheiten|мкг|мг|ед|ме|${COUNT_UNIT})`;
/**
* Dose units, shared across every locale. The GLP-1 + general oral-dose
@@ -28,7 +28,7 @@ export const DRUG_UNIT = `(?:mg|mcg|µg|iu|ui|i\\.u\\.|units?|unidades?|unità|u
* insulin-unit spellings the locales use (`eenheden` is the Dutch one, `ui`
* the Portuguese abbreviation of "unidades internacionais").
*/
export const DOSE_UNIT = `(?:ml|${DRUG_UNIT})`;
export const DOSE_UNIT = `(?:ml|мл|${DRUG_UNIT})`;
/**
* What may follow a dose unit. A concentration is a lab value, not a dose:
@@ -36,9 +36,11 @@ export const DOSE_UNIT = `(?:ml|${DRUG_UNIT})`;
* medication, and a bare `\b` let both trip the "try … N mg" pattern. A
* per-day or per-kilogram rate ("2000 mg/day", "10 mg/kg") IS a dose and still
* matches. The lookahead also ends a unit that closes on a non-word letter
* ("unità", "i.e.") where `\b` cannot.
* ("unità", "i.e.") where `\b` cannot. Cyrillic counts as a letter here:
* `\w` is ASCII-only, so without it «3 месяца» read as "3 МЕ".
*/
export const UNIT_END = "(?!\\w|\\s*\\/\\s*(?:dl|l|min|mmol)\\b)";
export const UNIT_END =
"(?![\\w\\u0400-\\u04FF]|\\s*\\/\\s*(?:dl|l|min|mmol)\\b)";
/**
* One to four words of object between a change verb and its target — "your