Files
componentsPC/app/Services/AiSuggestorService.php

400 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use App\Models\Component;
use App\Models\PCBuild;
use Exception;
class AiSuggestorService
{
protected $apiToken;
protected $baseUrl = 'https://gigachat.devices.sberbank.ru/api/v1/chat/completions';
public function __construct()
{
$this->apiToken = env('GIGACHAT_API_TOKEN');
if (!$this->apiToken) {
throw new Exception('GIGACHAT_API_TOKEN не установлен в .env');
}
}
public function suggest($task_id = null, $custom_prompt = null, $budget = null)
{
$prompt = $this->generatePrompt($task_id, $custom_prompt, $budget);
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiToken,
'Content-Type' => 'application/json',
])->withoutVerifying()
->post($this->baseUrl, [
'model' => 'GigaChat',
'messages' => [
[
'role' => 'user',
'content' => $prompt
]
],
'temperature' => 0.7,
'max_tokens' => 2500,
]);
if ($response->failed()) {
Log::error('Ошибка при запросе к ГигаЧату', [
'status' => $response->status(),
'body' => $response->body()
]);
throw new Exception('Не удалось получить ответ от ИИ.');
}
$responseData = $response->json();
$aiComponents = $this->parseResponse($responseData);
// ✅ Теперь можно безопасно логировать
// 🔹 ДЕБАГ: логируем полученные компоненты от ИИ
Log::info('Компоненты от ИИ:', [
'count' => count($aiComponents),
'types' => array_column($aiComponents, 'component_type_id'),
'components' => $aiComponents
]);
// 🔹 ШАГ 1: Удаляем дубликаты по name + component_type_id
$uniqueComponents = [];
$seen = [];
foreach ($aiComponents as $component) {
$key = $component['name'] . '|' . $component['component_type_id'];
if (!isset($seen[$key])) {
$seen[$key] = true;
$uniqueComponents[] = $component;
}
}
$aiComponents = $uniqueComponents;
// 🔹 ШАГ 2: Гарантируем наличие всех 7 типов компонентов
$requiredTypes = [1, 2, 3, 4, 5, 6, 7];
$existingTypes = array_column($aiComponents, 'component_type_id');
// ДЕБАГ: какие типы уже есть
Log::info('Проверка типов компонентов:', [
'required' => $requiredTypes,
'existing' => $existingTypes,
'missing' => array_diff($requiredTypes, $existingTypes)
]);
foreach ($requiredTypes as $typeId) {
if (!in_array($typeId, $existingTypes)) {
Log::warning('Добавляем недостающий тип компонента:', ['type_id' => $typeId]);
switch ($typeId) {
case 1:
$aiComponents[] = [
'name' => 'AMD Ryzen 5 5600',
'price' => 15000,
'component_type_id' => 1,
'specifications' => ['socket' => 'AM4', 'cores' => 6]
];
break;
case 2:
$aiComponents[] = [
'name' => 'NVIDIA RTX 4060',
'price' => 30000,
'component_type_id' => 2,
'specifications' => []
];
break;
case 3:
$aiComponents[] = [
'name' => 'ASUS TUF GAMING B660M-PLUS',
'price' => 8000,
'component_type_id' => 3,
'specifications' => ['chipset' => 'B660']
];
break;
case 4:
$aiComponents[] = [
'name' => 'Kingston FURY Beast DDR4 16GB',
'price' => 5000,
'component_type_id' => 4,
'specifications' => ['type' => 'DDR4', 'capacity' => '16GB']
];
break;
case 5:
$aiComponents[] = [
'name' => 'EVGA SuperNOVA 750 G2',
'price' => 8000,
'component_type_id' => 5,
'specifications' => ['power' => 750]
];
break;
case 6:
$aiComponents[] = [
'name' => 'Crucial P3 500GB NVMe',
'price' => 4000,
'component_type_id' => 6,
'specifications' => ['capacity' => 500, 'interface' => 'NVMe']
];
break;
case 7:
$aiComponents[] = [
'name' => 'Cooler Master H500P ATX Mid Tower Case',
'price' => 4000,
'component_type_id' => 7,
'specifications' => ['form_factor' => 'ATX']
];
break;
}
}
}
// ДЕБАГ: проверяем итоговый набор компонентов
Log::info('Итоговые компоненты перед созданием сборки:', [
'total_count' => count($aiComponents),
'types_present' => array_column($aiComponents, 'component_type_id'),
'unique_types_count' => count(array_unique(array_column($aiComponents, 'component_type_id')))
]);
// Проверка бюджета
if ($budget) {
$totalPrice = array_sum(array_column($aiComponents, 'price'));
if ($totalPrice > $budget) {
// Удаляем возможные дубликаты перед заменой
$uniqueComponents = [];
$seen = [];
foreach ($aiComponents as $component) {
$key = $component['name'] . '|' . $component['component_type_id'];
if (!isset($seen[$key])) {
$seen[$key] = true;
$uniqueComponents[] = $component;
}
}
$aiComponents = $uniqueComponents;
// Заменяем дорогие компоненты
foreach ($aiComponents as &$component) {
if ($component['component_type_id'] == 4) {
$component = [
'name' => 'Kingston FURY Beast DDR4 16GB',
'price' => 5000,
'component_type_id' => 4,
'specifications' => ['type' => 'DDR4', 'capacity' => '16GB']
];
}
if ($component['component_type_id'] == 6) {
$component = [
'name' => 'Crucial P3 500GB NVMe',
'price' => 4000,
'component_type_id' => 6,
'specifications' => ['capacity' => 500, 'interface' => 'NVMe']
];
}
if ($component['component_type_id'] == 1 && $totalPrice > $budget) {
$component = [
'name' => 'AMD Ryzen 5 5600',
'price' => 15000,
'component_type_id' => 1,
'specifications' => ['socket' => 'AM4', 'cores' => 6]
];
}
}
$totalPrice = array_sum(array_column($aiComponents, 'price'));
if ($totalPrice > $budget) {
throw new Exception("ИИ не смог уложиться в бюджет {$budget}₽ (итого: {$totalPrice}₽).");
}
}
}
// Создаём сборку
$build = PCBuild::create([
'user_id' => auth()->id(),
'name' => 'Сборка от ИИ',
'description' => "Сгенерировано ИИ на основе: " . ($custom_prompt ?: "задача #{$task_id}"),
'is_ai_generated' => true,
'ai_prompt' => $prompt,
]);
// Привязываем компоненты с защитой от дубликатов
$createdComponents = [];
$seenComponentIds = [];
foreach ($aiComponents as $componentData) {
$component = $this->findOrCreateComponent($componentData);
// Проверяем, что компонент ещё не добавлен в эту сборку
if (!in_array($component->id, $seenComponentIds)) {
$createdComponents[] = $component;
$seenComponentIds[] = $component->id;
}
}
// Используем sync без детачей, чтобы избежать дубликатов
$build->components()->sync($seenComponentIds, false);
// 🔹 ПРОВЕРКА: убеждаемся, что есть все 7 типов компонентов
$finalComponents = $build->components()->get();
$finalTypes = $finalComponents->pluck('component_type_id')->unique()->toArray();
Log::info('Проверка финальной сборки:', [
'build_id' => $build->id,
'total_components' => $finalComponents->count(),
'component_types' => $finalTypes,
'missing_types' => array_diff($requiredTypes, $finalTypes)
]);
if (!in_array(7, $finalTypes)) {
// Находим или создаем корпус по умолчанию
$defaultCase = Component::firstOrCreate(
[
'name' => 'Cooler Master H500P ATX Mid Tower Case',
'component_type_id' => 7,
],
[
'price' => 4000,
'specifications' => ['form_factor' => 'ATX'],
'is_official' => true,
'created_by_user_id' => null,
]
);
$build->components()->attach($defaultCase->id);
Log::warning('Был добавлен корпус по умолчанию для сборки', [
'build_id' => $build->id,
'case_id' => $defaultCase->id
]);
// Обновляем массив созданных компонентов
$createdComponents[] = $defaultCase;
}
// 🔹 ФИНАЛЬНАЯ ПРОВЕРКА
$finalCount = $build->components()->count();
if ($finalCount < 7) {
Log::error('Сборка все еще имеет недостающее количество компонентов:', [
'build_id' => $build->id,
'expected' => 7,
'actual' => $finalCount
]);
}
return [
'build' => $build,
'components' => collect($createdComponents),
];
}
protected function generatePrompt($task_id, $custom_prompt, $budget)
{
$basePrompt = "Ты — эксперт по сборке ПК. Твоя задача — предложить **полную и сбалансированную сборку из 7 компонентов**, подходящую под запрос пользователя.";
if ($budget) {
$basePrompt .= " Бюджет: {$budget} рублей";
}
if ($custom_prompt) {
$basePrompt .= " Запрос пользователя: '{$custom_prompt}'.";
} elseif ($task_id) {
$basePrompt .= " Задача: #{$task_id}.";
}
$basePrompt .= " Сгенерируй JSON-объект с рекомендованной сборкой ПК для игр. Обязательно уложись в бюджет не более 112500 рублей (это 75% от 150000). Если не можешь — выбери более дешёвые компоненты. Не пиши ничего кроме JSON.";
$basePrompt .= " Ты **обязан** включить **ровно по одному компоненту каждого типа**: Процессор, Видеокарта, Материнская плата, ОЗУ, Блок питания, SSD, Корпус.";
$basePrompt .= " Используй ТОЛЬКО эти ID типов: 1=Процессор, 2=Видеокарта, 3=Материнская плата, 4=ОЗУ, 5=Блок питания, 6=SSD, 7=Корпус.";
$basePrompt .= " Не пропускай ни один тип. Не дублируй компоненты.";
$basePrompt .= " Все компоненты должны быть реальными, совместимыми и актуальными на 2025 год.";
$basePrompt .= " Ты **обязан** подобрать компоненты так, чтобы **общая стоимость не превышала** {$budget} рублей.";
$basePrompt .= " Никогда не пиши значения вроде 5600MHz — всегда используй строки: \"5600MHz\" или числа: 5600.";
$basePrompt .= " Не ставь лишние кавычки перед скобками.";
$basePrompt .= " ВАЖНО: Верни ТОЛЬКО валидный JSON без пояснений, комментариев, markdown.";
$basePrompt .= " Все значения в specifications должны быть строками или числами.";
$basePrompt .= " Не используй обратные слэши, звёздочки, решётки.";
$basePrompt .= " Верни **только чистый JSON-массив из 7 объектов**, без пояснений, комментариев, маркдауна.";
$basePrompt .= " Формат: [{\"name\":\"Название\",\"price\":999.99,\"component_type_id\":1,\"specifications\":{\"socket\":\"AM5\",\"tdp\":105}}, ...]";
$basePrompt .= " В specifications используй только строки или числа. Например: \"max_power\": 800, а не 800_watt.";
return $basePrompt;
}
protected function parseResponse($responseData)
{
if (!isset($responseData['choices'][0]['message']['content'])) {
throw new Exception('Ответ ИИ не содержит содержимого.');
}
$content = $responseData['choices'][0]['message']['content'];
// Удаляем markdown-блоки
$content = preg_replace('/^```(?:json)?\s*|\s*```$/m', '', $content);
// Находим первый JSON-массив
if (!preg_match('/\[[\s\S]*\]/', $content, $matches)) {
throw new Exception('Не найден JSON-массив в ответе ИИ.');
}
$jsonStr = $matches[0];
// Попытка декодировать без "починки"
$parsed = json_decode($jsonStr, true, 512, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
if (!is_array($parsed)) {
throw new Exception('Ожидался массив компонентов.');
}
foreach ($parsed as $index => $item) {
if (!is_array($item)) {
throw new Exception("Элемент #{$index} не является объектом.");
}
}
return $parsed;
}
protected function findOrCreateComponent($data)
{
// Валидация обязательных полей
if (!isset($data['name']) || !isset($data['component_type_id'])) {
throw new Exception('Компонент должен содержать name и component_type_id.');
}
$data['price'] = (float) ($data['price'] ?? 0);
$data['component_type_id'] = (int) ($data['component_type_id'] ?? 1);
// Очищаем specifications от некорректных значений
$specifications = [];
if (is_array($data['specifications'] ?? null)) {
foreach ($data['specifications'] as $key => $value) {
// Преобразуем всё в строки или числа
if (is_numeric($value)) {
$specifications[$key] = (float) $value;
} else {
$specifications[$key] = (string) $value;
}
}
}
$component = Component::firstOrCreate(
[
'name' => $data['name'],
'component_type_id' => $data['component_type_id'],
],
[
'price' => $data['price'],
'specifications' => $specifications,
'is_official' => true,
'created_by_user_id' => null,
]
);
// Обновляем цену, если компонент уже существует
if ($component->wasRecentlyCreated === false) {
$component->update(['price' => $data['price']]);
}
return $component;
}
}