c5125e981e
Прогнал 250 вопросов через настоящий вебхук шесть раз и сравнил четыре модели вслепую силами судей-агентов. Итоги — в docs findings. Модель. Появился переключатель services.jivo_bot.llm: yandex (данные не покидают РФ) либо aitunnel (зарубежные модели через российский шлюз). По слепому сравнению Claude Haiku 4.5 выиграл финал у Gemini 157:24 и у обеих моделей Яндекса: он полнее, точнее по кнопкам и цифрам и говорит как человек, а не справочник. Три замка, потому что живая модель = самоуверенная модель: - PersonalDataMask — телефоны и почта вырезаются ДО отправки в любую модель (клиент оставляет номер в чате; за границу ПДн уходить не должны). - ChatTextCleaner — markdown и смайлики (Jivo их не рисует): было 191 ответ с разметкой, стало 0. - AnswerGuard — режет юридические гарантии («вас не оштрафуют», «мы гарантируем», «не нарушаем закон») и ЛЮБУЮ цену, которой нет в выданных статьях. Пусто после чистки → зовём живого специалиста. Поиск по инструкции переписан: выбрасываются вопросительные слова (из-за них «какие у вас тарифы» находило выгрузку в Excel), ранжирование сперва по заголовку и синонимам, выдача — три РАЗНЫЕ статьи и каждая целиком, плюс починка опечаток («каг сазадь праэкт») и учёт прошлой реплики в коротких «а это платно?». Инструкция: 40 → 51 статья. Новые — по фактам владельца: бесплатный тест и подарок 1000 ₽, «мы канал, а не реклама», СМС → через час звонок, средние показатели (8–12 из 100 разговаривают, ещё 5–7 по СМС), номер не вернётся 30 дней, замена брака, вывод остатка, поддержка 24/7, удаление данных, ниши. Вычищено враньё: минус баланса, «списали без заявки», пауза вечером, выходные, «Вся РФ», статус «Отказ» (его нет — есть «Не реализовано»), amoCRM, платный повторный сбор, нарастающий итог тарифных ступеней. Тесты: 106 из 106. Каждая найденная тупость закреплена тестом. НЕ НА ПРОДЕ. Выкат — только по прямой команде владельца. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
130 lines
7.1 KiB
PHP
130 lines
7.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\KnowledgeChunk;
|
|
use App\Services\Bot\KnowledgeSearch;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
/**
|
|
* Разбор пятого прогона четырьмя проверяющими (12.07.2026): поиск берёт соседнюю
|
|
* статью, бот выдумывает статусы и даёт юридические гарантии.
|
|
*/
|
|
beforeEach(function () {
|
|
KnowledgeChunk::query()->delete();
|
|
Artisan::call('help:rebuild-knowledge');
|
|
});
|
|
|
|
function topArticle(string $question): string
|
|
{
|
|
$hits = app(KnowledgeSearch::class)->search($question, 3);
|
|
expect($hits)->not->toBeEmpty();
|
|
|
|
return $hits[0]->title;
|
|
}
|
|
|
|
it('поиск берёт статью ПО ТЕМЕ, а не соседнюю с теми же словами', function () {
|
|
$cases = [
|
|
'как создать проект' => 'Создать проект — пошагово',
|
|
'какие поля надо заполнить при создании проекта' => 'Создать проект — пошагово',
|
|
'как запустить проект обратно' => 'Пауза и запуск проекта',
|
|
'что такое сделка' => 'Сделки',
|
|
'можно скачать заявки в csv' => 'Выгрузить сделки',
|
|
'есть ли абонентская плата' => 'Тарифы',
|
|
'в какое время дня приходят лиды' => 'Выгрузка по проекту',
|
|
'у меня один и тот же конкурент два раза' => 'Дубли конкурентов',
|
|
'нашел источник, что дальше делать' => 'Проект из найденного источника',
|
|
'а бесплатно можно?' => 'Бесплатный тест',
|
|
'дубликаты номеров бывают?' => 'Повторы и дубли номеров',
|
|
'насколько свежие заявки, сколько времени прошло' => 'Что вы получаете',
|
|
];
|
|
|
|
foreach ($cases as $q => $expected) {
|
|
expect(topArticle($q))->toContain($expected);
|
|
}
|
|
});
|
|
|
|
it('в инструкции нет выдуманных статусов «Отказ» и «Успешная»', function () {
|
|
// Судья 12.07.2026: бот советовал ставить статус «Отказ» — его нет. Пять статусов:
|
|
// Новая сделка / Просмотрено / В работе / Сделка / Не реализовано.
|
|
$all = KnowledgeChunk::query()->pluck('content')->implode(' ');
|
|
|
|
expect($all)->not->toContain('статус «Отказ»')
|
|
->and($all)->not->toContain('статус «Успешная»');
|
|
|
|
$deals = KnowledgeChunk::query()
|
|
->where('source_path', 'help/sdelki.md')
|
|
->orderBy('chunk_index')
|
|
->pluck('content')
|
|
->implode(' ');
|
|
|
|
expect($deals)->toContain('«Не реализовано»');
|
|
});
|
|
|
|
it('значение источника менять можно — бот не имеет права говорить «нельзя»', function () {
|
|
// Судья 12.07.2026: на «могу ли я поменять сайт в проекте» бот ответил «нельзя,
|
|
// заводите новый». Неправда: не меняется только ТИП источника, а сам сайт — меняется.
|
|
$hits = app(KnowledgeSearch::class)->search('могу ли я поменять сайт в уже созданном проекте', 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
|
|
expect($hits[0]->title)->toContain('Сменить источник')
|
|
->and($text)->toContain('Менять можно значение');
|
|
});
|
|
|
|
it('за повтор номера деньги не берутся, а списание идёт за каждую заявку', function () {
|
|
// Судья 12.07.2026: на «списываются ли деньги за дубли» бот ответил, что деньги
|
|
// списываются ТОЛЬКО за услуги Конкурентного поля. Это враньё про деньги.
|
|
$hits = app(KnowledgeSearch::class)->search('списывается ли деньги за дубли', 3);
|
|
expect($hits[0]->title)->toContain('Повторы и дубли номеров');
|
|
|
|
$article = KnowledgeChunk::query()
|
|
->where('source_path', 'help/dubli-nomerov.md')
|
|
->orderBy('chunk_index')
|
|
->pluck('content')
|
|
->implode(' ');
|
|
|
|
expect($article)->toContain('30 дней')
|
|
->and($article)->toContain('дважды за него не заплатите');
|
|
});
|
|
|
|
it('про штрафы бот не даёт гарантий — ответственность на клиенте', function () {
|
|
// Судья 12.07.2026: бот категорично обещал «Нет, вас не оштрафуют». Гарантий не даём.
|
|
$hits = app(KnowledgeSearch::class)->search('меня оштрафуют если я буду звонить по вашим номерам', 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
|
|
expect($text)->toContain('гарантий по вашей рекламе мы не даём');
|
|
});
|
|
|
|
it('зависшее изучение — это поддержка, а не «не хватило баланса»', function () {
|
|
// Судья 12.07.2026: бот выдумал причину «наверное, не хватает денег». При нехватке
|
|
// баланса портал вообще не запускает задание и прямо об этом говорит.
|
|
$hits = app(KnowledgeSearch::class)->search('изучение висит в очереди уже час', 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
|
|
expect($text)->toContain('висит дольше')
|
|
->and($text)->toContain('support@liderra.ru');
|
|
});
|
|
|
|
it('на старте не требует пополнения — подарок уже на балансе', function () {
|
|
foreach (['с чего начать?', 'я только зарегистрировался что дальше'] as $q) {
|
|
$hits = app(KnowledgeSearch::class)->search($q, 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
expect($text)->toContain('1000 ₽');
|
|
}
|
|
});
|
|
|
|
it('«верните деньги за заявку» — статья про замену находится', function () {
|
|
$hits = app(KnowledgeSearch::class)->search('верните деньги за плохую заявку', 3);
|
|
|
|
expect(collect($hits)->pluck('title')->implode(' | '))->toContain('заменим');
|
|
});
|
|
|
|
it('ночью и после лимита — отвечает по статье, без выдумок', function () {
|
|
$hits = app(KnowledgeSearch::class)->search('ночью заявки приходят?', 3);
|
|
$text = collect($hits)->pluck('content')->implode(' ');
|
|
|
|
expect($text)->toContain('Ночью заявки не приходят');
|
|
});
|