feat(bot): YandexGptClient — completion Lite, таймаут 8с, null при беде
This commit is contained in:
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user