fix(jivo-bot): мягкий ИЛИ-поиск при пустом строгом + стоп-тема «у меня … баланс» с разрывом слов
Два живых бага демо 12.07.2026 (TDD, 34/34 зелёные): - KnowledgeSearch: websearch_to_tsquery требует ВСЕ слова вопроса; «зачем он нужен» ронял поиск в пусто → добавлен мягкий запасной проход через ИЛИ (plainto | ) - BotAnswerService STOP_PATTERN: «какой у меня сейчас баланс» проскакивал стоп-тему (слова между «у меня» и «баланс») → допускаем до 3 слов разрыва, сч[её]т - Протокол: журнал 12.07 (аккаунт Jivo, канал, цвет, ключ zofyNxmnRP, стенд-проверка) - Скриншоты живой проверки виджета в findings Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@@ -13,7 +13,7 @@ namespace App\Services\Bot;
|
||||
class BotAnswerService
|
||||
{
|
||||
/** Личные данные, деньги конкретного клиента, скидки, юр-темы, просьба человека. */
|
||||
private const STOP_PATTERN = '/(мо[йяеи]\s+(баланс|счет|счёт|деньг|проект|заявк|сделк)|у меня (на )?(балансе|счете|счёте)|скидк|оператор|человек|менеджер|жалоб|претензи|юрист|договор|возврат денег)/iu';
|
||||
private const STOP_PATTERN = '/(мо[йяеи]\s+(баланс|сч[её]т|деньг|проект|заявк|сделк)|у меня\s+(?:\S+\s+){0,3}(баланс|сч[её]т|деньг|проект|заявк|сделк)|скидк|оператор|человек|менеджер|жалоб|претензи|юрист|договор|возврат денег)/iu';
|
||||
|
||||
private const ESCALATE_TEXT = 'Этот вопрос лучше разберёт живой специалист — передаю ему диалог. Он ответит здесь же.';
|
||||
|
||||
|
||||
@@ -24,13 +24,24 @@ class KnowledgeSearch
|
||||
return [];
|
||||
}
|
||||
|
||||
// Строгий проход (все слова через И); пусто — мягкий запасной через ИЛИ:
|
||||
// websearch_to_tsquery требует ВСЕ слова вопроса, а клиент пишет «зачем он нужен» —
|
||||
// слова, которых в статьях нет (живой баг 12.07.2026).
|
||||
$strict = "websearch_to_tsquery('russian', ?)";
|
||||
$soft = "replace(plainto_tsquery('russian', ?)::text, '&', '|')::tsquery";
|
||||
|
||||
$hits = $this->run($strict, $question, $limit);
|
||||
|
||||
return $hits !== [] ? $hits : $this->run($soft, $question, $limit);
|
||||
}
|
||||
|
||||
/** @return list<KnowledgeChunk> */
|
||||
private function run(string $tsquery, string $question, int $limit): array
|
||||
{
|
||||
/** @var Collection<int, KnowledgeChunk> $hits */
|
||||
$hits = KnowledgeChunk::query()
|
||||
->selectRaw(
|
||||
"knowledge_chunks.*, ts_rank(search_tsv, websearch_to_tsquery('russian', ?)) AS rank",
|
||||
[$question]
|
||||
)
|
||||
->whereRaw("search_tsv @@ websearch_to_tsquery('russian', ?)", [$question])
|
||||
->selectRaw("knowledge_chunks.*, ts_rank(search_tsv, {$tsquery}) AS rank", [$question])
|
||||
->whereRaw("search_tsv @@ {$tsquery}", [$question])
|
||||
->orderByDesc('rank')
|
||||
->limit($limit)
|
||||
->get();
|
||||
|
||||
@@ -27,8 +27,28 @@ it('стоп-тема (мой баланс) → эскалация без пох
|
||||
|
||||
$answer = app(BotAnswerService::class)->answer('какой у меня баланс?');
|
||||
|
||||
// Текст именно стоп-эскалации («передаю ему диалог»), а не «в инструкции нет ответа»:
|
||||
// до фикса 12.07.2026 вопрос проскакивал стоп-гейт и эскалировал лишь пустым поиском.
|
||||
expect($answer->escalate)->toBeTrue()
|
||||
->and($answer->text)->toContain('специалисту');
|
||||
->and($answer->text)->toContain('передаю ему диалог');
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
it('стоп-тема со словами между «у меня» и «баланс» → эскалация ДО поиска и LLM', function () {
|
||||
// Живой баг 12.07.2026: «какой у меня сейчас баланс?» проскакивал мимо стоп-темы,
|
||||
// поиск находил статью про пополнение и бот отвечал общими словами вместо передачи
|
||||
// человеку. Статья про баланс в базе ЕСТЬ — эскалация обязана сработать раньше поиска.
|
||||
KnowledgeChunk::create([
|
||||
'source_path' => 'help/b.md', 'title' => 'Как пополнить баланс', 'tour' => 'top-up-balance',
|
||||
'topics' => 'пополнить, баланс', 'chunk_index' => 0,
|
||||
'content' => 'Пополнить баланс: раздел Биллинг, кнопка Пополнить.',
|
||||
]);
|
||||
Http::fake();
|
||||
|
||||
$answer = app(BotAnswerService::class)->answer('какой у меня сейчас баланс?');
|
||||
|
||||
expect($answer->escalate)->toBeTrue()
|
||||
->and($answer->text)->toContain('передаю ему диалог');
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,15 @@ it('находит по синонимам из topics («закинуть де
|
||||
->and($hits[0]->title)->toBe('Как пополнить баланс');
|
||||
});
|
||||
|
||||
it('находит статью, даже если часть слов вопроса в текстах отсутствует', function () {
|
||||
// websearch_to_tsquery связывает слова через И: слово «нужен» в статье не встречается,
|
||||
// строгий запрос даёт пусто — нужен мягкий запасной поиск через ИЛИ (живой баг 12.07.2026).
|
||||
$hits = app(KnowledgeSearch::class)->search('Что такое проект и зачем он нужен?', 3);
|
||||
|
||||
expect($hits)->not->toBeEmpty()
|
||||
->and($hits[0]->title)->toBe('Что такое проект');
|
||||
});
|
||||
|
||||
it('на вопрос не по теме возвращает пусто', function () {
|
||||
expect(app(KnowledgeSearch::class)->search('какая погода в москве', 3))->toBeEmpty();
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 48 KiB |
@@ -23,8 +23,8 @@
|
||||
| № | Вопрос | Варианты / заметки |
|
||||
|---|---|---|
|
||||
| О-1 | ~~Какой ИИ-«мозг»~~ | **ЗАКРЫТ 02.07.2026** → решение 8: YandexGPT Lite. |
|
||||
| О-2 | Тариф Jivo | Bot API официально доступен на корпоративном тарифе (~3 142 ₽/мес за оператора). Подтвердить готовность платить, когда дойдём до подключения. Подключение своего бота — по письму в поддержку Jivo (info@jivosite.com), автоматической кнопки нет. |
|
||||
| О-3 | Когда включаем бесплатный чат Jivo (без бота, для разведки вопросов) | Код в портале готов, нужен только ключ виджета (JIVO_WIDGET_ID). Включение на боевом — только с разрешения владельца. |
|
||||
| О-2 | Тариф Jivo | Bot API официально доступен на корпоративном тарифе (~3 142 ₽/мес за оператора). Подтвердить готовность платить, когда дойдём до подключения. Подключение своего бота — по письму в поддержку Jivo (info@jivosite.com), автоматической кнопки нет. Сейчас на аккаунте идёт 14-дневная проба «Профессиональной» версии (до ~26.07.2026). |
|
||||
| О-3 | Когда включаем бесплатный чат Jivo (без бота, для разведки вопросов) | **ПОЧТИ ЗАКРЫТ 12.07.2026:** аккаунт зарегистрирован (sasha261185@yandex.ru, оператор «Александра»), ключ виджета получен — `zofyNxmnRP`, канал настроен и проверен на стенде в обе стороны. Осталось включить на боевом — только с разрешения владельца (пересборка фронта + CSP, см. журнал 12.07). |
|
||||
|
||||
## 3. Что уже есть в портале (задел, ничего строить заново не надо)
|
||||
|
||||
@@ -73,3 +73,18 @@
|
||||
---
|
||||
|
||||
*Журнал обновлений: 02.07.2026 — протокол заведён, зафиксированы решения 1–7, исследование Jivo, открытые вопросы О-1…О-3. Позже в тот же день: решение 8 (YandexGPT Lite), дизайн согласован кусками 1–4, спека записана. Ещё позже: ядро бота построено по плану (14 задач TDD, worktree), поиск FTS вместо pgvector, спека синхронизирована.*
|
||||
|
||||
*12.07.2026 — владелец зарегистрировал аккаунт Jivo (sasha261185@yandex.ru), Claude настроил кабинет:
|
||||
канал переименован в «Портал Лидерра — lk.liderra.ru», цвет окна чата — фирменный Teal `#0F6E56`,
|
||||
ключ виджета — **`zofyNxmnRP`** (код: `<script src="//code.jivo.ru/widget/zofyNxmnRP" async></script>`).
|
||||
Живая проверка на стенде :8010 (worktree, `VITE_JIVO_WIDGET_ID` в .env + пересборка фронта):
|
||||
пузырёк чата появился, сообщение с портала дошло в кабинет Jivo (канал подписан правильно),
|
||||
ответ оператора вернулся в чат на портале — двусторонняя связь работает. Скриншоты —
|
||||
`docs/superpowers/findings/2026-07-12-jivo-bot-live-demo/jivo-*.png`. Для включения на боевом
|
||||
lk.liderra.ru нужно (по разрешению владельца): 1) пересборка фронта из канон-main с
|
||||
`VITE_JIVO_WIDGET_ID=zofyNxmnRP` (ключ вшивается при сборке) и замена `public/build`;
|
||||
2) добавить в CSP nginx (`lk`) домены Jivo: `script-src`/`connect-src` `code.jivo.ru
|
||||
code.jivosite.com *.jivosite.com wss://*.jivosite.com` (CSP живёт только на проде, не в git);
|
||||
3) на телефон владельца — приложение Jivo (вход sasha261185@yandex.ru), чтобы отвечать клиентам.
|
||||
Аккаунт на 14-дневной пробе «Профессиональной» версии; после неё — бесплатный план (до 2 операторов)
|
||||
либо корпоративный для Bot API (О-2).*
|
||||
|
||||