'...', 'text' => '...'], ...] * @return array|null Ответ AI с названием основного файла */ public function run(array $fragments): ?array { Log::channel('ai')->info('Начало LLM запроса', [ 'fragments_count' => count($fragments), ]); $credential = $this->getCredentialTask->run('qwen'); if (!$credential) { Log::channel('ai')->error('API ключ не настроен', [ 'provider' => 'qwen', ]); return null; } $apiKey = $credential->payload['api_key'] ?? ''; $prompt = $this->buildPrompt($fragments); Log::channel('ai')->info('Отправка запроса к AI'); try { $response = Http::timeout(30) // Таймаут 30 секунд ->withHeaders([ 'Authorization' => 'Bearer ' . $apiKey, 'Content-Type' => 'application/json', ])->post('https://routerai.ru/api/v1/chat/completions', [ 'model' => 'qwen/qwen3.5-flash-02-23', 'messages' => [ ['role' => 'user', 'content' => $prompt], ], 'response_format' => ['type' => 'json_object'], 'temperature' => 0, 'max_tokens' => 500, ]); Log::channel('ai')->info('Ответ от AI', [ 'status' => $response->status(), 'successful' => $response->successful(), ]); if ($response->successful()) { $result = $response->json(); $llmResponse = json_decode($result['choices'][0]['message']['content'], true); Log::channel('ai')->info('Распознан ответ', [ 'main_file' => $llmResponse['main_file'] ?? null, ]); return $llmResponse; } else { Log::channel('ai')->error('Ошибка AI запроса', [ 'status' => $response->status(), 'body' => $response->body(), ]); } } catch (\Exception $e) { Log::channel('ai')->error('Исключение при запросе к AI', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); } return null; } /** * Формирует промпт для AI */ private function buildPrompt(array $fragments): string { $filesInfo = ""; foreach ($fragments as $index => $fragment) { $filesInfo .= "Файл #{$index}: {$fragment['filename']}\n"; $filesInfo .= "Текст: {$fragment['text']}\n\n"; } return "Ты — ассистент для определения основного файла с новостью. " . "Тебе предоставлены фрагменты текста из нескольких DOCX файлов.\n\n" . "СПИСОК ФАЙЛОВ:\n{$filesInfo}\n" . "ЗАДАЧА: Определи, какой файл содержит основной текст новости (не приложение, не документ, а именно новость/статью).\n\n" . "ВЕРНИ СТРОГО JSON:\n" . "{\n" . ' "main_file": "точное название файла с расширением .docx",' . "\n" . ' "reason": "краткое объяснение выбора"' . "\n" . "}\n\n" . "Если не можешь определить — верни первый файл из списка."; } }