df83fa836c
Шаг 1 «Конкурентное поле» — чекпоинт: - Финальный проход «Найти и объединить дубли» на поле и предложениях; клиент решает по каждой группе, ничего не склеиваем молча. - Тихая склейка только по сайту и коду справочника; телефон и людный номер у более чем 4 фирм — на решение клиента. - Колонка phones jsonb; телефон-дубли видны уже на «Предложениях». - Sonar даёт только имя и тип, сайт всегда через EXA; вскрываем все карточки без гейта «есть сайт». - Яндекс-карточки через локальный Playwright параллельно; рубрика из заголовка идёт в описание. - Фикс рекламных номеров: 2ГИС-парсер разбирает телефоны пообъектно и выкидывает рекламные кнопки с platforms/caption чужих фирм. Тесты: автоподбор 224/224 unit, 105/105 feature; фронт-спеки зелёные. Времянки app/scripts/diag-*/probe-* в гит не входят. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
4.8 KiB
PHP
101 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Services\Autopodbor\Agent\ChannelA\AitunnelQueryAnalyzer;
|
|
use Illuminate\Http\Client\Factory as HttpFactory;
|
|
use Illuminate\Support\Facades\Config;
|
|
|
|
// Шаг АНАЛИЗ канала А (ZAFIKSIROVANO §0-БИС / §12.7): мелкая ИИ-модель из описания клиента даёт
|
|
// несколько КОРОТКИХ запросов-рубрик для скрейпа категории 2ГИС/Яндекса (в тесте Омеги было ~6).
|
|
// Сеть мокаем. Промт восстановлен (оригинал жил в самоочистившемся collectA.js) — точим на прогоне.
|
|
|
|
it('без ключа отдаёт fallback — один запрос из описания (А не мёртв)', function () {
|
|
Config::set('services.aitunnel.key', '');
|
|
|
|
$a = new AitunnelQueryAnalyzer(app(HttpFactory::class));
|
|
|
|
expect($a->analyze('займы под залог авто и спецтехники', 'Красноярский край'))
|
|
->toBe(['займы под залог авто и спецтехники']);
|
|
});
|
|
|
|
it('разбирает JSON-массив запросов из ответа модели', function () {
|
|
Config::set('services.aitunnel', [
|
|
'key' => 'k', 'base_url' => 'https://api.aitunnel.ru/v1', 'chat_model' => 'gpt-4o-mini', 'timeout_sec' => 30,
|
|
]);
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'*/chat/completions' => $http->response([
|
|
'choices' => [['message' => ['content' => '["автоломбард","займ под залог авто","МФО","ломбард"]']]],
|
|
], 200),
|
|
]);
|
|
|
|
$a = new AitunnelQueryAnalyzer($http);
|
|
$out = $a->analyze('займы под залог авто', 'Красноярский край');
|
|
|
|
expect($out)->toBe(['автоломбард', 'займ под залог авто', 'МФО', 'ломбард']);
|
|
});
|
|
|
|
it('режет markdown-обёртку и пустые/дубли, ограничивает число', function () {
|
|
Config::set('services.aitunnel.key', 'k');
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake([
|
|
'*/chat/completions' => $http->response([
|
|
'choices' => [['message' => ['content' => "```json\n[\"автоломбард\",\"\",\"автоломбард\",\"займ под залог\"]\n```"]]],
|
|
], 200),
|
|
]);
|
|
|
|
$a = new AitunnelQueryAnalyzer($http);
|
|
$out = $a->analyze('займы', 'Красноярский край');
|
|
|
|
expect($out)->toBe(['автоломбард', 'займ под залог']);
|
|
});
|
|
|
|
it('добавляет в промт наш сайт и конкурентов, когда они переданы', function () {
|
|
Config::set('services.aitunnel', [
|
|
'key' => 'k', 'base_url' => 'https://api.aitunnel.ru/v1', 'chat_model' => 'gpt-4o-mini', 'timeout_sec' => 30,
|
|
]);
|
|
$http = app(HttpFactory::class);
|
|
$http->fake(['*/chat/completions' => $http->response(['choices' => [['message' => ['content' => '["автоломбард"]']]]], 200)]);
|
|
|
|
$a = new AitunnelQueryAnalyzer($http);
|
|
$a->analyze('займы под залог', 'Красноярск', 'lkomega.ru', ['comp1.ru', 'https://2gis.ru/firm/1']);
|
|
|
|
$http->assertSent(function ($req) {
|
|
$c = $req->data()['messages'][0]['content'] ?? '';
|
|
return str_contains($c, 'посмотри наш сайт lkomega.ru')
|
|
&& str_contains($c, 'а также наших главных конкурентов')
|
|
&& str_contains($c, 'comp1.ru');
|
|
});
|
|
});
|
|
|
|
it('без сайта и конкурентов — контекстные фразы в промт НЕ попадают', function () {
|
|
Config::set('services.aitunnel', [
|
|
'key' => 'k', 'base_url' => 'https://api.aitunnel.ru/v1', 'chat_model' => 'gpt-4o-mini', 'timeout_sec' => 30,
|
|
]);
|
|
$http = app(HttpFactory::class);
|
|
$http->fake(['*/chat/completions' => $http->response(['choices' => [['message' => ['content' => '["автоломбард"]']]]], 200)]);
|
|
|
|
$a = new AitunnelQueryAnalyzer($http);
|
|
$a->analyze('займы под залог', 'Красноярск'); // без сайта/конкурентов
|
|
|
|
$http->assertSent(function ($req) {
|
|
$c = $req->data()['messages'][0]['content'] ?? '';
|
|
return ! str_contains($c, 'посмотри наш сайт')
|
|
&& ! str_contains($c, 'наших главных конкурентов');
|
|
});
|
|
});
|
|
|
|
it('при ошибке сети — fallback из описания', function () {
|
|
Config::set('services.aitunnel.key', 'k');
|
|
|
|
$http = app(HttpFactory::class);
|
|
$http->fake(['*/chat/completions' => $http->response('boom', 500)]);
|
|
|
|
$a = new AitunnelQueryAnalyzer($http);
|
|
|
|
expect($a->analyze('ломбард', 'Красноярский край'))->toBe(['ломбард']);
|
|
});
|