From edbfd3e9930ebeff4bf55fbc60d94a5bbb404872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 2 Jul 2026 20:57:03 +0300 Subject: [PATCH] =?UTF-8?q?feat(bot):=20YandexGptClient=20=E2=80=94=20comp?= =?UTF-8?q?letion=20Lite,=20=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82=208?= =?UTF-8?q?=D1=81,=20null=20=D0=BF=D1=80=D0=B8=20=D0=B1=D0=B5=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/Services/Bot/YandexGptClient.php | 51 ++++++++++++++++++++++ app/tests/Unit/Bot/YandexGptClientTest.php | 47 ++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 app/app/Services/Bot/YandexGptClient.php create mode 100644 app/tests/Unit/Bot/YandexGptClientTest.php diff --git a/app/app/Services/Bot/YandexGptClient.php b/app/app/Services/Bot/YandexGptClient.php new file mode 100644 index 00000000..0fe77641 --- /dev/null +++ b/app/app/Services/Bot/YandexGptClient.php @@ -0,0 +1,51 @@ +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; + } + } +} diff --git a/app/tests/Unit/Bot/YandexGptClientTest.php b/app/tests/Unit/Bot/YandexGptClientTest.php new file mode 100644 index 00000000..3ac4d9d5 --- /dev/null +++ b/app/tests/Unit/Bot/YandexGptClientTest.php @@ -0,0 +1,47 @@ +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(); +});