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>
65 lines
3.0 KiB
PHP
65 lines
3.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Bot\AitunnelChatClient;
|
|
use App\Services\Bot\LlmClient;
|
|
use App\Services\Bot\PersonalDataMask;
|
|
use App\Services\Bot\YandexGptClient;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
/**
|
|
* Выбор модели под капотом (замечание владельца 12.07.2026: «может, проблема модели?»).
|
|
* Плюс главное ограничение: в зарубежную модель НЕЛЬЗЯ отправлять персональные данные —
|
|
* клиент пишет в чат свой телефон. Маскируем до отправки.
|
|
*/
|
|
it('провайдер выбирается настройкой: яндекс или зарубежная модель через AITunnel', function () {
|
|
config()->set('services.jivo_bot.llm', 'yandex');
|
|
expect(app(LlmClient::class))->toBeInstanceOf(YandexGptClient::class);
|
|
|
|
config()->set('services.jivo_bot.llm', 'aitunnel');
|
|
expect(app(LlmClient::class))->toBeInstanceOf(AitunnelChatClient::class);
|
|
});
|
|
|
|
it('маскирует телефоны и почту до отправки в модель', function () {
|
|
$mask = app(PersonalDataMask::class);
|
|
|
|
expect($mask->hide('мой номер +7 916 123-45-67, звоните'))
|
|
->toBe('мой номер <телефон>, звоните')
|
|
->and($mask->hide('телефон 89161234567'))->toBe('телефон <телефон>')
|
|
->and($mask->hide('пишите на ivan.petrov@mail.ru'))->toBe('пишите на <почта>')
|
|
->and($mask->hide('лимит 10000 заявок в день'))->toBe('лимит 10000 заявок в день');
|
|
});
|
|
|
|
it('зарубежная модель получает вопрос БЕЗ телефона клиента', function () {
|
|
config()->set('services.jivo_bot.llm', 'aitunnel');
|
|
config()->set('services.aitunnel.key', 'test-key');
|
|
config()->set('services.jivo_bot.aitunnel_model', 'claude-sonnet-5');
|
|
|
|
Http::fake(['*' => Http::response([
|
|
'choices' => [['message' => ['content' => 'Передал специалисту.']]],
|
|
], 200)]);
|
|
|
|
$answer = app(AitunnelChatClient::class)->complete(
|
|
'Ты консультант.',
|
|
'мой телефон 89161234567, перезвоните'
|
|
);
|
|
|
|
expect($answer)->toBe('Передал специалисту.');
|
|
|
|
Http::assertSent(function ($request) {
|
|
$body = json_encode($request->data(), JSON_UNESCAPED_UNICODE);
|
|
|
|
return str_contains($body, '<телефон>')
|
|
&& ! str_contains($body, '89161234567')
|
|
&& str_contains($body, 'claude-sonnet-5');
|
|
});
|
|
});
|
|
|
|
it('на ошибку зарубежной модели отвечает пусто — бот уйдёт в эскалацию', function () {
|
|
config()->set('services.aitunnel.key', 'test-key');
|
|
Http::fake(['*' => Http::response([], 500)]);
|
|
|
|
expect(app(AitunnelChatClient::class)->complete('Ты консультант.', 'вопрос'))->toBeNull();
|
|
});
|