9b83b2e035
Владелец 12.07.2026 дал суть: номера передают операторы связи, и только по тем абонентам, кто дал согласие на передачу данных третьим лицам и на рекламные предложения. Раньше бот на «а где вы берёте эти номера?» честно эскалировал — теперь отвечает сам, юридически корректно. + otkuda-zayavki.md: источник данных (операторы связи), согласие абонента как основание, 152-ФЗ, никаких «слитых» баз; практика для клиента (звонить можно и нужно быстро, вежливо завершать при отказе); честная оговорка «заявка — интерес, не гарантия продажи»; юрвопросы → живой специалист. Границы держатся (проверено живьём): «какой конкретно оператор — Билайн? МТС?» → эскалация; «эти номера только мне достаются?» → эскалация (эксклюзивность — жёсткая стоп-тема). Поставщик и модель распределения не раскрываются. TDD: тест на РЕАЛЬНОЙ базе знаний (3 формулировки вопроса → статья про операторов). 42/42 зелёные. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
84 lines
4.7 KiB
PHP
84 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\KnowledgeChunk;
|
|
use App\Services\Bot\KnowledgeSearch;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
KnowledgeChunk::create([
|
|
'source_path' => 'help/p.md', 'title' => 'Что такое проект', 'tour' => 'create-project',
|
|
'topics' => 'создать проект, заявка на лиды', 'chunk_index' => 0,
|
|
'content' => 'Проект — это заявка на поток клиентов с выбранного источника.',
|
|
]);
|
|
KnowledgeChunk::create([
|
|
'source_path' => 'help/b.md', 'title' => 'Как пополнить баланс', 'tour' => 'top-up-balance',
|
|
'topics' => 'пополнить, закинуть деньги, оплата', 'chunk_index' => 0,
|
|
'content' => 'Пополнить баланс: раздел Биллинг, кнопка Пополнить.',
|
|
]);
|
|
});
|
|
|
|
it('находит релевантный чанк и ранжирует его первым', function () {
|
|
$hits = app(KnowledgeSearch::class)->search('а что такое проект?', 3);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->and($hits[0]->title)->toBe('Что такое проект');
|
|
});
|
|
|
|
it('находит по синонимам из topics («закинуть деньги»)', function () {
|
|
$hits = app(KnowledgeSearch::class)->search('как закинуть деньги', 3);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->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 () {
|
|
// Живой урок 12.07.2026: бот принял «конкурентов» буквально и рассказал про раздел
|
|
// «Конкурентное поле» вместо ценности сервиса. Проверяем на настоящих статьях
|
|
// resources/help — в базе знаний обязана быть статья про смысл и выгоду сервиса.
|
|
KnowledgeChunk::query()->delete();
|
|
$this->artisan('help:rebuild-knowledge')->assertExitCode(0);
|
|
|
|
$hits = app(KnowledgeSearch::class)->search(
|
|
'а зачем лидерра вообще нужна и чем она отличается от конкурентов', 3
|
|
);
|
|
|
|
expect($hits)->not->toBeEmpty()
|
|
->and($hits[0]->title)->not->toContain('Конкурентное поле')
|
|
->and(mb_strtolower($hits[0]->title))->toContain('сервис');
|
|
});
|
|
|
|
it('«где вы берёте эти номера» → статья о происхождении данных и согласии абонента', function () {
|
|
// Живой урок 12.07.2026 (формулировка владельца): номера передают операторы связи,
|
|
// и только по тем абонентам, кто дал согласие на передачу данных третьим лицам
|
|
// и на рекламные предложения. Раньше бот эскалировал этот частый вопрос.
|
|
KnowledgeChunk::query()->delete();
|
|
$this->artisan('help:rebuild-knowledge')->assertExitCode(0);
|
|
|
|
foreach (['а где вы берёте эти номера?', 'откуда у вас данные людей', 'это законно вообще?'] as $q) {
|
|
$hits = app(KnowledgeSearch::class)->search($q, 3);
|
|
expect($hits)->not->toBeEmpty()
|
|
->and(collect($hits)->pluck('content')->implode(' '))->toContain('оператор');
|
|
}
|
|
});
|
|
|
|
it('на вопрос не по теме возвращает пусто', function () {
|
|
expect(app(KnowledgeSearch::class)->search('какая погода в москве', 3))->toBeEmpty();
|
|
});
|
|
|
|
it('не падает на спецсимволах в вопросе', function () {
|
|
expect(app(KnowledgeSearch::class)->search('проект & | ! ( )', 3))->toBeArray();
|
|
});
|