feat(bot): YandexGptClient — completion Lite, таймаут 8с, null при беде

This commit is contained in:
Дмитрий
2026-07-02 20:57:03 +03:00
parent 4cab703b82
commit edbfd3e993
2 changed files with 98 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Services\Bot;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
/**
* YandexGPT Lite (Yandex Cloud Foundation Models) мозг бота (протокол, решение 8).
* Возвращает null при любой беде (нет ключа, таймаут, 5xx) решение об эскалации
* принимает вызывающий. Таймаут 8 сек бюджет скорости из спеки §6.
*/
class YandexGptClient
{
public function complete(string $systemPrompt, string $userText): ?string
{
$cfg = (array) config('services.yandexgpt');
if (($cfg['api_key'] ?? '') === '' || ($cfg['folder_id'] ?? '') === '') {
return null;
}
try {
$response = Http::timeout((int) ($cfg['timeout_seconds'] ?? 8))
->withHeaders(['Authorization' => 'Api-Key '.$cfg['api_key']])
->post((string) $cfg['endpoint'], [
'modelUri' => sprintf('gpt://%s/%s', $cfg['folder_id'], $cfg['model']),
'completionOptions' => ['stream' => false, 'temperature' => 0.2, 'maxTokens' => 500],
'messages' => [
['role' => 'system', 'text' => $systemPrompt],
['role' => 'user', 'text' => $userText],
],
]);
if (! $response->ok()) {
Log::warning('YandexGPT non-OK', ['status' => $response->status()]);
return null;
}
$text = $response->json('result.alternatives.0.message.text');
return is_string($text) && $text !== '' ? $text : null;
} catch (\Throwable $e) {
Log::warning('YandexGPT failure', ['error' => $e->getMessage()]);
return null;
}
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use App\Services\Bot\YandexGptClient;
use Illuminate\Support\Facades\Http;
uses(Tests\TestCase::class);
beforeEach(function () {
config()->set('services.yandexgpt', [
'api_key' => 'test-key', 'folder_id' => 'b1gtest', 'model' => 'yandexgpt-lite/latest',
'endpoint' => 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion',
'timeout_seconds' => 8,
]);
});
it('шлёт правильный запрос и возвращает текст ответа', function () {
Http::fake([
'llm.api.cloud.yandex.net/*' => Http::response([
'result' => ['alternatives' => [['message' => ['role' => 'assistant', 'text' => 'Проект — это…']]]],
]),
]);
$text = app(YandexGptClient::class)->complete('системный наказ', 'что такое проект?');
expect($text)->toBe('Проект — это…');
Http::assertSent(function ($request) {
return $request->hasHeader('Authorization', 'Api-Key test-key')
&& $request['modelUri'] === 'gpt://b1gtest/yandexgpt-lite/latest'
&& $request['messages'][0]['role'] === 'system'
&& $request['messages'][1]['role'] === 'user'
&& $request['completionOptions']['temperature'] === 0.2;
});
});
it('пустой api_key → null (бот не настроен, не исключение)', function () {
config()->set('services.yandexgpt.api_key', '');
expect(app(YandexGptClient::class)->complete('s', 'u'))->toBeNull();
});
it('ошибка API → null (эскалация решается выше)', function () {
Http::fake(['llm.api.cloud.yandex.net/*' => Http::response('err', 500)]);
expect(app(YandexGptClient::class)->complete('s', 'u'))->toBeNull();
});