fix(bot): сторож ловит жёсткий отказ в замене в любой форме (живой баг 13.07)
This commit is contained in:
@@ -153,6 +153,7 @@ class AnswerGuard
|
||||
if ($this->isBanned($sentence)
|
||||
|| $this->priceTiedToNiche($sentence)
|
||||
|| $this->promisesReplacement($sentence)
|
||||
|| $this->refusesReplacementHarshly($sentence)
|
||||
|| $this->promisesSupportSla($sentence)
|
||||
|| $this->promisesNicheRelevance($sentence)
|
||||
|| $this->hasFactMismatch($sentence, $facts)
|
||||
@@ -218,6 +219,39 @@ class AnswerGuard
|
||||
return $promise && $object && ! $denial;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 ЖИВОЙ БАГ 13.07.2026: жёсткий отказ в замене/возврате в ЛЮБОЙ формулировке — не
|
||||
* только «не полагается» (единственное число). Прогон поймал бота на «Замены за
|
||||
* недозвоны НЕ ПОЛАГАЮТСЯ» (множественное число): старое BANNED-правило (см. константу
|
||||
* выше) знало только форму единственного числа и требовало ≤30 символов между словами —
|
||||
* «Замены ЗА НЕДОЗВОНЫ не полагаются» уже не влезало. Урок проекта: список формулировок
|
||||
* проигрывает гонку. Здесь — правило ПО СМЫСЛУ, как у promisesReplacement/priceTiedToNiche
|
||||
* и соседей: предмет (замена/возврат/компенсация/деньги) + отказ в любой форме, без
|
||||
* ограничения на расстояние (предмет и отказ ищутся независимо — по всему предложению),
|
||||
* и с исключением «мягкого пути» (support@/специалист/индивидуально), который снимает
|
||||
* тон рубки дверью и должен выживать.
|
||||
*
|
||||
* НЕ ловим голые «нет»/«нельзя»/«отказ» — они слишком частые и без тесной привязки к
|
||||
* предмету дают ложные срабатывания («денег на балансе нет» — про пустой баланс, не
|
||||
* про отказ в возврате). Такие короткие сентенции уже режутся через promisesReplacement
|
||||
* или старые BANNED-паттерны, где нужно.
|
||||
*/
|
||||
private function refusesReplacementHarshly(string $sentence): bool
|
||||
{
|
||||
$subject = preg_match('/(замен\w*|возврат\w*|компенсаци\w*|возмещ\w*|деньг\w*|средств\w*)/iu', $sentence) === 1;
|
||||
$refusal = preg_match(
|
||||
'/(не\s+полага\w+|не\s+положен\w*|не\s+предусмотрен\w*|невозможн\w*|не\s+делаем'
|
||||
.'|не\s+возвраща\w*|не\s+верн[ёе]м|не\s+меня\w*)/iu',
|
||||
$sentence
|
||||
) === 1;
|
||||
$softPath = preg_match(
|
||||
'/(support@liderra\.ru|специалист\w*\s+разбер\w*|индивидуальн\w*|напишите\s+нам)/iu',
|
||||
$sentence
|
||||
) === 1;
|
||||
|
||||
return $subject && $refusal && ! $softPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обещанный СРОК СВЯЗИ («перезвонят в течение рабочего дня», «ответим за пару часов»).
|
||||
* Поддержка круглосуточная (podderzhka.md), никакого «рабочего дня» у нас нет — это
|
||||
|
||||
@@ -184,3 +184,181 @@ it('без карточки: число ИЗ статьи (обычная, не
|
||||
|
||||
expect($out)->toContain('300');
|
||||
});
|
||||
|
||||
/**
|
||||
* 🔴 ЖИВОЙ БАГ 13.07.2026: правило владельца — ЗАМЕНЫ ЗАЯВКИ НЕТ (недозвон не брак), но
|
||||
* бот не должен РУБИТЬ ДВЕРЬЮ («не полагается», «не вернём»). Старое правило BANNED
|
||||
* (строка ~74) знало только форму ЕДИНСТВЕННОГО числа «не полагается» — «не полагаются»
|
||||
* (множественное) прошло мимо на живом прогоне. Урок проекта: правила по СПИСКУ
|
||||
* ФОРМУЛИРОВОК проигрывают гонку — нужно правило ПО СМЫСЛУ (как promisesReplacement и
|
||||
* соседи), а не очередная строка в BANNED.
|
||||
*/
|
||||
it('🔴 ЖИВОЙ БАГ: «Замены за недозвоны не полагаются» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Замены за недозвоны не полагаются — это нормальная часть работы.',
|
||||
'Недозвон — обычная часть работы с контактами.'
|
||||
);
|
||||
|
||||
expect($out)->not->toContain('не полагаются');
|
||||
});
|
||||
|
||||
it('старая форма «Замена не полагается» — тоже рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean('Замена не полагается.', 'Недозвон — обычная часть работы.');
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Возврат денег за недозвон не предусмотрен» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Возврат денег за недозвон не предусмотрен.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Компенсации за такие заявки мы не делаем» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Компенсации за такие заявки мы не делаем.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Деньги за недозвоны не возвращаются» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Деньги за недозвоны не возвращаются.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Заменить заявку нельзя, возврата нет» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Заменить заявку нельзя, возврата нет.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Мы не меняем и не возвращаем деньги за такие номера» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Мы не меняем и не возвращаем деньги за такие номера.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Такие заявки не подлежат замене» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Такие заявки не подлежат замене.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
it('«Возмещение по недозвонам не положено» — рубка дверью, вырезается', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Возмещение по недозвонам не положено.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toBe('');
|
||||
});
|
||||
|
||||
/**
|
||||
* Честные фразы — сгладить тему, но НЕ рубить дверью. Их резать НЕЛЬЗЯ: без них ответ
|
||||
* потеряет весь смысл и клиент уйдёт с пустым ответом (эскалация «оставьте телефон»).
|
||||
*/
|
||||
it('честное: путь на support@ для спорного случая — НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Спорный случай опишите на support@liderra.ru — специалист разберёт его индивидуально.',
|
||||
'Поддержка работает круглосуточно.'
|
||||
);
|
||||
|
||||
expect($out)->toContain('support@liderra.ru')
|
||||
->and($out)->toContain('специалист разберёт его индивидуально');
|
||||
});
|
||||
|
||||
it('честное: «недозвон — обычная часть работы, попробуйте СМС и повтор» — НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Недозвон — обычная часть работы: попробуйте СМС и повторный звонок позже.',
|
||||
'Недозвон — обычная часть работы.'
|
||||
);
|
||||
|
||||
expect($out)->toContain('попробуйте СМС и повторный звонок позже');
|
||||
});
|
||||
|
||||
it('честное: «из ста номеров обычно отвечают 8–12 человек» — НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Из ста номеров обычно отвечают 8–12 человек.',
|
||||
'Из ста номеров обычно отвечают 8–12 человек.'
|
||||
);
|
||||
|
||||
expect($out)->toContain('Из ста номеров обычно отвечают');
|
||||
});
|
||||
|
||||
it('честное: «если заявка явно не по теме, напишите нам — разберёмся» — НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean(
|
||||
'Если заявка явно не по теме, напишите нам — разберёмся.',
|
||||
'Заявка не по теме — напишите нам.'
|
||||
);
|
||||
|
||||
expect($out)->toContain('напишите нам — разберёмся');
|
||||
});
|
||||
|
||||
/**
|
||||
* Ложные срабатывания: слово «нет» — самое частое слово в русском, встречается в
|
||||
* контекстах, никак не связанных с заменой/возвратом/деньгами. Правило НЕ должно их резать.
|
||||
*/
|
||||
it('ложное срабатывание: «заявок сегодня нет» — про заявки, не про замену/деньги, НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
$out = $guard->clean('Заявок сегодня нет — это нормально, бывают дни потише.', 'Заявок сегодня нет.');
|
||||
|
||||
expect($out)->toContain('Заявок сегодня нет');
|
||||
});
|
||||
|
||||
it('ложное срабатывание: «денег на балансе нет» (не отказ в замене/возврате) — НЕ режется', function () {
|
||||
$guard = new App\Services\Bot\AnswerGuard;
|
||||
|
||||
// Слово «деньг\w*» — триггер моего правила, «нет» — тоже, но смысл предложения не про
|
||||
// отказ в замене/возврате, а про то, что баланс пуст. Правило не должно это резать.
|
||||
$out = $guard->clean(
|
||||
'Денег на балансе нет — пополните счёт, чтобы получать новые заявки.',
|
||||
'Пополнить баланс можно картой или по счёту.'
|
||||
);
|
||||
|
||||
expect($out)->toContain('Денег на балансе нет');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user